Skip to content

feat(frontend): network selector for Studio / Bradbury targets#1606

Merged
MuncleUscles merged 14 commits intomainfrom
feat/network-selector
Apr 28, 2026
Merged

feat(frontend): network selector for Studio / Bradbury targets#1606
MuncleUscles merged 14 commits intomainfrom
feat/network-selector

Conversation

@MuncleUscles
Copy link
Copy Markdown
Member

@MuncleUscles MuncleUscles commented Apr 22, 2026

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

  • New networkStore (Pinia, persisted): exposes currentNetwork, chain, rpcUrl, wsUrl, chainId, isStudio, availableNetworks, isLocked.
  • Reactive RPC URL — rpc.ts resolves the URL from the store per call; on non-Studio networks it skips the WS session header entirely.
  • useGenlayer watches currentNetwork/rpcUrl and re-inits the genlayer-js client on change.
  • 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 via the upgrade hook.
  • Stores expose filtered deployedContracts / transactions computeds 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 (100 GEN / 24h, gated on 0.01 ETH mainnet).

Also

  • Bumps genlayer-js ^0.28.4^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).

Open resolutions captured in the plan

  • Chain ID collision Asimov/Bradbury (4221): intentional, same underlying network. v1 ships Bradbury only; Asimov reachable via VITE_GENLAYER_NETWORK=testnetAsimov deployment override.
  • Monaco linter (sim_lintContract): stays pointed at the Studio backend regardless of target (linter is a developer tool, not a chain operation).
  • WebSocket on testnet: dropped entirely; tx status falls back to polling eth_getTransactionByHash via the existing refreshPendingTransactions path.

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 store
    • useGenlayer: picks chain from network store; honors Bradbury
    • useConfig: isStudioNetwork flip; canUpdateValidators false on testnet
    • useSetupStores: sim_* warmup skipped on non-Studio
    • consensusStore: fetchFinalityWindowTime no-op on non-Studio

Manual (to run before merge):

  • Fresh load on Studio localnet — deploy / read / write / simulate / attach
  • Switch to Bradbury via dropdown — MetaMask prompt fires, wallet chain switches, contract interactions route to Bradbury
  • Reject the MetaMask switch — dropdown reverts, toast shows
  • Reload — last-selected network is restored
  • Contract attach on Bradbury (requires genlayer-js@1.1.0 patch to be deployed)
  • Faucet button on Bradbury opens testnet-faucet.genlayer.foundation in a new tab
  • Dexie v5 upgrade on existing users — contracts and pending txs stay visible and scoped to localnet
  • VITE_LOCK_NETWORK=true hides the dropdown

Summary by CodeRabbit

  • New Features

    • Header network selector with persisted runtime network switching, wallet chain enforcement, and reactive RPC/WebSocket/client behavior.
    • Multi-network support: per-network scoping for contracts/transactions, DB migration/backfill, and network-aware UI/layout and simulator behavior.
    • Testnet flow: external faucet link and gated Studio-only tooling.
  • Bug Fixes

    • Prevent cross-network transaction collisions, preserve pending txs per network, and revert UI on wallet switch rejection.
  • Documentation

    • Added network selector implementation plan and updated RPC method availability/divergence docs.
  • Chores

    • Updated client dependency to a newer genlayer-js release.

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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a persisted Pinia networkStore, NetworkSelector UI, and multi-network runtime support: RPC/WS/genlayer client lifecycles become network-aware, DB/store schemas and migrations gain chain scoping, Studio-only features are gated, and tests/adapters updated; also bumps genlayer-js.

Changes

