feat(frontend): network selector for Studio / Bradbury targets#1606
feat(frontend): network selector for Studio / Bradbury targets#1606MuncleUscles merged 14 commits intomainfrom
Conversation
Adds a header dropdown that lets the hosted Studio frontend target either the local Studio backend or the Bradbury testnet at runtime, instead of being pinned to a single backend at build time. Contract deploy, read, write, simulate, and attach all work on both targets. Foundations - `networkStore` (Pinia, persisted): `currentNetwork`, `chain`, `rpcUrl`, `wsUrl`, `chainId`, `isStudio`, `availableNetworks`, `isLocked`. - Reactive RPC URL: `rpc.ts` resolves from the store per call; on non-Studio networks it skips the WS session header entirely. - `useGenlayer` watches `currentNetwork`/`rpcUrl` and re-inits the client. - WebSocket singleton gains teardown + URL-change reconnect; non-Studio returns a no-op stub (testnets have no push events). - `useAppKit` registers Bradbury alongside the Studio localnet so MetaMask can switch without a manual add step. - `useChainEnforcer` reads chain info from the network store and accepts an explicit target override (used by the selector on switch). Data scoping (Dexie v5) - `deployedContracts` and `transactions` gain a `chainId` column; existing rows backfill to the build-time `VITE_CHAIN_ID`. - Stores expose a filtered `deployedContracts` / `transactions` computed scoped to the current chain; internal flat state stays accessible via `allDeployedContracts` / `allTransactions` + setter actions. - Pending transactions are no longer silently dropped on network switch — they stay in storage scoped to their original chain. Graceful degradation on non-Studio networks - `useConfig` adds `isStudioNetwork`; validator / provider / finality-window gates now require both `!hosted` AND `isStudioNetwork`. - Node Logs pane hides on testnet (Splitpanes collapse to contracts-only). - `nodeStore.getValidatorsData` / `getProvidersData` and `consensusStore.fetchFinalityWindowTime` bail early on testnet. - `useSetupStores` skips the WS wait and sim_* warmup when non-Studio. - Faucet button swaps to an external link to https://testnet-faucet.genlayer.foundation on testnet. Also bumps `genlayer-js` to ^1.1.0 (picks up the SDK patch that hides the `gen_getContractSchema` semantic divergence between Studio and the node — see docs/plans/studio-testnet-format-alignment.md § Schema method divergence). Full plan in docs/plans/studio-network-selector.md.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a persisted Pinia Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant NetworkSelector
participant NetworkStore
participant Wallet
participant WebSocketClient
participant RpcClient
participant GenlayerClient
participant DexieDB
User->>NetworkSelector: choose network
NetworkSelector->>NetworkStore: setCurrentNetwork(name)
NetworkStore-->>NetworkSelector: currentNetwork updated
NetworkSelector->>Wallet: ensureCorrectChain(target)
alt Wallet accepts
Wallet-->>NetworkSelector: success
NetworkStore->>WebSocketClient: ensureWebSocketClient(wsUrl)
WebSocketClient-->>WebSocketClient: dispose/create as needed
NetworkStore->>RpcClient: rpcUrl resolves per-request
NetworkStore->>GenlayerClient: watcher triggers re-init
GenlayerClient-->>NetworkStore: client reinitialized
NetworkStore->>DexieDB: computed views filter by chainId
NetworkSelector-->>User: success notification
else Wallet rejects (error 4001)
Wallet-->>NetworkSelector: rejection
NetworkSelector->>NetworkStore: revert to previous network
NetworkSelector-->>User: show canceled notification
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
frontend/src/stores/node.ts (2)
77-106:⚠️ Potential issue | 🟡 MinorReset non-Studio loading state reactively.
These guards only run when callers invoke the fetch methods. Since setup now skips Studio-only initialization on non-Studio networks,
isLoadingValidatorData/isLoadingProviderscan remain at their initialtruevalues after opening directly on Bradbury.🧹 Suggested reset watcher
+ watch( + () => networkStore.isStudio, + (isStudio) => { + if (!isStudio) { + validators.value = []; + nodeProviders.value = []; + isLoadingValidatorData.value = false; + isLoadingProviders.value = false; + } + }, + { immediate: true }, + ); + async function getValidatorsData() {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/stores/node.ts` around lines 77 - 106, The loading flags remain true when the app skips Studio-only init on non-Studio networks; add a reactive watcher that observes networkStore.isStudio (or use watchEffect) and when it becomes false set isLoadingValidatorData.value = false and isLoadingProviders.value = false to ensure both loaders are reset; update the store near the existing getValidatorsData/getProvidersData functions to include this watcher so the flags are cleared whenever networkStore.isStudio is false.
43-66:⚠️ Potential issue | 🟠 MajorRebind node log listeners when the WebSocket endpoint changes.
webSocketClientis captured once, but runtime network switching can dispose/recreate the singleton. After Studio → non-Studio → Studio, this store may stay subscribed to the stale client and miss logs from the new connection.🔁 Suggested direction
-import { computed, ref } from 'vue'; +import { computed, ref, watch } from 'vue'; - const webSocketClient = useWebSocketClient(); + let webSocketClient = useWebSocketClient(); - trackedEvents.forEach((eventName) => { - webSocketClient.on(eventName, (eventData: any) => { - let role: 'leader' | 'validator' | undefined; - if ( - eventData.name === 'execution_finished' && - eventData.transaction_hash - ) { - if (!leaderTxHashes.has(eventData.transaction_hash)) { - leaderTxHashes.add(eventData.transaction_hash); - role = 'leader'; - } else { - role = 'validator'; - } - } - addLog({ - scope: eventData.scope, - name: eventData.name, - type: eventData.type, - message: eventData.message, - data: eventData.data, - role, - }); - }); - }); + const handleTrackedEvent = (eventData: any) => { + let role: 'leader' | 'validator' | undefined; + if ( + eventData.name === 'execution_finished' && + eventData.transaction_hash + ) { + if (!leaderTxHashes.has(eventData.transaction_hash)) { + leaderTxHashes.add(eventData.transaction_hash); + role = 'leader'; + } else { + role = 'validator'; + } + } + addLog({ + scope: eventData.scope, + name: eventData.name, + type: eventData.type, + message: eventData.message, + data: eventData.data, + role, + }); + }; + + const subscribeTrackedEvents = () => { + trackedEvents.forEach((eventName) => { + webSocketClient.on(eventName, handleTrackedEvent); + }); + }; + + const unsubscribeTrackedEvents = () => { + trackedEvents.forEach((eventName) => { + webSocketClient.off(eventName, handleTrackedEvent); + }); + }; + + subscribeTrackedEvents(); + + watch( + () => networkStore.wsUrl, + () => { + unsubscribeTrackedEvents(); + webSocketClient = useWebSocketClient(); + subscribeTrackedEvents(); + }, + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/stores/node.ts` around lines 43 - 66, The current listener loop captures the webSocketClient once (trackedEvents, webSocketClient, leaderTxHashes, addLog) and never rebinds when the WebSocket instance is replaced, causing missed logs after network switches; modify the store to detect changes to webSocketClient (or expose a subscribe/ready event) and on change remove the old listeners (use the client's off/removeListener API) and re-register the trackedEvents handlers on the new webSocketClient, ensuring leaderTxHashes is preserved or reset appropriately and cleaning up listeners on teardown to avoid duplicates.frontend/src/hooks/useWebSocketClient.ts (2)
300-313:⚠️ Potential issue | 🟠 MajorResolve the async hook for no-op WebSocket clients.
When
useWebSocketClient()returnsnullClient,client.on('connect')is a no-op, so this promise never resolves. Return immediately for the stub and unregister the one-shot connect handler after it fires.Suggested fix
export async function useWebSocketClientAsync( url?: string, ): Promise<NativeWebSocketClient> { const client = useWebSocketClient(url); - if (client.connected) { + if (client === nullClient || client.connected) { return client; } return new Promise<NativeWebSocketClient>((resolve) => { - client.on('connect', () => { + const handleConnect = () => { + client.off('connect', handleConnect); resolve(client); - }); + }; + client.on('connect', handleConnect); }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/hooks/useWebSocketClient.ts` around lines 300 - 313, The async hook useWebSocketClientAsync can hang when useWebSocketClient() returns the no-op/null client because client.on('connect') does nothing; fix it by detecting the stub client (e.g., compare to the known nullClient or a falsy/noop flag on the returned NativeWebSocketClient) and return it immediately instead of registering a listener, and when you do register a one-shot connect handler use a self-removing callback (call client.off('connect', handler) inside the handler) so the promise resolves exactly once and you don't leak listeners; keep the existing early-return when client.connected is true.
267-282:⚠️ Potential issue | 🟠 MajorKeep the WebSocket client facade stable across URL changes.
Replacing the singleton breaks existing subscribers that captured the old object once, such as
frontend/src/stores/connectionStatus.tsandfrontend/src/hooks/useContractListener.ts. After switching away and back, their handlers remain on the disconnected instance and won’t receive events from the recreated socket. Reconnect the existing client in place or expose a stable facade that delegates to the active socket.Suggested direction
- if (webSocketClient && currentUrl && currentUrl !== resolvedUrl) { - disposeWebSocketClient(); - } + if (webSocketClient && currentUrl && currentUrl !== resolvedUrl) { + currentUrl = resolvedUrl; + webSocketClient.reconnectTo(resolvedUrl); + return webSocketClient; + }// Add on NativeWebSocketClient; keep eventHandlers on the same instance. public reconnectTo(url: string) { this.shouldReconnect = false; this.stopPingInterval(); if (this.ws) { this.ws.close(); this.ws = null; } this.url = normalizeWebSocketUrl(url); this.id = null; this.connected = false; this.reconnectAttempts = 0; this.reconnectTimeout = 1000; this.shouldReconnect = true; this.connect(); }Also applies to: 321-347
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/hooks/useWebSocketClient.ts` around lines 267 - 282, The current logic tears down and recreates the webSocketClient singleton (disposeWebSocketClient + new NativeWebSocketClient(resolvedUrl)), which breaks subscribers that hold references to the original instance; instead add a stable facade method on NativeWebSocketClient (e.g., reconnectTo(url: string)) that resets internal ws, url, id, connected, reconnectAttempts, reconnectTimeout, ping interval, and then calls connect(), and update the URL-change branch to call webSocketClient.reconnectTo(resolvedUrl) rather than disposing and replacing the object so handlers attached to webSocketClient remain active; also replace other recreate sites (the other block mentioned) to use reconnectTo and preserve isInitializing/currentUrl semantics.
🧹 Nitpick comments (6)
frontend/test/unit/stores/contracts.behavioral.test.ts (1)
68-81: Consider asserting againstnetworkStore.chainIdexplicitly.
toBeTypeOf('number')confirms the field is stamped but doesn't verify it matches the current network. SinceaddDeployedContractuseschainId ?? networkStore.chainId, an equality check againstuseNetworkStore().chainIdwould catch regressions where a wrong/stale chainId is stamped.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/test/unit/stores/contracts.behavioral.test.ts` around lines 68 - 81, The test currently only asserts the stamped chainId is a number; update it to assert the stamped value equals the current network chainId by comparing the found deployed contract's chainId to useNetworkStore().chainId. Locate the test case that calls useContractsStore(), addDeployedContract(...) and reads store.deployedContracts (the 'deployed' variable) and replace the toBeTypeOf('number') assertion on (deployed as any).chainId with an equality assertion against useNetworkStore().chainId to ensure the contract is stamped with the actual current network chainId.frontend/src/components/Tutorial/TutorialContainer.vue (1)
17-119:stepsis not reactive tocanUpdateValidatorschanges.
stepsis initialized once with a literal.valuesnapshot, so ifnetworkStore.currentNetworkchanges after this component mounts (now possible at runtime via the new selector), the Validators step text won't update until the component remounts. Consider makingstepsacomputed, or readingcanUpdateValidators.valuelazily inside a getter/template binding onTutorialStep.♻️ Suggested refactor
-const steps = ref([ +const steps = computed(() => [ ... { header: { title: 'Validators' }, target: '#tutorial-validators', content: canUpdateValidators.value ? 'Configure the number of validators and set up their parameters here.' : 'Here you can see the validators that are currently running on the network.', placement: 'right', }, ... -]); +]);Note: downstream mutations of
steps.value[...]would need to be reviewed, but I don't see any in this file.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/Tutorial/TutorialContainer.vue` around lines 17 - 119, The steps array is created once and captures a snapshot of canUpdateValidators, so the Validators step won't update when networkStore.currentNetwork changes; change the implementation in TutorialContainer.vue to make steps reactive—either convert steps (currently a ref named steps) into a computed(() => [...]) that builds the array and reads canUpdateValidators.value when producing the Validators step content, or keep steps as-is but replace the Validators step's content with a lazy getter/computed that returns canUpdateValidators.value ? 'Configure the number of validators...' : 'Here you can see the validators...' so the UI updates without remounting.frontend/src/components/Simulator/AccountSelect.vue (1)
17-17: Consider sourcing the faucet URL from the network/chain config rather than hard-coding.As more non-Studio networks are added, hard-coding
PUBLIC_TESTNET_FAUCET_URLand a Bradbury-specific tooltip ("100 GEN / 24h, requires 0.01 ETH on mainnet") on thev-elsebranch will force this component to be edited each time. Moving the faucet URL (and ideally the tooltip copy) onto the chain definition consumed vianetworkStorewould keep this component network-agnostic and consistent with the rest of the runtime-config approach in this PR.Also applies to: 221-230
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/Simulator/AccountSelect.vue` at line 17, The component hard-codes PUBLIC_TESTNET_FAUCET_URL and a Bradbury-specific tooltip in AccountSelect.vue; instead read the faucet URL (and optional tooltip/copy) from the network/chain config exposed by networkStore so the component is network-agnostic. Replace uses of PUBLIC_TESTNET_FAUCET_URL with a value like networkStore.currentChain.faucetUrl (or similar field you add to the chain definition), and pull tooltip text from networkStore.currentChain.faucetTooltip (or a default fallback) for the v-else branch; update the chain definitions to include these properties so no component edits are required when adding networks.frontend/test/unit/hooks/useSetupStores.behavioral.test.ts (1)
225-238: ResetisStudioin afinallyblock (orafterEach) to avoid cross-test pollution.If any assertion above throws,
mockNetworkStore.isStudio = truenever runs and subsequent tests inheritisStudio = false, which could mask regressions in the Studio-path tests.🔧 Proposed fix
it('skips sim_* calls when the current network is non-Studio', async () => { mockNetworkStore.isStudio = false; - - const { useSetupStores } = await import('@/hooks/useSetupStores'); - const { setupStores } = useSetupStores(); - await setupStores(); - - expect(mockNodeStore.getValidatorsData).not.toHaveBeenCalled(); - expect(mockNodeStore.getProvidersData).not.toHaveBeenCalled(); - expect(mockConsensusStore.fetchFinalityWindowTime).not.toHaveBeenCalled(); - - // Reset for other tests. - mockNetworkStore.isStudio = true; + try { + const { useSetupStores } = await import('@/hooks/useSetupStores'); + const { setupStores } = useSetupStores(); + await setupStores(); + + expect(mockNodeStore.getValidatorsData).not.toHaveBeenCalled(); + expect(mockNodeStore.getProvidersData).not.toHaveBeenCalled(); + expect(mockConsensusStore.fetchFinalityWindowTime).not.toHaveBeenCalled(); + } finally { + mockNetworkStore.isStudio = true; + } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/test/unit/hooks/useSetupStores.behavioral.test.ts` around lines 225 - 238, The test mutates mockNetworkStore.isStudio at the end of the test but does not guarantee reset if an assertion throws; update the test to restore mockNetworkStore.isStudio in a finally block (or move the reset into an afterEach) so the Studio flag is always reset, e.g., wrap the import/useSetupStores and setupStores invocation in try { ... } finally { mockNetworkStore.isStudio = true }, referencing mockNetworkStore.isStudio and the useSetupStores()/setupStores() call to locate the change.frontend/test/unit/services/RpcClient.behavioral.test.ts (1)
100-115: Assert the hook is not called on non-Studio networks.
mockWsClient.ononly proves no listener was registered. To protect the “skip WS entirely” contract, also assertuseWebSocketClientitself was not invoked.🧪 Suggested assertion
it('on non-Studio: skips WS entirely and omits the session header', async () => { mockNetworkStore.isStudio = false; vi.resetModules(); + const hooks = await import('@/hooks'); const { RpcClient } = await import('@/clients/rpc'); const client = new RpcClient(); await client.call({ method: 'ping', params: [] }); // No WS interactions at all + expect(hooks.useWebSocketClient).not.toHaveBeenCalled(); expect(mockWsClient.on).not.toHaveBeenCalled();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/test/unit/services/RpcClient.behavioral.test.ts` around lines 100 - 115, The test currently only checks that mockWsClient.on wasn't called but must also assert that useWebSocketClient was not invoked to guarantee WebSocket is skipped; update the test in RpcClient.behavioral.test.ts (the "on non-Studio: skips WS entirely..." case) to add an expectation that the useWebSocketClient mock/function was not called (alongside the existing expect(mockWsClient.on).not.toHaveBeenCalled()) so the test verifies the RpcClient code path never obtains a WebSocket client via useWebSocketClient.frontend/src/components/global/NetworkSelector.vue (1)
33-62: Switch flow briefly shows mixed state; consider ordering chain enforcement before committing the store.
setCurrentNetwork(name)is called beforeensureCorrectChain(). This immediately updatesnetworkStore.chain/rpcUrl, causing theuseGenlayerwatcher to re-init the client anduseTransactionsStore'swatch(networkStore.chainId)to clear WS subscriptions — all before the wallet confirms the chain switch. If the user rejects (4001), yousetCurrentNetwork(previous)which triggers another round of re-init/resubscribe.Two minor consequences worth considering:
- UX flicker: the header label and Dexie-scoped transactions list momentarily switch, then snap back.
- An in-flight RPC issued against the new chain during this window can fail unexpectedly.
Passing an explicit
targettoensureCorrectChain(...)(the hook already supports it) and only callingsetCurrentNetworkonce the wallet switch resolves would avoid the double-transition.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/global/NetworkSelector.vue` around lines 33 - 62, The switchNetwork function updates networkStore (via networkStore.setCurrentNetwork(name)) before ensuring the external wallet actually switched, causing transient store/WS re-inits; change the flow to call ensureCorrectChain with an explicit target (use the existing ensureCorrectChain(target: NetworkName) support) and await that first when isExternalWallet.value is true, and only call networkStore.setCurrentNetwork(name) after ensureCorrectChain resolves successfully; keep isSwitching state, previous rollback and notifications as-is (use networkStore.chainName for success text after setting the store) and ensure you still revert to previous on any thrown error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/plans/studio-network-selector.md`:
- Line 99: The documentation is inconsistent about which network is used to
backfill legacy Dexie rows (one place says configured startup network, another
says `localnet`); update the backfill rule so both descriptions state the same
behavior: legacy rows are backfilled to the configured startup network's chain
ID (use `VITE_GENLAYER_NETWORK` / `VITE_CHAIN_ID`), and adjust any references in
the text that mention `localnet` accordingly; ensure the note about getters
(`deployedContracts`, `transactions`) still mentions they filter by
`networkStore.chainId` so the upgrade behavior is unambiguous.
In `@frontend/src/clients/rpc.ts`:
- Around line 21-34: The current Studio WebSocket gate (guarded by
networkStore.isStudio and using
useWebSocketClient()/webSocketClient.on('connect')) can hang indefinitely;
change the Promise so it races the 'connect' handler against a bounded timeout
(e.g., 3–10s). If the timeout fires, remove the 'connect' listener, either
reject the Promise so the caller can handle the error or resolve and continue
without a session id (headers['x-session-id'] = ''), and clear the timer if
'connect' wins; ensure you handle webSocketClient.connected immediately as
before and always cleanup listeners/timeouts to avoid leaks.
In `@frontend/src/components/global/NetworkSelector.vue`:
- Around line 103-109: The warning banner is currently gated by
!networkStore.isStudio so users on Studio won't see the "pending transactions
will be hidden after switching" message; update NetworkSelector.vue to show the
banner for any cross-network switch by removing the !networkStore.isStudio
condition and use only pendingOnCurrentNetwork > 0 (i.e., change
v-if="!networkStore.isStudio && pendingOnCurrentNetwork > 0" to
v-if="pendingOnCurrentNetwork > 0"), keeping the existing text and CSS classes
intact.
In `@frontend/src/stores/network.ts`:
- Around line 89-118: availableNetworks currently exposes deployment-pinned
networks as normal switch targets; modify the computed to mark such entries as
non-selectable (add a selectable: boolean field) or, alternatively, when
currentNetwork is not in SELECTABLE_NETWORKS return only the selectable list
plus a single pinned entry that has selectable: false so UI dropdowns can
disable switching to/from it; update the code that constructs items (referencing
availableNetworks, SELECTABLE_NETWORKS, currentNetwork, and CHAINS) to set
selectable: true for regular SELECTABLE_NETWORKS entries and selectable: false
for the pinned/current out-of-list entry.
- Around line 36-44: readInitialNetwork currently always prefers localStorage
and calls localStorage.getItem without protection, allowing a deployment with
VITE_LOCK_NETWORK=true to still boot a stale persisted network; fix by 1)
wrapping all localStorage reads/writes in try/catch like the write path to avoid
exceptions in restricted storage contexts, 2) when VITE_LOCK_NETWORK (or config
key checked at runtime) is true explicitly bypass any
hydration/read-from-localStorage in readInitialNetwork and return the
environment-configured network (from getRuntimeConfig('VITE_GENLAYER_NETWORK')),
and 3) move persistence of currentNetwork out of manual getItem/setItem into
pinia-plugin-persistedstate and ensure the plugin is configured to skip
hydration when the lock flag is set so the persisted value is ignored when
locked (referencing readInitialNetwork and the currentNetwork/pinia-persist
configuration).
---
Outside diff comments:
In `@frontend/src/hooks/useWebSocketClient.ts`:
- Around line 300-313: The async hook useWebSocketClientAsync can hang when
useWebSocketClient() returns the no-op/null client because client.on('connect')
does nothing; fix it by detecting the stub client (e.g., compare to the known
nullClient or a falsy/noop flag on the returned NativeWebSocketClient) and
return it immediately instead of registering a listener, and when you do
register a one-shot connect handler use a self-removing callback (call
client.off('connect', handler) inside the handler) so the promise resolves
exactly once and you don't leak listeners; keep the existing early-return when
client.connected is true.
- Around line 267-282: The current logic tears down and recreates the
webSocketClient singleton (disposeWebSocketClient + new
NativeWebSocketClient(resolvedUrl)), which breaks subscribers that hold
references to the original instance; instead add a stable facade method on
NativeWebSocketClient (e.g., reconnectTo(url: string)) that resets internal ws,
url, id, connected, reconnectAttempts, reconnectTimeout, ping interval, and then
calls connect(), and update the URL-change branch to call
webSocketClient.reconnectTo(resolvedUrl) rather than disposing and replacing the
object so handlers attached to webSocketClient remain active; also replace other
recreate sites (the other block mentioned) to use reconnectTo and preserve
isInitializing/currentUrl semantics.
In `@frontend/src/stores/node.ts`:
- Around line 77-106: The loading flags remain true when the app skips
Studio-only init on non-Studio networks; add a reactive watcher that observes
networkStore.isStudio (or use watchEffect) and when it becomes false set
isLoadingValidatorData.value = false and isLoadingProviders.value = false to
ensure both loaders are reset; update the store near the existing
getValidatorsData/getProvidersData functions to include this watcher so the
flags are cleared whenever networkStore.isStudio is false.
- Around line 43-66: The current listener loop captures the webSocketClient once
(trackedEvents, webSocketClient, leaderTxHashes, addLog) and never rebinds when
the WebSocket instance is replaced, causing missed logs after network switches;
modify the store to detect changes to webSocketClient (or expose a
subscribe/ready event) and on change remove the old listeners (use the client's
off/removeListener API) and re-register the trackedEvents handlers on the new
webSocketClient, ensuring leaderTxHashes is preserved or reset appropriately and
cleaning up listeners on teardown to avoid duplicates.
---
Nitpick comments:
In `@frontend/src/components/global/NetworkSelector.vue`:
- Around line 33-62: The switchNetwork function updates networkStore (via
networkStore.setCurrentNetwork(name)) before ensuring the external wallet
actually switched, causing transient store/WS re-inits; change the flow to call
ensureCorrectChain with an explicit target (use the existing
ensureCorrectChain(target: NetworkName) support) and await that first when
isExternalWallet.value is true, and only call
networkStore.setCurrentNetwork(name) after ensureCorrectChain resolves
successfully; keep isSwitching state, previous rollback and notifications as-is
(use networkStore.chainName for success text after setting the store) and ensure
you still revert to previous on any thrown error.
In `@frontend/src/components/Simulator/AccountSelect.vue`:
- Line 17: The component hard-codes PUBLIC_TESTNET_FAUCET_URL and a
Bradbury-specific tooltip in AccountSelect.vue; instead read the faucet URL (and
optional tooltip/copy) from the network/chain config exposed by networkStore so
the component is network-agnostic. Replace uses of PUBLIC_TESTNET_FAUCET_URL
with a value like networkStore.currentChain.faucetUrl (or similar field you add
to the chain definition), and pull tooltip text from
networkStore.currentChain.faucetTooltip (or a default fallback) for the v-else
branch; update the chain definitions to include these properties so no component
edits are required when adding networks.
In `@frontend/src/components/Tutorial/TutorialContainer.vue`:
- Around line 17-119: The steps array is created once and captures a snapshot of
canUpdateValidators, so the Validators step won't update when
networkStore.currentNetwork changes; change the implementation in
TutorialContainer.vue to make steps reactive—either convert steps (currently a
ref named steps) into a computed(() => [...]) that builds the array and reads
canUpdateValidators.value when producing the Validators step content, or keep
steps as-is but replace the Validators step's content with a lazy
getter/computed that returns canUpdateValidators.value ? 'Configure the number
of validators...' : 'Here you can see the validators...' so the UI updates
without remounting.
In `@frontend/test/unit/hooks/useSetupStores.behavioral.test.ts`:
- Around line 225-238: The test mutates mockNetworkStore.isStudio at the end of
the test but does not guarantee reset if an assertion throws; update the test to
restore mockNetworkStore.isStudio in a finally block (or move the reset into an
afterEach) so the Studio flag is always reset, e.g., wrap the
import/useSetupStores and setupStores invocation in try { ... } finally {
mockNetworkStore.isStudio = true }, referencing mockNetworkStore.isStudio and
the useSetupStores()/setupStores() call to locate the change.
In `@frontend/test/unit/services/RpcClient.behavioral.test.ts`:
- Around line 100-115: The test currently only checks that mockWsClient.on
wasn't called but must also assert that useWebSocketClient was not invoked to
guarantee WebSocket is skipped; update the test in RpcClient.behavioral.test.ts
(the "on non-Studio: skips WS entirely..." case) to add an expectation that the
useWebSocketClient mock/function was not called (alongside the existing
expect(mockWsClient.on).not.toHaveBeenCalled()) so the test verifies the
RpcClient code path never obtains a WebSocket client via useWebSocketClient.
In `@frontend/test/unit/stores/contracts.behavioral.test.ts`:
- Around line 68-81: The test currently only asserts the stamped chainId is a
number; update it to assert the stamped value equals the current network chainId
by comparing the found deployed contract's chainId to useNetworkStore().chainId.
Locate the test case that calls useContractsStore(), addDeployedContract(...)
and reads store.deployedContracts (the 'deployed' variable) and replace the
toBeTypeOf('number') assertion on (deployed as any).chainId with an equality
assertion against useNetworkStore().chainId to ensure the contract is stamped
with the actual current network chainId.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3a48b2b4-d6e7-4c46-8378-2aeaebdb34c1
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (35)
docs/plans/studio-network-selector.mddocs/plans/studio-testnet-format-alignment.mdfrontend/package.jsonfrontend/src/clients/rpc.tsfrontend/src/components/Header.vuefrontend/src/components/Simulator/AccountSelect.vuefrontend/src/components/Tutorial/TutorialContainer.vuefrontend/src/components/global/NetworkSelector.vuefrontend/src/hooks/useAppKit.tsfrontend/src/hooks/useChainEnforcer.tsfrontend/src/hooks/useConfig.tsfrontend/src/hooks/useDb.tsfrontend/src/hooks/useGenlayer.tsfrontend/src/hooks/useNetworks.tsfrontend/src/hooks/useSetupStores.tsfrontend/src/hooks/useWebSocketClient.tsfrontend/src/stores/consensus.tsfrontend/src/stores/contracts.tsfrontend/src/stores/index.tsfrontend/src/stores/network.tsfrontend/src/stores/node.tsfrontend/src/stores/transactions.tsfrontend/src/types/store.tsfrontend/src/utils/runtimeConfig.tsfrontend/src/views/Simulator/SimulatorView.vuefrontend/test/unit/hooks/useConfig.test.tsfrontend/test/unit/hooks/useGenlayer.test.tsfrontend/test/unit/hooks/useSetupStores.behavioral.test.tsfrontend/test/unit/hooks/useWebSocketClient.test.tsfrontend/test/unit/services/RpcClient.behavioral.test.tsfrontend/test/unit/stores/consensus.behavioral.test.tsfrontend/test/unit/stores/contracts.behavioral.test.tsfrontend/test/unit/stores/contracts.test.tsfrontend/test/unit/stores/transactions.behavioral.test.tsfrontend/test/unit/stores/transactions.test.ts
| deployedContracts: '++id, contractId, [chainId+address]' | ||
| transactions: '++id, type, statusName, chainId, [chainId+hash], [chainId+contractAddress], [chainId+localContractId]' | ||
| ``` | ||
| Backfill existing rows with the current `VITE_GENLAYER_NETWORK`'s chain ID on upgrade. Getters (`deployedContracts`, `transactions`) filter by `networkStore.chainId` before returning. |
There was a problem hiding this comment.
Make the Dexie backfill target consistent.
Line 99 says legacy rows are backfilled to the configured startup network’s chain ID, but Line 203 says they are backfilled to localnet. Pick one rule here—likely the configured VITE_GENLAYER_NETWORK / VITE_CHAIN_ID value—so upgrade behavior is unambiguous.
Also applies to: 203-203
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/plans/studio-network-selector.md` at line 99, The documentation is
inconsistent about which network is used to backfill legacy Dexie rows (one
place says configured startup network, another says `localnet`); update the
backfill rule so both descriptions state the same behavior: legacy rows are
backfilled to the configured startup network's chain ID (use
`VITE_GENLAYER_NETWORK` / `VITE_CHAIN_ID`), and adjust any references in the
text that mention `localnet` accordingly; ensure the note about getters
(`deployedContracts`, `transactions`) still mentions they filter by
`networkStore.chainId` so the upgrade behavior is unambiguous.
| // WebSocket session ID is only meaningful on Studio (no WS on testnets). | ||
| if (networkStore.isStudio) { | ||
| const webSocketClient = useWebSocketClient(); | ||
| await new Promise<void>((resolve) => { | ||
| if (webSocketClient.connected) { | ||
| resolve(); | ||
| }); | ||
| } | ||
| }); | ||
| } else { | ||
| webSocketClient.on('connect', () => { | ||
| resolve(); | ||
| }); | ||
| } | ||
| }); | ||
| headers['x-session-id'] = webSocketClient.id ?? ''; | ||
| } |
There was a problem hiding this comment.
Add a timeout for the Studio WebSocket gate.
If the Studio WS URL is misconfigured or never emits connect, every JSON-RPC call waits forever and deploy/read/write flows freeze. Reject or fall back after a bounded timeout.
⏱️ Suggested bounded wait
if (networkStore.isStudio) {
const webSocketClient = useWebSocketClient();
await new Promise<void>((resolve) => {
if (webSocketClient.connected) {
resolve();
} else {
- webSocketClient.on('connect', () => {
+ const timeout = window.setTimeout(() => {
+ webSocketClient.off('connect', onConnect);
+ resolve();
+ }, 10_000);
+ const onConnect = () => {
+ window.clearTimeout(timeout);
+ webSocketClient.off('connect', onConnect);
resolve();
- });
+ };
+ webSocketClient.on('connect', onConnect);
}
});
headers['x-session-id'] = webSocketClient.id ?? '';
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // WebSocket session ID is only meaningful on Studio (no WS on testnets). | |
| if (networkStore.isStudio) { | |
| const webSocketClient = useWebSocketClient(); | |
| await new Promise<void>((resolve) => { | |
| if (webSocketClient.connected) { | |
| resolve(); | |
| }); | |
| } | |
| }); | |
| } else { | |
| webSocketClient.on('connect', () => { | |
| resolve(); | |
| }); | |
| } | |
| }); | |
| headers['x-session-id'] = webSocketClient.id ?? ''; | |
| } | |
| // WebSocket session ID is only meaningful on Studio (no WS on testnets). | |
| if (networkStore.isStudio) { | |
| const webSocketClient = useWebSocketClient(); | |
| await new Promise<void>((resolve) => { | |
| if (webSocketClient.connected) { | |
| resolve(); | |
| } else { | |
| const timeout = window.setTimeout(() => { | |
| webSocketClient.off('connect', onConnect); | |
| resolve(); | |
| }, 10_000); | |
| const onConnect = () => { | |
| window.clearTimeout(timeout); | |
| webSocketClient.off('connect', onConnect); | |
| resolve(); | |
| }; | |
| webSocketClient.on('connect', onConnect); | |
| } | |
| }); | |
| headers['x-session-id'] = webSocketClient.id ?? ''; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/clients/rpc.ts` around lines 21 - 34, The current Studio
WebSocket gate (guarded by networkStore.isStudio and using
useWebSocketClient()/webSocketClient.on('connect')) can hang indefinitely;
change the Promise so it races the 'connect' handler against a bounded timeout
(e.g., 3–10s). If the timeout fires, remove the 'connect' listener, either
reject the Promise so the caller can handle the error or resolve and continue
without a session id (headers['x-session-id'] = ''), and clear the timer if
'connect' wins; ensure you handle webSocketClient.connected immediately as
before and always cleanup listeners/timeouts to avoid leaks.
| <div | ||
| v-if="!networkStore.isStudio && pendingOnCurrentNetwork > 0" | ||
| class="mx-1 mt-1 border-t border-gray-200 px-2 pt-2 text-[11px] text-amber-600 dark:border-gray-700" | ||
| > | ||
| {{ pendingOnCurrentNetwork }} pending transaction(s) on the current | ||
| network will be hidden after switching. | ||
| </div> |
There was a problem hiding this comment.
Warning banner only appears on non-Studio — asymmetric.
The v-if="!networkStore.isStudio && pendingOnCurrentNetwork > 0" guard means the "pending transactions will be hidden after switching" banner is never shown when the user is currently on Studio and about to switch to testnet (or between two Studio networks). Pending localnet transactions would then disappear from the UI silently after switching.
If the intent is to warn in all cross-chain switches, drop the !networkStore.isStudio clause.
🔧 Proposed change
- <div
- v-if="!networkStore.isStudio && pendingOnCurrentNetwork > 0"
- class="mx-1 mt-1 border-t border-gray-200 px-2 pt-2 text-[11px] text-amber-600 dark:border-gray-700"
- >
+ <div
+ v-if="pendingOnCurrentNetwork > 0"
+ class="mx-1 mt-1 border-t border-gray-200 px-2 pt-2 text-[11px] text-amber-600 dark:border-gray-700"
+ >📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div | |
| v-if="!networkStore.isStudio && pendingOnCurrentNetwork > 0" | |
| class="mx-1 mt-1 border-t border-gray-200 px-2 pt-2 text-[11px] text-amber-600 dark:border-gray-700" | |
| > | |
| {{ pendingOnCurrentNetwork }} pending transaction(s) on the current | |
| network will be hidden after switching. | |
| </div> | |
| <div | |
| v-if="pendingOnCurrentNetwork > 0" | |
| class="mx-1 mt-1 border-t border-gray-200 px-2 pt-2 text-[11px] text-amber-600 dark:border-gray-700" | |
| > | |
| {{ pendingOnCurrentNetwork }} pending transaction(s) on the current | |
| network will be hidden after switching. | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/global/NetworkSelector.vue` around lines 103 - 109,
The warning banner is currently gated by !networkStore.isStudio so users on
Studio won't see the "pending transactions will be hidden after switching"
message; update NetworkSelector.vue to show the banner for any cross-network
switch by removing the !networkStore.isStudio condition and use only
pendingOnCurrentNetwork > 0 (i.e., change v-if="!networkStore.isStudio &&
pendingOnCurrentNetwork > 0" to v-if="pendingOnCurrentNetwork > 0"), keeping the
existing text and CSS classes intact.
| function readInitialNetwork(): NetworkName { | ||
| const persisted = localStorage.getItem(STORAGE_KEY) as NetworkName | null; | ||
| if (persisted && persisted in CHAINS) return persisted; | ||
|
|
||
| const fromEnv = getRuntimeConfig( | ||
| 'VITE_GENLAYER_NETWORK', | ||
| 'localnet', | ||
| ) as NetworkName; | ||
| if (fromEnv in CHAINS) return fromEnv; |
There was a problem hiding this comment.
Make locked deployments ignore stale persisted networks.
readInitialNetwork() always prefers localStorage, so a deployment with VITE_LOCK_NETWORK=true can still boot into a previously persisted network while the selector is hidden. That makes the operator’s configured network unenforceable. Also wrap the read path like the write path, since localStorage.getItem can throw in restricted storage contexts.
Prefer moving currentNetwork persistence to pinia-plugin-persistedstate and explicitly bypass hydration when the network is locked.
Suggested direction
+function coerceNetwork(value: string | null): NetworkName | null {
+ return value && value in CHAINS ? (value as NetworkName) : null;
+}
+
function readInitialNetwork(): NetworkName {
- const persisted = localStorage.getItem(STORAGE_KEY) as NetworkName | null;
- if (persisted && persisted in CHAINS) return persisted;
-
- const fromEnv = getRuntimeConfig(
+ const configured =
+ coerceNetwork(
+ getRuntimeConfig(
+ 'VITE_GENLAYER_NETWORK',
+ 'localnet',
+ ),
+ ) ?? 'localnet';
+
+ if (getRuntimeConfigBoolean('VITE_LOCK_NETWORK', false)) {
+ return configured;
+ }
+
+ try {
+ const persisted = coerceNetwork(localStorage.getItem(STORAGE_KEY));
+ if (persisted) return persisted;
+ } catch {
+ // localStorage may be unavailable; fall back to configured network.
+ }
+
+ return configured;
- 'VITE_GENLAYER_NETWORK',
- 'localnet',
- ) as NetworkName;
- if (fromEnv in CHAINS) return fromEnv;
-
- return 'localnet';
}As per coding guidelines, frontend/src/stores/**/*.{ts,tsx}: Persist frontend Pinia stores to localStorage using pinia-plugin-persistedstate.
Also applies to: 118-128
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/stores/network.ts` around lines 36 - 44, readInitialNetwork
currently always prefers localStorage and calls localStorage.getItem without
protection, allowing a deployment with VITE_LOCK_NETWORK=true to still boot a
stale persisted network; fix by 1) wrapping all localStorage reads/writes in
try/catch like the write path to avoid exceptions in restricted storage
contexts, 2) when VITE_LOCK_NETWORK (or config key checked at runtime) is true
explicitly bypass any hydration/read-from-localStorage in readInitialNetwork and
return the environment-configured network (from
getRuntimeConfig('VITE_GENLAYER_NETWORK')), and 3) move persistence of
currentNetwork out of manual getItem/setItem into pinia-plugin-persistedstate
and ensure the plugin is configured to skip hydration when the lock flag is set
so the persisted value is ignored when locked (referencing readInitialNetwork
and the currentNetwork/pinia-persist configuration).
| const availableNetworks = computed< | ||
| { name: NetworkName; label: string; isStudio: boolean; chainId: number }[] | ||
| >(() => { | ||
| const list = SELECTABLE_NETWORKS.map((name) => { | ||
| const c = CHAINS[name]; | ||
| return { | ||
| name, | ||
| label: c.name, | ||
| isStudio: Boolean(c.isStudio), | ||
| chainId: c.id, | ||
| }; | ||
| }); | ||
|
|
||
| // If the operator pinned a network outside the selectable list (e.g. Asimov | ||
| // via VITE_GENLAYER_NETWORK), surface it so the user can at least see where | ||
| // they are — but they cannot switch away from it via the dropdown. | ||
| if (!list.some((n) => n.name === currentNetwork.value)) { | ||
| const c = CHAINS[currentNetwork.value]; | ||
| list.push({ | ||
| name: currentNetwork.value, | ||
| label: c.name, | ||
| isStudio: Boolean(c.isStudio), | ||
| chainId: c.id, | ||
| }); | ||
| } | ||
|
|
||
| return list; | ||
| }); | ||
|
|
||
| function setCurrentNetwork(name: NetworkName) { |
There was a problem hiding this comment.
Don’t expose deployment-pinned networks as normal switch targets.
When currentNetwork is testnetAsimov, availableNetworks becomes [localnet, testnetBradbury, testnetAsimov]. A dropdown using this list can switch away from Asimov, and then Asimov disappears because it is no longer current. Add explicit selectable metadata or return only the pinned current network when it is outside SELECTABLE_NETWORKS.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/stores/network.ts` around lines 89 - 118, availableNetworks
currently exposes deployment-pinned networks as normal switch targets; modify
the computed to mark such entries as non-selectable (add a selectable: boolean
field) or, alternatively, when currentNetwork is not in SELECTABLE_NETWORKS
return only the selectable list plus a single pinned entry that has selectable:
false so UI dropdowns can disable switching to/from it; update the code that
constructs items (referencing availableNetworks, SELECTABLE_NETWORKS,
currentNetwork, and CHAINS) to set selectable: true for regular
SELECTABLE_NETWORKS entries and selectable: false for the pinned/current
out-of-list entry.
…gh symlink `frontend/src/assets/examples/contracts` is a symlink to the repo-root `examples/contracts/` dir. Vite's dev server `server.fs.allow` defaults to the workspace root (`frontend/`), which meant `?raw` dynamic imports against the symlink target served raw text with an empty Content-Type instead of the expected JS module. The browser would then reject 8 of 9 example files as "Failed to fetch dynamically imported module" — only `storage.py` loaded, because `stores/tutorial.ts` imports it statically and that import-graph entry triggered the transform. - Add `server.fs.allow: ['..']` so the dev server can serve + transform files from the repo root (covers the symlinked examples). - Wrap each `loader()` call in `setupStores` with try/catch so one bad example can't block the rest from loading.
The previous implementation used uuidv4() strings for the `id` field. JSON- RPC 2.0 allows either, but the Go node on Bradbury/Asimov rejects string ids with `cannot unmarshal string into Go struct field Request.id of type int`, which showed up in the browser console as a JSON-RPC -32700 error on every RPC call from the frontend to a testnet. Switch to a monotonic integer counter — works on both the Studio Python backend and the Go testnet nodes.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
frontend/src/clients/rpc.ts (1)
29-37:⚠️ Potential issue | 🟠 MajorBound the Studio WebSocket wait.
This still waits indefinitely if the Studio WebSocket never emits
connect, so JSON-RPC calls can hang forever. Please add a timeout and clean up the listener when either path wins.⏱️ Proposed bounded wait
- await new Promise<void>((resolve) => { + await new Promise<void>((resolve) => { if (webSocketClient.connected) { resolve(); } else { - webSocketClient.on('connect', () => { + const onConnect = () => { + window.clearTimeout(timeout); + webSocketClient.off('connect', onConnect); resolve(); - }); + }; + const timeout = window.setTimeout(() => { + webSocketClient.off('connect', onConnect); + resolve(); + }, 10_000); + webSocketClient.on('connect', onConnect); } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/clients/rpc.ts` around lines 29 - 37, The current Promise awaiting webSocketClient.connect can hang indefinitely; change it to a bounded wait by adding a timeout (e.g., 10s) and by registering a named connect handler so you can remove it on either outcome: if webSocketClient.connected is true resolve immediately; otherwise register a handler function for webSocketClient.on('connect', handler) that clears the timeout, removes the listener (webSocketClient.off/removeListener), and resolves; also start a setTimeout that, when fired, removes the connect listener and rejects the Promise with a clear timeout error so JSON-RPC callers won't hang. Ensure you clear the timeout on successful connect and remove the listener on timeout to avoid leaks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/hooks/useSetupStores.ts`:
- Around line 28-32: setupStores (in useSetupStores) only runs at bootstrap so
switching networks via setCurrentNetwork leaves Studio-only initialization
(useWebSocketClientAsync, validators, providers, finality window, reconnection
listener) uninitialized; update useSetupStores to watch
networkStore.currentNetwork (or networkStore) and re-run or extract and call the
Studio-only setup sequence when the network becomes a Studio chain: detect
transitions to Studio and invoke the same initialization path that setupStores
uses (including useWebSocketClientAsync and calls that prime getValidatorsData,
getProvidersData, fetchFinalityWindowTime and the reconnection listener), and
ensure teardown/cleanup occurs when leaving Studio to avoid duplicated listeners
or resources.
---
Duplicate comments:
In `@frontend/src/clients/rpc.ts`:
- Around line 29-37: The current Promise awaiting webSocketClient.connect can
hang indefinitely; change it to a bounded wait by adding a timeout (e.g., 10s)
and by registering a named connect handler so you can remove it on either
outcome: if webSocketClient.connected is true resolve immediately; otherwise
register a handler function for webSocketClient.on('connect', handler) that
clears the timeout, removes the listener (webSocketClient.off/removeListener),
and resolves; also start a setTimeout that, when fired, removes the connect
listener and rejects the Promise with a clear timeout error so JSON-RPC callers
won't hang. Ensure you clear the timeout on successful connect and remove the
listener on timeout to avoid leaks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6978a869-13f5-489a-9446-5121cebdd62c
📒 Files selected for processing (3)
frontend/src/clients/rpc.tsfrontend/src/hooks/useSetupStores.tsfrontend/vite.config.ts
| // WebSocket push events are Studio-only. On non-Studio chains, skip the | ||
| // async wait (the client is a no-op stub) and move on. | ||
| if (networkStore.isStudio) { | ||
| await useWebSocketClientAsync(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find where network switches are handled and whether they re-run Studio-only init.
rg -nP --type=ts --type=vue -C3 '\b(setCurrentNetwork|switchNetwork|currentNetwork|networkStore\.(chainId|isStudio|currentNetwork))\b'
echo '---'
# Look for watchers on network/chain changes that might re-init WS or fetch Studio data.
rg -nP --type=ts --type=vue -C5 '\bwatch(Effect)?\s*\(\s*\(\s*\)\s*=>\s*[^,]*network'
echo '---'
# Confirm whether setupStores is called more than once.
rg -nP -C2 '\buseSetupStores\b|\bsetupStores\s*\('
echo '---'
# See if WS client or node/consensus stores have their own re-init hooks on network change.
rg -nP --type=ts -C3 '\b(reconnect|reinit|reinitialize|teardown|destroy)\b' frontend/src/hooks frontend/src/stores frontend/src/clients 2>/dev/nullRepository: genlayerlabs/genlayer-studio
Length of output: 15417
🏁 Script executed:
# Check getValidatorsData, getProvidersData, fetchFinalityWindowTime, setupReconnectionListener for isStudio guards
rg -n "getValidatorsData|getProvidersData|fetchFinalityWindowTime|setupReconnectionListener" frontend/src/stores frontend/src/hooks -A 8 | head -100
echo "---"
# Look at the full useGenlayer implementation for network switch handling
cat -n frontend/src/hooks/useGenlayer.ts | head -80
echo "---"
# Check network store for currentNetwork/network change setters or watchers
rg -n "setCurrentNetwork|currentNetwork|changeNetwork" frontend/src/stores -A 3 -B 1Repository: genlayerlabs/genlayer-studio
Length of output: 10748
🏁 Script executed:
# Search for watchers on network changes in stores and App
rg -n "watch.*currentNetwork|watch.*isStudio|watch.*networkStore" frontend/src --type ts -A 5
echo "---"
# Check if setupStores is called anywhere other than App.vue and test
rg -n "setupStores\(" frontend/src -B 2
echo "---"
# Check the test for network switch scenario
rg -n "networkStore.isStudio = true\|networkStore.isStudio = false" frontend/test -B 3 -A 10Repository: genlayerlabs/genlayer-studio
Length of output: 528
🏁 Script executed:
# Check if there's any initialization in the stores themselves on currentNetwork change
cat -n frontend/src/stores/network.ts | grep -A 20 "setCurrentNetwork"
echo "---"
# Look for any effects or watchers in the main stores that might re-init on network switch
rg -n "watchEffect|onMounted" frontend/src/stores/node.ts frontend/src/stores/consensus.ts -A 3
echo "---"
# Check if useSetupStores is called in useGenlayer or elsewhere on init
rg -n "useSetupStores\|setupStores" frontend/src/hooks/useGenlayer.ts frontend/src/hooks/useSetupStores.tsRepository: genlayerlabs/genlayer-studio
Length of output: 761
Network-switch behavior leaves Studio-only state uninitialized.
setupStores is invoked only once at app bootstrap (App.vue onBeforeMount). When a user switches networks via setCurrentNetwork, the reactive networkStore.currentNetwork changes (watched by useGenlayer to re-init the client), but setupStores is never re-run. As a result, booting on Bradbury and switching to Studio will not initialize the WebSocket client, validators, providers, finality window, or reconnection listener. These remain permanently uninitialized for that Studio session, even though the per-method guards (getValidatorsData, getProvidersData, fetchFinalityWindowTime) would permit them to run on Studio.
The symmetric case (boot on Studio → switch to Bradbury) is benign because the per-method guards prevent new fetches, though existing data persists in state.
Consider watching networkStore.currentNetwork in useSetupStores or the network store itself to re-run the initialization sequence, or extract the Studio-only setup (WS, validators, providers, finality, reconnection) into separate watchers that trigger on network change.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/hooks/useSetupStores.ts` around lines 28 - 32, setupStores (in
useSetupStores) only runs at bootstrap so switching networks via
setCurrentNetwork leaves Studio-only initialization (useWebSocketClientAsync,
validators, providers, finality window, reconnection listener) uninitialized;
update useSetupStores to watch networkStore.currentNetwork (or networkStore) and
re-run or extract and call the Studio-only setup sequence when the network
becomes a Studio chain: detect transitions to Studio and invoke the same
initialization path that setupStores uses (including useWebSocketClientAsync and
calls that prime getValidatorsData, getProvidersData, fetchFinalityWindowTime
and the reconnection listener), and ensure teardown/cleanup occurs when leaving
Studio to avoid duplicated listeners or resources.
On Bradbury (and any non-Studio network) the deploy section was hidden entirely, and the Monaco linter was firing against the testnet RPC: - Deploy UI was gated on `nodeStore.hasAtLeastOneValidator`. On testnet we don't fetch validators (sim_* is Studio-only), so the gate was always false and users saw "Run and Debug" with no way to deploy. Drop the gate on non-Studio networks — there are no validators to "configure first." - `LEADER_ONLY` / `LEADER_SELF_VALIDATOR` execution modes are Studio debug features; testnet only supports `NORMAL`. Hide the Execution Mode dropdown on non-Studio networks (default stays `NORMAL`). - The Monaco linter calls `sim_lintContract`, which is Studio-only. Previously routed through the reactive RpcClient, so on Bradbury it would hit the testnet RPC and log a -32601. Pin the linter to `VITE_JSON_RPC_SERVER_URL` directly regardless of selected network; suppress the -32601 case (method missing means no linter available).
When the user selects Bradbury in the network dropdown, the "View in explorer" link on each transaction row was still pointing at the Studio explorer (derived from the current hostname). Route it through the chain's own `blockExplorers.default.url` when on a non-Studio network, so Bradbury txs open at https://explorer-bradbury.genlayer.com/tx/<hash>. Studio-path resolution is unchanged: env override > hostname heuristic > localhost fallback.
Picks up genlayerlabs/genlayer-js#157 — the SDK now passes a future timestamp for _validUntil in v6 addTransaction calls, so deploys to Bradbury from the Studio frontend actually land instead of reverting inside the EVM wrapper with no reason string.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/views/Simulator/RunDebugView.vue (1)
73-84:⚠️ Potential issue | 🟡 MinorExecution mode stays
NORMALwhen hidden on testnet — confirm that's the intended default.When
isStudioNetworkis false the selector is hidden, butexecutionModekeeps its initial'NORMAL'value and is still forwarded toConstructorParameters,ContractReadMethods, andContractWriteMethods. If a user switches networks after picking'LEADER_ONLY', that stale value remains in effect on the testnet path. Consider resettingexecutionModeto'NORMAL'whenisStudioNetworkflips to false (or ignoring it entirely downstream on non-Studio), so the hidden UI can't silently influence testnet transactions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/views/Simulator/RunDebugView.vue` around lines 73 - 84, Reset or ignore the hidden executionMode when the network flips out of Studio: in RunDebugView.vue detect changes to isStudioNetwork and when it becomes false set executionMode = 'NORMAL' (or alternatively change the props passed into ConstructorParameters, ContractReadMethods, and ContractWriteMethods to pass :executionMode="isStudioNetwork ? executionMode : 'NORMAL'") so a previously selected 'LEADER_ONLY' cannot silently affect testnet behavior; reference the executionMode state and the isStudioNetwork flag and update either the watcher or the prop-binding for those components accordingly.
🧹 Nitpick comments (1)
frontend/src/views/Simulator/RunDebugView.vue (1)
6-19: Consolidate duplicate@/hooksimports.
useContractQueriesanduseConfigare both exported from@/hooksbut imported on two separate lines. Merge into a single import for clarity.♻️ Proposed refactor
-import { useContractQueries } from '@/hooks'; +import { useContractQueries, useConfig } from '@/hooks'; import MainTitle from '@/components/Simulator/MainTitle.vue'; import { ref, watch, computed } from 'vue'; @@ import type { ExecutionMode } from '@/types'; -import { useConfig } from '@/hooks';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/views/Simulator/RunDebugView.vue` around lines 6 - 19, The file imports useContractQueries and useConfig from the same module twice; consolidate by merging both named imports into a single import from '@/hooks' (replace the separate import lines for useContractQueries and useConfig with one line importing both symbols) so symbols useContractQueries and useConfig are imported together in RunDebugView.vue.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/services/monacoLinter.ts`:
- Around line 55-73: The linter currently applies Monaco markers from any
returned response and doesn't clear markers on errors, causing stale
diagnostics; fix it by snapshotting the request id before the fetch (use
nextLintRequestId), attach that id to the outbound RPC, and after awaiting the
response only apply markers if the returned id matches the snapshot to avoid
stale updates; additionally, on any error path (response.error, network/fetch
failures, or when sim_lintContract is unavailable) call the existing
marker-clear routine (the same code path that uses monaco.editor.setModelMarkers
or the module's marker clearing function) to remove old diagnostics so the
editor isn't left with false markers. Ensure you reference the jsonrpc id field
and nextLintRequestId, check response.error and fetch exceptions, and gate
marker application accordingly.
---
Outside diff comments:
In `@frontend/src/views/Simulator/RunDebugView.vue`:
- Around line 73-84: Reset or ignore the hidden executionMode when the network
flips out of Studio: in RunDebugView.vue detect changes to isStudioNetwork and
when it becomes false set executionMode = 'NORMAL' (or alternatively change the
props passed into ConstructorParameters, ContractReadMethods, and
ContractWriteMethods to pass :executionMode="isStudioNetwork ? executionMode :
'NORMAL'") so a previously selected 'LEADER_ONLY' cannot silently affect testnet
behavior; reference the executionMode state and the isStudioNetwork flag and
update either the watcher or the prop-binding for those components accordingly.
---
Nitpick comments:
In `@frontend/src/views/Simulator/RunDebugView.vue`:
- Around line 6-19: The file imports useContractQueries and useConfig from the
same module twice; consolidate by merging both named imports into a single
import from '@/hooks' (replace the separate import lines for useContractQueries
and useConfig with one line importing both symbols) so symbols
useContractQueries and useConfig are imported together in RunDebugView.vue.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b74b1328-3c2f-4f5d-a60b-49923407b23c
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (4)
frontend/package.jsonfrontend/src/services/monacoLinter.tsfrontend/src/utils/explorerUrl.tsfrontend/src/views/Simulator/RunDebugView.vue
✅ Files skipped from review due to trivial changes (1)
- frontend/package.json
| const res = await fetch(STUDIO_RPC_URL, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| jsonrpc: '2.0', | ||
| method: 'sim_lintContract', | ||
| params: [code, 'contract.py'], | ||
| id: nextLintRequestId++, | ||
| }), | ||
| }); | ||
| const response = (await res.json()) as JsonRPCResponse<LintResponse>; | ||
|
|
||
| if (response.error) { | ||
| console.error('[Monaco Linter] Error from server:', response.error); | ||
| // `sim_lintContract` is Studio-only; quietly disable the linter if the | ||
| // backend doesn't expose it (e.g. hosted deployment with no Studio). | ||
| if (response.error.code !== -32601) { | ||
| console.error('[Monaco Linter] Error from server:', response.error); | ||
| } | ||
| return; |
There was a problem hiding this comment.
Prevent stale Monaco markers on slow or failed lint requests.
An older lint response can still apply markers after the user edits the buffer, and fail-soft error responses return without clearing existing linter markers. That can leave false diagnostics visible when Studio linting is unavailable or delayed.
Proposed marker-state guard
export async function lintGenVMCode(
editor: Monaco.editor.IStandaloneCodeEditor,
monaco: typeof Monaco,
) {
const code = editor.getValue();
+ const clearLintMarkers = () => {
+ const model = editor.getModel();
+ if (model) {
+ monaco.editor.setModelMarkers(model, 'genvm-linter', []);
+ }
+ currentMarkers = [];
+ };
try {
const res = await fetch(STUDIO_RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@
});
const response = (await res.json()) as JsonRPCResponse<LintResponse>;
+
+ if (editor.getValue() !== code) {
+ return;
+ }
if (response.error) {
// `sim_lintContract` is Studio-only; quietly disable the linter if the
// backend doesn't expose it (e.g. hosted deployment with no Studio).
if (response.error.code !== -32601) {
console.error('[Monaco Linter] Error from server:', response.error);
}
+ clearLintMarkers();
return;
}
@@
} catch (error) {
console.error('[Monaco Linter] Failed to lint code:', error);
+ if (editor.getValue() === code) {
+ clearLintMarkers();
+ }
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/services/monacoLinter.ts` around lines 55 - 73, The linter
currently applies Monaco markers from any returned response and doesn't clear
markers on errors, causing stale diagnostics; fix it by snapshotting the request
id before the fetch (use nextLintRequestId), attach that id to the outbound RPC,
and after awaiting the response only apply markers if the returned id matches
the snapshot to avoid stale updates; additionally, on any error path
(response.error, network/fetch failures, or when sim_lintContract is
unavailable) call the existing marker-clear routine (the same code path that
uses monaco.editor.setModelMarkers or the module's marker clearing function) to
remove old diagnostics so the editor isn't left with false markers. Ensure you
reference the jsonrpc id field and nextLintRequestId, check response.error and
fetch exceptions, and gate marker application accordingly.
Tx status updates depended entirely on WebSocket transaction_status_updated events via useTransactionListener. Those events only exist on the Studio backend — on Bradbury there is no WS, so the UI never learned when a tx moved from PENDING to CANCELED / FINALIZED / *_TIMEOUT and everything appeared stuck at 'pending' forever (even after the tx had been cancelled by the consensus contract). - Add 5s interval polling in transactionsStore that refreshes any undecided txs on non-Studio networks, using isDecidedState from the SDK so polling stops once each tx reaches a terminal state. - Loop auto-starts on store creation when the initial network is non-Studio, and on every subsequent switch into a non-Studio chain. Stops on switch back to Studio (WS takes over again). - refreshPendingTransactions filter updated to use isDecidedState too, so CANCELED / *_TIMEOUT txs stop being re-queried.
The SDK exposes the same on-chain identifier under two names:
- Studio (gen_getTransaction) returns { hash, ... }
- Testnet (consensus contract getTransactionData) returns { txId, ... }
The store's updateTransaction looked up by hash only, so on Bradbury
the polling refresh found nothing and silently logged 'Transaction
not found' while the UI stayed stuck on PENDING. Accept either name
in the lookup until the SDK unifies its types (TODO upstream).
27dba04 to
5f98127
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/stores/transactions.ts (1)
144-162:⚠️ Potential issue | 🟡 MinorPolling amplifies the silent-drop behavior on transient RPC misses.
refreshPendingTransactionstreats a falsygetTransactionresult as "tx no longer exists" and deletes it from both the in-memory store and Dexie. With this PR, the function is now invoked every 5s on non-Studio networks, so a single transient RPC blip that returnsnull(rather than throwing) for a still-valid pending tx will permanently evict the user's transaction record. Consider distinguishing "definitively not found" from "lookup failed" before deleting — e.g., let lookup errors propagate so the outertry/catchkeeps the entry, and only delete on an explicit not-found signal.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/stores/transactions.ts` around lines 144 - 162, refreshPendingTransactions currently treats any falsy result from getTransaction as "not found" and deletes the in-memory entry and Dexie row; change the logic so only an explicit not-found signal triggers removeTransaction and db.transactions.delete. Specifically, update the block in refreshPendingTransactions that calls getTransaction(tx.hash) to: (1) let RPC/lookup failures propagate (or have getTransaction throw) so the outer try/catch preserves the entry, (2) treat a distinct return value (e.g., a defined NOT_FOUND sentinel or a specific status field) as the sole condition to call removeTransaction and db.transactions.where('hash').equals(tx.hash).delete(), and (3) continue to stamp and update the entry via updateTransaction and db.transactions.modify only when a valid newTx is returned. Ensure the change references getTransaction, refreshPendingTransactions, updateTransaction, removeTransaction, and db.transactions to locate and modify the code.
🧹 Nitpick comments (1)
frontend/src/stores/transactions.ts (1)
69-80: Add an in-flight guard to the polling loop.
setIntervalschedules a new tick every 5s regardless of whether the previousrefreshPendingTransactionshas resolved. On slow testnet RPCs or with many pending txs,Promise.allovergetTransactioncalls can easily exceed 5s, causing overlapping refreshes that hammer the RPC, mutateallTransactionsconcurrently, and can race theremoveTransactionpath on transient null responses. Prefer a re-entrancy guard or a self-reschedulingsetTimeout.♻️ Proposed in-flight guard
function startUndecidedPolling() { if (undecidedPollTimer) return; + let inFlight = false; undecidedPollTimer = setInterval(async () => { + if (inFlight) return; if (!hasUndecidedTx()) return; + inFlight = true; try { await refreshPendingTransactions(); } catch (err) { // Keep polling even if a single refresh fails. console.error('Failed polling transaction statuses', err); + } finally { + inFlight = false; } }, NON_STUDIO_POLL_INTERVAL_MS); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/stores/transactions.ts` around lines 69 - 80, The polling loop in startUndecidedPolling uses setInterval and can re-enter refreshPendingTransactions before the prior call finishes, causing overlapping RPC calls and concurrent mutations of allTransactions (and races with removeTransaction); add an in-flight guard (e.g., a boolean like isRefreshing) or convert to a self-rescheduling setTimeout so a new tick is scheduled only after refreshPendingTransactions completes: in startUndecidedPolling (and using undecidedPollTimer, hasUndecidedTx, NON_STUDIO_POLL_INTERVAL_MS, refreshPendingTransactions, and removeTransaction) set/clear the guard around the await refreshPendingTransactions() call (or await then setTimeout for next tick) to prevent concurrent executions and ensure only one refresh runs at a time.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/stores/transactions.ts`:
- Around line 24-28: The computed selector "transactions" is overly permissive
by allowing t.chainId === undefined; remove that fallback so it only returns
transactions matching networkStore.chainId. Update the filter in the computed
TransactionItem[] (the transactions constant that uses
allTransactions.value.filter) to use (t) => t.chainId === networkStore.chainId,
relying on the Dexie migration and addTransaction() to ensure chainId is always
present.
---
Outside diff comments:
In `@frontend/src/stores/transactions.ts`:
- Around line 144-162: refreshPendingTransactions currently treats any falsy
result from getTransaction as "not found" and deletes the in-memory entry and
Dexie row; change the logic so only an explicit not-found signal triggers
removeTransaction and db.transactions.delete. Specifically, update the block in
refreshPendingTransactions that calls getTransaction(tx.hash) to: (1) let
RPC/lookup failures propagate (or have getTransaction throw) so the outer
try/catch preserves the entry, (2) treat a distinct return value (e.g., a
defined NOT_FOUND sentinel or a specific status field) as the sole condition to
call removeTransaction and
db.transactions.where('hash').equals(tx.hash).delete(), and (3) continue to
stamp and update the entry via updateTransaction and db.transactions.modify only
when a valid newTx is returned. Ensure the change references getTransaction,
refreshPendingTransactions, updateTransaction, removeTransaction, and
db.transactions to locate and modify the code.
---
Nitpick comments:
In `@frontend/src/stores/transactions.ts`:
- Around line 69-80: The polling loop in startUndecidedPolling uses setInterval
and can re-enter refreshPendingTransactions before the prior call finishes,
causing overlapping RPC calls and concurrent mutations of allTransactions (and
races with removeTransaction); add an in-flight guard (e.g., a boolean like
isRefreshing) or convert to a self-rescheduling setTimeout so a new tick is
scheduled only after refreshPendingTransactions completes: in
startUndecidedPolling (and using undecidedPollTimer, hasUndecidedTx,
NON_STUDIO_POLL_INTERVAL_MS, refreshPendingTransactions, and removeTransaction)
set/clear the guard around the await refreshPendingTransactions() call (or await
then setTimeout for next tick) to prevent concurrent executions and ensure only
one refresh runs at a time.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f958fe1e-6292-45c0-9637-c3eab81f5d30
📒 Files selected for processing (1)
frontend/src/stores/transactions.ts
| const transactions = computed<TransactionItem[]>(() => | ||
| allTransactions.value.filter( | ||
| (t) => t.chainId === undefined || t.chainId === networkStore.chainId, | ||
| ), | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm the Dexie v5 migration unconditionally backfills chainId on transactions.
rg -nP -C5 '\b(version\s*\(\s*5\)|upgrade|chainId)\b' -g 'frontend/src/**/*.{ts,vue}' | rg -nP -C3 'transactions'Repository: genlayerlabs/genlayer-studio
Length of output: 6285
🏁 Script executed:
# Check the full Dexie initialization flow and when upgrades run
cat -n frontend/src/hooks/useDb.ts | head -100Repository: genlayerlabs/genlayer-studio
Length of output: 3666
🏁 Script executed:
# Verify how the store is initialized and if it waits for db readiness
rg -n 'useTransactionStore|useDb' frontend/src/stores/transactions.ts -A 3 -B 3Repository: genlayerlabs/genlayer-studio
Length of output: 545
🏁 Script executed:
# Check if there are any other code paths that create or load transactions
rg -n 'allTransactions\.value\s*=' frontend/src/stores/transactions.tsRepository: genlayerlabs/genlayer-studio
Length of output: 270
🏁 Script executed:
# Find where allTransactions is populated at line 90 - look for loadTransactions or similar
rg -n 'allTransactions.value = items|loadTransactions|load.*transaction' frontend/src/stores/transactions.ts -B 5 -A 2Repository: genlayerlabs/genlayer-studio
Length of output: 238
🏁 Script executed:
# Check the full store initialization and return statement
sed -n '1,250p' frontend/src/stores/transactions.ts | tail -100Repository: genlayerlabs/genlayer-studio
Length of output: 2852
🏁 Script executed:
# Verify if Dexie's upgrade function blocks access or if it's lazy
rg -n 'return \{' frontend/src/stores/transactions.ts -A 30Repository: genlayerlabs/genlayer-studio
Length of output: 522
🏁 Script executed:
# Find all calls to setAllTransactions to understand the data loading flow
rg -n 'setAllTransactions' frontend/src --type ts --type vue -B 3 -A 1Repository: genlayerlabs/genlayer-studio
Length of output: 100
🏁 Script executed:
# Check the getTransaction implementation to see db access pattern
rg -n 'function getTransaction|const getTransaction' frontend/src/stores/transactions.ts -A 15Repository: genlayerlabs/genlayer-studio
Length of output: 840
🏁 Script executed:
# Search for where transactions are initially loaded - look for db.transactions access
rg -n 'db\.transactions\.(toArray|toCollection)' frontend/src/stores/transactions.ts -B 2 -A 3Repository: genlayerlabs/genlayer-studio
Length of output: 54
🏁 Script executed:
# Search for setAllTransactions calls across the codebase
rg -n 'setAllTransactions' frontend/srcRepository: genlayerlabs/genlayer-studio
Length of output: 348
🏁 Script executed:
# Search for any initialization of the transaction store or data loading
rg -n 'transactions\.(setAllTransactions|load|init|fetch)' frontend/src -A 2Repository: genlayerlabs/genlayer-studio
Length of output: 54
🏁 Script executed:
# Check if there's any code that accesses db.transactions at store creation
rg -n 'db\.transactions' frontend/src/stores/transactions.tsRepository: genlayerlabs/genlayer-studio
Length of output: 346
🏁 Script executed:
# Look for web search to understand Dexie's upgrade behavior
web_search
Does Dexie block table access until all version upgrades complete?Repository: genlayerlabs/genlayer-studio
Length of output: 166
🏁 Script executed:
# Check the full useSetupStores context to see when migrations are guaranteed to be done
sed -n '70,90p' frontend/src/hooks/useSetupStores.tsRepository: genlayerlabs/genlayer-studio
Length of output: 762
🏁 Script executed:
# Find where useSetupStores is called
rg -n 'useSetupStores' frontend/src -B 2 -A 2Repository: genlayerlabs/genlayer-studio
Length of output: 2343
🏁 Script executed:
# Confirm that db.transactions is only accessed after await in setup
rg -n 'await db\.transactions|db\.transactions\.where|db\.transactions\.toArray' frontend/src/stores/transactions.tsRepository: genlayerlabs/genlayer-studio
Length of output: 346
🌐 Web query:
Dexie migration upgrade block table access guarantee
💡 Result:
Yes, Dexie migrations (via db.version(x).upgrade) block access to tables during the upgrade process. The upgrade runs in a special readwrite transaction opened atomically with the database open call. Per IndexedDB specification, this transaction serializes all other transactions on the same database—any attempts to access tables (read or write) from other transactions will wait until the upgrade transaction completes or fail with TransactionInactiveError if nested improperly. Key guarantees and behaviors: - Database remains closed to new connections until upgrade finishes (db.open promise resolves only after). - No concurrent table access within the same connection during upgrade. - Multiple tabs/connections trigger 'blocked' events; Dexie closes old connections by default on 'versionchange'. - For Dexie Cloud synced tables, client-side .upgrade is unsupported and breaks sync—use server-side migrations instead. To minimize blocking for large migrations: - Keep upgrade functions fast (avoid long loops; migrate lazily via hooks like 'reading'). - Use db.on('ready') for post-upgrade work with exclusive access. - Test upgrades thoroughly as failures prevent version bump and repeat on retry. Official docs confirm upgrade(trans) receives a Transaction for table operations like trans.table('friends').toCollection.modify, executed before normal access resumes (1,13,15).
Citations:
- 1: https://dexie.org/docs/Version/Version.upgrade()
- 2: https://dexie.org/docs/Dexie/Dexie.on.blocked
- 3: https://old.dexie.org/docs/Version/Version.upgrade()
- 4: https://dexie.org/docs/Transaction/Transaction.table()
- 5: https://stackoverflow.com/questions/73358596/dexies-open-method-neither-secceed-nor-failed-y
- 6: Error: Unable to delete database because it's blocked dexie/Dexie.js#1779
Remove the chainId === undefined fallback; migration is guaranteed to complete before use.
The Dexie v5 upgrade (frontend/src/hooks/useDb.ts:76–89) unconditionally backfills all transactions without chainId, and this migration blocks table access until completion. The data-loading code in useSetupStores.ts explicitly awaits db.transactions.toArray(), ensuring all legacy records are stamped before the store is populated. New transactions are also always stamped with chainId in addTransaction() (line 96). The fallback predicate t.chainId === undefined is unnecessary defensive coding and can be safely tightened to t.chainId === networkStore.chainId.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/stores/transactions.ts` around lines 24 - 28, The computed
selector "transactions" is overly permissive by allowing t.chainId ===
undefined; remove that fallback so it only returns transactions matching
networkStore.chainId. Update the filter in the computed TransactionItem[] (the
transactions constant that uses allTransactions.value.filter) to use (t) =>
t.chainId === networkStore.chainId, relying on the Dexie migration and
addTransaction() to ensure chainId is always present.
useContractListener only registered contracts on the Studio 'deployed_contract' WebSocket event. Bradbury has no WS push, so deploys landed (ACCEPTED on chain) but the contract never appeared in the UI — there was no way to interact with it. Add a vue watch on the transactions array that fires when a deploy tx reaches ACCEPTED/FINALIZED with a non-zero recipient (which the consensus contract sets to the newly created ghost address). On non-Studio chains, register the contract via addDeployedContract so the user can immediately call read/write methods.
GenLayer's Go-based RPC server (Bradbury) rejects requests missing the `id` field — Reown/Wagmi balance polling was failing with 'Field validation for ID failed'. Some viem transport paths surface a body without `id` despite the http transport's defaults. Provide a custom `viem.custom` transport for genlayer-localnet and genlayer-bradbury via WagmiAdapter's `transports` map. The transport explicitly stamps both `jsonrpc: '2.0'` and a numeric `id` on every request, so eth_getBalance / eth_call / eth_sendRawTransaction etc. all parse cleanly server-side.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
frontend/src/stores/transactions.ts (2)
109-131:⚠️ Potential issue | 🟡 Minor
updateTransactionfalls back totxId, but in-memory rows are keyed only byhash.
tx.hash ?? tx.txIdis resolved againstt.hash === id, which works only becauseaddTransactionalways stamps the originalhashand the polling commit message ("stamp the original hash onto SDK getTransaction responses") preserves it. If anywhere in the codebase a non-StudiogetTransactionresponse reachesupdateTransactionwithout that re-stamping (e.g., a future caller that bypassesrefreshPendingTransactions), this lookup will silently miss and only emit a warn. Worth adding a short comment or asserting the invariant here so the contract isn't lost.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/stores/transactions.ts` around lines 109 - 131, updateTransaction uses tx.hash ?? tx.txId to compute id but then looks up rows by t.hash, which will miss entries if a non-Studio response with only txId reaches this function; update the code to either assert the invariant that stored transactions always have a hash (e.g., add a runtime check in updateTransaction) or normalize incoming tx objects by stamping tx.hash = tx.hash ?? tx.txId before the lookup; reference the updateTransaction function and the addTransaction/refreshPendingTransactions flow in your change so the contract is preserved and failures become explicit rather than silent warns.
137-163:⚠️ Potential issue | 🟠 MajorUnindexed testnet transactions can be prematurely deleted on transient RPC misses during the pre-indexing window.
refreshPendingTransactionsimmediately deletes both the in-memory entry and the Dexie row whenevergetTransactionreturns undefined. With 5-second polling on non-Studio chains, any transient miss during the pre-indexing window right after transaction submission will silently drop the transaction and its DB record. On Studio this risk is mitigated by WebSocket push updates, but the new polling cadence amplifies the risk on testnet.The outer try-catch protects against thrown RPC errors, but pre-indexing window misses return undefined rather than throw, bypassing this protection.
Implement one of these mitigations:
- Gate deletion on an explicit "not found" signal from the SDK rather than falsy return
- Track submission timestamp on
addTransactionand only delete after N consecutive misses or a grace period (e.g., 30+ seconds)- Differentiate between transient miss and permanent not-found at the API level
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/stores/transactions.ts` around lines 137 - 163, refreshPendingTransactions currently deletes transactions when getTransaction returns undefined, causing premature removal during pre-indexing RPC misses; modify addTransaction to record a submission timestamp (e.g., addedAt) into the TransactionItem and persist it to db.transactions, then change refreshPendingTransactions to avoid immediate deletion: when getTransaction returns undefined, check the tx.addedAt (and optionally a miss counter stored in-memory or in db) and only call removeTransaction()/db.transactions.delete() after a configurable grace period (e.g., 30s) or N consecutive misses; keep immediate updates/deletes when getTransaction returns a real "not found" signal if your SDK provides one, otherwise rely on the timestamp/grace logic; update any references to transactionsStatusNameToNumber and db.transactions.where('hash') to use the new fields consistently.
🧹 Nitpick comments (2)
frontend/src/hooks/useAppKit.ts (1)
15-34: Surface HTTP failures and consider a request timeout.
res.json()is invoked unconditionally, so non-2xx responses with non-JSON bodies (HTML error pages from a proxy/CDN, empty 502s, etc.) reject with an opaque JSON parse error, hiding the actual status. Checkingres.okfirst preserves the upstream HTTP status in the thrown error and makes wallet-RPC failures debuggable. Additionally, the fetch has no timeout — a stalled RPC will hang the wallet flow indefinitely; anAbortSignal.timeout(...)would bound it.♻️ Proposed refactor
function makeStrictJsonRpcTransport(rpcUrl: string) { return custom({ async request({ method, params }) { const id = rpcIdCounter++; const res = await fetch(rpcUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id, method, params }), + signal: AbortSignal.timeout(30_000), }); + if (!res.ok) { + const text = await res.text().catch(() => ''); + const err: any = new Error( + `RPC HTTP ${res.status} ${res.statusText}${text ? `: ${text.slice(0, 200)}` : ''}`, + ); + err.status = res.status; + throw err; + } const json = await res.json(); if (json.error) { const err: any = new Error(json.error.message ?? 'RPC error'); err.code = json.error.code; err.data = json.error.data; throw err; } return json.result; }, }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/hooks/useAppKit.ts` around lines 15 - 34, In makeStrictJsonRpcTransport, guard against non-2xx and non-JSON responses and add a request timeout: check res.ok immediately after fetch and if false throw an Error that includes res.status and res.statusText (and, when possible, the response text/body) instead of blindly calling res.json(); only call res.json() when content-type indicates JSON (or wrap parsing in try/catch and surface parse errors). Also create an AbortController/AbortSignal.timeout (or AbortController with setTimeout) and pass its signal to fetch so the request is aborted after a bounded timeout, ensuring the thrown error on timeout is handled the same way as HTTP errors; update the request implementation inside makeStrictJsonRpcTransport to use these checks and signal handling.frontend/src/stores/transactions.ts (1)
218-222: Initial poll kick-off runs before transactions are hydrated — benign but wasteful.At store creation,
allTransactionsis[]; the watcher only fires onchainIdchanges, so this guard correctly handles the "user lands directly on testnet" case. The interval will tick (every 5s) finding nothing untilsetAllTransactions(...)runs inuseSetupStores. Functionally correct, but you could defer kick-off tosetAllTransactionsto avoid empty ticks and slightly cleaner ordering:Optional: gate the kick-off on hydration
function setAllTransactions(items: TransactionItem[]) { allTransactions.value = items; + if (!networkStore.isStudio) startUndecidedPolling(); } @@ - // Kick off non-Studio polling at store creation if the user lands directly - // on a testnet (chainId watcher only fires on changes, not on initial load). - if (!networkStore.isStudio) { - startUndecidedPolling(); - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/stores/transactions.ts` around lines 218 - 222, The current store starts startUndecidedPolling() during creation when networkStore.isStudio is false, causing empty interval ticks before transactions are hydrated into allTransactions; instead, remove that initial call and kick off polling from setAllTransactions once hydration occurs (or update setAllTransactions to start startUndecidedPolling when it sets a non-empty allTransactions and networkStore.isStudio is false), referencing startUndecidedPolling, setAllTransactions, allTransactions, and networkStore.isStudio so polling only begins after transactions are hydrated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/stores/transactions.ts`:
- Around line 48-59: The watch callback currently calls subscriptions.clear()
which only mutates the local Set and can leave server-side WS topic
registrations active; before clearing, iterate the existing subscriptions Set
and emit an 'unsubscribe' message for each topic through the WS client (same
client used by initSubscriptions()/initSubscriptions flow), then clear the Set
and proceed to
stopUndecidedPolling()/initSubscriptions()/startUndecidedPolling() as before so
old-chain server-side subscriptions are removed even when the WS URL doesn't
change.
---
Outside diff comments:
In `@frontend/src/stores/transactions.ts`:
- Around line 109-131: updateTransaction uses tx.hash ?? tx.txId to compute id
but then looks up rows by t.hash, which will miss entries if a non-Studio
response with only txId reaches this function; update the code to either assert
the invariant that stored transactions always have a hash (e.g., add a runtime
check in updateTransaction) or normalize incoming tx objects by stamping tx.hash
= tx.hash ?? tx.txId before the lookup; reference the updateTransaction function
and the addTransaction/refreshPendingTransactions flow in your change so the
contract is preserved and failures become explicit rather than silent warns.
- Around line 137-163: refreshPendingTransactions currently deletes transactions
when getTransaction returns undefined, causing premature removal during
pre-indexing RPC misses; modify addTransaction to record a submission timestamp
(e.g., addedAt) into the TransactionItem and persist it to db.transactions, then
change refreshPendingTransactions to avoid immediate deletion: when
getTransaction returns undefined, check the tx.addedAt (and optionally a miss
counter stored in-memory or in db) and only call
removeTransaction()/db.transactions.delete() after a configurable grace period
(e.g., 30s) or N consecutive misses; keep immediate updates/deletes when
getTransaction returns a real "not found" signal if your SDK provides one,
otherwise rely on the timestamp/grace logic; update any references to
transactionsStatusNameToNumber and db.transactions.where('hash') to use the new
fields consistently.
---
Nitpick comments:
In `@frontend/src/hooks/useAppKit.ts`:
- Around line 15-34: In makeStrictJsonRpcTransport, guard against non-2xx and
non-JSON responses and add a request timeout: check res.ok immediately after
fetch and if false throw an Error that includes res.status and res.statusText
(and, when possible, the response text/body) instead of blindly calling
res.json(); only call res.json() when content-type indicates JSON (or wrap
parsing in try/catch and surface parse errors). Also create an
AbortController/AbortSignal.timeout (or AbortController with setTimeout) and
pass its signal to fetch so the request is aborted after a bounded timeout,
ensuring the thrown error on timeout is handled the same way as HTTP errors;
update the request implementation inside makeStrictJsonRpcTransport to use these
checks and signal handling.
In `@frontend/src/stores/transactions.ts`:
- Around line 218-222: The current store starts startUndecidedPolling() during
creation when networkStore.isStudio is false, causing empty interval ticks
before transactions are hydrated into allTransactions; instead, remove that
initial call and kick off polling from setAllTransactions once hydration occurs
(or update setAllTransactions to start startUndecidedPolling when it sets a
non-empty allTransactions and networkStore.isStudio is false), referencing
startUndecidedPolling, setAllTransactions, allTransactions, and
networkStore.isStudio so polling only begins after transactions are hydrated.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 54433889-3450-4e7e-87f1-bc3719f565a8
📒 Files selected for processing (5)
frontend/src/hooks/useAppKit.tsfrontend/src/hooks/useContractListener.tsfrontend/src/stores/transactions.tsfrontend/test/unit/hooks/useContractListener.behavioral.test.tsfrontend/test/unit/hooks/useContractListener.test.ts
|
🎉 This PR is included in version 0.118.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
Adds a header dropdown that lets the hosted Studio frontend target either the local Studio backend or the Bradbury testnet at runtime, instead of being pinned to a single backend at build time. Contract deploy, read, write, simulate, and attach all work on both targets. Studio-only features (node logs, validators, providers, finality tuning, internal faucet) hide cleanly on testnet.
Full plan:
docs/plans/studio-network-selector.md.What's in it
Foundations
networkStore(Pinia, persisted): exposescurrentNetwork,chain,rpcUrl,wsUrl,chainId,isStudio,availableNetworks,isLocked.rpc.tsresolves the URL from the store per call; on non-Studio networks it skips the WS session header entirely.useGenlayerwatchescurrentNetwork/rpcUrland re-inits the genlayer-js client on change.useAppKitregisters Bradbury alongside the Studio localnet so MetaMask can switch without a manual add step.useChainEnforcerreads chain info from the network store and accepts an explicit target override (used by the selector on switch).Data scoping (Dexie v5)
deployedContractsandtransactionsgain achainIdcolumn; existing rows backfill to the build-timeVITE_CHAIN_IDvia the upgrade hook.deployedContracts/transactionscomputeds scoped to the current chain; internal flat state stays accessible viaallDeployedContracts/allTransactions+ setter actions.Graceful degradation on non-Studio networks
useConfigaddsisStudioNetwork; validator / provider / finality-window gates now require both!hostedANDisStudioNetwork.nodeStore.getValidatorsData/getProvidersDataandconsensusStore.fetchFinalityWindowTimebail early on testnet.useSetupStoresskips the WS wait and sim_* warmup when non-Studio.https://testnet-faucet.genlayer.foundationon testnet (100 GEN / 24h, gated on 0.01 ETH mainnet).Also
genlayer-js^0.28.4→^1.1.0(picks up the SDK patch that hides thegen_getContractSchemasemantic divergence between Studio and the node — seedocs/plans/studio-testnet-format-alignment.md§ Schema method divergence).Open resolutions captured in the plan
VITE_GENLAYER_NETWORK=testnetAsimovdeployment override.sim_lintContract): stays pointed at the Studio backend regardless of target (linter is a developer tool, not a chain operation).eth_getTransactionByHashvia the existingrefreshPendingTransactionspath.Test plan
Automated:
npm run build— clean (vue-tsc passes)npm run lint— 0 errors (pre-existing warnings only)npm test— 189/189 pass, including 5 new cases:RpcClient.behavioral: non-Studio skips WS, resolves URL from storeuseGenlayer: picks chain from network store; honors BradburyuseConfig:isStudioNetworkflip;canUpdateValidatorsfalse on testnetuseSetupStores: sim_* warmup skipped on non-StudioconsensusStore:fetchFinalityWindowTimeno-op on non-StudioManual (to run before merge):
genlayer-js@1.1.0patch to be deployed)testnet-faucet.genlayer.foundationin a new tabVITE_LOCK_NETWORK=truehides the dropdownSummary by CodeRabbit
New Features
Bug Fixes
Documentation
Chores