From d71001a7b8c86cbe1a8ae5f90d1a4367dd0dc26d Mon Sep 17 00:00:00 2001 From: CryptoMickle <318943357+CryptoMickle@users.noreply.github.com> Date: Tue, 1 Sep 2026 20:35:53 +0200 Subject: [PATCH 1/7] perf: reduce Somnia VRF frontend latency --- frontend/app/page.tsx | 52 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 6d41536..90e7e88 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -3046,6 +3046,7 @@ function DelvewornGame() { useRef<{ name: string; startedAt: number; + lastStageAt: number; } | null>(null); const bossRewardRef = @@ -3067,9 +3068,15 @@ function DelvewornGame() { return; } + const now = + runtimeNowMs(); + console.info( - `[${ACTIVE_ECOSYSTEM_NAME.toUpperCase()} TIMING] ${timing.name} ${stage}: +${runtimeNowMs() - timing.startedAt}ms` + `[${ACTIVE_ECOSYSTEM_NAME.toUpperCase()} TIMING] ${timing.name} ${stage}: +${now - timing.startedAt}ms total, +${now - timing.lastStageAt}ms stage` ); + + timing.lastStageAt = + now; } useEffect(() => { @@ -3580,7 +3587,10 @@ function DelvewornGame() { }, pollingInterval: - 500, + Math.max( + TESTNET_POLLING_MS, + 250 + ), onLogs: ( @@ -5799,6 +5809,9 @@ function DelvewornGame() { functionName: SessionAction, + waitForStatus = + true, + args: readonly unknown[] = [] ): Promise< @@ -5835,6 +5848,27 @@ function DelvewornGame() { data ); + timingLog( + "Somnia smart-account transaction confirmed" + ); + + if (!waitForStatus) { + /* + Thirdweb's smart-account sender has already waited for the ERC-4337 + user operation receipt before returning the transaction hash. VRF + actions are completed by the lean RandomnessFulfilled watcher, so a + second HTTP receipt lookup only delays the animation without adding a + stronger confirmation signal. + */ + return { + hash: + result.transactionHash, + logs: [], + bundleId: + "", + }; + } + const receipt = await waitForReceipt( result.transactionHash @@ -5898,6 +5932,7 @@ function DelvewornGame() { ) { return sendDungeonSomniaSessionCall( functionName, + waitForStatus, args ); } @@ -6736,9 +6771,15 @@ function DelvewornGame() { return; } + const actionStartedAt = + runtimeNowMs(); + actionTimingRef.current = { name: functionName, - startedAt: runtimeNowMs(), + startedAt: + actionStartedAt, + lastStageAt: + actionStartedAt, }; timingLog( @@ -6856,7 +6897,10 @@ function DelvewornGame() { ); timingLog( - "sendDungeonSessionCall returned" + expectedRequestKind === + RequestKind.None + ? "transaction flow completed" + : "transaction confirmed; VRF wait active" ); transactionSubmitted = From d0b6cd53b65f7f10f24c662434e9b7b2570958e7 Mon Sep 17 00:00:00 2001 From: CryptoMickle <318943357+CryptoMickle@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:30:29 +0200 Subject: [PATCH 2/7] perf: add Somnia bundler latency benchmark --- frontend/app/page.tsx | 279 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 274 insertions(+), 5 deletions(-) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 90e7e88..b19ac93 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -2072,6 +2072,29 @@ type VrfCacheEntry = { receivedAt: number; }; +type LatencyBenchmarkSample = { + id: string; + action: string; + mode: string; + submissionMs: number; + vrfMs: number; + stateSyncMs: number; + totalMs: number; +}; + +type ActionTiming = { + name: string; + mode: string; + expectedRequestKind: number; + startedAt: number; + lastStageAt: number; + submissionConfirmedAt: number | null; + vrfEventReceivedAt: number | null; + stateReadStartedAt: number | null; + stateReadCompletedAt: number | null; + sampleCompleted: boolean; +}; + /* ============================================================ GENERIC HELPERS @@ -3017,6 +3040,14 @@ function DelvewornGame() { ] = useState(false); + const [ + latencySamples, + setLatencySamples, + ] = + useState< + LatencyBenchmarkSample[] + >([]); + const canonicalRecoveryRef = useRef(false); @@ -3043,11 +3074,9 @@ function DelvewornGame() { >(null); const actionTimingRef = - useRef<{ - name: string; - startedAt: number; - lastStageAt: number; - } | null>(null); + useRef< + ActionTiming | null + >(null); const bossRewardRef = useRef( @@ -3079,6 +3108,92 @@ function DelvewornGame() { now; } + function completeLatencyBenchmark() { + const timing = + actionTimingRef.current; + + if ( + !timing || + timing.sampleCompleted || + timing.expectedRequestKind === + RequestKind.None + ) { + return; + } + + const completedAt = + runtimeNowMs(); + + const submissionConfirmedAt = + timing.submissionConfirmedAt ?? + completedAt; + + const vrfEventReceivedAt = + timing.vrfEventReceivedAt ?? + timing.stateReadCompletedAt ?? + completedAt; + + const stateReadStartedAt = + timing.stateReadStartedAt ?? + vrfEventReceivedAt; + + const stateReadCompletedAt = + timing.stateReadCompletedAt ?? + completedAt; + + const sample: + LatencyBenchmarkSample = { + id: + `${timing.startedAt}-${timing.name}`, + action: + timing.name, + mode: + timing.mode, + submissionMs: + Math.max( + 0, + submissionConfirmedAt - + timing.startedAt + ), + vrfMs: + Math.max( + 0, + vrfEventReceivedAt - + submissionConfirmedAt + ), + stateSyncMs: + Math.max( + 0, + stateReadCompletedAt - + stateReadStartedAt + ), + totalMs: + Math.max( + 0, + completedAt - + timing.startedAt + ), + }; + + timing.sampleCompleted = + true; + + setLatencySamples( + (samples) => [ + sample, + ...samples, + ].slice( + 0, + 8 + ) + ); + + console.info( + `[${ACTIVE_ECOSYSTEM_NAME.toUpperCase()} BENCHMARK]`, + sample + ); + } + useEffect(() => { connectedAddressRef.current = connectedAddress; @@ -3455,6 +3570,22 @@ function DelvewornGame() { entry ); + const actionTiming = + actionTimingRef.current; + + if ( + actionTiming && + actionTiming.vrfEventReceivedAt === + null && + actionTiming.expectedRequestKind === + entry.kind && + entry.receivedAt >= + actionTiming.startedAt + ) { + actionTiming.vrfEventReceivedAt = + entry.receivedAt; + } + timingLog( `RandomnessFulfilled received via ${source} (request ${requestId.toString()}, kind ${Number(kind)})` ); @@ -3875,6 +4006,8 @@ function DelvewornGame() { stage ); + completeLatencyBenchmark(); + void finalizeCanonicalAction( playerAddress, displayStartedAt, @@ -4116,6 +4249,18 @@ function DelvewornGame() { playerAddress.toLowerCase() ) { try { + const actionTiming = + actionTimingRef.current; + + if ( + actionTiming && + actionTiming.stateReadStartedAt === + null + ) { + actionTiming.stateReadStartedAt = + runtimeNowMs(); + } + timingLog( `starting state read via ${cached.source}` ); @@ -4130,6 +4275,11 @@ function DelvewornGame() { `state read via ${cached.source} completed` ); + if (actionTiming) { + actionTiming.stateReadCompletedAt = + runtimeNowMs(); + } + if ( resolvedState.pendingRequestId === BigInt(0) @@ -6776,10 +6926,29 @@ function DelvewornGame() { actionTimingRef.current = { name: functionName, + mode: + isRiseWallet + ? "RISE Instant Play" + : somniaSessionMode && + hasSomniaSession + ? "Somnia Instant Play" + : "MetaMask Standard Play", + expectedRequestKind: + RequestKind.None, startedAt: actionStartedAt, lastStageAt: actionStartedAt, + submissionConfirmedAt: + null, + vrfEventReceivedAt: + null, + stateReadStartedAt: + null, + stateReadCompletedAt: + null, + sampleCompleted: + false, }; timingLog( @@ -6869,6 +7038,13 @@ function DelvewornGame() { expectedRequestKind !== RequestKind.None ) { + if ( + actionTimingRef.current + ) { + actionTimingRef.current.expectedRequestKind = + expectedRequestKind; + } + setActionReady( false ); @@ -6896,6 +7072,13 @@ function DelvewornGame() { RequestKind.None ); + if ( + actionTimingRef.current + ) { + actionTimingRef.current.submissionConfirmedAt = + runtimeNowMs(); + } + timingLog( expectedRequestKind === RequestKind.None @@ -8340,6 +8523,92 @@ function DelvewornGame() { )} + {ACTIVE_ECOSYSTEM_NAME === + "Somnia" && ( +
+
+
+

+ BUNDLER + VRF BENCHMARK +

+

+ Instant submit includes signing, sponsorship, bundler and confirmation. Standard submit also includes the wallet approval. +

+
+ + {latencySamples.length > + 0 && ( + + )} +
+ + {latencySamples.length === + 0 ? ( +

+ Run Attack, Storm, Potion or enter a room to record the first sample. +

+ ) : ( + <> +
+ + + + +
+ +
+ + {latencySamples[0].mode} · {latencySamples[0].action} · show history + +
+ {latencySamples.map( + (sample) => ( +

+ {sample.mode} · {sample.action}: submit {(sample.submissionMs / 1_000).toFixed(2)}s · VRF {(sample.vrfMs / 1_000).toFixed(2)}s · state {(sample.stateSyncMs / 1_000).toFixed(2)}s · total {(sample.totalMs / 1_000).toFixed(2)}s +

+ ) + )} +
+
+ + )} +
+ )} + {/* =================================================== COMPACT STICKY HUD =================================================== */} From 2c376d4f25097eb0f04c53cd0a1a3c1a176808f0 Mon Sep 17 00:00:00 2001 From: CryptoMickle <318943357+CryptoMickle@users.noreply.github.com> Date: Wed, 2 Sep 2026 00:44:07 +0200 Subject: [PATCH 3/7] perf: split Somnia smart-account latency stages --- frontend/app/page.tsx | 72 ++++++++++++---- frontend/app/somnia-session-keys.ts | 124 +++++++++++++++++++++++++++- 2 files changed, 178 insertions(+), 18 deletions(-) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index b19ac93..2fb1469 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -2076,18 +2076,30 @@ type LatencyBenchmarkSample = { id: string; action: string; mode: string; + smartAccount: SmartAccountLatencyBreakdown | null; submissionMs: number; vrfMs: number; stateSyncMs: number; totalMs: number; }; +type SmartAccountLatencyBreakdown = { + preparationMs: number; + gasEstimationMs: number; + paymasterMs: number; + bundlerSubmissionMs: number; + inclusionWaitMs: number; + receiptPollCount: number; + totalMs: number; +}; + type ActionTiming = { name: string; mode: string; expectedRequestKind: number; startedAt: number; lastStageAt: number; + smartAccount: SmartAccountLatencyBreakdown | null; submissionConfirmedAt: number | null; vrfEventReceivedAt: number | null; stateReadStartedAt: number | null; @@ -3149,6 +3161,8 @@ function DelvewornGame() { timing.name, mode: timing.mode, + smartAccount: + timing.smartAccount, submissionMs: Math.max( 0, @@ -5998,6 +6012,11 @@ function DelvewornGame() { data ); + if (actionTimingRef.current) { + actionTimingRef.current.smartAccount = + result.benchmark; + } + timingLog( "Somnia smart-account transaction confirmed" ); @@ -6939,6 +6958,8 @@ function DelvewornGame() { actionStartedAt, lastStageAt: actionStartedAt, + smartAccount: + null, submissionConfirmedAt: null, vrfEventReceivedAt: @@ -8532,7 +8553,7 @@ function DelvewornGame() { BUNDLER + VRF BENCHMARK

- Instant submit includes signing, sponsorship, bundler and confirmation. Standard submit also includes the wallet approval. + Instant Play split follows Thirdweb's ERC-4337 flow. Prepare includes local work and gas estimation; inclusion is the wait after bundler submission.

@@ -8559,25 +8580,35 @@ function DelvewornGame() {

) : ( <> -
+
+ + @@ -8596,11 +8627,18 @@ function DelvewornGame() {
{latencySamples.map( - (sample) => ( + (sample) => { + const smart = + sample.smartAccount; + + return (

- {sample.mode} · {sample.action}: submit {(sample.submissionMs / 1_000).toFixed(2)}s · VRF {(sample.vrfMs / 1_000).toFixed(2)}s · state {(sample.stateSyncMs / 1_000).toFixed(2)}s · total {(sample.totalMs / 1_000).toFixed(2)}s + {sample.mode} · {sample.action}: {smart + ? `prep ${(smart.preparationMs / 1_000).toFixed(2)}s (estimate ${(smart.gasEstimationMs / 1_000).toFixed(2)}s) · paymaster ${(smart.paymasterMs / 1_000).toFixed(2)}s · bundler ${(smart.bundlerSubmissionMs / 1_000).toFixed(2)}s · inclusion ${(smart.inclusionWaitMs / 1_000).toFixed(2)}s (${smart.receiptPollCount} polls) · ` + : `submit ${(sample.submissionMs / 1_000).toFixed(2)}s · `}VRF {(sample.vrfMs / 1_000).toFixed(2)}s · state {(sample.stateSyncMs / 1_000).toFixed(2)}s · total {(sample.totalMs / 1_000).toFixed(2)}s

- ) + ); + } )}
diff --git a/frontend/app/somnia-session-keys.ts b/frontend/app/somnia-session-keys.ts index 932017a..28c999c 100644 --- a/frontend/app/somnia-session-keys.ts +++ b/frontend/app/somnia-session-keys.ts @@ -35,6 +35,40 @@ export type SomniaSessionHandle = { record: SomniaSessionRecord; }; +export type SomniaSessionTransactionBenchmark = { + preparationMs: number; + gasEstimationMs: number; + paymasterMs: number; + bundlerSubmissionMs: number; + inclusionWaitMs: number; + receiptPollCount: number; + totalMs: number; +}; + +function benchmarkNowMs() { + return globalThis.performance?.now() ?? Date.now(); +} + +function jsonRpcMethod( + body: BodyInit | null | undefined +): string | null { + if (typeof body !== "string") { + return null; + } + + try { + const payload = JSON.parse(body) as { + method?: unknown; + }; + + return typeof payload.method === "string" + ? payload.method + : null; + } catch { + return null; + } +} + const somniaChain = defineChain({ id: activeDeployment.chain.id, name: activeDeployment.chain.name, @@ -236,8 +270,96 @@ export async function sendSomniaSessionTransaction( data, value: BigInt(0), }); + const startedAt = benchmarkNowMs(); + const originalFetch = globalThis.fetch; + let gasEstimationMs = 0; + let paymasterMs = 0; + let bundlerSubmissionMs = 0; + let receiptPollCount = 0; + let userOpSubmissionStartedAt: number | null = null; + let userOpSubmittedAt: number | null = null; + + const benchmarkFetch: typeof globalThis.fetch = async ( + input, + init + ) => { + const method = jsonRpcMethod(init?.body); + const requestStartedAt = benchmarkNowMs(); + + if ( + method === "eth_sendUserOperation" && + userOpSubmissionStartedAt === null + ) { + userOpSubmissionStartedAt = requestStartedAt; + } - return sendTransaction({ account, transaction }); + if (method === "eth_getUserOperationReceipt") { + receiptPollCount += 1; + } + + try { + return await originalFetch(input, init); + } finally { + const requestCompletedAt = benchmarkNowMs(); + const requestMs = Math.max( + 0, + requestCompletedAt - requestStartedAt + ); + + if (method === "eth_estimateUserOperationGas") { + gasEstimationMs += requestMs; + } + + if (method === "pm_sponsorUserOperation") { + paymasterMs += requestMs; + } + + if (method === "eth_sendUserOperation") { + bundlerSubmissionMs += requestMs; + userOpSubmittedAt = requestCompletedAt; + } + } + }; + + globalThis.fetch = benchmarkFetch; + + try { + const result = await sendTransaction({ account, transaction }); + const completedAt = benchmarkNowMs(); + const totalMs = Math.max(0, completedAt - startedAt); + const preparationWindowMs = Math.max( + 0, + (userOpSubmissionStartedAt ?? completedAt) - startedAt + ); + const preparationMs = Math.max( + 0, + preparationWindowMs - paymasterMs + ); + const inclusionWaitMs = Math.max( + 0, + userOpSubmittedAt === null + ? 0 + : completedAt - userOpSubmittedAt + ); + const benchmark: SomniaSessionTransactionBenchmark = { + preparationMs, + gasEstimationMs, + paymasterMs, + bundlerSubmissionMs, + inclusionWaitMs, + receiptPollCount, + totalMs, + }; + + return { + ...result, + benchmark, + }; + } finally { + if (globalThis.fetch === benchmarkFetch) { + globalThis.fetch = originalFetch; + } + } } export async function revokeSomniaSession( From b821619d98e9b799b2e68d83ab79779a8e6e1ab5 Mon Sep 17 00:00:00 2001 From: CryptoMickle <318943357+CryptoMickle@users.noreply.github.com> Date: Wed, 2 Sep 2026 01:36:08 +0200 Subject: [PATCH 4/7] perf: poll Somnia user operations faster --- frontend/app/page.tsx | 7 +++-- frontend/app/somnia-session-keys.ts | 48 ++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 2fb1469..f6ad4a1 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -2090,6 +2090,7 @@ type SmartAccountLatencyBreakdown = { bundlerSubmissionMs: number; inclusionWaitMs: number; receiptPollCount: number; + receiptPollingIntervalMs: number; totalMs: number; }; @@ -6008,7 +6009,7 @@ function DelvewornGame() { const result = await sendSomniaSessionTransaction( - somniaSessionHandle.account, + somniaSessionHandle.record, data ); @@ -8553,7 +8554,7 @@ function DelvewornGame() { BUNDLER + VRF BENCHMARK

- Instant Play split follows Thirdweb's ERC-4337 flow. Prepare includes local work and gas estimation; inclusion is the wait after bundler submission. + Instant Play split follows Thirdweb's ERC-4337 flow. Prepare includes local work and gas estimation; inclusion uses fast receipt polling after bundler submission.

@@ -8634,7 +8635,7 @@ function DelvewornGame() { return (

{sample.mode} · {sample.action}: {smart - ? `prep ${(smart.preparationMs / 1_000).toFixed(2)}s (estimate ${(smart.gasEstimationMs / 1_000).toFixed(2)}s) · paymaster ${(smart.paymasterMs / 1_000).toFixed(2)}s · bundler ${(smart.bundlerSubmissionMs / 1_000).toFixed(2)}s · inclusion ${(smart.inclusionWaitMs / 1_000).toFixed(2)}s (${smart.receiptPollCount} polls) · ` + ? `prep ${(smart.preparationMs / 1_000).toFixed(2)}s (estimate ${(smart.gasEstimationMs / 1_000).toFixed(2)}s) · paymaster ${(smart.paymasterMs / 1_000).toFixed(2)}s · bundler ${(smart.bundlerSubmissionMs / 1_000).toFixed(2)}s · inclusion ${(smart.inclusionWaitMs / 1_000).toFixed(2)}s (${smart.receiptPollCount} polls @ ${smart.receiptPollingIntervalMs}ms) · ` : `submit ${(sample.submissionMs / 1_000).toFixed(2)}s · `}VRF {(sample.vrfMs / 1_000).toFixed(2)}s · state {(sample.stateSyncMs / 1_000).toFixed(2)}s · total {(sample.totalMs / 1_000).toFixed(2)}s

); diff --git a/frontend/app/somnia-session-keys.ts b/frontend/app/somnia-session-keys.ts index 28c999c..ea137d9 100644 --- a/frontend/app/somnia-session-keys.ts +++ b/frontend/app/somnia-session-keys.ts @@ -10,7 +10,12 @@ import { } from "thirdweb/extensions/erc4337"; import { createWallet, type Account } from "thirdweb/wallets"; import { privateKeyToAccount } from "thirdweb/wallets/private-key"; -import { smartWallet } from "thirdweb/wallets/smart"; +import { + bundleUserOp, + createAndSignUserOp, + smartWallet, + waitForUserOpReceipt, +} from "thirdweb/wallets/smart"; import { prepareTransaction, sendTransaction, @@ -29,6 +34,7 @@ import { } from "./somnia-session-storage"; const CLOCK_SKEW_MS = 30_000; +const USER_OPERATION_RECEIPT_POLL_INTERVAL_MS = 250; export type SomniaSessionHandle = { account: Account; @@ -42,6 +48,7 @@ export type SomniaSessionTransactionBenchmark = { bundlerSubmissionMs: number; inclusionWaitMs: number; receiptPollCount: number; + receiptPollingIntervalMs: number; totalMs: number; }; @@ -259,10 +266,21 @@ export async function createSomniaSession( } export async function sendSomniaSessionTransaction( - account: Account, + record: SomniaSessionRecord, data: Hex ) { const client = thirdwebClient(); + const sessionSigner = privateKeyToAccount({ + client, + privateKey: record.sessionPrivateKey, + }); + const smartWalletOptions = { + chain: somniaChain, + sponsorGas: true, + overrides: { + accountAddress: record.smartAccountAddress, + }, + } as const; const transaction = prepareTransaction({ client, chain: somniaChain, @@ -324,7 +342,25 @@ export async function sendSomniaSessionTransaction( globalThis.fetch = benchmarkFetch; try { - const result = await sendTransaction({ account, transaction }); + const signedUserOp = await createAndSignUserOp({ + transactions: [transaction], + adminAccount: sessionSigner, + client, + smartWalletOptions, + }); + const bundlerOptions = { + chain: somniaChain, + client, + }; + const userOpHash = await bundleUserOp({ + userOp: signedUserOp, + options: bundlerOptions, + }); + const receipt = await waitForUserOpReceipt({ + ...bundlerOptions, + userOpHash, + intervalMs: USER_OPERATION_RECEIPT_POLL_INTERVAL_MS, + }); const completedAt = benchmarkNowMs(); const totalMs = Math.max(0, completedAt - startedAt); const preparationWindowMs = Math.max( @@ -348,11 +384,15 @@ export async function sendSomniaSessionTransaction( bundlerSubmissionMs, inclusionWaitMs, receiptPollCount, + receiptPollingIntervalMs: + USER_OPERATION_RECEIPT_POLL_INTERVAL_MS, totalMs, }; return { - ...result, + chain: somniaChain, + client, + transactionHash: receipt.transactionHash, benchmark, }; } finally { From 6690d169a83ee446a54711c61ce794d4842e5b31 Mon Sep 17 00:00:00 2001 From: CryptoMickle <318943357+CryptoMickle@users.noreply.github.com> Date: Wed, 2 Sep 2026 02:38:40 +0200 Subject: [PATCH 5/7] perf: add Somnia VRF websocket fast path --- frontend/.env.example | 1 + frontend/app/chain-clients.ts | 20 +- frontend/app/chain-config.ts | 5 +- frontend/app/globals.css | 47 ++++ frontend/app/page-runtime-bindings.ts | 8 +- frontend/app/page.tsx | 370 ++++++++++++++++++++++++-- frontend/app/somnia-session-keys.ts | 17 +- 7 files changed, 440 insertions(+), 28 deletions(-) diff --git a/frontend/.env.example b/frontend/.env.example index e1dc548..2be9b54 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -12,6 +12,7 @@ NEXT_PUBLIC_RISE_TESTNET_EXPLORER_URL=https://explorer.testnet.riselabs.xyz # The public RISE deployment remains the default. NEXT_PUBLIC_SOMNIA_SHANNON_DUNGEON_ADDRESS=0x07c5D071132ae95C3708031790b3feC740F4c292 NEXT_PUBLIC_SOMNIA_SHANNON_RPC_URL=https://dream-rpc.somnia.network/ +NEXT_PUBLIC_SOMNIA_SHANNON_WS_URL=wss://dream-rpc.somnia.network/ws NEXT_PUBLIC_SOMNIA_SHANNON_EXPLORER_URL=https://shannon-explorer.somnia.network/ # Optional Somnia ERC-4337 Instant Play prototype. Keep disabled until the diff --git a/frontend/app/chain-clients.ts b/frontend/app/chain-clients.ts index a47f785..34827fe 100644 --- a/frontend/app/chain-clients.ts +++ b/frontend/app/chain-clients.ts @@ -10,12 +10,28 @@ export function createActivePublicClient() { } export function createActiveWebSocketClient() { - if (!activeDeployment.wsUrl) { + if ( + !activeDeployment.realtime.websocket || + activeDeployment.realtime.shreds || + !activeDeployment.wsUrl + ) { return null; } return createPublicClient({ chain: activeDeployment.chain, - transport: webSocket(activeDeployment.wsUrl), + cacheTime: 0, + transport: webSocket(activeDeployment.wsUrl, { + keepAlive: { + interval: 5_000, + }, + reconnect: { + attempts: 100, + delay: 500, + }, + retryCount: 5, + retryDelay: 100, + timeout: 15_000, + }), }); } diff --git a/frontend/app/chain-config.ts b/frontend/app/chain-config.ts index 1ffece1..6bd9c8c 100644 --- a/frontend/app/chain-config.ts +++ b/frontend/app/chain-config.ts @@ -233,6 +233,9 @@ export const deployments = { rpcUrl: process.env.NEXT_PUBLIC_SOMNIA_SHANNON_RPC_URL ?? "https://dream-rpc.somnia.network/", + wsUrl: + process.env.NEXT_PUBLIC_SOMNIA_SHANNON_WS_URL ?? + "wss://dream-rpc.somnia.network/ws", explorerUrl: process.env.NEXT_PUBLIC_SOMNIA_SHANNON_EXPLORER_URL ?? "https://shannon-explorer.somnia.network/", @@ -246,7 +249,7 @@ export const deployments = { : null, }, realtime: { - websocket: false, + websocket: true, shreds: false, }, randomness: { diff --git a/frontend/app/globals.css b/frontend/app/globals.css index c18e492..bb6f64c 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -25,6 +25,53 @@ body { font-family: Arial, Helvetica, sans-serif; } +@keyframes combat-intent-attack { + 0%, 100% { transform: rotate(-7deg) translateY(0); } + 45% { transform: rotate(13deg) translateY(-3px); } +} + +@keyframes combat-intent-storm { + 0%, 100% { filter: brightness(1); transform: scale(.96); } + 50% { filter: brightness(1.45); transform: scale(1.08); } +} + +@keyframes combat-intent-potion { + 0%, 100% { transform: rotate(-4deg) translateY(1px); } + 50% { transform: rotate(6deg) translateY(-4px); } +} + +@keyframes combat-intent-enter { + 0%, 100% { filter: brightness(.9); transform: scale(.96); } + 50% { filter: brightness(1.2); transform: scale(1.04); } +} + +.combat-intent-attack { + animation: combat-intent-attack 1.15s ease-in-out infinite; +} + +.combat-intent-storm { + animation: combat-intent-storm .9s ease-in-out infinite; +} + +.combat-intent-potion { + animation: combat-intent-potion 1.2s ease-in-out infinite; +} + +.combat-intent-enter, +.combat-intent-random { + animation: combat-intent-enter 1.35s ease-in-out infinite; +} + +@media (prefers-reduced-motion: reduce) { + .combat-intent-attack, + .combat-intent-storm, + .combat-intent-potion, + .combat-intent-enter, + .combat-intent-random { + animation: none; + } +} + .delveworn-practice-mode { --delveworn-mode-accent: #22d3ee; --delveworn-mode-accent-soft: rgba(34, 211, 238, .12); diff --git a/frontend/app/page-runtime-bindings.ts b/frontend/app/page-runtime-bindings.ts index ebe0e7c..e9833b1 100644 --- a/frontend/app/page-runtime-bindings.ts +++ b/frontend/app/page-runtime-bindings.ts @@ -1,5 +1,8 @@ import { activeDeployment } from "./chain-config"; -import { createActivePublicClient } from "./chain-clients"; +import { + createActivePublicClient, + createActiveWebSocketClient, +} from "./chain-clients"; import { wagmiConfig } from "./wagmi-runtime"; import { ensureActiveChain } from "./wallet-network"; import { @@ -71,6 +74,9 @@ export const ACTIVE_CHAIN_ID = export const publicClient = createActivePublicClient(); +export const eventWebSocketClient = + createActiveWebSocketClient(); + export { ACTIVE_ECOSYSTEM_NAME, ACTIVE_NETWORK_LABEL, diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index f6ad4a1..677012a 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -67,6 +67,7 @@ import { delayedRandomnessText, delayedRandomnessTitle, ensureActiveChain, + eventWebSocketClient, isMetaMaskConnector, isRiseWalletConnector, publicClient, @@ -86,6 +87,7 @@ import { } from "./somnia-session-storage"; import { type SomniaSessionHandle, + type SomniaSessionTransactionPhase, } from "./somnia-session-keys"; /* @@ -189,6 +191,10 @@ async function createConnectedWalletClient( const realtimeClient = createRiseShredsClient(); +const realtimeReadClient = + realtimeClient ?? + eventWebSocketClient; + /* ============================================================ ABI @@ -1920,7 +1926,7 @@ async function fetchPlayerState( const reader = source === "realtime" - ? realtimeClient ?? publicClient + ? realtimeReadClient ?? publicClient : publicClient; let snapshot: unknown; @@ -2067,16 +2073,28 @@ type VrfCacheEntry = { player: Address; requestId: bigint; kind: number; - source: - "realtime" | "canonical"; + source: VrfCompletionSource; receivedAt: number; }; +type VrfCompletionSource = + | "shreds" + | "websocket" + | "http" + | "state-polling"; + +type ActionProgressPhase = + | "idle" + | SomniaSessionTransactionPhase + | "vrf" + | "syncing"; + type LatencyBenchmarkSample = { id: string; action: string; mode: string; smartAccount: SmartAccountLatencyBreakdown | null; + vrfSource: VrfCompletionSource | null; submissionMs: number; vrfMs: number; stateSyncMs: number; @@ -2103,6 +2121,7 @@ type ActionTiming = { smartAccount: SmartAccountLatencyBreakdown | null; submissionConfirmedAt: number | null; vrfEventReceivedAt: number | null; + vrfSource: VrfCompletionSource | null; stateReadStartedAt: number | null; stateReadCompletedAt: number | null; sampleCompleted: boolean; @@ -2996,6 +3015,14 @@ function DelvewornGame() { RequestKind.None ); + const [ + actionProgressPhase, + setActionProgressPhase, + ] = + useState( + "idle" + ); + const [ vrfDelayed, setVrfDelayed, @@ -3164,6 +3191,8 @@ function DelvewornGame() { timing.mode, smartAccount: timing.smartAccount, + vrfSource: + timing.vrfSource, submissionMs: Math.max( 0, @@ -3496,7 +3525,7 @@ function DelvewornGame() { /* ========================================================== - RISE SHREDS VRF EVENT CACHE + REALTIME VRF EVENT CACHE ========================================================== */ @@ -3514,7 +3543,7 @@ function DelvewornGame() { /* The lean RandomnessFulfilled completion signal is consumed through two independent paths: - 1. Shreds / WebSocket: fastest path. + 1. Shreds or standard WebSocket: fastest path. 2. HTTP event polling: reliability fallback. The HTTP watcher is intentionally always active. If RISE closes @@ -3532,7 +3561,7 @@ function DelvewornGame() { number, source: - "realtime" | "canonical" + VrfCompletionSource ) => { const normalizedPlayer = getAddress( @@ -3575,8 +3604,8 @@ function DelvewornGame() { }; /* - The same completion can arrive first through Shreds and later - through the HTTP watcher. Preserve the original receivedAt so a + The same completion can arrive first through a realtime feed and + later through the HTTP watcher. Preserve the original receivedAt so a delayed duplicate from a previous attack can never masquerade as the completion for a new attack of the same kind. */ @@ -3599,6 +3628,9 @@ function DelvewornGame() { ) { actionTiming.vrfEventReceivedAt = entry.receivedAt; + + actionTiming.vrfSource = + entry.source; } timingLog( @@ -3685,7 +3717,7 @@ function DelvewornGame() { Number( args.kind ), - "realtime" + "shreds" ); } catch ( error @@ -3716,6 +3748,76 @@ function DelvewornGame() { }) : () => {}; + const unwatchWebSocketEvents = + eventWebSocketClient + ? eventWebSocketClient.watchContractEvent({ + address: + DUNGEON_ADDRESS, + + abi: + dungeonAbi, + + eventName: + "RandomnessFulfilled", + + args: { + player: + watchedAddress, + }, + + onLogs: + ( + logs + ) => { + for ( + const log + of logs + ) { + const args = + log.args as { + player?: + Address; + + requestId?: + bigint; + + kind?: + number; + }; + + if ( + !args.player || + args.requestId === + undefined || + args.kind === + undefined + ) { + continue; + } + + ingestVrfResult( + args.player, + args.requestId, + Number( + args.kind + ), + "websocket" + ); + } + }, + + onError: + ( + error + ) => { + console.warn( + `${ACTIVE_NETWORK_LABEL} WebSocket interrupted; HTTP event fallback remains active:`, + error + ); + }, + }) + : () => {}; + const unwatchHttpEvents = publicClient.watchContractEvent({ address: @@ -3774,7 +3876,7 @@ function DelvewornGame() { Number( args.kind ), - "canonical" + "http" ); } }, @@ -3792,6 +3894,7 @@ function DelvewornGame() { return () => { unwatchShreds(); + unwatchWebSocketEvents(); unwatchHttpEvents(); vrfResultsRef.current.clear(); }; @@ -3993,6 +4096,10 @@ function DelvewornGame() { stage: string ): PlayerState { + setActionProgressPhase( + "syncing" + ); + setPlayer( resolvedState ); @@ -4283,7 +4390,10 @@ function DelvewornGame() { const resolvedState = await fetchPlayerState( playerAddress, - cached.source + cached.source === + "http" + ? "canonical" + : "realtime" ); timingLog( @@ -4379,6 +4489,17 @@ function DelvewornGame() { changed ) ) { + const actionTiming = + actionTimingRef.current; + + if ( + actionTiming && + !actionTiming.vrfSource + ) { + actionTiming.vrfSource = + "state-polling"; + } + const remainingDisplay = MIN_VRF_DISPLAY_MS - ( @@ -5537,6 +5658,10 @@ function DelvewornGame() { "connector.getProvider start" ); + setActionProgressPhase( + "preparing" + ); + const provider = ( await connector @@ -5664,6 +5789,10 @@ function DelvewornGame() { "wallet_sendPreparedCalls start" ); + setActionProgressPhase( + "submitting" + ); + const result = await provider.request({ method: @@ -5704,6 +5833,10 @@ function DelvewornGame() { ); } + setActionProgressPhase( + "inclusion" + ); + if (!waitForStatus) { timingLog( `bundle accepted (${bundleId})` @@ -5934,6 +6067,10 @@ function DelvewornGame() { args, } as never); + setActionProgressPhase( + "submitting" + ); + const hash = await walletClient .sendTransaction({ @@ -5946,6 +6083,10 @@ function DelvewornGame() { data, }); + setActionProgressPhase( + "inclusion" + ); + const receipt = await waitForReceipt( hash @@ -6010,7 +6151,14 @@ function DelvewornGame() { const result = await sendSomniaSessionTransaction( somniaSessionHandle.record, - data + data, + ( + phase + ) => { + setActionProgressPhase( + phase + ); + } ); if (actionTimingRef.current) { @@ -6965,6 +7113,8 @@ function DelvewornGame() { null, vrfEventReceivedAt: null, + vrfSource: + null, stateReadStartedAt: null, stateReadCompletedAt: @@ -7085,6 +7235,10 @@ function DelvewornGame() { setRollingKind( expectedRequestKind ); + + setActionProgressPhase( + "preparing" + ); } const sessionResult = @@ -7111,6 +7265,15 @@ function DelvewornGame() { transactionSubmitted = true; + if ( + expectedRequestKind !== + RequestKind.None + ) { + setActionProgressPhase( + "vrf" + ); + } + let resolved: PlayerState | null = null; @@ -7652,6 +7815,10 @@ function DelvewornGame() { RequestKind.None ); + setActionProgressPhase( + "idle" + ); + setPendingAction( null ); @@ -8393,6 +8560,9 @@ function DelvewornGame() { let rollingText = "The dungeon is making questionable decisions..."; + let rollingIconClass = + "combat-intent-random"; + if ( requestKind === RequestKind.Monster @@ -8402,6 +8572,9 @@ function DelvewornGame() { ? "👑" : "🚪"; + rollingIconClass = + "combat-intent-enter"; + rollingTitle = isBossRoom ? "MANAGEMENT INCOMING" @@ -8420,6 +8593,9 @@ function DelvewornGame() { rollingIcon = "⚔️"; + rollingIconClass = + "combat-intent-attack"; + rollingTitle = "ROLLING ATTACK"; @@ -8434,6 +8610,9 @@ function DelvewornGame() { rollingIcon = "⚡"; + rollingIconClass = + "combat-intent-storm"; + rollingTitle = "UNLEASHING STORM"; @@ -8448,6 +8627,9 @@ function DelvewornGame() { rollingIcon = "🧪"; + rollingIconClass = + "combat-intent-potion"; + rollingTitle = "DRINKING SUSPICIOUS LIQUID"; @@ -8455,12 +8637,109 @@ function DelvewornGame() { "Healing is easy. Retaliation is less convenient."; } + if ( + actionProgressPhase === + "preparing" + ) { + rollingLabel = + "ACTION · 1 OF 5"; + + rollingTitle = + "PREPARING MOVE"; + + rollingText = + "Building the restricted session action and estimating its cost..."; + } + + if ( + actionProgressPhase === + "sponsoring" + ) { + rollingLabel = + "ACTION · 2 OF 5"; + + rollingTitle = + "SPONSORING MOVE"; + + rollingText = + "Thirdweb is approving sponsored gas for this zero-value action..."; + } + + if ( + actionProgressPhase === + "submitting" + ) { + rollingLabel = + "ACTION · 3 OF 5"; + + rollingTitle = + "SENDING ONCHAIN"; + + rollingText = + `The signed move is being sent to ${ACTIVE_ECOSYSTEM_NAME}...`; + } + + if ( + actionProgressPhase === + "inclusion" + ) { + rollingLabel = + "ACTION · 4 OF 5"; + + rollingTitle = + "WAITING FOR INCLUSION"; + + rollingText = + `The bundler accepted the move. Waiting for ${ACTIVE_ECOSYSTEM_NAME} to include it...`; + } + + if ( + actionProgressPhase === + "vrf" + ) { + rollingLabel = + "VRF · 5 OF 5"; + } + + const actionProgressOrder: + ActionProgressPhase[] = + hasSomniaSession + ? [ + "preparing", + "sponsoring", + "submitting", + "inclusion", + "vrf", + ] + : [ + "preparing", + "submitting", + "inclusion", + "vrf", + ]; + + const actionProgressIndex = + actionProgressOrder.indexOf( + actionProgressPhase + ); + + if ( + actionProgressIndex >= + 0 + ) { + rollingLabel = + `${actionProgressPhase === "vrf" ? "VRF" : "ACTION"} · ${actionProgressIndex + 1} OF ${actionProgressOrder.length}`; + } + if ( canonicalSyncing ) { rollingIcon = "⛓️"; + rollingIconClass = + "combat-intent-random"; + rollingTitle = "FINALIZING ACTION"; @@ -8475,6 +8754,9 @@ function DelvewornGame() { rollingIcon = "⏳"; + rollingIconClass = + "combat-intent-random"; + rollingLabel = ACTIVE_NETWORK_LABEL.toUpperCase(); @@ -8607,7 +8889,18 @@ function DelvewornGame() { : "—"} /> {sample.mode} · {sample.action}: {smart ? `prep ${(smart.preparationMs / 1_000).toFixed(2)}s (estimate ${(smart.gasEstimationMs / 1_000).toFixed(2)}s) · paymaster ${(smart.paymasterMs / 1_000).toFixed(2)}s · bundler ${(smart.bundlerSubmissionMs / 1_000).toFixed(2)}s · inclusion ${(smart.inclusionWaitMs / 1_000).toFixed(2)}s (${smart.receiptPollCount} polls @ ${smart.receiptPollingIntervalMs}ms) · ` - : `submit ${(sample.submissionMs / 1_000).toFixed(2)}s · `}VRF {(sample.vrfMs / 1_000).toFixed(2)}s · state {(sample.stateSyncMs / 1_000).toFixed(2)}s · total {(sample.totalMs / 1_000).toFixed(2)}s + : `submit ${(sample.submissionMs / 1_000).toFixed(2)}s · `}VRF {(sample.vrfMs / 1_000).toFixed(2)}s ({sample.vrfSource ?? "unknown"}) · state {(sample.stateSyncMs / 1_000).toFixed(2)}s · total {(sample.totalMs / 1_000).toFixed(2)}s

); } @@ -9148,7 +9441,7 @@ function DelvewornGame() {
-
+
{rollingIcon}
@@ -9164,15 +9457,46 @@ function DelvewornGame() { {rollingText}

-
- - - - - - + {!vrfDelayed && + !canonicalSyncing && + actionProgressIndex >= 0 && ( +
+ {actionProgressOrder.map( + ( + phase, + index + ) => ( + + ) + )} +
+ )} -
+ {( + vrfDelayed || + canonicalSyncing || + actionProgressIndex < 0 + ) && ( +
+ + + +
+ )} {supportsRandomnessRetry() && vrfRetryAvailable && diff --git a/frontend/app/somnia-session-keys.ts b/frontend/app/somnia-session-keys.ts index ea137d9..22450ad 100644 --- a/frontend/app/somnia-session-keys.ts +++ b/frontend/app/somnia-session-keys.ts @@ -52,6 +52,12 @@ export type SomniaSessionTransactionBenchmark = { totalMs: number; }; +export type SomniaSessionTransactionPhase = + | "preparing" + | "sponsoring" + | "submitting" + | "inclusion"; + function benchmarkNowMs() { return globalThis.performance?.now() ?? Date.now(); } @@ -267,7 +273,8 @@ export async function createSomniaSession( export async function sendSomniaSessionTransaction( record: SomniaSessionRecord, - data: Hex + data: Hex, + onPhase?: (phase: SomniaSessionTransactionPhase) => void ) { const client = thirdwebClient(); const sessionSigner = privateKeyToAccount({ @@ -297,6 +304,8 @@ export async function sendSomniaSessionTransaction( let userOpSubmissionStartedAt: number | null = null; let userOpSubmittedAt: number | null = null; + onPhase?.("preparing"); + const benchmarkFetch: typeof globalThis.fetch = async ( input, init @@ -309,6 +318,11 @@ export async function sendSomniaSessionTransaction( userOpSubmissionStartedAt === null ) { userOpSubmissionStartedAt = requestStartedAt; + onPhase?.("submitting"); + } + + if (method === "pm_sponsorUserOperation") { + onPhase?.("sponsoring"); } if (method === "eth_getUserOperationReceipt") { @@ -356,6 +370,7 @@ export async function sendSomniaSessionTransaction( userOp: signedUserOp, options: bundlerOptions, }); + onPhase?.("inclusion"); const receipt = await waitForUserOpReceipt({ ...bundlerOptions, userOpHash, From e944e15ca4feca0f25e8bac491fb74caf17a6fd8 Mon Sep 17 00:00:00 2001 From: CryptoMickle <318943357+CryptoMickle@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:19:58 +0200 Subject: [PATCH 6/7] fix: allow Somnia VRF websocket events --- frontend/app/page.tsx | 20 ++++++++++---------- frontend/next.config.ts | 1 + 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 677012a..6a017c7 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -191,10 +191,6 @@ async function createConnectedWalletClient( const realtimeClient = createRiseShredsClient(); -const realtimeReadClient = - realtimeClient ?? - eventWebSocketClient; - /* ============================================================ ABI @@ -1926,7 +1922,7 @@ async function fetchPlayerState( const reader = source === "realtime" - ? realtimeReadClient ?? publicClient + ? realtimeClient ?? publicClient : publicClient; let snapshot: unknown; @@ -3173,14 +3169,15 @@ function DelvewornGame() { timing.stateReadCompletedAt ?? completedAt; - const stateReadStartedAt = - timing.stateReadStartedAt ?? - vrfEventReceivedAt; - const stateReadCompletedAt = timing.stateReadCompletedAt ?? completedAt; + const stateSyncStartedAt = + timing.vrfEventReceivedAt ?? + timing.stateReadStartedAt ?? + stateReadCompletedAt; + const sample: LatencyBenchmarkSample = { id: @@ -3209,7 +3206,7 @@ function DelvewornGame() { Math.max( 0, stateReadCompletedAt - - stateReadStartedAt + stateSyncStartedAt ), totalMs: Math.max( @@ -3765,6 +3762,9 @@ function DelvewornGame() { watchedAddress, }, + poll: + false, + onLogs: ( logs diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 8ecf714..075fe28 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -12,6 +12,7 @@ const contentSecurityPolicy = [ [ "connect-src 'self'", "https://dream-rpc.somnia.network", + "wss://dream-rpc.somnia.network", "https://testnet.riselabs.xyz", "wss://testnet.riselabs.xyz", "https://*.thirdweb.com", From 3d3fca6bed4bfd2bd84f2cf8b1bd417807753bbe Mon Sep 17 00:00:00 2001 From: CryptoMickle <318943357+CryptoMickle@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:22:09 +0200 Subject: [PATCH 7/7] feat: position Somnia as a verified run --- README.md | 4 +- frontend/app/page.tsx | 188 ++++++++++++++++------------ frontend/app/practice-mode-link.tsx | 8 +- frontend/app/somnia-session-keys.ts | 2 +- 4 files changed, 117 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 0317cf0..22980f2 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ The frontend uses `frontendSnapshotV3()`, `claimRelic(bool)` and `equipOwnedReli | Environment | Status | Scope | | --- | --- | --- | | RISE Testnet | Public beta | Current wallet-connected deployment and frontend integration. | -| Somnia Shannon Testnet | Verified opt-in deployment | Delveworn [`0x07c5…c292`](https://shannon-explorer.somnia.network/address/0x07c5D071132ae95C3708031790b3feC740F4c292) uses a native VRF adapter and Somnia's coordinator-funded Reactivity/drand flow. A live monster request completed with `callbackSuccess=true`; the public frontend remains on RISE unless explicitly configured otherwise. Standard MetaMask play is live, while Thirdweb ERC-4337 Instant Play is implemented behind a disabled-by-default feature flag pending sponsored-gas configuration and live QA. | +| Somnia Shannon Testnet | Verified opt-in deployment | Delveworn [`0x07c5…c292`](https://shannon-explorer.somnia.network/address/0x07c5D071132ae95C3708031790b3feC740F4c292) uses a native VRF adapter and Somnia's coordinator-funded Reactivity/drand flow. A live monster request completed with `callbackSuccess=true`; the public frontend remains on RISE unless explicitly configured otherwise. Standard MetaMask play is live, while Thirdweb ERC-4337 Popup-free Play is available behind a deployment feature flag. It removes repeated wallet approvals but still waits for bundling, block inclusion and verified randomness. | | Local Anvil | Development only | Deterministic contract, relic, balance and request/callback testing through `DevRandomnessAdapter`. | | Chainlink VRF v2.5 adapter | Implemented and test-covered | Adapter support exists, but no public deployment is presented as production-ready. | | Other EVM networks | Architecture target | The core is designed for adapter-based deployments; these networks are not yet advertised as supported public deployments. | @@ -248,7 +248,7 @@ NEXT_PUBLIC_DEPLOYMENT=somniaShannon NEXT_PUBLIC_SOMNIA_SHANNON_DUNGEON_ADDRESS=0x07c5D071132ae95C3708031790b3feC740F4c292 ``` -The optional Somnia Instant Play prototype additionally requires: +The optional Somnia Popup-free Play session additionally requires: ```text NEXT_PUBLIC_SOMNIA_SESSION_KEYS_ENABLED=true diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 6a017c7..ba82dbc 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -3255,7 +3255,7 @@ function DelvewornGame() { setSomniaSessionHandle(null); setPlayer(null); setWalletMessage( - "The Instant Play session expired. Approve a new temporary session to continue." + "The popup-free session expired. Approve a new temporary session to continue." ); }, remainingMs); @@ -4901,7 +4901,7 @@ function DelvewornGame() { ); setWalletMessage( - "The previous Instant Play session expired or was revoked. Approve a new temporary session to continue." + "The previous popup-free session expired or was revoked. Approve a new temporary session to continue." ); } } @@ -4928,7 +4928,7 @@ function DelvewornGame() { !supportsThirdwebSessionKeys() ) { throw new Error( - "Somnia Instant Play is not enabled for this deployment." + "Somnia popup-free play is not enabled for this deployment." ); } @@ -5100,7 +5100,7 @@ function DelvewornGame() { ); setWalletMessage( - "Could not enable Somnia Instant Play. The MetaMask approval, smart-account deployment or sponsored session request failed." + "Could not enable Somnia popup-free play. The MetaMask approval, smart-account deployment or sponsored session request failed." ); } finally { setSomniaSessionCreating( @@ -6128,7 +6128,7 @@ function DelvewornGame() { !hasSomniaSession ) { throw new Error( - "Somnia Instant Play session is not active." + "Somnia popup-free session is not active." ); } @@ -7099,8 +7099,10 @@ function DelvewornGame() { ? "RISE Instant Play" : somniaSessionMode && hasSomniaSession - ? "Somnia Instant Play" - : "MetaMask Standard Play", + ? "Somnia Popup-free Play" + : ACTIVE_ECOSYSTEM_NAME === "Somnia" + ? "Somnia Verified Run" + : "MetaMask Standard Play", expectedRequestKind: RequestKind.None, startedAt: @@ -8053,20 +8055,26 @@ function DelvewornGame() {
VIEW CONTRACT · {DUNGEON_ADDRESS.slice(0, 6)}…{DUNGEON_ADDRESS.slice(-4)} ↗ @@ -8089,7 +8097,7 @@ function DelvewornGame() { > {activeInstantPlayProvider() === "rise-wallet" ? "⚡ RISE WALLET · INSTANT PLAY" - : "⚡ METAMASK · INSTANT PLAY"} + : "🔑 METAMASK · POPUP-FREE PLAY"} )}
@@ -8168,7 +8178,9 @@ function DelvewornGame() {

- Instant Play + {supportsThirdwebSessionKeys() + ? "Popup-free Play" + : "Instant Play"}

@@ -8217,7 +8229,9 @@ function DelvewornGame() { > {grantPermissions.isPending || somniaSessionCreating ? "CREATING SESSION..." - : "🔑 ENABLE INSTANT PLAY"} + : supportsThirdwebSessionKeys() + ? "🔑 ENABLE POPUP-FREE PLAY" + : "🔑 ENABLE INSTANT PLAY"} {walletMessage && ( @@ -8792,7 +8806,9 @@ function DelvewornGame() {

- 🔑 INSTANT PLAY ACTIVE + {hasSomniaSession + ? "🔑 POPUP-FREE VERIFIED PLAY ACTIVE" + : "🔑 INSTANT PLAY ACTIVE"} - )} -
+ {latencySamples.length > + 0 && ( + + )} +
- {latencySamples.length === - 0 ? ( -

- Run Attack, Storm, Potion or enter a room to record the first sample. -

- ) : ( - <> -
+ {latencySamples.length === + 0 ? ( +

+ Run Attack, Storm, Potion or enter a room to record the first sample. +

+ ) : ( + <> +
-
- -
- - {latencySamples[0].mode} · {latencySamples[0].action} · show history - -
- {latencySamples.map( - (sample) => { - const smart = - sample.smartAccount; - - return ( -

- {sample.mode} · {sample.action}: {smart - ? `prep ${(smart.preparationMs / 1_000).toFixed(2)}s (estimate ${(smart.gasEstimationMs / 1_000).toFixed(2)}s) · paymaster ${(smart.paymasterMs / 1_000).toFixed(2)}s · bundler ${(smart.bundlerSubmissionMs / 1_000).toFixed(2)}s · inclusion ${(smart.inclusionWaitMs / 1_000).toFixed(2)}s (${smart.receiptPollCount} polls @ ${smart.receiptPollingIntervalMs}ms) · ` - : `submit ${(sample.submissionMs / 1_000).toFixed(2)}s · `}VRF {(sample.vrfMs / 1_000).toFixed(2)}s ({sample.vrfSource ?? "unknown"}) · state {(sample.stateSyncMs / 1_000).toFixed(2)}s · total {(sample.totalMs / 1_000).toFixed(2)}s -

- ); - } - )}
-
- - )} - + +
+ + {latencySamples[0].mode} · {latencySamples[0].action} · show history + +
+ {latencySamples.map( + (sample) => { + const smart = + sample.smartAccount; + + return ( +

+ {sample.mode} · {sample.action}: {smart + ? `prep ${(smart.preparationMs / 1_000).toFixed(2)}s (estimate ${(smart.gasEstimationMs / 1_000).toFixed(2)}s) · paymaster ${(smart.paymasterMs / 1_000).toFixed(2)}s · bundler ${(smart.bundlerSubmissionMs / 1_000).toFixed(2)}s · inclusion ${(smart.inclusionWaitMs / 1_000).toFixed(2)}s (${smart.receiptPollCount} polls @ ${smart.receiptPollingIntervalMs}ms) · ` + : `submit ${(sample.submissionMs / 1_000).toFixed(2)}s · `}VRF {(sample.vrfMs / 1_000).toFixed(2)}s ({sample.vrfSource ?? "unknown"}) · state {(sample.stateSyncMs / 1_000).toFixed(2)}s · total {(sample.totalMs / 1_000).toFixed(2)}s +

+ ); + } + )} +
+
+ + )} +
+ )} {/* =================================================== @@ -8992,8 +9016,10 @@ function DelvewornGame() { {!player.hasStarted ? ( VIEW CONTRACT · {DUNGEON_ADDRESS.slice(0, 6)}…{DUNGEON_ADDRESS.slice(-4)} ↗ @@ -9012,7 +9038,9 @@ function DelvewornGame() { disabled={busy} className="delveworn-primary-cta mt-7 w-full rounded-xl py-4 text-lg font-black transition disabled:opacity-50" > - ⚔️ START ONCHAIN RUN + {ACTIVE_ECOSYSTEM_NAME === "Somnia" + ? "⛓️ START VERIFIED RUN" + : "⚔️ START ONCHAIN RUN"}

Gameplay actions use your selected wallet mode.

@@ -10359,7 +10387,9 @@ function DelvewornGame() {

V8.6.9b · {runtimeFooterLabel()} · {supportsInstantPlay() - ? `${activeInstantPlayProvider() === "rise-wallet" ? "RISE" : "Somnia"} Instant Play · ` + ? activeInstantPlayProvider() === "rise-wallet" + ? "RISE Instant Play · " + : "Somnia Popup-free Play · " : ""}MetaMask Standard Play

diff --git a/frontend/app/practice-mode-link.tsx b/frontend/app/practice-mode-link.tsx index 696baa6..2048b0a 100644 --- a/frontend/app/practice-mode-link.tsx +++ b/frontend/app/practice-mode-link.tsx @@ -6,14 +6,16 @@ import { usePathname } from "next/navigation"; export default function PracticeModeLink() { const pathname = usePathname(); - if (pathname === "/practice") return null; + const isPractice = pathname === "/practice"; return ( - Practice · No VRF + {isPractice + ? "Verified Run · Onchain" + : "Play now · Instant local"} ); } diff --git a/frontend/app/somnia-session-keys.ts b/frontend/app/somnia-session-keys.ts index 22450ad..71a0eda 100644 --- a/frontend/app/somnia-session-keys.ts +++ b/frontend/app/somnia-session-keys.ts @@ -105,7 +105,7 @@ function thirdwebClient() { if (!clientId) { throw new Error( - "Somnia Instant Play requires NEXT_PUBLIC_THIRDWEB_CLIENT_ID." + "Somnia Popup-free Play requires NEXT_PUBLIC_THIRDWEB_CLIENT_ID." ); }