Cohort / File(s) Summary
Docs & Plans
docs/plans/studio-network-selector.md, docs/plans/studio-testnet-format-alignment.md
New Studio Network Selector design and testnet RPC/schema availability/divergence documentation.
Network Store & Runtime Config
frontend/src/stores/network.ts, frontend/src/stores/index.ts, frontend/src/utils/runtimeConfig.ts
Add persisted networkStore (currentNetwork, chain/rpc/ws resolution, isLocked, availableNetworks) and export; add VITE_LOCK_NETWORK runtime key.
UI: Selector & Header
frontend/src/components/global/NetworkSelector.vue, frontend/src/components/Header.vue
New header dropdown to switch networks with switch concurrency guard, pending-tx warning, wallet enforce/revert flow.
RPC Client & Linting
frontend/src/clients/rpc.ts, frontend/src/services/monacoLinter.ts
RPC endpoint resolves from networkStore.rpcUrl; monotonic numeric request IDs; x-session-id only for Studio; Studio-only WS readiness gating; linter uses direct Studio fetch and soft-fails if method missing.
WebSocket Client Lifecycle
frontend/src/hooks/useWebSocketClient.ts
URL-keyed singleton with teardown/recreate on URL change, nullClient fallback, exported disposeWebSocketClient/ensureWebSocketClient, and updated async signatures.
GenLayer / AppKit / Chains
frontend/src/hooks/useGenlayer.ts, frontend/src/hooks/useNetworks.ts, frontend/src/hooks/useAppKit.ts
useGenlayer re-inits from networkStore.chain/rpcUrl; createGenlayerLocalnet reads runtime overrides; add createGenlayerBradbury; include Bradbury in AppKit networks and strict transport.
Chain Enforcer & Config
frontend/src/hooks/useChainEnforcer.ts, frontend/src/hooks/useConfig.ts
ensureCorrectChain accepts optional target and uses networkStore defaults; useChainEnforcer returns isExternalWallet; useConfig exposes reactive isStudioNetwork and makes capability flags computed.
DB Migration & Setup
frontend/src/hooks/useDb.ts, frontend/src/hooks/useSetupStores.ts
Dexie v5 migration adds chainId indices and backfills legacy rows; conditional Studio-only setup; hydrations use setAll* APIs; example contract loading fault-tolerant.
Multi-chain Store Refactors
frontend/src/stores/contracts.ts, frontend/src/stores/transactions.ts, frontend/src/stores/node.ts, frontend/src/stores/consensus.ts
Stores keep canonical all* lists and expose chain-filtered computed views; actions stamp records with chainId; Studio-only RPC calls short-circuit on non-Studio networks.
UI Conditional Changes
frontend/src/components/Simulator/AccountSelect.vue, frontend/src/views/Simulator/SimulatorView.vue, frontend/src/views/Simulator/RunDebugView.vue, frontend/src/components/Tutorial/TutorialContainer.vue
Faucet links to external testnet when non-Studio; hide NodeLogs/Studio panels when not Studio; tutorial/validator UI gated by isStudioNetwork.
Types
frontend/src/types/store.ts
Add optional chainId?: number to DeployedContract and TransactionItem.
Tests
frontend/test/unit/**, frontend/test/unit/hooks/**, frontend/test/unit/stores/**
Updated tests to mock/use networkStore, assert chain-stamping, WS URL behavior, computed refs, and use new setter-based hydration APIs.
Dependency & Build
frontend/package.json, frontend/vite.config.ts
Bump genlayer-js to ^1.1.1; add Vite server.fs.allow: ['..'].

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

run-tests

Suggested reviewers

  • epsjunior
  • cristiam86
  • kstroobants

Poem

🐰 Hopping between chains with glee,

I remember which one you chose for me.
WS sleeps when Studio's away,
RPC and DB keep each chain at bay.
Switch with care — a rabbit’s hooray! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(frontend): network selector for Studio / Bradbury targets' clearly summarizes the main change—adding a runtime network selector dropdown for the frontend that allows switching between Studio and Bradbury. It is concise, follows conventional commits, and directly reflects the primary feature in the changeset.
Description check ✅ Passed The PR description is comprehensive and well-structured. It includes a clear summary, itemized 'What's in it' sections covering foundations, data scoping, and graceful degradation, specific testing details (automated and manual), and documented open resolutions. It goes beyond the template structure but provides complete context about the implementation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/network-selector

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Reset 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 / isLoadingProviders can remain at their initial true values 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 | 🟠 Major

Rebind node log listeners when the WebSocket endpoint changes.

webSocketClient is 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 | 🟠 Major

Resolve the async hook for no-op WebSocket clients.

When useWebSocketClient() returns nullClient, 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 | 🟠 Major

Keep 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.ts and frontend/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 against networkStore.chainId explicitly.

toBeTypeOf('number') confirms the field is stamped but doesn't verify it matches the current network. Since addDeployedContract uses chainId ?? networkStore.chainId, an equality check against useNetworkStore().chainId would 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: steps is not reactive to canUpdateValidators changes.

steps is initialized once with a literal .value snapshot, so if networkStore.currentNetwork changes after this component mounts (now possible at runtime via the new selector), the Validators step text won't update until the component remounts. Consider making steps a computed, or reading canUpdateValidators.value lazily inside a getter/template binding on TutorialStep.

♻️ 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_URL and a Bradbury-specific tooltip ("100 GEN / 24h, requires 0.01 ETH on mainnet") on the v-else branch will force this component to be edited each time. Moving the faucet URL (and ideally the tooltip copy) onto the chain definition consumed via networkStore would 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: Reset isStudio in a finally block (or afterEach) to avoid cross-test pollution.

If any assertion above throws, mockNetworkStore.isStudio = true never runs and subsequent tests inherit isStudio = 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.on only proves no listener was registered. To protect the “skip WS entirely” contract, also assert useWebSocketClient itself 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 before ensureCorrectChain(). This immediately updates networkStore.chain/rpcUrl, causing the useGenlayer watcher to re-init the client and useTransactionsStore's watch(networkStore.chainId) to clear WS subscriptions — all before the wallet confirms the chain switch. If the user rejects (4001), you setCurrentNetwork(previous) which triggers another round of re-init/resubscribe.

Two minor consequences worth considering:

  1. UX flicker: the header label and Dexie-scoped transactions list momentarily switch, then snap back.
  2. An in-flight RPC issued against the new chain during this window can fail unexpectedly.

Passing an explicit target to ensureCorrectChain(...) (the hook already supports it) and only calling setCurrentNetwork once 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2ae6e87 and 8e5b88c.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (35)
  • docs/plans/studio-network-selector.md
  • docs/plans/studio-testnet-format-alignment.md
  • frontend/package.json
  • frontend/src/clients/rpc.ts
  • frontend/src/components/Header.vue
  • frontend/src/components/Simulator/AccountSelect.vue
  • frontend/src/components/Tutorial/TutorialContainer.vue
  • frontend/src/components/global/NetworkSelector.vue
  • frontend/src/hooks/useAppKit.ts
  • frontend/src/hooks/useChainEnforcer.ts
  • frontend/src/hooks/useConfig.ts
  • frontend/src/hooks/useDb.ts
  • frontend/src/hooks/useGenlayer.ts
  • frontend/src/hooks/useNetworks.ts
  • frontend/src/hooks/useSetupStores.ts
  • frontend/src/hooks/useWebSocketClient.ts
  • frontend/src/stores/consensus.ts
  • frontend/src/stores/contracts.ts
  • frontend/src/stores/index.ts
  • frontend/src/stores/network.ts
  • frontend/src/stores/node.ts
  • frontend/src/stores/transactions.ts
  • frontend/src/types/store.ts
  • frontend/src/utils/runtimeConfig.ts
  • frontend/src/views/Simulator/SimulatorView.vue
  • frontend/test/unit/hooks/useConfig.test.ts
  • frontend/test/unit/hooks/useGenlayer.test.ts
  • frontend/test/unit/hooks/useSetupStores.behavioral.test.ts
  • frontend/test/unit/hooks/useWebSocketClient.test.ts
  • frontend/test/unit/services/RpcClient.behavioral.test.ts
  • frontend/test/unit/stores/consensus.behavioral.test.ts
  • frontend/test/unit/stores/contracts.behavioral.test.ts
  • frontend/test/unit/stores/contracts.test.ts
  • frontend/test/unit/stores/transactions.behavioral.test.ts
  • frontend/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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +21 to +34
// 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 ?? '';
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
// 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.

Comment on lines +103 to +109
<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>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested 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"
>
{{ 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.

Comment on lines +36 to +44
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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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).

Comment on lines +89 to +118
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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
frontend/src/clients/rpc.ts (1)

29-37: ⚠️ Potential issue | 🟠 Major

Bound 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8e5b88c and c3a6f9d.

📒 Files selected for processing (3)
  • frontend/src/clients/rpc.ts
  • frontend/src/hooks/useSetupStores.ts
  • frontend/vite.config.ts

Comment on lines +28 to +32
// 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();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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/null

Repository: 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 1

Repository: 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 10

Repository: 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.ts

Repository: 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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Execution mode stays NORMAL when hidden on testnet — confirm that's the intended default.

When isStudioNetwork is false the selector is hidden, but executionMode keeps its initial 'NORMAL' value and is still forwarded to ConstructorParameters, ContractReadMethods, and ContractWriteMethods. If a user switches networks after picking 'LEADER_ONLY', that stale value remains in effect on the testnet path. Consider resetting executionMode to 'NORMAL' when isStudioNetwork flips 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 @/hooks imports.

useContractQueries and useConfig are both exported from @/hooks but 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

📥 Commits

Reviewing files that changed from the base of the PR and between c3a6f9d and aa3c1d8.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • frontend/package.json
  • frontend/src/services/monacoLinter.ts
  • frontend/src/utils/explorerUrl.ts
  • frontend/src/views/Simulator/RunDebugView.vue
✅ Files skipped from review due to trivial changes (1)
  • frontend/package.json

Comment on lines +55 to 73
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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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).
@MuncleUscles MuncleUscles force-pushed the feat/network-selector branch from 27dba04 to 5f98127 Compare April 27, 2026 20:31
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Polling amplifies the silent-drop behavior on transient RPC misses.

refreshPendingTransactions treats a falsy getTransaction result 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 returns null (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 outer try/catch keeps 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.

setInterval schedules a new tick every 5s regardless of whether the previous refreshPendingTransactions has resolved. On slow testnet RPCs or with many pending txs, Promise.all over getTransaction calls can easily exceed 5s, causing overlapping refreshes that hammer the RPC, mutate allTransactions concurrently, and can race the removeTransaction path on transient null responses. Prefer a re-entrancy guard or a self-rescheduling setTimeout.

♻️ 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

📥 Commits

Reviewing files that changed from the base of the PR and between aa3c1d8 and 27dba04.

📒 Files selected for processing (1)
  • frontend/src/stores/transactions.ts

Comment on lines +24 to +28
const transactions = computed<TransactionItem[]>(() =>
allTransactions.value.filter(
(t) => t.chainId === undefined || t.chainId === networkStore.chainId,
),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 -100

Repository: 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 3

Repository: 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.ts

Repository: 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 2

Repository: 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 -100

Repository: 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 30

Repository: 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 1

Repository: 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 15

Repository: 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 3

Repository: genlayerlabs/genlayer-studio

Length of output: 54


🏁 Script executed:

# Search for setAllTransactions calls across the codebase
rg -n 'setAllTransactions' frontend/src

Repository: 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 2

Repository: 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.ts

Repository: 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.ts

Repository: genlayerlabs/genlayer-studio

Length of output: 762


🏁 Script executed:

# Find where useSetupStores is called
rg -n 'useSetupStores' frontend/src -B 2 -A 2

Repository: 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.ts

Repository: 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:


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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

updateTransaction falls back to txId, but in-memory rows are keyed only by hash.

tx.hash ?? tx.txId is resolved against t.hash === id, which works only because addTransaction always stamps the original hash and the polling commit message ("stamp the original hash onto SDK getTransaction responses") preserves it. If anywhere in the codebase a non-Studio getTransaction response reaches updateTransaction without that re-stamping (e.g., a future caller that bypasses refreshPendingTransactions), 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 | 🟠 Major

Unindexed testnet transactions can be prematurely deleted on transient RPC misses during the pre-indexing window.

refreshPendingTransactions immediately deletes both the in-memory entry and the Dexie row whenever getTransaction returns 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 addTransaction and 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. Checking res.ok first 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; an AbortSignal.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, allTransactions is []; the watcher only fires on chainId changes, so this guard correctly handles the "user lands directly on testnet" case. The interval will tick (every 5s) finding nothing until setAllTransactions(...) runs in useSetupStores. Functionally correct, but you could defer kick-off to setAllTransactions to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 27dba04 and d5600d7.

📒 Files selected for processing (5)
  • frontend/src/hooks/useAppKit.ts
  • frontend/src/hooks/useContractListener.ts
  • frontend/src/stores/transactions.ts
  • frontend/test/unit/hooks/useContractListener.behavioral.test.ts
  • frontend/test/unit/hooks/useContractListener.test.ts

Comment thread frontend/src/stores/transactions.ts
@MuncleUscles MuncleUscles merged commit f3a77d0 into main Apr 28, 2026
10 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 0.118.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant