Skip to content

feat: mock development environment (wallet, RPC, GraphQL) - #72

Draft
belopash wants to merge 70 commits into
developfrom
cursor/mock-wallet-d5dd
Draft

feat: mock development environment (wallet, RPC, GraphQL)#72
belopash wants to merge 70 commits into
developfrom
cursor/mock-wallet-d5dd

Conversation

@belopash

@belopash belopash commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Set MOCK_WALLET=true in .env to activate a fully offline development stack — no browser wallet, no live APIs needed.

What it does

pnpm dev with MOCK_WALLET=true starts two extra in-process servers and replaces RainbowKit with a simple account picker:

Component Detail
Mock wallet wagmi mock connector — no MetaMask/WalletConnect needed
Mock RPC (port 8545) In-process EIP-1193 server; handles reads, writes, receipts
Mock GraphQL (port 4321) Returns fixture data for every Squid operation

Four fixture personas

Address Label Pre-loaded state
0xf39F…2266 Alice (empty) No SQD, no history
0x7099…79C8 Bob (delegator) 200k SQD, 50k delegated
0x3C44…93BC Carol (worker operator) 500k SQD, 2 registered workers
0x90F7…b406 Dave (vesting) 50k SQD + 1M in vesting

Account switching uses the real disconnect → connect → select flow. State (transactions, balances) lives only for the dev server process lifetime.

Usage

# .env
MOCK_WALLET=true          # master switch
MOCK_RPC_PORT=8545        # optional
MOCK_GRAPHQL_PORT=4321    # optional
pnpm dev   # app starts at localhost:3005 — click Connect Wallet to pick a persona

Key files

  • packages/server/src/services/mockRpcServer.ts — EIP-1193 server with in-memory EVM state
  • packages/server/src/services/mockGraphqlServer.ts — fixture responses for all 30+ GraphQL operations
  • packages/client/src/components/MockConnectDialog.tsx — persona picker dialog
  • packages/client/src/config.tsbuildMockConfig(), sessionStorage-based account selection
Open in Web Open in Cursor 

cursoragent and others added 2 commits April 24, 2026 21:56
Introduces a MOCK_WALLET_ADDRESS environment variable that, when set to a
valid 0x-prefixed Ethereum address, makes the app use wagmi's built-in mock
connector instead of RainbowKit.  The mock connector auto-connects on startup
so the entire app is immediately in a 'connected' state without requiring a
browser wallet extension or WalletConnect session.

Changes:
- packages/client/vite.config.ts   — expose MOCK_WALLET_ADDRESS as a build-time define
- packages/client/src/config.ts    — export mockConfig (createConfig + mock connector) when the var is set
- packages/client/src/components/MockWalletAutoConnect.tsx — new component that calls connect() on the mock connector on first render
- packages/client/src/App.tsx      — branch on mockConfig to swap in the lightweight mock provider tree (no RainbowKitProvider)
- .env.example                     — document the new variable

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Account switcher (client):
- packages/client/src/components/MockAccountSwitcher.tsx — new component
  rendered inside UserMenu when MOCK_WALLET_ADDRESS is set. Shows 4 preset
  Hardhat addresses plus a free-text input; selecting any address persists
  it to localStorage and reloads the page so the mock wagmi connector picks
  it up without restarting the dev server.
- packages/client/src/config.ts — resolveMockAddress() prefers the
  localStorage key (set by MockAccountSwitcher) over the build-time env var,
  enabling per-reload address switching without a Vite restart.
- packages/client/src/layouts/NetworkLayout/UserMenu.tsx — render
  MockAccountSwitcher at the bottom of the dropdown in mock mode only.

Mock GraphQL fixture server (server):
- packages/server/src/services/mockGraphqlServer.ts — lightweight in-process
  HTTP server (Node http, zero extra deps) that returns realistic canned
  responses for every Squid GraphQL operation (workers, gateways, token).
  Activated by MOCK_GRAPHQL=true, listens on MOCK_GRAPHQL_PORT (default 4321).
- packages/server/src/env.ts — isMockGraphql() helper; getWorkers/Gateways/
  TokenSquidUrl() all redirect to the mock server URL when active.
- packages/server/src/main.ts — await startMockGraphqlServer() before the
  tRPC server starts when MOCK_GRAPHQL=true.
- .env.example — documents MOCK_GRAPHQL and MOCK_GRAPHQL_PORT variables.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
@cursor cursor Bot changed the title feat: mock wallet connector for local development feat: mock wallet, account switcher, and mock GraphQL fixture server Apr 24, 2026
cursoragent and others added 2 commits April 24, 2026 23:08
…lect-account flow

- Single MOCK_WALLET=true env flag activates everything (no more MOCK_WALLET_ADDRESS,
  MOCK_GRAPHQL separate flags)
- 4 fixture accounts (Alice/Bob/Carol/Dave) with different roles and balances
- Account selection dialog (MockConnectDialog) with role descriptions
- sessionStorage persists the selected account across page reloads
- MockWalletAutoConnect re-fires connect() after reload when selection is stored
- Disconnect clears sessionStorage so next connect starts fresh
- Mock RPC server (mockRpcServer.ts) handles eth_call, eth_sendTransaction,
  eth_getBalance etc. with in-memory state that resets on restart
- Mock GraphQL server returns account-scoped data per fixture persona
- ConnectButton branches on isMockMode constant to avoid calling useConnectModal
  outside RainbowKitProvider

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
mockGraphqlServer.ts:
- Rewrote with full field coverage matching exact GraphQL document shapes
- Fixed workersSummary to use onlineWorkersCount (not onlineWorkers)
- Added all worker fields: online, jailed, dialOk, statusHistory, uptime24Hours,
  capedDelegation, delegationCount, locked, lockEnd, bond, claimableReward,
  claimedReward, queries24Hours/90Days, servedData, storedData, dayUptimes
