Skip to content

refactor(evm-wallet-experiment): split coordinator-vat into home/away#939

Merged
grypez merged 23 commits intomainfrom
grypez/evm-wallet-home-away
Apr 22, 2026
Merged

refactor(evm-wallet-experiment): split coordinator-vat into home/away#939
grypez merged 23 commits intomainfrom
grypez/evm-wallet-home-away

Conversation

@grypez
Copy link
Copy Markdown
Contributor

@grypez grypez commented Apr 20, 2026

Splits the monolithic 3,275-line coordinator-vat into four focused components — home coordinator, away coordinator, delegator vat, and redeemer vat — and replaces raw caveat bytes with a discriminated union of decoded semantic grant types.

Architecture before:

coordinator-vat (3,275 lines)   — home and away concerns mixed together
delegation-vat  (211 lines)     — limited grant handling
delegation-grant.ts             — builds grants from raw CaveatSpec arrays

Architecture after:

home-coordinator (2,388 lines)  — keyring, signing, delegation building, peer-relay relay
delegator-vat    (245 lines)    — caveat encoding, unsigned grant construction
away-coordinator (1,999 lines)  — grant reception, typed delegation routing
redeemer-vat     (57 lines)     — away-side grant storage
delegation-twin                 — semantic execution wrapper per grant

Note

High Risk
Large refactor touching core wallet execution/routing and delegation redemption paths (UserOps, direct tx submission, spend limits), with significant behavioral surface area and risk of regressions across home/away flows.

Overview
Splits the wallet subcluster’s coordinator responsibilities by introducing role-based clustering: makeWalletClusterConfig now selects home-coordinator vs away-coordinator bundles and swaps the auxiliary vat to delegator (home) or redeemer (away), with updated tests and build bundling.

Replaces the prior raw DelegationGrant/CaveatSpec-driven twin flow with semantic, discriminated grant types (TransferNativeGrant, TransferFungibleGrant) and a simplified METHOD_CATALOG; removes the delegation-grant builder module and rewrites makeDelegationTwin to enforce per-grant interface guards plus local spend/limit tracking.

Adds an away-coordinator vat implementing away-side routing, grant receipt/storage integration, and redemption execution paths (incl. ERC-4337/UserOp vs direct 7702 submission) plus new shared tx-utils helpers for gas/result validation.

Reviewed by Cursor Bugbot for commit 088e26c. Bugbot is set up for automated code reviews on this repo. Configure here.

@grypez grypez force-pushed the grypez/evm-wallet-home-away branch 2 times, most recently from d7ab81d to 1ea6fef Compare April 20, 2026 20:06
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 20, 2026

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 70.99%
⬇️ -7.42%
8136 / 11460
🔵 Statements 70.83%
⬇️ -7.39%
8271 / 11677
🔵 Functions 71.94%
⬇️ -3.88%
1975 / 2745
🔵 Branches 64.59%
⬇️ -11.58%
3291 / 5095
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/evm-wallet-experiment/src/cluster-config.ts 100%
🟰 ±0%
90%
⬆️ +4.29%
100%
🟰 ±0%
100%
🟰 ±0%
packages/evm-wallet-experiment/src/index.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
packages/evm-wallet-experiment/src/types.ts 100%
⬆️ +11.12%
100%
⬆️ +100.00%
100%
⬆️ +75.00%
100%
⬆️ +11.12%
packages/evm-wallet-experiment/src/lib/delegation-twin.ts 92.68%
⬆️ +2.85%
84.61%
⬆️ +4.06%
100%
⬆️ +7.15%
92.68%
⬆️ +2.85%
66-68, 84-85
packages/evm-wallet-experiment/src/lib/method-catalog.ts 66.66%
⬇️ -23.34%
50%
🟰 ±0%
0%
⬇️ -75.00%
100%
🟰 ±0%
1
packages/evm-wallet-experiment/src/lib/tx-utils.ts 0% 0% 0% 0% 11-55
packages/evm-wallet-experiment/src/vats/away-coordinator.ts 0% 0% 0% 0% 47-1998
packages/evm-wallet-experiment/src/vats/delegator-vat.ts 78.04% 76.66% 81.81% 80% 31, 102-108, 217-228
packages/evm-wallet-experiment/src/vats/redeemer-vat.ts 92.85% 83.33% 85.71% 100% 8
Generated in workflow #4347 for commit 088e26c by the Vitest Coverage Report Action

@grypez grypez marked this pull request as ready for review April 20, 2026 20:22
@grypez grypez requested a review from a team as a code owner April 20, 2026 20:22
Comment thread packages/evm-wallet-experiment/src/lib/method-catalog.ts
@grypez grypez force-pushed the grypez/evm-wallet-home-away branch from 1ea6fef to 5485331 Compare April 20, 2026 20:34
Comment thread packages/evm-wallet-experiment/src/vats/away-coordinator.ts
Comment thread packages/evm-wallet-experiment/src/vats/away-coordinator.ts
@grypez grypez changed the title refactor(evm-wallet-experiment): split coordinator-vat into home/away coordinators with semantic grant types refactor(evm-wallet-experiment): split coordinator-vat into home/away Apr 20, 2026
Comment thread packages/evm-wallet-experiment/test/e2e/docker/helpers/docker-exec.ts Outdated
Comment thread packages/evm-wallet-experiment/src/vats/away-coordinator.ts Outdated
Comment thread packages/evm-wallet-experiment/src/vats/away-coordinator.ts Outdated
Comment thread packages/evm-wallet-experiment/src/lib/delegation-twin.ts
grypez and others added 17 commits April 21, 2026 15:13
…n and slim method-catalog

Remove CaveatSpec, CaveatSpecStruct, BigIntStruct, and the old
DelegationGrant type (which carried raw caveat bytes on the away side).

Replace with TransferNativeGrant | TransferFungibleGrant — a discriminated
union where each variant carries pre-decoded semantic fields (to, maxAmount,
token) alongside the signed Delegation. This means the away side never
needs to decode caveat bytes; the home encodes once when building the grant.

Slim method-catalog to only the two methods this refactor introduces:
transferNative and transferFungible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vat that builds and stores delegation grants on the home side. Exposes:
- buildTransferNativeGrant: valueLte + allowedTargets caveats (conditional)
- buildTransferFungibleGrant: allowedTargets(token) + allowedMethods(ERC20
  transfer selector) always; erc20TransferAmount + allowedCalldata(to)
  conditionally
- storeGrant / removeGrant / listGrants: persisted in baggage

Grants are returned unsigned (status: 'unsigned'); signing is done by
the home coordinator before storeGrant is called.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Simple away-side store for DelegationGrant values received from the home
coordinator. Grants are keyed by delegation.id and persisted in baggage.
Exposes receiveGrant / removeGrant / listGrants; used by away-coordinator
when rebuilding the away sheaf.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tor-vat

Home coordinator keeps all shared infrastructure (signing, tx submission,
ERC-20, swap, smart account, OcapURL) and adds delegation management via
the new delegator-vat plus a homeSection exo.

New additions:
- buildTransferNativeGrant / buildTransferFungibleGrant: sign + store
  via delegator-vat
- signDelegationInGrant: resolves DelegationManager from chain contracts,
  signs via keyring or external signer, finalizes delegation
- listGrants / revokeGrant: list and on-chain disable grants
- getHomeSection(): returns the pre-built homeSection exo
- homeSection: local exo with transferNative/transferFungible that submit
  direct transactions; throws after 2 uses per method (demo limit)

Removes: all away-side peer protocol (connectToPeer, registerAwayWallet,
handleSigningRequest, handleRedemptionRequest) and the old delegation
routing in sendTransaction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
makeDelegationTwin({ grant, redeemFn }) builds a discoverable exo that
enforces the grant's constraints locally before submitting an Execution:

- transferNative: validates recipient (eq constraint or any) and amount
  (lte constraint or any), using M.interface guards
- transferFungible: validates token (must match), recipient (optional eq),
  and tracks cumulative spend with reserve-before-await rollback on error;
  concurrent calls cannot together exceed the budget

DelegationSection is a discriminated union carrying method and (for
transferFungible) token — used by away-coordinator routing to filter
matching sections and propagate constraint errors correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…er-relay support

Away coordinator manages the wallet API on the agent (away) side.
Shares infrastructure with home (provider, smart account, tx submission)
and routes semantic calls through delegation twins before falling back to
the home section.

Key design:
- receiveDelegation(grant): stores in redeemer-vat, rebuilds delegation
  sections from all stored grants via makeDelegationTwin
- transferNative / transferFungible: filter delegation sections by method
  and token; if any matched sections exist, try them and propagate errors
  (constraint violations are not swallowed); fall back to homeSection only
  if no sections match
- makeRedeemFn: submits delegation UserOp locally when bundler/smart
  account is available; relays to homeCoordRef.redeemDelegation() in
  peer-relay mode (away has no bundler; home's smart account is delegate)
- connectToPeer(ocapUrl): redeems URL, fetches homeSection, rebuilds routing
- initializeKeyring / unlockKeyring / isKeyringLocked, signMessage,
  signTypedData, listGrants, refreshPeerAccounts,
  sendDelegateAddressToPeer: peer setup and key management

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ion-grant

Delete the monolithic coordinator-vat (3275 lines mixing home and away
concerns) and the old delegation-vat, now superseded by the home/away
split and the new delegator-vat/redeemer-vat pair.

Remove delegation-grant.ts, which built grants from raw CaveatSpec lists;
that role now belongs to delegator-vat, which encodes caveats and returns
pre-decoded TransferNativeGrant / TransferFungibleGrant values.

Update package.json build script: coordinator-vat → home-coordinator +
away-coordinator. Remove delegation-grant exports from index.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tor API

Replace old coordinator API references (createDelegation, provisionTwin,
listDelegations, pushDelegationToAway) with the new semantic grant methods
(buildTransferNativeGrant, receiveDelegation, listGrants, transferNative).

- wallet-setup.ts: launchWalletSubcluster now accepts role:'home'|'away'
  and points to home-coordinator.bundle/away-coordinator.bundle with the
  appropriate delegator/redeemer auxiliary vat
- setup-wallets.ts: replace createDelegation+provisionTwin with
  buildTransferNativeGrant+receiveDelegation
- docker-e2e.test.ts: delegation redemption suite uses new grant API;
  DelegationGrant type replaces old Delegation type; transferNative
  replaces sendTransaction for delegated ETH sends
- run-delegation-twin-e2e.mjs: rewrites twin test using
  buildTransferFungibleGrant+receiveDelegation+transferFungible
- docker-exec.ts: callVat now passes --raw to the CLI and decodes
  smallcaps CapData inline, fixing BigInt values arriving as
  '1000000000000000000n' strings from prettifySmallcaps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…unters

Normalize token to lowercase in makeDelegationTwin and the
transferFungible routing filter so checksummed vs. lowercase addresses
always match.

Introduce delegationTwinMap in rebuildRouting so existing twins are
reused instead of recreated, preserving in-memory spent counters across
receiveDelegation calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cker-exec

Replace the manual typeof/in/Array.isArray guard with an is() check
against a CapDataStruct, per review feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… integration test

Same pattern as the docker-exec fix: replace manual typeof/Array.isArray
guard with assert() against CapDataStruct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move applyGasBuffer, validateGasEstimate, and validateTokenCallResult
out of both coordinator vats into src/lib/tx-utils.ts. Both vats now
import from the shared module.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ation twin

The twin's M.eq guard is built with a lowercased token. Passing a
checksummed address from the outer transferFungible call caused the
guard to reject it. Compute tokenLower once and use it for both the
section filter and the exo call.

Adds a regression test that constructs a twin with a checksummed token
and asserts section.token is normalized.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Updates all seven node test scripts and three source files to work with
the home/away coordinator split introduced in this branch.

Source changes:
- types.ts: add totalLimit field to TransferNativeGrant
- delegator-vat.ts: support totalLimit (NativeTokenTransferAmount caveat)
  in buildTransferNativeGrant
- home-coordinator.ts: thread totalLimit through to delegator vat
- away-coordinator.ts: forward getAccounts, signMessage, and signTypedData
  to homeCoordRef when a peer wallet is connected

Test script changes:
- Replace createDelegation with buildTransferNativeGrant/buildTransferFungibleGrant
- Update redeemDelegation calls from { delegationId } to { delegation }
- Replace listDelegations with listGrants; use grant.delegation.* for assertions
- Rewrite callExpectError to use try/catch (kernel.queueMessage throws on
  rejection rather than returning error capdata)
- Add role: 'away' to walletConfig2 in peer tests so away-coordinator is loaded
- spending-limits: add EOA → smart-account funding step so on-chain ETH
  transfer executions succeed; fix TOTAL_LIMIT constant so remaining budget
  fits within the per-tx limit for the ceiling-exhaustion test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove HOME_SECTION_LIMIT demo counter from homeSection exo (rate-limiting
  belongs on-chain via limitedCalls caveat, not in the vat)
- Add superstruct validation (DelegationGrantStruct) on baggage restore in
  delegator-vat and redeemer-vat instead of unsafe `as` casts
- Tighten delegation-twin guard/type mismatch: M.bigint() guard now matches
  `amount: bigint` param type; remove redundant BigInt coercions
- Add home-coordinator.test.ts covering configureBundler URL/chainId
  validation and revokeGrant error paths
- Add peer-wallet integration test: delegation relay (away → home)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rdinator

CLI callers (Docker e2e, kernel-cli) serialize bigints as numeric strings in
JSON. The away coordinator is the external boundary — coerce amount to
BigInt() in transferNative/transferFungible before forwarding to delegation
twins whose M.bigint() / M.lte(bigint) guards now strictly require bigints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e twin

The transferFungible twin correctly tracked budget via spent/max with
pre-reserve + rollback, but transferNative only checked maxAmount (per-call)
and silently ignored totalLimit (cumulative cap). Add the same pattern:
- cumulativeMax = totalLimit ?? 2^256-1
- Reserve before await, roll back on redeemFn failure

Retain the per-call maxAmount body check for test environments where
interface guards are mocked out.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@grypez grypez force-pushed the grypez/evm-wallet-home-away branch from 62111cc to 9c824f4 Compare April 21, 2026 19:39
Comment thread packages/evm-wallet-experiment/src/vats/away-coordinator.ts
homeSection was persisted but homeCoordRef was not, so after a vat restart
getAccounts (home fallback), signMessage, signTypedData, sendDelegateAddressToPeer,
and makeRedeemFn's home relay path all silently broke.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/evm-wallet-experiment/src/index.ts
Comment thread packages/evm-wallet-experiment/src/types.ts
…; re-export structs

maxAmount meant different things across grant types:
- TransferNativeGrant: per-call ETH limit (ValueLteEnforcer)
- TransferFungibleGrant: cumulative transfer cap (ERC20TransferAmountEnforcer)

Rename TransferFungibleGrant.maxAmount to totalLimit to match the cumulative
naming already used in TransferNativeGrant. Also re-export
DelegationGrantStruct, TransferNativeGrantStruct, TransferFungibleGrantStruct
from the package's public index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/evm-wallet-experiment/src/vats/away-coordinator.ts Outdated
… getCapabilities

grant.maxAmount may be a string when the grant crosses a JSON boundary.
String.prototype.toString(16) ignores the radix and returns the raw string,
producing garbage hex input to weiToEth(). Wrap with BigInt() first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/evm-wallet-experiment/src/vats/away-coordinator.ts Outdated
The old comment "errors are not swallowed and do not fall through to the
home section" was misleading — intermediate errors ARE discarded; only the
last one propagates. The try-all-twins pattern is intentional: a budget-
exhausted twin A falls through to twin B with remaining budget. Document
the actual behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0c68d6f. Configure here.

Comment thread packages/evm-wallet-experiment/src/lib/delegation-twin.test.ts
grypez and others added 2 commits April 22, 2026 10:45
…in test

The inline TransferFungibleGrant object silently accepted maxAmount due to
TypeScript's union excess-property checking allowing fields from sibling
members.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of silently discarding intermediate errors and only rethrowing
the last one, both transferNative and transferFungible routing now
collect errors from all failed twins and throw a single Error with
{ cause: errors[] }, preserving the full failure context.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@grypez grypez enabled auto-merge April 22, 2026 15:00
@grypez grypez added this pull request to the merge queue Apr 22, 2026
Merged via the queue into main with commit 7565626 Apr 22, 2026
33 checks passed
@grypez grypez deleted the grypez/evm-wallet-home-away branch April 22, 2026 15:14
sirtimid added a commit that referenced this pull request Apr 24, 2026
…cripts and docs

The delegator vat was renamed from `delegation` to `delegator` (and its
bundle from `delegation-vat.bundle` to `delegator-vat.bundle`) in PR #939
when `coordinator-vat` was split. The setup scripts and the setup guide
were not updated, so they configure a vat key (`delegation`) the home
coordinator doesn't read at bootstrap (`vats.delegator` is undefined) and
reference a stale bundle artifact that `yarn build` no longer produces.

Also brings the README's endowment description in line with the current
globals list and adds a note to Recent Improvements summarizing this PR's
direction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sirtimid added a commit that referenced this pull request Apr 24, 2026
- Scripts and setup-guide referenced the stale `coordinator-vat.bundle`;
  canonical build emits `home-coordinator.bundle` / `away-coordinator.bundle`
  (since PR #939). setup-home.sh now points at the home bundle, setup-away.sh
  at the away bundle, and the setup-guide example (under the home-device
  section) at the home bundle.
- README endowment paragraph was only accurate for the home role; clarify
  that the fourth vat is `delegator` (home) or `redeemer` (away) and that
  `redeemer` does not receive `crypto`.
- Add negative tests for the new crypto-endowment guards in `makeKeyring`
  (throwaway branch) and `generateSalt` that assert on the actionable
  error substring, so a future rewording cannot silently regress the
  diagnostic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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