- currentEpoch: blockTimeL1, lastBlockL1, lastBlockTimestampL1 (used by epoch timer)
- myDelegations returns Bob's delegation with deposit/claimableReward/claimedReward
- myWorkers/myWorkersCount returns Carol's 2 workers correctly
- vestingsByAccount/vestingByAddress/accountsByOwner return Dave's vesting account
- All timeseries now use requested from/to dates (not hardcoded PAST/NOW)
- RewardTimeseries: proper {workerReward, stakerReward} value shape
- AprTimeseries: proper {workerApr, stakerApr} value shape
- TransfersByTypeTimeseries: proper {deposit,withdraw,transfer,reward,release} shape
- workerDelegationInfo: adds liveness/dTenure/trafficWeight/settings fields
- 4 workers on the public list (Alice's, Bob's, Carol's x2) for realistic list view

mockRpcServer.ts:
- handleMulticall3: returns proper ABI-encoded Result[] instead of empty array,
  so wagmi useReadContracts gets success=true + 32-byte zero per call rather
  than treating the response as a failed multicall
- All unrecognised eth_call selectors now return 32-byte zero instead of '0x'

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
@cursor cursor Bot changed the title feat: mock wallet, account switcher, and mock GraphQL fixture server feat: full mock development environment (wallet, RPC, GraphQL) Apr 24, 2026
belopash and others added 24 commits April 25, 2026 15:35
- plans/wagmi-viem-testing.md: mock environment plan (Anvil + TS deploy
  harness + log-driven mini-indexer + Vitest unit/integration); replaces
  the hand-rolled mock RPC and mock GraphQL servers.
- plans/playwright-e2e.md: post-connect app-flow E2E plan built on top
  of the mock-stack package.

Made-with: Cursor
Adds the Vitest test runner to both @subsquid/client and @subsquid/server.

@subsquid/client:
- Two Vitest projects: 'unit' (jsdom, parallel threads, canned RPC transport)
  and 'integration' (jsdom, single-fork, will be wired to Anvil + mini-indexer
  via @subsquid/mock-stack in Phase 8).
- src/test/setup.ts wires @testing-library/jest-dom + post-test cleanup.
- src/test/render.tsx exposes renderWithProviders + TestProviders helpers.
- src/test/wagmi/{testConfig,customTransport}.ts: wagmi mock connector +
  viem 'custom' transport for deterministic Layer-1 hook tests.
- src/test/anvil/{global-setup,snapshot}.ts: placeholders for Phase 8.
- Drop @types/jest, add vitest/jsdom/@testing-library/* /msw deps.
- Scripts: test, test:unit, test:integration, test:watch.

@subsquid/server:
- Single 'node' Vitest project. test + test:watch scripts.

Root:
- turbo.json: 'test' task with dependsOn ['^build', 'transit'].
- package.json: 'test' script.

This unblocks Layer-1 test development. Layer-2 lights up in Phase 8 when the
mock-stack package + anvil bootstrap land.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Replaces module-level mockConfig/rainbowConfig constants with a single
factory that App.tsx calls inside a useMemo. Tests can now obtain a wagmi
config matching the production tree without depending on import-time env
reads.

App.tsx collapses the two WagmiProvider branches into one tree with the
RainbowKitProvider / MockWalletAutoConnect choice happening inside.

Mock account selection still uses the existing sessionStorage + reload
flow (MockConnectDialog unchanged) — Phase 10 will replace that with a
connector 'change' event once the mock-stack package and friends land.

Investigation note (recorded for Phase 10): wagmi.useSwitchAccount
switches between connector instances, not accounts within one connector,
so it's the wrong tool. The wagmi mock connector's onAccountsChanged hook
emits a 'change' event that updates connection.accounts reactively — the
new switcher will call connector.emitter.emit('change', { accounts: [...] })
with the picked address hoisted to index 0.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Adds connectMockConnector() helper that programmatically connects the
wagmi mock connector before tests render hooks — wagmi's
defaultConnected feature flips an internal flag but doesn't emit a
connect event, so useAccount() would otherwise report disconnected on
first render.

Strips the multicall3 contract from chains exposed by createUnitWagmiConfig
so useReadContract calls hit eth_call directly with human-readable
function selectors instead of being aggregated through Multicall3.aggregate3.

Reference spec wagmi-harness.test.tsx covers:
- useAccount resolves the first fixture address from the mock connector
- useReadContract resolves with a value supplied by the canned RPC map

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Adds the @subsquid/mock-stack workspace package — the single owner of
the mock environment lifecycle (anvil + deploy harness + mini-indexer).

Public API:
  startMockStack(opts) → MockStackHandle
    .rpcUrl, .graphqlUrl, .deployments, .indexer, .stop()

Phase 4 ships the skeleton:
  - package.json with stack:prepare / stack:rebuild scripts (avoiding
    the 'prepare' name which pnpm treats as a lifecycle hook)
  - tsconfig.json + turbo.json with cache inputs covering both
    submodules and contracts/
  - foundry.toml ready for Phase 5 mock contracts
  - src/index.ts exporting the public types; startMockStack() throws
    until Phases 5–7 land
  - scripts/prepare.ts stub
  - PORT_VERSION placeholder for the upstream squid commit pin
  - README.md describing intent + status

@subsquid/mock-stack added as a devDependency of @subsquid/client so
Vitest globalSetup (Phase 8) can import startMockStack() without
declaring a per-spec dep.

.gitignore covers out/, .anvil-state.json, .deployments.json, lib/.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
…lticall3)

contracts/MockERC20.sol — minimal ERC-20 base with public mint/burn,
plus MockSQD (18 dec), MockUSDC (6 dec), MockWETH (18 dec, +
deposit/withdraw for raw ETH wrapping). Inlined to keep forge build
fast and free of external lib dependencies.

contracts/MockV3Router.sol — minimal Pancake/Uniswap V3 SwapRouter
stub. Implements exactInputSingle and exactInput with deterministic
1:1 swaps and no AMM math; emits MockSwap so the indexer can track
trades. Tokens are pre-funded into the router by the deploy harness
(Phase 6).

contracts/Multicall3.sol — vendored copy of the canonical mds1
Multicall3 contract. Deployed at a non-canonical address; the wagmi
config picks up the actual address via createAppWagmiConfig's
multicall3Override option (driven by .deployments.json in mock mode).

forge build produces:
  out/MockERC20.sol/{MockERC20,MockSQD,MockUSDC,MockWETH}.json
  out/MockV3Router.sol/MockV3Router.json
  out/Multicall3.sol/Multicall3.json

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Submodule:
- sqd-network-contracts (branch: main) added at /sqd-network-contracts.
  forge build produces artifacts at packages/contracts/artifacts/<File>.sol/<Contract>.json.

mock-stack package:
- src/chain.ts: spawnAnvil() with eth_blockNumber boot polling. Resolves
  the anvil binary via FOUNDRY_PATH or ~/.foundry/bin, falling back to PATH.
- src/artifacts.ts: typed readers for local mock-stack artifacts +
  network-contracts + portal-contracts submodules.
- src/personas.ts: 4 fixture personas (Alice/Bob/Carol/Dave) with
  deterministic anvil dev keys, mirroring MOCK_FIXTURE_ACCOUNTS in
  client/config.ts.
- src/deploy.ts: TS deploy harness. Phase 6 ships the minimal
  mock-only deploy (Multicall3 + MockSQD/USDC/WETH + MockV3Router) so
  consumers can build against the harness. Network/portal contract
  deploys layer in incrementally as Phase 8 specs need them. Also
  exports dumpAnvilState() / loadAnvilState() — these use the
  hex-compressed gzip format from anvil_dumpState/anvil_loadState
  RPC methods, NOT the --load-state CLI flag (which expects raw JSON).
- src/deployments.ts: read/write .deployments.json.
- scripts/prepare.ts: orchestrates spawn → deploy → dump state →
  shutdown. Smoke-tested round-trip: dump → fresh anvil → loadState
  → balances + bytecode preserved.
- scripts/_round-trip-smoke.ts: manual smoke test (not wired to a
  Turbo task; underscore-prefixed).

Address override:
- @subsquid/common exports getContractAddresses({ network?, override? })
  so callers can layer in mock-stack-deployed addresses.
- packages/server/src/env.ts.getContractAddresses() now reads
  packages/mock-stack/.deployments.json (or MOCK_STACK_DEPLOYMENTS
  env override) when isMockMode() is true and merges those addresses
  into the live contract address book.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Lights up startMockStack() end-to-end:
  1. spawnAnvil + loadAnvilState (Phase 6)
  2. GraphQL HTTP server replicating mockGraphqlServer.ts contract
  3. Indexer runtime exposing lastBlock + waitUntilCaughtUp barrier

Pieces added:
  src/indexer/prng.ts        — mulberry32 PRNG + FNV-1a hashSeed; replaces
                               Math.random() so synthetic timeseries are
                               reproducible run-to-run (plan §4.4 requirement).
  src/indexer/synthetic.ts   — port of mockGraphqlServer.ts resolveFixture +
                               makeWorker / numericSeries / objectSeries
                               helpers, with PRNG-driven variance and
                               persona lookups self-contained in the package.
                               Exports SYNTHETIC_OPERATIONS (the 41 named
                               operations from packages/server/graphql/*.graphql).
  src/indexer/dispatcher.ts  — registerResolver/dispatch contract: custom
                               resolvers override synthetic, otherwise
                               synthetic fallback, otherwise empty data
                               with a console.warn diagnostic.
  src/indexer/server.ts      — minimal HTTP shell (no GraphQL parser);
                               accepts {operationName, variables}, returns
                               {data}. CORS-permissive to match legacy
                               server. Default port 4321.
  src/indexer/runtime.ts     — lastBlock polling + resetAndReplay /
                               waitUntilCaughtUp barriers.
  src/__tests__/parity.test.ts — enumerates *.graphql operations and
                               asserts every one is handled. Wired via
                               new vitest.config.ts.
  src/index.ts (rewrite)     — startMockStack() now ships a working stack:
                               anvil + state load + graphql server +
                               indexer; stop() unwinds in reverse.

Smoke-tested via scripts/_start-smoke.ts:
  rpc=http://127.0.0.1:8545  graphql=http://127.0.0.1:4321/graphql
  deployments populated (5 contracts), allWorkers returns 4 workers,
  indexer.lastBlock matches prepare bootBlock=8.

Phase 7's per-operation chain-derived overrides (replacing synthetic
fallbacks with entity-store-backed resolvers) layer in via
registerResolver() in follow-up commits as Phase 8 specs need them.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Wires the integration project's globalSetup to startMockStack() and
runs an in-process tRPC server against it, then ships two reference
specs:

  status.integration.test.ts:
    - tRPC client → in-process server → mockGraphqlServer fallback
      (squidNetworkHeight) → assertion. Proves the full stack roundtrips.

  anvil-balance.integration.test.ts:
    - viem PublicClient → anvil → assert Bob has 10 ETH (anvil_setBalance)
    - viem readContract(MockSQD.balanceOf) → assert 200000 SQD
    Validates that .anvil-state.json round-trip preserves persona seeding.

Wiring:
  test/anvil/global-setup.ts — boots @subsquid/mock-stack, sets server
    env vars (MOCK_WALLET=true, MOCK_GRAPHQL_PORT, MOCK_STACK_DEPLOYMENTS,
    RPC_URL) BEFORE importing appRouter, starts an ephemeral tRPC server,
    publishes URLs via Vitest's ctx.provide() (consumed by inject() in
    specs).
  test/anvil/types.ts — ProvidedContext augmentations.
  test/anvil/snapshot.ts — beforeEach evm_snapshot/evm_revert pair so
    each spec starts from the post-deploy state.
  test/msw/server.ts — MSW v2 setupServer scaffold (handlers added per-spec
    in Phase 9).
  packages/server/package.json — add ./router, ./trpc, ./env subpath
    exports so the integration globalSetup can import them.
  Vitest 4 globalSetup signature: declare a minimal structural type for
    the ctx argument since vitest 4 doesn't export GlobalSetupContext.

Tests run in singleFork mode (set in vitest.config.ts in Phase 1) so
all integration specs share one anvil + indexer.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
src/test/msw/trpc-error-handlers.ts:
  trpcError(procedure, { code, message, httpStatus? }) builds an MSW
  handler that returns a tRPC-shaped error JSON. Pattern matches both
  unbatched (httpLink) and comma-separated batched (httpBatchLink)
  request URLs, so callers don't have to know which link type the spec
  under test uses.

src/test/__tests__/trpc-error.test.ts:
  Reference spec — wires MSW v2's setupServer (via existing
  test/msw/server.ts), uses trpcError() to force INTERNAL_SERVER_ERROR
  on status.get, and asserts the resulting TRPCClientError carries the
  expected code + message.

turbo.json:
  Tighten the 'test' task config — drop the dangling coverage/**
  output (warns when not produced), pass through MOCK_* env vars so
  cache busts when those flip locally.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Previously MockConnectDialog called window.location.reload() after
persisting the picked persona index — that worked but was a poor UX
hit, threw away in-memory state, and was unusable in tests.

Phase 0 of the testing plan analysed wagmi.useSwitchAccount and
confirmed it switches between connector instances, not accounts within
one connector — wrong tool for the job. The right tool is the wagmi
mock connector's own onAccountsChanged hook, which emits a 'change'
event that updates the connection's account list reactively.

MockConnectDialog now:
  1. Persists the index via setMockAccountIndex (reload survivability).
  2. Connects the mock connector if it isn't already.
  3. Calls connector.onAccountsChanged with the picked address hoisted
     to position 0. useAccount() updates everywhere, no remount.
  Falls back to the old reload flow if no mock connector is registered
  (defensive — should not trigger in practice).

The legacy mockRpcServer.ts / mockGraphqlServer.ts in @subsquid/server
stay in place — replacing them in pnpm dev mode would force every
contributor to install foundry. That migration is a follow-up
workstream tracked separately.

Test: src/test/__tests__/account-switch.test.tsx exercises the
onAccountsChanged path against the wagmi mock connector and asserts
useAccount().address updates without a remount.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
.github/workflows/test.yaml:
  Triggers on PRs and pushes to main/develop. Steps:
   - Checkout with submodules: recursive (network + portal contracts).
   - Install Node 22 + pnpm 9 + Foundry (foundry-rs/foundry-toolchain@v1).
   - Cache pnpm store, .turbo, mock-stack out/ + .anvil-state.json +
     .deployments.json + submodule artifact dirs, keyed on contract +
     mock-stack source SHAs so PRs not touching contracts skip
     forge build + the deploy harness.
   - forge build per submodule + the local mock contracts (skipped
     when artefacts already exist).
   - pnpm --filter @subsquid/mock-stack stack:prepare (skipped when
     .anvil-state.json already exists).
   - pnpm tsc, pnpm lint, pnpm test in sequence.

AGENTS.md §Testing & Quality Gates:
  Replace the 'no automated test suite' note with the Vitest split
  (unit / integration projects, mock-stack parity test) and document
  Foundry as an integration-test prerequisite. Add 'pnpm test' to
  the Finish-the-Task checklist.

README.md:
  New 'Testing' section pointing at pnpm test, the unit/integration
  filter scripts, and the Foundry + stack:prepare prerequisites,
  plus a link to packages/mock-stack/README.md for the mock environment
  internals.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Test envs are deterministic — there's only one mock topology — so the
indirection via process.env was pure ceremony. This refactor strips it.

Server runtime override:
  packages/server/src/env.ts adds setRuntimeOverride({ network,
  squidGraphqlUrl, rpcUrl, contractAddressOverride, mockMode }) — every
  getter checks the override first, then falls through to process.env.
  Tests configure the server in one call instead of mutating
  process.env.MOCK_WALLET / MOCK_GRAPHQL / MOCK_GRAPHQL_PORT /
  MOCK_STACK_DEPLOYMENTS / RPC_URL / NETWORK before importing appRouter.

Ephemeral ports:
  spawnAnvil() and startGraphqlServer() default to OS-allocated free
  ports. startMockStack() now defaults to ephemeral RPC + GraphQL ports
  too. Concurrent runs and rerun loops can't collide on 8545/4321.
  The dev-mode pnpm dev workflow pins them via MOCK_RPC_PORT /
  MOCK_GRAPHQL_PORT env vars (unchanged).

Auto-prepare:
  startMockStack({ autoPrepare: true }) runs forge build + the deploy
  harness in-process if packages/mock-stack/.anvil-state.json is
  missing. Vitest's globalSetup opts in, so 'pnpm test' on a fresh
  checkout works with no manual setup step beyond having Foundry
  installed. runPrepare() (new src/prepare.ts) is the single
  implementation shared by the script and the auto-prepare path.

CI:
  test.yaml drops the explicit 'Prepare mock-stack snapshot' step plus
  the network/portal contract forge-build steps (they're not yet
  consumed by the harness — their builds were dead work). pnpm test
  handles everything via auto-prepare.

Vitest config:
  Lift the duplicated define block into a shared TEST_DEFINES constant
  to keep both projects aligned.

Verified: deleted out/ + .anvil-state.json + .deployments.json; ran
'pnpm test' from cold; auto-prepare ran forge build + deploy + tests
(12/12 green). Also verified an already-cached run (no rebuild).

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Mock mode is now a separate run target, not a runtime flag flipped via
env vars. .env.mock has the same shape as .env.example (no special
MOCK_* keys) and just points the squid + RPC URLs at the in-process
mock servers.

Server side:
  - New main.mock.ts entrypoint: starts the in-process mock GraphQL +
    mock RPC servers, then runs main.ts as usual. main.ts itself no
    longer has any mock-mode branching — it reads its env unchanged.
  - dev:mock script uses --env-file=../../.env.mock + main.mock.ts.
  - env.ts loses isMockMode / isMockGraphql / getMockGraphqlPort / the
    .deployments.json fallback in getContractAddresses + the
    'mockMode' field on RuntimeOverride. The three getXxxSquidUrl
    helpers fold into a single getSquidUrl(envKey) that composes the
    network prefix.
  - mockRpcServer.ts / mockGraphqlServer.ts hardcode the dev ports
    (8545 / 4321) — they're a fixed contract with .env.mock.

Client side:
  - vite.config.ts switches to defineConfig(({ mode }) => ...). When
    mode === 'mock' it loads .env.mock and injects
    process.env.MOCK = 'true'. dev:mock script adds --mode mock.
  - config.ts derives isMockMode from process.env.MOCK; deletes the
    MOCK_RPC_URL re-export and the AppMode union; createAppWagmiConfig
    no longer takes a 'mode' arg (reads isMockMode internally).
  - App.tsx simplified — no more mode/mockRpcUrl plumbing.
  - vitest config and global-setup updated to match.

.env.mock + .env.example:
  - .env.mock: WALLET_CONNECT_PROJECT_ID + *_SQUID_API_URL all pointed
    at http://localhost:4321/graphql; ARBITRUM_ONE_RPC_URL pointed at
    http://localhost:8545. Same shape as .env.example, no flags.
  - .env.example: drop MOCK_WALLET / MOCK_RPC_PORT / MOCK_GRAPHQL_PORT;
    add a pointer to dev:mock.

Verified:
  - pnpm dev: squid URLs resolve to mainnet endpoints, RPC undefined.
  - pnpm dev:mock: squid URLs resolve to localhost:4321, RPC to
    localhost:8545, mock servers boot.
  - pnpm test: 12/12 green.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Run mock-mode local development with 'pnpm mock' instead of
'pnpm dev:mock'. Updates root, client, server, turbo task name, plus
all comments / .env.example / README references.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
P-A:
  src/deploy.ts grows from 5-mock-deploy to 15-contract deploy in
  dependency order (Router proxy + impl, NetworkController, Staking,
  WorkerRegistration, RewardTreasury, SoftCap,
  DistributedRewardsDistribution, RewardCalculation, GatewayRegistry
  proxy + impl + initialize, VestingFactory). Roles wired:
  Staking.REWARDS_DISTRIBUTOR_ROLE -> Distributor;
  Treasury.setWhitelistedDistributor; Distributor.REWARDS_TREASURY_ROLE
  -> Treasury; NetworkController.setAllowedVestedTarget for the four
  user-action targets. epochLength set to 2 blocks so workers go
  active quickly in tests.

  src/chain.ts spawns anvil with --code-size-limit 50000 (the network
  contracts exceed EIP-170's 24KB runtime limit; Arbitrum mainnet
  doesn't enforce it either).

P-B:
  src/seed.ts (new) — extracted from deploy.ts. mints + sets ETH per
  persona, then runs realistic on-chain operations:
    * Carol: approve(WorkerRegistration, 2*bondAmount); register two
      workers with deterministic test peerIds (sha2-256 multihash
      framing of fixed seed strings).
    * Bob: approve(Staking, 50_000); deposit(workerId=1, 50_000).
    * (Dave's vesting deferred — VESTING_CREATOR_ROLE flow lands when
      the vesting page chain-derives.)
  anvil_mine 0x10 between Carol's registers and Bob's deposit so
  workers are 'active' by the time deposit's guard runs.

  src/prepare.ts auto-runs forge build for sqd-network-contracts
  artefacts when missing (same pattern as the local mock-stack
  artefacts).

Verified: 'pnpm --filter @subsquid/mock-stack stack:prepare' from
cold installs everything, registers Carol's two workers (ids 1, 2),
delegates 50k SQD from Bob to worker 1, dumps state at bootBlock=50.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
P-C — runtime.ts no longer just polls lastBlock; it subscribes to
WorkerRegistration + Staking logs and projects them into a TS Map
entity store.

src/indexer/entities.ts:
  EntityStore { workers, workersByPeerId, workersByOwner,
                delegations, delegationsByOwner, delegationsByWorker }
  Helpers: createEntityStore(), clearEntities(), delegationKey(),
  addToSetMap().

src/indexer/mappings/workerRegistration.ts:
  Pure function applying WorkerRegistered, WorkerDeregistered,
  MetadataUpdated to the entity store.

src/indexer/mappings/staking.ts:
  Pure function applying Deposited and Withdrawn (Rewarded/Claimed
  stay synthetic for now).

src/indexer/runtime.ts (rewrite):
  - Bootstrap: viem.getLogs(fromBlock=0, toBlock='latest', address=[…])
    for every contract in deployments. Apply mappings, advance cursor.
  - Tail loop: every pollIntervalMs, fetch logs from cursor onward.
  - resetAndReplay() clears the store and re-bootstraps from genesis.
  - waitUntilCaughtUp() awaits any in-flight bootstrap, then waits
    until lastBlock catches the chain head.

Indexer is wired into startMockStack() and the entity store is
exposed on the public handle (handle.indexer.store) so resolvers
in P-D can consult it.

Smoke-tested via scripts/_indexer-smoke.ts:
  workers: 2 (ids 1, 2; peerIds = 0x12206d6f… (test multihash bytes);
              owner = Carol)
  delegations: 1 (worker 1 ← Bob, 50000 SQD)

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
P-D adds three resolver modules that consult the entity store +
on-chain reads instead of returning hand-rolled placeholders.

src/indexer/operations/workers.ts:
  registerWorkerResolvers({ store, client, deployments, blockTimestamp })
  Replaces the synthetic fallback for:
    allWorkers           — entity store, sorted by id
    workerByPeerId       — base58-decode the queried peerId, look up
                           via store.workersByPeerId
    myWorkers            — store.workersByOwner intersection
    myWorkersCount       — count of myWorkers
    workerDelegationInfo — entity worker + sum of delegations
    workersByOwner       — id/name/peerId/ownerId/bond/claimableReward
    myDelegations        — store.delegationsByOwner -> worker projection
    delegationsByOwner   — flat delegation list
  Worker fixture uses bs58-encoded peerId from the on-chain bytes;
  numeric workerId becomes the GraphQL string id; ownerId is the
  registrar's lower-cased address. Off-chain metric fields (apr,
  uptime, queries, served data) stay synthetic but are now seeded by
  workerId so each worker has visually distinct numbers.
  bondAmount + delegationLimitCoefficient are read once from
  NetworkController (cached) so the cap math matches the contract.

src/indexer/operations/network.ts:
  registerNetworkResolvers({ client, deployments, getLastBlock })
  - settings: NetworkController.bondAmount() via viem readContract.
  - squidNetworkHeight: indexer.lastBlock.
  - currentEpoch: NetworkController.epochNumber/nextEpoch/epochLength.

src/indexer/operations/token.ts:
  registerTokenResolvers({ client, deployments, store })
  - sources / accountsByOwner: MockSQD.balanceOf via viem.
  - vestingByAddress / vestingsByAccount: still null/empty (vesting
    deploy deferred).

src/indexer/dispatcher.ts: dispatch() is async-aware — chains can
return Promises and the server awaits them. Resolver type widened to
allow Promise<unknown> returns. Parity test updated.

src/indexer/server.ts: wraps dispatch(...) in an async IIFE so the
HTTP handler can await it and surface 500s on resolver errors.

bs58 added as a dependency. startMockStack() registers all three
resolver groups after starting the indexer; clearResolvers() at the
top wipes any prior registrations so hot-restarts don't leak.

Smoke-tested via scripts/_resolver-smoke.ts:
  allWorkers: 2 workers, ids 1 + 2, peerIds 'QmVhq…' base58
  myWorkers([carol]): 2 workers
  myDelegations([bob]): 1 worker with delegations[]
  sources(bob): balance=150_000 SQD (200k initial − 50k delegated;
                chain-derived, no longer hand-rolled)
  settings: bondAmount from NetworkController

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Phase 10 of the original wagmi-viem-testing plan: replace the legacy
in-process mock servers with the chain-derived mock-stack.

packages/server/src/main.mock.ts (rewrite):
  Boots startMockStack({ autoPrepare: true, rpcPort: 8545,
                        graphqlPort: 4321 }), then setRuntimeOverride
  with the resulting URLs + deployments map. Hands off to main.ts
  unchanged. SIGINT/SIGTERM unwind the stack cleanly.

packages/server/package.json:
  Add @subsquid/mock-stack as a runtime dep.

Deleted:
  packages/server/src/services/mockGraphqlServer.ts (625 lines of
    hand-rolled fixtures with placeholder worker IDs)
  packages/server/src/services/mockRpcServer.ts (570 lines of
    selector-dispatch eth_call shim that drifted from on-chain state)

packages/client/src/test/__tests__/anvil-balance.integration.test.ts:
  Bob's seeded balance is now 150_000 SQD (200k initial − 50k
  delegated to Carol's worker during seed). ETH balance asserted
  within 0.1 ETH of 10 to allow for gas spent on approve + deposit.

Verified:
  - pnpm tsc + pnpm lint clean.
  - pnpm mock boots: 15 contracts deployed, GraphQL on 4321, anvil
    on 8545, server log shows mock-stack-deployed addresses for
    SQD/WORKER_REGISTRATION/STAKING/etc. (no longer mainnet defaults).
  - pnpm test: all 12 tests pass.

Now the GraphQL surface is consistent with on-chain state: worker IDs
are real numeric strings (1, 2), peerIds are base58 multihashes from
contract bytes, owner addresses match registered events, delegation
amounts come from Staking.Deposited logs.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
worker-registration.integration.test.ts: registers a new worker on
chain as Alice (mints + approves + register), waits for the indexer
tail loop to ingest the WorkerRegistered log, queries allWorkers via
the GraphQL endpoint and asserts:
  - the new worker's peerId (base58 of the supplied multihash bytes)
    is present
  - the count grew by 1 over the seeded baseline
  - ownerId matches Alice's lower-cased address
  - id is the next numeric workerId after the baseline

This is the proof that the chain → indexer → resolver pipeline is
fully connected end-to-end without any synthetic intervention.

README.md updated to describe what 'pnpm mock' actually does — full
mock-stack with deployed contracts, persona seeding, log-driven
indexer, chain-derived GraphQL surface.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Deferred work from the Phase 10 plan: VestingFactory creates a
SubsquidVesting contract for Dave at seed time, the indexer ingests
VestingCreated events, and the token-squid resolvers project the
vesting entity into the GraphQL accounts shape with a chain-derived
locked balance.

src/seed.ts:
  createVestingForDave() — calls VestingFactory.createVesting (the
  deployer holds VESTING_CREATOR_ROLE from construction) with
  start=now-30d, duration=365d, immediateRelease=10%, total=1M SQD.
  Pulls the Vesting contract address from the receipt's
  VestingCreated event topic, then mints 1M SQD into it so
  release() has something to release.

src/indexer/entities.ts:
  Added VestingEntity { id, beneficiaryId, startTimestamp,
                        durationSeconds, expectedTotalAmount }
  + vestings + vestingsByBeneficiary maps. clearEntities() updated.

src/indexer/mappings/vestingFactory.ts:
  Decodes VestingCreated logs and projects into the entity store.

src/indexer/runtime.ts:
  Subscribes to deployments.VESTING_FACTORY in addition to
  WORKER_REGISTRATION + STAKING.

src/indexer/operations/token.ts:
  - vestingByAddress: looks up by vesting contract id, returns the
    projected account (id, type=VESTING, balance from MockSQD.balanceOf,
    owner = beneficiary).
  - vestingsByAccount: returns all vestings for a beneficiary.
  - accountsByOwner: returns the USER account + any VESTING accounts
    where owner=address (matches the squid OR filter).
  Resolvers no longer return null/empty placeholders.

Smoke-tested:
  Dave has USER account (50_000 SQD) + VESTING account
  (1_000_000 SQD locked, owner=Dave). vestingByAddress(<vesting addr>)
  returns the same account.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
…resolvers

The mini-indexer projected every contract event into a TS Map of
hand-rolled entities (workers, delegations, portal pool deposits, etc.)
which resolvers then read from. Two stores to keep in sync, ~800 lines of
mapping code.

The contracts already expose enumeration views (getActiveWorkerIds,
getOwnedWorkers, Staking.delegates, getPortalCount, …) and per-record
view functions (getWorker, Staking.getDeposit, Pool.getPoolInfo, …) that
make the indexer mostly redundant. The only thing the indexer is still
needed for is enumerating dynamic addresses that aren't enumerable
on-chain: portal pool addresses (PortalPoolFactory.PoolCreated) and
vesting contract addresses keyed by beneficiary (VestingFactory.VestingCreated).

This commit slims the indexer to just that:

src/indexer/registry.ts (new):
  IndexerRegistry { portalPools, portalPoolCreatedAt, vestings,
                    vestingsByBeneficiary }
  + helpers (createRegistry, clearRegistry, rememberPortalPool,
    rememberVesting). That's the entire indexer state.

src/indexer/runtime.ts (rewrite):
  Subscribes only to PortalPoolFactory + VestingFactory. Bootstrap +
  tail loop unchanged in shape; mappings just call the slim
  remember*() helpers.

src/indexer/mappings/{portalFactory,vestingFactory}.ts:
  Slimmed to a single rememberX() call each.

src/indexer/operations/workers.ts (rewrite):
  Read-through. allWorkers → WorkerRegistration.getActiveWorkerIds()
  + getWorker(id) + Staking.delegated(id) per worker.
  myWorkers → getOwnedWorkers(owner) + project per id.
  myDelegations → Staking.delegates(staker) + getDeposit(staker, id).
  workerByPeerId → WorkerRegistration.workerIds(peerIdBytes).
  No entity store consulted.

src/indexer/operations/token.ts (rewrite):
  sources / accountsByOwner → MockSQD.balanceOf via viem;
  delegationCount derived from Staking.delegates(staker).length.
  vestingByAddress / vestingsByAccount → registry tells us the
  vesting addresses, MockSQD.balanceOf(<vesting>) gives the locked
  amount.

src/indexer/operations/portals.ts (rewrite):
  portalPools / portalPoolById → for each id in registry,
  Pool.getPoolInfo() + getActiveStake() + getRewardToken().
  poolProvidersByOwner → for each pool × owner, getProviderStake(owner).
  gatewaysSummary.totalPortalPoolTvl → sum of getActiveStake() across pools.
  poolEvents → empty (per-event timeline isn't reproducible from views;
  cosmetic in mock mode anyway).

Deleted:
  src/indexer/entities.ts                         (5 KB / 158 lines)
  src/indexer/mappings/staking.ts                 (~70 lines)
  src/indexer/mappings/workerRegistration.ts      (~80 lines)
  src/indexer/mappings/portalPool.ts              (~70 lines)

src/index.ts:
  Public handle exposes handle.indexer.registry instead of
  handle.indexer.store. EntityStore + DelegationEntity +
  WorkerEntity + PortalPoolEntity + PortalPoolDepositEntity +
  VestingEntity exports gone; IndexerRegistry replaces them.

Verified:
  - pnpm --filter @subsquid/mock-stack stack:prepare clean.
  - resolver smoke: allWorkers/myWorkers/myDelegations/sources/
    settings/accountsByOwner/portalPools/poolProvidersByOwner/
    gatewaysSummary all return correct chain-derived data.
  - pnpm tsc + pnpm lint clean.
  - pnpm test: 13/13 pass. The worker-registration integration test
    runs in ~150ms now (was ~2700ms) because there's no indexer wait;
    the resolver reads getActiveWorkerIds directly so newly-registered
    workers are visible on the next anvil block.
  - pnpm mock boots cleanly.

Net: ~600 lines of indexer/projection code removed; zero drift risk
between contract storage and resolver responses.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
User reported 'Failed running src/main.mock.ts. Waiting for file
changes before restarting...' with no further info — the tsx --watch
flag was swallowing the actual error. Three fixes so a single
`pnpm mock` either works end-to-end or fails with a clear,
actionable message.

packages/server/package.json:
  Drop --watch from the mock script. There's no reason to hot-reload
  the server in mock mode (every save tears down anvil + the booted
  state) and tsx's --watch error UX hides the real failure behind a
  cryptic 'waiting for file changes' message.

packages/server/src/main.mock.ts:
  - Pre-flight: ensure 'forge' and 'anvil' are on PATH before doing
    anything; if not, print the foundryup install snippet and exit 1.
  - Wrap startMockStack() in try/catch so any deploy harness or
    indexer bootstrap failure surfaces with a hint about the most
    common causes (port already taken, submodule missing, forge build
    error) plus the original stack trace.

packages/mock-stack/src/chain.ts:
  ensurePortFree(port) probes the requested port up front; on
  EADDRINUSE the error explains the cause ('previous pnpm mock still
  running, find with lsof -i:8545') instead of letting anvil's spawn
  fail silently and waitForAnvil report a generic 10s timeout.

packages/mock-stack/src/indexer/server.ts:
  Same EADDRINUSE → friendly-message wrap for the GraphQL HTTP server's
  listen() call (port 4321 in mock mode).

Verified:
  - Cold start: rm .anvil-state.json + .deployments.json, run
    'pnpm mock' once. forge build runs, deploy harness runs, anvil +
    GraphQL come up, server reports 'Server is running on port 3001'.
  - Stale-process failure: leaving a previous mock running and
    starting a second yields a clear 'Port 8545 is already in use'
    error and exit 1, no 'waiting for file changes' dangling state.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
User reported a fresh-checkout crash:
  Error: Foundry artifact not found:
    sqd-portal-contracts/out/PortalPoolImplementation.sol/PortalPoolImplementation.json

The portal/worker/token resolver modules eagerly read ABI artefacts at
top-level module load. On a fresh checkout the portal out/ dir doesn't
exist yet — startMockStack()'s autoPrepare runs forge build, but only
*after* the resolver modules have already been imported and crashed.

This commit fixes the crash AND splits the dev workflow into two
processes so the chain (slow, long-lived) and the app (fast, hot-
reloading) restart independently.

Lazy ABIs (fixes the crash):
  packages/mock-stack/src/indexer/operations/{portals,token,workers}.ts:
  Wrap each top-level networkArtifact()/portalArtifact() call in a
  lazy getter so module import is free even when forge artefacts
  aren't on disk yet. The first resolver call triggers the read.

New 'mock:chain' (long-lived):
  packages/mock-stack/scripts/serve.ts spawns autoPrepare + anvil:8545
  + GraphQL:4321 + a heartbeat loop. Holds the snapshot files. Foundry
  preflight + actionable error on EADDRINUSE / forge failure.
  Wired as 'pnpm --filter @subsquid/mock-stack mock:chain' and root
  'pnpm mock:chain'.

New 'mock:app' (hot-reload):
  packages/server/src/main.app.ts replaces main.mock.ts. Doesn't own
  the chain — polls .deployments.json on disk (waits if it's not there
  yet, so order doesn't matter), then probes the anvil + GraphQL
  endpoints with a 60s budget, then setRuntimeOverride() + delegates
  to main.ts. Runs under tsx --watch so saving a server file doesn't
  tear down anvil.
  Wired as 'pnpm --filter @subsquid/server mock:app' (server) +
  'pnpm --filter @subsquid/client mock:app' (vite mock mode) + root
  'pnpm mock:app'.

Convenience:
  Root 'pnpm mock' runs both 'mock:chain' and 'mock:app' as parallel
  turbo tasks for one-off work; the recommended dev loop is
  'pnpm mock:chain' in one terminal + 'pnpm mock:app' in another.

Verified end-to-end:
  - Cold (no .deployments.json, no out/): 'pnpm mock:chain' takes ~4s
    to print 'chain is ready' (forge build skipped since cached); the
    chain is fully populated (15 contracts, persona seeding,
    portal pool, vesting). 'pnpm mock:app' connects in ~3s.
  - Plain 'pnpm mock' from cold: both halves come up together in
    parallel; chain reaches 'ready', server prints 'Server is running
    on port 3001'.
  - GraphQL roundtrip: curl POST to :4321 returns the chain-derived
    allWorkers list with real workerIds + base58 peerIds.
  - pnpm tsc + pnpm lint clean. pnpm test 13/13 green.

Docs: README.md + AGENTS.md describe the new two-process layout.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
cursoragent and others added 6 commits April 26, 2026 07:34
Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Front the mock anvil with a tiny JSON-RPC HTTP shim that splices
`l1BlockNumber := number` into every `eth_getBlockBy*` response
when the upstream omits it. From the consumer's POV the mock chain
now looks like a real Arbitrum nitro RPC: any code reading
`(block as any).l1BlockNumber` works transparently in mock mode
without a special branch, and on Arbitrum's well-known quirk where
`block.number` already equals the L1 block number, the value
space is consistent end-to-end.

Anvil 1.5+ already exposes the field natively, but the shim is
defensive against older anvil versions and any other test chain
plugged into `startMockStack`. Existing l1BlockNumber values from
upstream are passed through unchanged; non-block methods are
forwarded verbatim; batched requests are handled by pairing
request/response IDs.

Lifecycle is owned by `startMockStack`: anvil now binds an
ephemeral port and the shim takes the caller-requested public port
(e.g. 8545 for `pnpm mock:chain`).

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
…ation

- Add console.error to tRPC onError so all errors are always logged to
  stderr, not just when Sentry is configured
- Add per-procedure timing middleware in trpc.ts that logs --> / <-- with
  elapsed ms for every query/mutation, making slow calls immediately visible
- Add HTTP request/response logging on the Node server (method, path,
  status, ms) to complement the tRPC-level logs
- Set server.requestTimeout = 10_000 ms so the Node http.Server closes
  stale connections that were never aborted by the client
- Add a 4 500 ms AbortSignal to every Squid GraphQL fetch so the server
  returns a proper timeout error before the browser's 5 s client abort
  fires; previously slow GraphQL responses caused frontend timeouts with
  no corresponding server-side error logged
- Improve GraphQL error surfacing: check HTTP status before parsing JSON,
  and join all GraphQL error messages instead of only the first
- Wire up the pre-existing (but dead) errorToastTimestamps dedup map in
  client.ts so duplicate error toasts are suppressed within 3 s
- Add MutationCache.onError handler so mutation errors also show a toast

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
@cursor cursor Bot changed the title feat: full mock development environment (wallet, RPC, GraphQL) feat: mock development environment (wallet, RPC, GraphQL) Apr 26, 2026
belopash and others added 12 commits April 26, 2026 19:09
fix: improve server error logging and fix request timeout misconfiguration
The mock myDelegations resolver was ignoring the peerId variable,
returning all delegated workers instead of just the one matching the
current worker page. Worker.tsx takes delegationsData?.[0] (safe in
production where the query is filtered to one worker), so in mock mode
the undelegate dialog showed the deposit for the wrong worker — causing
the Staking contract to revert with "Insufficient staked amount" when
the submitted amount exceeded what was actually staked in the viewed
worker.

Fix resolves peerId → workerId via WorkerRegistration.workerIds() and
skips non-matching workers, mirroring the peerId_eq filter in the
production myDelegations GraphQL query.

Made-with: Cursor
…h local RPC

The shim was rejecting OPTIONS preflight requests with 405 and no
Access-Control-Allow-Origin header, causing browsers to block every
cross-origin JSON-RPC call from the dev client (localhost:3005) to the
mock chain (localhost:8545).  Set the CORS headers on every response
and short-circuit OPTIONS with 204 so the browser's preflight succeeds.

Made-with: Cursor
Add `blockTime` option to `SpawnAnvilOpts` / `StartMockStackOpts` that
passes `--block-time <N>` to anvil for interval mining. `pnpm mock:chain`
now sets `blockTime: 12`; test callers are unaffected (instant mining).

Made-with: Cursor
…ISO strings

The UI computes epoch end time as
  (epoch.end - lastBlockL1 + 1) * blockTimeL1 + new Date(lastBlockTimestampL1)
so epoch.start/end must be block numbers. The resolver was passing them
through blockTimestamp(), producing ISO strings and causing NaN in the UI.

Made-with: Cursor
pnpm mock:chain now runs two processes under one terminal:
  - chain   (anvil + shim at :8545) — inline, stable across reloads
  - indexer (indexer + GraphQL at :4321) — spawned via `tsx watch`, so
    changes to resolver / indexer source files restart only this child

startMockStack gains an `externalRpcUrl` option that skips spawning anvil
and attaches the indexer to an already-running chain. _indexer.ts is the
private entry point used by the tsx watch child.

Made-with: Cursor
…pochLength

NetworkController.epochLength() was returning 0 because .anvil-state.json
was generated from an older contract build with a different storage layout.

- Drop stale nextEpoch()/epochNumber() reads; compute epoch number and
  boundaries from getLastBlock() / epochLength() directly
- Throw a clear actionable error (mentioning `pnpm stack:rebuild`) instead
  of silently falling back to a magic number when epochLength is 0

Made-with: Cursor
…blockTimeL1 in ms

Two bugs:
- `epochLength` storage variable is deprecated (holds lock period, always 0);
  the actual epoch length is `workerEpochLength`. Switch ABI + resolver to read
  `workerEpochLength` alongside the live `epochNumber()` / `nextEpoch()` calls.
- `blockTimeL1` was 12 (seconds) but the UI adds it directly to a ms timestamp,
  so it must be 12_000 ms. Fixed in both network and synthetic resolvers.

Made-with: Cursor
All magic numbers (CHAIN_ID, RPC_PORT, GRAPHQL_PORT, BLOCK_TIME_SEC,
EPOCH_LENGTH_BLOCKS, WORKER/STAKER_REWARD_PER_CYCLE_SQD) are now exported
from a single src/config.ts.  Runtime values take effect on the next
`pnpm mock:chain` restart; deploy-time values (★) also require
`pnpm stack:rebuild`.  No behaviour changes.

Made-with: Cursor
wagmi's built-in reconnect calls connector.connect({ isReconnecting: true }),
which the mock() connector rejects when provider.connected === false (resets
on every page load, since defaultConnected is false).

Fix:
- Override connect/disconnect on each persona connector to write/clear the
  chosen connector id ('mock-persona-N') in localStorage under sqd.mock.persona.
- Also override connector id to our stable 'mock-persona-N' (wagmi's mock()
  hardcodes id:'mock' for every instance, breaking config.connectors lookup).
- Add MockPersonaManager (null component inside WagmiProvider + QueryClientProvider)
  that calls useMockPersonaRestore: once wagmi reaches status 'disconnected',
  reads localStorage and calls connect() directly on the stored connector.
- Persist SourceContext.selectedSourceId per wallet address in localStorage
  so the active gateway source survives page reloads too.

Made-with: Cursor
…orkers

Unify the seed step and the auto-distributor behind a single
`commitRewardChunks` primitive that mirrors the production reward bot:
each commit covers exactly REWARD_BLOCKS_PER_COMMIT (10) blocks, ranges
are strictly contiguous and never overlap, and every commit pays every
active worker the per-cycle amounts. Both phases now share the same
cadence and payload — preseeding emits the first chunk, the
auto-distributor keeps emitting the rest.

Also fix the indexer's per-delegation `claimableReward` projection: it
used to be hardcoded to '0', which made the Delegations-page "Total
reward" column disagree with the on-chain `Staking.claimable` shown on
the Assets page. The mock indexer now replays `Distributed` events from
DistributedRewardsDistribution and attributes the staker portion
pro-rata to each delegation's deposit, matching the on-chain pending
reward exactly.

Made-with: Cursor
@subsquid subsquid deleted a comment from cursor Bot Apr 26, 2026
cursoragent and others added 10 commits April 26, 2026 19:29
- PageTitle: remove Box wrapper inside h4 Typography; apply color directly on Typography
- NetworkPageTitle: move layout div outside h1 Typography; Typography now wraps only the title text
- Summary.ColumnLabel: render as component="div" so block children are valid; fix SquaredChip label Typography to use component="span"
- NoItems: render children directly instead of wrapping them in a Typography p element; Typography is only used for the default message fallback
- PaginatedTable: pass emptyMessage via message prop instead of wrapping in Typography inside NoItems
- GatewaysPage: use NoItems message prop directly instead of Typography child
- AutoExtension: use component="span" on Typography inside FormControlLabel label to avoid p-inside-label
- DashboardPage: use Stack component="span" inside Tab label to avoid div-inside-span
- PageTabs: use Box component="span" inside Tab label to avoid div-inside-span

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
fix(client): correct invalid DOM nesting warnings
… logger

- Add packages/server/src/logger.ts: pino logger with ISO timestamps and
  structured JSON output. Sentry is wired at the logger level — every error()
  and fatal() call automatically calls Sentry.captureException / captureMessage
  when a Sentry client is initialised, so callers never need to call Sentry
  directly.
- Update main.ts: log structured startup context via logger.info; replace
  inline Sentry.captureException calls in onError / unhandledRejection /
  uncaughtException with logger.error / logger.fatal (Sentry forwarding is
  now handled by the logger).
- Update trpc.ts: replace console.log/error with logger.debug/warn/error,
  include structured fields (trpcType, trpcPath, ms).
- Update main.app.ts: replace console.log/error with logger.info/error,
  include structured fields (deploymentsPath, url, label, contractCount).
- Add pino dependency to packages/server.

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
feat(server): replace console.log with pino, connect Sentry to logger
Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
…r-squid URL overrides

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
…mple)

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
…s real-indexer modes)

Co-authored-by: Alexander Belopashentsev <belopash@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants