From f7e8d6e357bca609abd0b0c5231a0c7c0251ab87 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 31 Mar 2026 15:00:32 +0200 Subject: [PATCH 1/9] refactor(evm-wallet-experiment): use new kernel-cli queueMessage subcommand Leverage the new `ocap daemon queueMessage [args]` syntax from 2f7ecbca0 across all scripts, docs, and the OpenClaw plugin. This eliminates the error-prone `daemon exec queueMessage '["kref","method",[args]]'` pattern with its nested JSON escaping, and removes the manual `parse_capdata` helper since the CLI now auto-decodes CapData via prettifySmallcaps. Also adds OCAP_HOME env var support and fixes a wrong OCAP_BIN path in update-limits.sh. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../evm-wallet-experiment/docs/setup-guide.md | 60 ++++---- .../openclaw-plugin/daemon.ts | 118 +++++---------- .../scripts/home-interactive.mjs | 5 +- .../scripts/setup-away.sh | 133 ++++++++--------- .../scripts/setup-home.sh | 135 ++++++++---------- .../scripts/update-limits.sh | 95 +++++------- 6 files changed, 219 insertions(+), 327 deletions(-) diff --git a/packages/evm-wallet-experiment/docs/setup-guide.md b/packages/evm-wallet-experiment/docs/setup-guide.md index f3a477607..85a7731cd 100644 --- a/packages/evm-wallet-experiment/docs/setup-guide.md +++ b/packages/evm-wallet-experiment/docs/setup-guide.md @@ -251,7 +251,7 @@ The script will: 4. Create a Hybrid smart account and fund it if needed (may trigger a MetaMask approval for the funding tx) 5. Show the OCAP URL and `setup-away.sh` command -Use `--reset` to purge all kernel state and start fresh. The SQLite database is at `~/.ocap/kernel-interactive.sqlite`. +Use `--reset` to purge all kernel state and start fresh. The SQLite database is at `$OCAP_HOME/kernel-interactive.sqlite` (defaults to `~/.ocap/kernel-interactive.sqlite`). **Note:** Interactive mode uses a Hybrid smart account (different address from the EOA) instead of EIP-7702 stateless. This is because EIP-7702 requires signing an authorization transaction that MetaMask Mobile does not support. The smart account is auto-funded from the EOA if its balance is below 0.05 ETH. @@ -335,7 +335,7 @@ When creating a delegation manually, add caveats to the `caveats` array. The `te ```bash # Delegation with 0.05 ETH total limit and 0.01 ETH per-transaction limit -yarn ocap daemon exec queueMessage '["ko4", "createDelegation", [{ +yarn ocap daemon queueMessage ko4 createDelegation '[{ "delegate": "0xAWAY_SMART_ACCOUNT", "caveats": [ { @@ -350,7 +350,7 @@ yarn ocap daemon exec queueMessage '["ko4", "createDelegation", [{ } ], "chainId": 11155111 -}]]' +}]' ``` ### Changing limits @@ -436,7 +436,7 @@ The home device holds the master wallet keys and runs a kernel daemon that the a yarn ocap daemon start ``` -This starts the OCAP daemon at `~/.ocap/daemon.sock` with persistent storage at `~/.ocap/kernel.sqlite`. +This starts the OCAP daemon at `~/.ocap/daemon.sock` with persistent storage at `~/.ocap/kernel.sqlite`. You can override the `~/.ocap` base directory by setting the `OCAP_HOME` environment variable (e.g. `export OCAP_HOME=/data/ocap`). ### 2b. Initialize remote comms (QUIC) @@ -495,34 +495,34 @@ Note the `rootKref` from the output (e.g. `ko4`). This is the wallet coordinator ```bash # Initialize with your mnemonic (SRP) — plaintext storage -yarn ocap daemon exec queueMessage '["ko4", "initializeKeyring", [{"type": "srp", "mnemonic": "your twelve word mnemonic phrase here"}]]' +yarn ocap daemon queueMessage ko4 initializeKeyring '[{"type": "srp", "mnemonic": "your twelve word mnemonic phrase here"}]' # Or encrypt the mnemonic at rest with a password: SALT="$(node -e "process.stdout.write(require('crypto').randomBytes(16).toString('hex'))")" -yarn ocap daemon exec queueMessage "[\"ko4\", \"initializeKeyring\", [{\"type\": \"srp\", \"mnemonic\": \"your twelve word mnemonic phrase here\", \"password\": \"your-password\", \"salt\": \"$SALT\"}]]" +yarn ocap daemon queueMessage ko4 initializeKeyring "[{\"type\": \"srp\", \"mnemonic\": \"your twelve word mnemonic phrase here\", \"password\": \"your-password\", \"salt\": \"$SALT\"}]" # Verify -yarn ocap daemon exec queueMessage '["ko4", "getAccounts", []]' +yarn ocap daemon queueMessage ko4 getAccounts ``` When a password is provided, the mnemonic is encrypted with AES-256-GCM (PBKDF2 key derivation). After a daemon restart, the keyring will be locked — unlock it before signing: ```bash -yarn ocap daemon exec queueMessage '["ko4", "unlockKeyring", ["your-password"]]' +yarn ocap daemon queueMessage ko4 unlockKeyring '["your-password"]' ``` ### 2e. Configure the provider ```bash -yarn ocap daemon exec queueMessage '["ko4", "configureProvider", [{"chainId": 11155111, "rpcUrl": "https://sepolia.infura.io/v3/YOUR_INFURA_KEY"}]]' +yarn ocap daemon queueMessage ko4 configureProvider '[{"chainId": 11155111, "rpcUrl": "https://sepolia.infura.io/v3/YOUR_INFURA_KEY"}]' # For other chains, adjust chainId and rpcUrl: -# yarn ocap daemon exec queueMessage '["ko4", "configureProvider", [{"chainId": 8453, "rpcUrl": "https://base-mainnet.infura.io/v3/YOUR_INFURA_KEY"}]]' +# yarn ocap daemon queueMessage ko4 configureProvider '[{"chainId": 8453, "rpcUrl": "https://base-mainnet.infura.io/v3/YOUR_INFURA_KEY"}]' ``` ### 2f. Issue an OCAP URL for the away device ```bash -yarn ocap daemon exec queueMessage '["ko4", "issueOcapUrl", []]' +yarn ocap daemon queueMessage ko4 issueOcapUrl ``` Save the returned `ocap:...` URL and the listen addresses from `getStatus` above — you'll give both to the away device. @@ -565,19 +565,19 @@ The away wallet gets a throwaway key (for signing UserOps within delegations). U ```bash ENTROPY="0x$(node -e "process.stdout.write(require('crypto').randomBytes(32).toString('hex'))")" -yarn ocap daemon exec queueMessage "[\"ko4\", \"initializeKeyring\", [{\"type\": \"throwaway\", \"entropy\": \"$ENTROPY\"}]]" +yarn ocap daemon queueMessage ko4 initializeKeyring "[{\"type\": \"throwaway\", \"entropy\": \"$ENTROPY\"}]" ``` ### 3f. Connect to the home wallet ```bash -yarn ocap daemon exec queueMessage '["ko4", "connectToPeer", ["ocap:zgAu...YOUR_OCAP_URL_HERE"]]' +yarn ocap daemon queueMessage ko4 connectToPeer '["ocap:zgAu...YOUR_OCAP_URL_HERE"]' ``` ### 3g. Verify the connection ```bash -yarn ocap daemon exec queueMessage '["ko4", "getCapabilities", []]' +yarn ocap daemon queueMessage ko4 getCapabilities ``` Should show `hasPeerWallet: true`. @@ -594,10 +594,10 @@ For manual setup, the steps are: ```bash # Home device (EIP-7702 — EOA becomes the smart account): -yarn ocap daemon exec queueMessage '["ko4", "createSmartAccount", [{"chainId": 11155111, "implementation": "stateless7702"}]]' +yarn ocap daemon queueMessage ko4 createSmartAccount '[{"chainId": 11155111, "implementation": "stateless7702"}]' # Away device (Hybrid — deploys on first UserOp): -yarn ocap daemon exec queueMessage '["ko4", "createSmartAccount", [{"chainId": 11155111}]]' +yarn ocap daemon queueMessage ko4 createSmartAccount '[{"chainId": 11155111}]' ``` The home EOA's existing ETH balance is used directly for delegated transfers — no separate funding step needed. @@ -605,32 +605,32 @@ The home EOA's existing ETH balance is used directly for delegated transfers — 2. Read the delegate address from the away device (sent automatically by the setup flow after peer connection): ```bash -yarn ocap daemon exec queueMessage '["ko4", "getDelegateAddress", []]' +yarn ocap daemon queueMessage ko4 getDelegateAddress ``` 3. Create the delegation on the home device (delegate = away smart account). See [Spending limits](#spending-limits) for adding caveats: ```bash -yarn ocap daemon exec queueMessage '["ko4", "createDelegation", [{"delegate": "0xAWAY_SMART_ACCOUNT", "caveats": [], "chainId": 11155111}]]' +yarn ocap daemon queueMessage ko4 createDelegation '[{"delegate": "0xAWAY_SMART_ACCOUNT", "caveats": [], "chainId": 11155111}]' ``` 4. Push the delegation to the away device (if connected): ```bash -yarn ocap daemon exec queueMessage '["ko4", "pushDelegationToAway", []]' +yarn ocap daemon queueMessage ko4 pushDelegationToAway '[]' ``` Or transfer manually if the away device is offline: ```bash # On the away device: -yarn ocap daemon exec queueMessage '["ko4", "receiveDelegation", []]' +yarn ocap daemon queueMessage ko4 receiveDelegation '[]' ``` 5. Verify: ```bash -yarn ocap daemon exec queueMessage '["ko4", "getCapabilities", []]' +yarn ocap daemon queueMessage ko4 getCapabilities # Should show delegationCount: 1 ``` @@ -659,26 +659,26 @@ You can also call the wallet coordinator directly. Replace `ko4` with your `root ```bash # List accounts -yarn ocap daemon exec queueMessage '["ko4", "getAccounts", []]' +yarn ocap daemon queueMessage ko4 getAccounts # Check capabilities (local keys, peer wallet, delegations, bundler) -yarn ocap daemon exec queueMessage '["ko4", "getCapabilities", []]' +yarn ocap daemon queueMessage ko4 getCapabilities # Sign a message -yarn ocap daemon exec queueMessage '["ko4", "signMessage", ["hello world"]]' +yarn ocap daemon queueMessage ko4 signMessage '["hello world"]' # Sign a transaction -yarn ocap daemon exec queueMessage '["ko4", "signTransaction", [{"to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", "value": "0x2386F26FC10000", "chainId": 11155111}]]' +yarn ocap daemon queueMessage ko4 signTransaction '[{"to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", "value": "0x2386F26FC10000", "chainId": 11155111}]' # Query the chain (eth_getBalance, eth_blockNumber, etc.) -yarn ocap daemon exec queueMessage '["ko4", "request", ["eth_getBalance", ["0x71fA1599e6c6FE46CD2A798E136f3ba22863cF82", "latest"]]]' -yarn ocap daemon exec queueMessage '["ko4", "request", ["eth_blockNumber", []]]' +yarn ocap daemon queueMessage ko4 request '["eth_getBalance", ["0x71fA1599e6c6FE46CD2A798E136f3ba22863cF82", "latest"]]' +yarn ocap daemon queueMessage ko4 request '["eth_blockNumber", []]' # Create a delegation for another address -yarn ocap daemon exec queueMessage '["ko4", "createDelegation", [{"delegate": "0x...", "caveats": [], "chainId": 11155111}]]' +yarn ocap daemon queueMessage ko4 createDelegation '[{"delegate": "0x...", "caveats": [], "chainId": 11155111}]' # List active delegations -yarn ocap daemon exec queueMessage '["ko4", "listDelegations", []]' +yarn ocap daemon queueMessage ko4 listDelegations ``` ## 6. How it works @@ -690,7 +690,7 @@ Agent (AI) ├─ wallet_send ──→ │ └─ wallet_sign ──→ │ │ - yarn ocap daemon exec queueMessage + yarn ocap daemon queueMessage │ OCAP Daemon (Unix socket) │ diff --git a/packages/evm-wallet-experiment/openclaw-plugin/daemon.ts b/packages/evm-wallet-experiment/openclaw-plugin/daemon.ts index 9c478066a..30c0528c5 100644 --- a/packages/evm-wallet-experiment/openclaw-plugin/daemon.ts +++ b/packages/evm-wallet-experiment/openclaw-plugin/daemon.ts @@ -1,12 +1,12 @@ /** * Daemon communication layer for the OpenClaw wallet plugin. * - * Spawns `ocap daemon exec` commands and decodes Endo CapData responses. + * Spawns `ocap daemon queueMessage` commands. The CLI auto-decodes CapData + * via prettifySmallcaps, so no manual CapData unwrapping is needed here. */ import { spawn } from 'node:child_process'; type ExecResult = { stdout: string; stderr: string; code: number | null }; -type CapDataLike = { body: string; slots: unknown[] }; export type WalletCallOptions = { cliPath: string; @@ -51,89 +51,25 @@ export function makeWalletCaller(options: { } /** - * Check if a value looks like Endo CapData. - * - * @param value - The parsed JSON value. - * @returns True when value has CapData shape. - */ -function isCapDataLike(value: unknown): value is CapDataLike { - if (typeof value !== 'object' || value === null) { - return false; - } - if (!('body' in value) || !('slots' in value)) { - return false; - } - const { body } = value as { body?: unknown }; - const { slots } = value as { slots?: unknown }; - return typeof body === 'string' && Array.isArray(slots); -} - -/** - * Decode daemon JSON output, unwrapping Endo CapData. - * - * @param raw - Raw stdout from `ocap daemon exec`. - * @param method - Wallet method name (for better errors). - * @returns The decoded value. - */ -function decodeCapData(raw: string, method: string): unknown { - let parsed: unknown; - try { - parsed = JSON.parse(raw); - } catch { - throw new Error(`Wallet ${method} returned non-JSON output`); - } - - if (!isCapDataLike(parsed)) { - return parsed; - } - - if (!parsed.body.startsWith('#')) { - throw new Error(`Wallet ${method} returned invalid CapData body`); - } - - const bodyContent = parsed.body.slice(1); - - // Handle error bodies from vat exceptions (e.g. "#error:message") - if (bodyContent.startsWith('error:')) { - throw new Error(`Wallet ${method} vat error: ${bodyContent.slice(6)}`); - } - - let decoded: unknown; - try { - decoded = JSON.parse(bodyContent); - } catch { - throw new Error(`Wallet ${method} returned undecodable CapData body`); - } - - // Handle Endo CapData error encoding: #{"#error": "message", ...} - if (decoded !== null && typeof decoded === 'object' && '#error' in decoded) { - const errorMsg = (decoded as Record)['#error']; - throw new Error( - `Wallet ${method} failed: ${typeof errorMsg === 'string' ? errorMsg : JSON.stringify(errorMsg)}`, - ); - } - - return decoded; -} - -/** - * Run an `ocap daemon exec` command and return its output. + * Run an `ocap daemon queueMessage` command and return its output. * * @param options - Execution options. * @param options.cliPath - Path to the ocap CLI. - * @param options.method - The daemon RPC method. - * @param options.params - The method parameters. + * @param options.walletKref - KRef of the target kernel object. + * @param options.method - Method name to invoke. + * @param options.argsJson - JSON-encoded array of arguments. * @param options.timeoutMs - Timeout in ms. * @returns The command result. */ -async function runDaemonExec(options: { +async function runDaemonQueueMessage(options: { cliPath: string; + walletKref: string; method: string; - params: unknown; + argsJson: string; timeoutMs: number; }): Promise { - const { cliPath, method, params, timeoutMs } = options; - const daemonArgs = ['daemon', 'exec', method, JSON.stringify(params)]; + const { cliPath, walletKref, method, argsJson, timeoutMs } = options; + const daemonArgs = ['daemon', 'queueMessage', walletKref, method, argsJson]; // If cliPath points to a .mjs file, invoke it via node. const command = cliPath.endsWith('.mjs') ? 'node' : cliPath; @@ -160,7 +96,9 @@ async function runDaemonExec(options: { try { child.kill('SIGKILL'); } finally { - reject(new Error(`ocap daemon exec timed out after ${timeoutMs}ms`)); + reject( + new Error(`ocap daemon queueMessage timed out after ${timeoutMs}ms`), + ); } }, timeoutMs); @@ -179,15 +117,19 @@ async function runDaemonExec(options: { /** * Call a wallet coordinator method via the OCAP daemon. * + * The CLI's `daemon queueMessage` auto-decodes CapData via prettifySmallcaps, + * so the output is already a decoded JSON value. + * * @param options - Call options. * @returns The decoded response value. */ async function callWallet(options: WalletCallOptions): Promise { const { cliPath, walletKref, method, args, timeoutMs } = options; - const result = await runDaemonExec({ + const result = await runDaemonQueueMessage({ cliPath, - method: 'queueMessage', - params: [walletKref, method, args], + walletKref, + method, + argsJson: JSON.stringify(args), timeoutMs, }); @@ -196,5 +138,21 @@ async function callWallet(options: WalletCallOptions): Promise { throw new Error(`Wallet ${method} failed (exit ${result.code}): ${detail}`); } - return decodeCapData(result.stdout.trim(), method); + const raw = result.stdout.trim(); + let decoded: unknown; + try { + decoded = JSON.parse(raw); + } catch { + throw new Error(`Wallet ${method} returned non-JSON output`); + } + + // Handle error objects from vat exceptions (decoded by prettifySmallcaps) + if (decoded !== null && typeof decoded === 'object' && '#error' in decoded) { + const errorMsg = (decoded as Record)['#error']; + throw new Error( + `Wallet ${method} failed: ${typeof errorMsg === 'string' ? errorMsg : JSON.stringify(errorMsg)}`, + ); + } + + return decoded; } diff --git a/packages/evm-wallet-experiment/scripts/home-interactive.mjs b/packages/evm-wallet-experiment/scripts/home-interactive.mjs index 6853b1d4e..f0728e629 100644 --- a/packages/evm-wallet-experiment/scripts/home-interactive.mjs +++ b/packages/evm-wallet-experiment/scripts/home-interactive.mjs @@ -152,7 +152,10 @@ function parseArgs(argv) { quicPort: 4002, reset: false, rpcUrl: '', - dbPath: join(homedir(), '.ocap', 'kernel-interactive.sqlite'), + dbPath: join( + process.env.OCAP_HOME || join(homedir(), '.ocap'), + 'kernel-interactive.sqlite', + ), }; for (let i = 2; i < argv.length; i++) { diff --git a/packages/evm-wallet-experiment/scripts/setup-away.sh b/packages/evm-wallet-experiment/scripts/setup-away.sh index ff74e9afb..30a0b4697 100755 --- a/packages/evm-wallet-experiment/scripts/setup-away.sh +++ b/packages/evm-wallet-experiment/scripts/setup-away.sh @@ -161,36 +161,11 @@ info() { echo -e "${CYAN}→${RESET} $*" >&2; } ok() { echo -e " ${GREEN}✓${RESET} $*" >&2; } fail() { echo -e " ${RED}✗${RESET} $*" >&2; exit 1; } -parse_capdata() { +# Decode JSON output — for strings, output the raw value; for other types, output JSON. +json_value() { node -e " - const raw = require('fs').readFileSync('/dev/stdin', 'utf8').trim(); - if (!raw) { - process.stderr.write('parse_capdata: empty input\n'); - process.exit(1); - } - let data; - try { data = JSON.parse(raw); } catch (e) { - process.stderr.write('parse_capdata: invalid JSON: ' + raw.slice(0, 200) + '\n'); - process.exit(1); - } - if (!data.body || typeof data.body !== 'string') { - process.stderr.write('parse_capdata: missing body field: ' + raw.slice(0, 200) + '\n'); - process.exit(1); - } - if (!data.body.startsWith('#')) { - process.stderr.write('parse_capdata: unexpected body prefix: ' + data.body.slice(0, 200) + '\n'); - process.exit(1); - } - if (data.slots && data.slots.length > 0) { - process.stderr.write('parse_capdata: cannot handle slot references\n'); - process.exit(1); - } - let value; - try { value = JSON.parse(data.body.slice(1)); } catch (e) { - process.stderr.write('parse_capdata: invalid CapData body: ' + data.body.slice(0, 200) + '\n'); - process.exit(1); - } - process.stdout.write(typeof value === 'string' ? value : JSON.stringify(value)); + const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); + process.stdout.write(typeof v === 'string' ? v : JSON.stringify(v)); " } @@ -211,6 +186,25 @@ daemon_exec() { echo "$result" } +# Run `ocap daemon queueMessage` (auto-decodes CapData via prettifySmallcaps). +# Usage: daemon_qm [--quiet] [--raw] KREF METHOD [ARGS_JSON] [--timeout N] +# --quiet suppresses the stderr log line +# --raw outputs raw CapData instead of decoded result +daemon_qm() { + local quiet=false + if [[ "${1:-}" == "--quiet" ]]; then + quiet=true + shift + fi + local method="${2:-}" + local result + result=$(node "$OCAP_BIN" daemon queueMessage "$@") + if [[ -n "$result" && "$quiet" == false ]]; then + echo " [queueMessage $method] $result" >&2 + fi + echo "$result" +} + # --------------------------------------------------------------------------- # 1. Build # --------------------------------------------------------------------------- @@ -427,12 +421,11 @@ info "Initializing throwaway keyring..." # is unavailable inside vats). The entropy is passed to the keyring vat which uses # it as the private key for the throwaway account. ENTROPY="0x$(node -e "process.stdout.write(require('crypto').randomBytes(32).toString('hex'))")" -daemon_exec --quiet queueMessage "[\"$ROOT_KREF\", \"initializeKeyring\", [{\"type\":\"throwaway\",\"entropy\":\"$ENTROPY\"}]]" >/dev/null +daemon_qm --quiet "$ROOT_KREF" initializeKeyring "[{\"type\":\"throwaway\",\"entropy\":\"$ENTROPY\"}]" >/dev/null ok "Throwaway keyring initialized" info "Verifying accounts..." -ACCOUNTS_RAW=$(daemon_exec queueMessage "[\"$ROOT_KREF\", \"getAccounts\", []]") -ACCOUNTS=$(echo "$ACCOUNTS_RAW" | parse_capdata) +ACCOUNTS=$(daemon_qm "$ROOT_KREF" getAccounts) ok "Local throwaway account: $ACCOUNTS" # --------------------------------------------------------------------------- @@ -447,12 +440,11 @@ if [[ -n "$INFURA_KEY" ]]; then fi info "Configuring provider ($(chain_name "$CHAIN_ID"), chain $CHAIN_ID)..." - PROVIDER_PARAMS=$(KREF="$ROOT_KREF" CID="$CHAIN_ID" URL="$RPC_URL" node -e " - const p = JSON.stringify([process.env.KREF, 'configureProvider', [{ chainId: Number(process.env.CID), rpcUrl: process.env.URL }]]); - process.stdout.write(p); + PROVIDER_ARGS=$(CID="$CHAIN_ID" URL="$RPC_URL" node -e " + process.stdout.write(JSON.stringify([{ chainId: Number(process.env.CID), rpcUrl: process.env.URL }])); ") - daemon_exec queueMessage "$PROVIDER_PARAMS" >/dev/null + daemon_qm "$ROOT_KREF" configureProvider "$PROVIDER_ARGS" >/dev/null ok "Provider configured — $RPC_URL" fi @@ -465,12 +457,11 @@ if [[ -n "$PIMLICO_KEY" ]]; then BUNDLER_URL="${PIMLICO_BASE}?apikey=${PIMLICO_KEY}" info "Configuring bundler (Pimlico)..." - BUNDLER_PARAMS=$(KREF="$ROOT_KREF" CID="$CHAIN_ID" BURL="$BUNDLER_URL" node -e " - const p = JSON.stringify([process.env.KREF, 'configureBundler', [{ bundlerUrl: process.env.BURL, chainId: Number(process.env.CID), usePaymaster: true }]]); - process.stdout.write(p); + BUNDLER_ARGS=$(CID="$CHAIN_ID" BURL="$BUNDLER_URL" node -e " + process.stdout.write(JSON.stringify([{ bundlerUrl: process.env.BURL, chainId: Number(process.env.CID), usePaymaster: true }])); ") - daemon_exec queueMessage "$BUNDLER_PARAMS" >/dev/null + daemon_qm "$ROOT_KREF" configureBundler "$BUNDLER_ARGS" >/dev/null ok "Bundler configured — Pimlico (chain $CHAIN_ID)" # Create a Hybrid smart account (counterfactual — no on-chain tx needed). @@ -478,12 +469,11 @@ if [[ -n "$PIMLICO_KEY" ]]; then # The away device can't use stateless7702 because the throwaway EOA has # no ETH to pay for the on-chain EIP-7702 authorization tx. info "Setting up smart account (Hybrid, counterfactual)..." - SA_PARAMS=$(KREF="$ROOT_KREF" CID="$CHAIN_ID" node -e " - const p = JSON.stringify([process.env.KREF, 'createSmartAccount', [{ chainId: Number(process.env.CID) }]]); - process.stdout.write(p); + SA_ARGS=$(CID="$CHAIN_ID" node -e " + process.stdout.write(JSON.stringify([{ chainId: Number(process.env.CID) }])); ") - SA_RAW=$(daemon_exec queueMessage "$SA_PARAMS" --timeout 120) - SMART_ACCOUNT=$(echo "$SA_RAW" | parse_capdata | node -e " + SA_RESULT=$(daemon_qm "$ROOT_KREF" createSmartAccount "$SA_ARGS" --timeout 120) + SMART_ACCOUNT=$(echo "$SA_RESULT" | node -e " const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); process.stdout.write(d.address || ''); " 2>/dev/null || echo "") @@ -502,12 +492,11 @@ fi info "Connecting to home wallet..." -CONNECT_PARAMS=$(KREF="$ROOT_KREF" PEER_URL="$OCAP_URL" node -e " - const p = JSON.stringify([process.env.KREF, 'connectToPeer', [process.env.PEER_URL]]); - process.stdout.write(p); +CONNECT_ARGS=$(PEER_URL="$OCAP_URL" node -e " + process.stdout.write(JSON.stringify([process.env.PEER_URL])); ") -daemon_exec queueMessage "$CONNECT_PARAMS" --timeout 120 >/dev/null +daemon_qm "$ROOT_KREF" connectToPeer "$CONNECT_ARGS" --timeout 120 >/dev/null # --------------------------------------------------------------------------- # 9. Wait for peer wallet connection @@ -515,12 +504,11 @@ daemon_exec queueMessage "$CONNECT_PARAMS" --timeout 120 >/dev/null info "Waiting for peer wallet connection (up to 60s)..." for i in $(seq 1 60); do - CAPS_RAW=$(daemon_exec --quiet queueMessage "[\"$ROOT_KREF\", \"getCapabilities\", []]" 2>/dev/null) || CAPS_RAW="" - if [[ -n "$CAPS_RAW" ]]; then - HAS_PEER=$(echo "$CAPS_RAW" | node -e " + CAPS_RESULT=$(daemon_qm --quiet "$ROOT_KREF" getCapabilities 2>/dev/null) || CAPS_RESULT="" + if [[ -n "$CAPS_RESULT" ]]; then + HAS_PEER=$(echo "$CAPS_RESULT" | node -e " try { - const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); - const v = JSON.parse(d.body.slice(1)); + const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); process.stdout.write(String(v.hasPeerWallet)); } catch { process.stdout.write('false'); } " 2>/dev/null || echo "false") @@ -540,8 +528,7 @@ ok "Peer wallet connected and verified" # --------------------------------------------------------------------------- info "Caching home device accounts for offline use..." -CACHED_RAW=$(daemon_exec queueMessage "[\"$ROOT_KREF\", \"refreshPeerAccounts\", []]") -CACHED_ACCOUNTS=$(echo "$CACHED_RAW" | parse_capdata) +CACHED_ACCOUNTS=$(daemon_qm "$ROOT_KREF" refreshPeerAccounts) ok "Cached peer accounts: $CACHED_ACCOUNTS" # --------------------------------------------------------------------------- @@ -563,11 +550,7 @@ fi # --------------------------------------------------------------------------- info "Sending delegate address to home device..." -SEND_ADDR_PARAMS=$(KREF="$ROOT_KREF" ADDR="$DELEGATE_ADDR" node -e " - const p = JSON.stringify([process.env.KREF, 'sendDelegateAddressToPeer', [process.env.ADDR]]); - process.stdout.write(p); -") -if daemon_exec --quiet queueMessage "$SEND_ADDR_PARAMS" --timeout 15 >/dev/null 2>&1; then +if daemon_qm --quiet "$ROOT_KREF" sendDelegateAddressToPeer "[\"$DELEGATE_ADDR\"]" --timeout 15 >/dev/null 2>&1; then ok "Delegate address sent to home device: $DELEGATE_ADDR" else echo -e " ${YELLOW}Could not send delegate address automatically.${RESET}" >&2 @@ -596,12 +579,11 @@ DEL_COUNT="0" POLL_FAILURES=0 MANUAL_SKIP=false for i in $(seq 1 300); do - CAPS_RAW=$(daemon_exec --quiet queueMessage "[\"$ROOT_KREF\", \"getCapabilities\", []]" 2>/dev/null) || CAPS_RAW="" - if [[ -n "$CAPS_RAW" ]]; then + CAPS_RESULT=$(daemon_qm --quiet "$ROOT_KREF" getCapabilities 2>/dev/null) || CAPS_RESULT="" + if [[ -n "$CAPS_RESULT" ]]; then POLL_FAILURES=0 - DEL_COUNT=$(echo "$CAPS_RAW" | node -e " - const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); - const v = JSON.parse(d.body.slice(1)); + DEL_COUNT=$(echo "$CAPS_RESULT" | node -e " + const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); process.stdout.write(String(v.delegationCount)); " 2>/dev/null || echo "0") if [[ "$DEL_COUNT" != "0" ]]; then @@ -611,7 +593,7 @@ for i in $(seq 1 300); do else POLL_FAILURES=$((POLL_FAILURES + 1)) if [[ "$POLL_FAILURES" -ge 5 ]]; then - fail "Daemon appears to be down (5 consecutive failed polls). Check: tail -f ~/.ocap/daemon.log" + fail "Daemon appears to be down (5 consecutive failed polls). Check: tail -f ${OCAP_HOME:-~/.ocap}/daemon.log" fi fi if [[ "$i" -eq 300 ]]; then @@ -637,6 +619,7 @@ if [[ "$MANUAL_SKIP" == true && "$DEL_COUNT" == "0" ]]; then fail "No delegation JSON provided" fi + # Accept both CapData format (from old CLI) and plain JSON (from new CLI) DELEGATION_INNER=$(echo "$DELEGATION_JSON" | node -e " const raw = require('fs').readFileSync('/dev/stdin', 'utf8').trim(); let data; @@ -658,18 +641,16 @@ if [[ "$MANUAL_SKIP" == true && "$DEL_COUNT" == "0" ]]; then ") info "Receiving delegation..." - RECEIVE_PARAMS=$(KREF="$ROOT_KREF" DEL="$DELEGATION_INNER" node -e " - const p = JSON.stringify([process.env.KREF, 'receiveDelegation', [JSON.parse(process.env.DEL)]]); - process.stdout.write(p); + RECEIVE_ARGS=$(DEL="$DELEGATION_INNER" node -e " + process.stdout.write(JSON.stringify([JSON.parse(process.env.DEL)])); ") - daemon_exec queueMessage "$RECEIVE_PARAMS" >/dev/null + daemon_qm "$ROOT_KREF" receiveDelegation "$RECEIVE_ARGS" >/dev/null ok "Delegation received (manual)" - CAPS_FINAL_RAW=$(daemon_exec --quiet queueMessage "[\"$ROOT_KREF\", \"getCapabilities\", []]" 2>/dev/null) || CAPS_FINAL_RAW="" - DEL_COUNT=$(echo "$CAPS_FINAL_RAW" | node -e " + CAPS_FINAL=$(daemon_qm --quiet "$ROOT_KREF" getCapabilities 2>/dev/null) || CAPS_FINAL="" + DEL_COUNT=$(echo "$CAPS_FINAL" | node -e " try { - const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); - const v = JSON.parse(d.body.slice(1)); + const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); process.stdout.write(String(v.delegationCount)); } catch { process.stdout.write('0'); } " 2>/dev/null || echo "0") @@ -692,7 +673,7 @@ $(echo -e "${GREEN}${BOLD}")═════════════════ $(echo -e "${DIM}")Cached accounts :$(echo -e "${RESET}") $CACHED_ACCOUNTS $(echo -e "${DIM}")Peer connected :$(echo -e "${RESET}") $(echo -e "${GREEN}")true$(echo -e "${RESET}") - Watch daemon logs: $(echo -e "${DIM}")tail -f ~/.ocap/daemon.log$(echo -e "${RESET}") + Watch daemon logs: $(echo -e "${DIM}")tail -f ${OCAP_HOME:-~/.ocap}/daemon.log$(echo -e "${RESET}") Stop the daemon: $(echo -e "${DIM}")yarn ocap daemon stop$(echo -e "${RESET}") Purge all state: $(echo -e "${DIM}")yarn ocap daemon purge --force$(echo -e "${RESET}") diff --git a/packages/evm-wallet-experiment/scripts/setup-home.sh b/packages/evm-wallet-experiment/scripts/setup-home.sh index 6a9fb1c3b..51152fbee 100755 --- a/packages/evm-wallet-experiment/scripts/setup-home.sh +++ b/packages/evm-wallet-experiment/scripts/setup-home.sh @@ -173,38 +173,11 @@ info() { echo -e "${CYAN}→${RESET} $*" >&2; } ok() { echo -e " ${GREEN}✓${RESET} $*" >&2; } fail() { echo -e " ${RED}✗${RESET} $*" >&2; exit 1; } -# Parse the value out of Endo CapData JSON ({"body":"#...","slots":[...]}). -# Simple values only (strings, arrays, objects without slot references). -parse_capdata() { +# Decode JSON output — for strings, output the raw value; for other types, output JSON. +json_value() { node -e " - const raw = require('fs').readFileSync('/dev/stdin', 'utf8').trim(); - if (!raw) { - process.stderr.write('parse_capdata: empty input\n'); - process.exit(1); - } - let data; - try { data = JSON.parse(raw); } catch (e) { - process.stderr.write('parse_capdata: invalid JSON: ' + raw.slice(0, 200) + '\n'); - process.exit(1); - } - if (!data.body || typeof data.body !== 'string') { - process.stderr.write('parse_capdata: missing body field: ' + raw.slice(0, 200) + '\n'); - process.exit(1); - } - if (!data.body.startsWith('#')) { - process.stderr.write('parse_capdata: unexpected body prefix: ' + data.body.slice(0, 200) + '\n'); - process.exit(1); - } - if (data.slots && data.slots.length > 0) { - process.stderr.write('parse_capdata: cannot handle slot references\n'); - process.exit(1); - } - let value; - try { value = JSON.parse(data.body.slice(1)); } catch (e) { - process.stderr.write('parse_capdata: invalid CapData body: ' + data.body.slice(0, 200) + '\n'); - process.exit(1); - } - process.stdout.write(typeof value === 'string' ? value : JSON.stringify(value)); + const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); + process.stdout.write(typeof v === 'string' ? v : JSON.stringify(v)); " } @@ -225,6 +198,25 @@ daemon_exec() { echo "$result" } +# Run `ocap daemon queueMessage` (auto-decodes CapData via prettifySmallcaps). +# Usage: daemon_qm [--quiet] [--raw] KREF METHOD [ARGS_JSON] [--timeout N] +# --quiet suppresses the stderr log line +# --raw outputs raw CapData instead of decoded result +daemon_qm() { + local quiet=false + if [[ "${1:-}" == "--quiet" ]]; then + quiet=true + shift + fi + local method="${2:-}" + local result + result=$(node "$OCAP_BIN" daemon queueMessage "$@") + if [[ -n "$result" && "$quiet" == false ]]; then + echo " [queueMessage $method] $result" >&2 + fi + echo "$result" +} + # --------------------------------------------------------------------------- # 1. Build # --------------------------------------------------------------------------- @@ -389,22 +381,20 @@ else info "Initializing keyring with SRP..." fi -INIT_PARAMS=$(KREF="$ROOT_KREF" SRP="$MNEMONIC" PW="$KEYRING_PASSWORD" node -e " +INIT_ARGS=$(SRP="$MNEMONIC" PW="$KEYRING_PASSWORD" node -e " const opts = { type: 'srp', mnemonic: process.env.SRP }; if (process.env.PW) { opts.password = process.env.PW; opts.salt = require('crypto').randomBytes(16).toString('hex'); } - const p = JSON.stringify([process.env.KREF, 'initializeKeyring', [opts]]); - process.stdout.write(p); + process.stdout.write(JSON.stringify([opts])); ") -daemon_exec --quiet queueMessage "$INIT_PARAMS" >/dev/null +daemon_qm --quiet "$ROOT_KREF" initializeKeyring "$INIT_ARGS" >/dev/null ok "Keyring initialized" info "Verifying accounts..." -ACCOUNTS_RAW=$(daemon_exec queueMessage "[\"$ROOT_KREF\", \"getAccounts\", []]") -ACCOUNTS=$(echo "$ACCOUNTS_RAW" | parse_capdata) +ACCOUNTS=$(daemon_qm "$ROOT_KREF" getAccounts) ok "Accounts: $ACCOUNTS" # --------------------------------------------------------------------------- @@ -413,12 +403,11 @@ ok "Accounts: $ACCOUNTS" info "Configuring provider ($(chain_name "$CHAIN_ID"), chain $CHAIN_ID)..." -PROVIDER_PARAMS=$(KREF="$ROOT_KREF" CID="$CHAIN_ID" URL="$RPC_URL" node -e " - const p = JSON.stringify([process.env.KREF, 'configureProvider', [{ chainId: Number(process.env.CID), rpcUrl: process.env.URL }]]); - process.stdout.write(p); +PROVIDER_ARGS=$(CID="$CHAIN_ID" URL="$RPC_URL" node -e " + process.stdout.write(JSON.stringify([{ chainId: Number(process.env.CID), rpcUrl: process.env.URL }])); ") -daemon_exec queueMessage "$PROVIDER_PARAMS" >/dev/null +daemon_qm "$ROOT_KREF" configureProvider "$PROVIDER_ARGS" >/dev/null ok "Provider configured — $RPC_URL" # --------------------------------------------------------------------------- @@ -430,23 +419,21 @@ if [[ -n "$PIMLICO_KEY" ]]; then BUNDLER_URL="${PIMLICO_BASE}?apikey=${PIMLICO_KEY}" info "Configuring bundler (Pimlico)..." - BUNDLER_PARAMS=$(KREF="$ROOT_KREF" CID="$CHAIN_ID" BURL="$BUNDLER_URL" node -e " - const p = JSON.stringify([process.env.KREF, 'configureBundler', [{ bundlerUrl: process.env.BURL, chainId: Number(process.env.CID), usePaymaster: true }]]); - process.stdout.write(p); + BUNDLER_ARGS=$(CID="$CHAIN_ID" BURL="$BUNDLER_URL" node -e " + process.stdout.write(JSON.stringify([{ bundlerUrl: process.env.BURL, chainId: Number(process.env.CID), usePaymaster: true }])); ") - daemon_exec queueMessage "$BUNDLER_PARAMS" >/dev/null + daemon_qm "$ROOT_KREF" configureBundler "$BUNDLER_ARGS" >/dev/null ok "Bundler configured — Pimlico (chain $CHAIN_ID)" # Promote the EOA to a smart account via EIP-7702 authorization. # The EOA's address stays the same — no separate contract, no funding needed. info "Setting up smart account (EIP-7702 stateless)..." - SA_PARAMS=$(KREF="$ROOT_KREF" CID="$CHAIN_ID" node -e " - const p = JSON.stringify([process.env.KREF, 'createSmartAccount', [{ chainId: Number(process.env.CID), implementation: 'stateless7702' }]]); - process.stdout.write(p); + SA_ARGS=$(CID="$CHAIN_ID" node -e " + process.stdout.write(JSON.stringify([{ chainId: Number(process.env.CID), implementation: 'stateless7702' }])); ") - SA_RAW=$(daemon_exec queueMessage "$SA_PARAMS" --timeout 60) - HOME_SMART_ACCOUNT=$(echo "$SA_RAW" | parse_capdata | node -e " + SA_RESULT=$(daemon_qm "$ROOT_KREF" createSmartAccount "$SA_ARGS" --timeout 60) + HOME_SMART_ACCOUNT=$(echo "$SA_RESULT" | node -e " const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); process.stdout.write(d.address || ''); " 2>/dev/null || echo "") @@ -464,8 +451,7 @@ fi # --------------------------------------------------------------------------- info "Issuing OCAP URL for the away device..." -OCAP_URL_RAW=$(daemon_exec queueMessage "[\"$ROOT_KREF\", \"issueOcapUrl\", []]") -OCAP_URL=$(echo "$OCAP_URL_RAW" | parse_capdata) +OCAP_URL=$(daemon_qm "$ROOT_KREF" issueOcapUrl | json_value) # Strip trailing comma (kernel emits ocap:...@peerId, when no relays are known) OCAP_URL="${OCAP_URL%,}" @@ -577,14 +563,12 @@ echo -e " ${DIM}Or paste the delegate address here to skip waiting.${RESET}" >& POLL_FAILURES=0 for i in $(seq 1 300); do - DELEGATE_RAW=$(daemon_exec --quiet queueMessage "[\"$ROOT_KREF\", \"getDelegateAddress\", []]" 2>/dev/null) || DELEGATE_RAW="" - if [[ -n "$DELEGATE_RAW" ]]; then + DELEGATE_RESULT=$(daemon_qm --quiet "$ROOT_KREF" getDelegateAddress 2>/dev/null) || DELEGATE_RESULT="" + if [[ -n "$DELEGATE_RESULT" ]]; then POLL_FAILURES=0 - DELEGATE_ADDR=$(echo "$DELEGATE_RAW" | node -e " - const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); - if (!d.body || !d.body.startsWith('#')) { process.exit(0); } - const v = JSON.parse(d.body.slice(1)); - if (v && typeof v === 'string' && /^0x[\da-fA-F]{40}$/.test(v)) { + DELEGATE_ADDR=$(echo "$DELEGATE_RESULT" | node -e " + const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); + if (typeof v === 'string' && /^0x[\da-fA-F]{40}$/.test(v)) { process.stdout.write(v); } " 2>/dev/null || echo "") @@ -595,7 +579,7 @@ for i in $(seq 1 300); do else POLL_FAILURES=$((POLL_FAILURES + 1)) if [[ "$POLL_FAILURES" -ge 5 ]]; then - fail "Daemon appears to be down (5 consecutive failed polls). Check: tail -f ~/.ocap/daemon.log" + fail "Daemon appears to be down (5 consecutive failed polls). Check: tail -f ${OCAP_HOME:-~/.ocap}/daemon.log" fi fi if [[ "$i" -eq 300 ]]; then @@ -606,7 +590,7 @@ for i in $(seq 1 300); do if [[ -z "$DELEGATE_ADDR" ]]; then echo -e "\n ${DIM}No delegate address provided. You can create the delegation manually later:${RESET}" >&2 - echo -e " ${DIM}yarn ocap daemon exec queueMessage '[\"$ROOT_KREF\", \"createDelegation\", [{\"delegate\": \"0xADDRESS\", \"caveats\": [{\"type\":\"nativeTokenTransferAmount\",\"enforcer\":\"0xF71af580b9c3078fbc2BBF16FbB8EEd82b330320\",\"terms\":\"0x...\"}], \"chainId\": $CHAIN_ID}]]'${RESET}\n" >&2 + echo -e " ${DIM}yarn ocap daemon queueMessage $ROOT_KREF createDelegation '[{\"delegate\": \"0xADDRESS\", \"caveats\": [{\"type\":\"nativeTokenTransferAmount\",\"enforcer\":\"0xF71af580b9c3078fbc2BBF16FbB8EEd82b330320\",\"terms\":\"0x...\"}], \"chainId\": $CHAIN_ID}]'${RESET}\n" >&2 exit 0 fi break @@ -661,12 +645,10 @@ else echo -e " ${DIM}Caveats: $CAVEATS_JSON${RESET}" >&2 fi -DEL_PARAMS=$(KREF="$ROOT_KREF" DEL="$DELEGATE_ADDR" CID="$CHAIN_ID" CAVS="$CAVEATS_JSON" node -e " - const p = JSON.stringify([process.env.KREF, 'createDelegation', [{ delegate: process.env.DEL, caveats: JSON.parse(process.env.CAVS), chainId: Number(process.env.CID) }]]); - process.stdout.write(p); +DEL_ARGS=$(DEL="$DELEGATE_ADDR" CID="$CHAIN_ID" CAVS="$CAVEATS_JSON" node -e " + process.stdout.write(JSON.stringify([{ delegate: process.env.DEL, caveats: JSON.parse(process.env.CAVS), chainId: Number(process.env.CID) }])); ") -DEL_RAW=$(daemon_exec queueMessage "$DEL_PARAMS") -DEL_INNER=$(echo "$DEL_RAW" | parse_capdata) +DEL_JSON=$(daemon_qm "$ROOT_KREF" createDelegation "$DEL_ARGS") ok "Delegation created" # --------------------------------------------------------------------------- @@ -674,11 +656,10 @@ ok "Delegation created" # --------------------------------------------------------------------------- HAS_AWAY="false" -CAPS_RAW=$(daemon_exec --quiet queueMessage "[\"$ROOT_KREF\", \"getCapabilities\", []]" 2>/dev/null) || CAPS_RAW="" -if [[ -n "$CAPS_RAW" ]]; then - HAS_AWAY=$(echo "$CAPS_RAW" | node -e " - const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); - const v = JSON.parse(d.body.slice(1)); +CAPS_RESULT=$(daemon_qm --quiet "$ROOT_KREF" getCapabilities 2>/dev/null) || CAPS_RESULT="" +if [[ -n "$CAPS_RESULT" ]]; then + HAS_AWAY=$(echo "$CAPS_RESULT" | node -e " + const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); process.stdout.write(String(v.hasAwayWallet === true)); " 2>&1) || { echo -e " ${YELLOW}Warning: Failed to parse capabilities — cannot auto-push delegation${RESET}" >&2 @@ -690,11 +671,10 @@ fi if [[ "$HAS_AWAY" == "true" ]]; then info "Pushing delegation to away device..." - PUSH_PARAMS=$(KREF="$ROOT_KREF" DEL="$DEL_INNER" node -e " - const p = JSON.stringify([process.env.KREF, 'pushDelegationToAway', [JSON.parse(process.env.DEL)]]); - process.stdout.write(p); + PUSH_ARGS=$(DEL="$DEL_JSON" node -e " + process.stdout.write(JSON.stringify([JSON.parse(process.env.DEL)])); ") - PUSH_OUTPUT=$(daemon_exec --quiet queueMessage "$PUSH_PARAMS" --timeout 30 2>&1) && { + PUSH_OUTPUT=$(daemon_qm --quiet "$ROOT_KREF" pushDelegationToAway "$PUSH_ARGS" --timeout 30 2>&1) && { ok "Delegation pushed to away device" } || { echo -e " ${RED}✗${RESET} Push failed — falling back to manual transfer" >&2 @@ -711,14 +691,15 @@ if [[ "$HAS_AWAY" != "true" ]]; then $(echo -e "${YELLOW}${BOLD}") Copy this delegation JSON and paste it into the away device script when prompted:$(echo -e "${RESET}") -$(echo -e "${BOLD}")$DEL_RAW$(echo -e "${RESET}") +$(echo -e "${BOLD}")$DEL_JSON$(echo -e "${RESET}") EOF fi +OCAP_DIR="${OCAP_HOME:-~/.ocap}" cat >&2 <&2 + echo "Error: ocap CLI not found at $OCAP_BIN. Run 'yarn workspace @metamask/kernel-cli build' first." >&2 exit 1 fi @@ -67,42 +67,19 @@ info() { echo -e "${CYAN}→${RESET} $*" >&2; } ok() { echo -e " ${GREEN}✓${RESET} $*" >&2; } fail() { echo -e " ${RED}✗${RESET} $*" >&2; exit 1; } -parse_capdata() { - node -e " - const raw = require('fs').readFileSync('/dev/stdin', 'utf8').trim(); - if (!raw) { process.stderr.write('parse_capdata: empty input\n'); process.exit(1); } - let data; - try { data = JSON.parse(raw); } catch (e) { - process.stderr.write('parse_capdata: invalid JSON: ' + raw.slice(0, 200) + '\n'); - process.exit(1); - } - if (!data.body || typeof data.body !== 'string') { - process.stderr.write('parse_capdata: missing body field: ' + raw.slice(0, 200) + '\n'); - process.exit(1); - } - if (!data.body.startsWith('#')) { - process.stderr.write('parse_capdata: body does not start with #: ' + data.body.slice(0, 100) + '\n'); - process.exit(1); - } - let value; - try { value = JSON.parse(data.body.slice(1)); } catch (e) { - process.stderr.write('parse_capdata: invalid CapData body: ' + data.body.slice(0, 200) + '\n'); - process.exit(1); - } - process.stdout.write(typeof value === 'string' ? value : JSON.stringify(value)); - " -} - -daemon_exec() { +# Run `ocap daemon queueMessage` (auto-decodes CapData via prettifySmallcaps). +# Usage: daemon_qm [--quiet] [--raw] KREF METHOD [ARGS_JSON] [--timeout N] +daemon_qm() { local quiet=false if [[ "${1:-}" == "--quiet" ]]; then quiet=true shift fi + local method="${2:-}" local result - result=$(node "$OCAP_BIN" daemon exec "$@") + result=$(node "$OCAP_BIN" daemon queueMessage "$@") if [[ -n "$result" && "$quiet" == false ]]; then - echo " [daemon exec $1] $result" >&2 + echo " [queueMessage $method] $result" >&2 fi echo "$result" } @@ -112,10 +89,8 @@ daemon_exec() { # --------------------------------------------------------------------------- info "Fetching current delegations..." -ACCOUNTS_RAW=$(daemon_exec --quiet queueMessage "[\"$ROOT_KREF\", \"getAccounts\", []]") -ACCOUNTS=$(echo "$ACCOUNTS_RAW" | parse_capdata) -DEL_LIST_RAW=$(daemon_exec --quiet queueMessage "[\"$ROOT_KREF\", \"listDelegations\", []]") -DEL_LIST=$(echo "$DEL_LIST_RAW" | parse_capdata) +ACCOUNTS=$(daemon_qm --quiet "$ROOT_KREF" getAccounts) +DEL_LIST=$(daemon_qm --quiet "$ROOT_KREF" listDelegations) # Show only active (signed) delegations issued by this device ACTIVE_INFO=$(ACCTS="$ACCOUNTS" node -e " @@ -235,7 +210,7 @@ echo -e " ${DIM}Each revocation submits a UserOp and waits for on-chain confirm REVOKE_FAILED=0 while read -r DEL_ID; do echo -e " ${DIM}Revoking $DEL_ID...${RESET}" >&2 - REVOKE_OUTPUT=$(daemon_exec --quiet queueMessage "[\"$ROOT_KREF\", \"revokeDelegation\", [\"$DEL_ID\"]]" --timeout 120) || { + REVOKE_OUTPUT=$(daemon_qm --quiet "$ROOT_KREF" revokeDelegation "[\"$DEL_ID\"]" --timeout 120) || { echo -e " ${RED}✗${RESET} Failed to revoke delegation $DEL_ID" >&2 if [[ -n "$REVOKE_OUTPUT" ]]; then echo -e " ${DIM}Reason: $REVOKE_OUTPUT${RESET}" >&2 @@ -243,16 +218,14 @@ while read -r DEL_ID; do REVOKE_FAILED=$((REVOKE_FAILED + 1)) continue } - # Check if the daemon returned a CapData-wrapped error (exit code is still 0). - # The CapData body contains "#error" as a JSON key; grep the raw output. - if echo "$REVOKE_OUTPUT" | grep -q '#error'; then - ERR_MSG=$(echo "$REVOKE_OUTPUT" | parse_capdata | node -e " + # Check if the result is an error object (decoded by prettifySmallcaps). + if echo "$REVOKE_OUTPUT" | grep -q '"#error"'; then + ERR_MSG=$(echo "$REVOKE_OUTPUT" | node -e " const raw = require('fs').readFileSync('/dev/stdin','utf8').trim(); try { const d = JSON.parse(raw); process.stdout.write(d['#error'] || raw); } catch (e) { - process.stderr.write('Failed to parse error body: ' + e.message + '\n'); process.stdout.write(raw || 'Unknown error'); } " 2>/dev/null) || ERR_MSG="Unknown error" @@ -261,8 +234,11 @@ while read -r DEL_ID; do REVOKE_FAILED=$((REVOKE_FAILED + 1)) continue fi - # Extract userOpHash from CapData and print explorer link - USER_OP_HASH=$(echo "$REVOKE_OUTPUT" | parse_capdata 2>/dev/null) || USER_OP_HASH="" + # The decoded result is the userOpHash string (already unwrapped from CapData). + USER_OP_HASH=$(echo "$REVOKE_OUTPUT" | node -e " + const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); + if (typeof v === 'string') process.stdout.write(v); + " 2>/dev/null) || USER_OP_HASH="" if [[ -n "$USER_OP_HASH" && "$USER_OP_HASH" == 0x* ]]; then if [[ "$CHAIN_ID" == "1" ]]; then EXPLORER_URL="https://eth.blockscout.com/op/${USER_OP_HASH}" @@ -291,12 +267,10 @@ else echo -e " ${DIM}Caveats: $CAVEATS_JSON${RESET}" >&2 fi -DEL_PARAMS=$(KREF="$ROOT_KREF" DEL="$DELEGATE_ADDR" CID="$CHAIN_ID" CAVS="$CAVEATS_JSON" node -e " - const p = JSON.stringify([process.env.KREF, 'createDelegation', [{ delegate: process.env.DEL, caveats: JSON.parse(process.env.CAVS), chainId: Number(process.env.CID) }]]); - process.stdout.write(p); +DEL_ARGS=$(DEL="$DELEGATE_ADDR" CID="$CHAIN_ID" CAVS="$CAVEATS_JSON" node -e " + process.stdout.write(JSON.stringify([{ delegate: process.env.DEL, caveats: JSON.parse(process.env.CAVS), chainId: Number(process.env.CID) }])); ") -DEL_RAW=$(daemon_exec queueMessage "$DEL_PARAMS") -DEL_JSON=$(echo "$DEL_RAW" | parse_capdata) +DEL_JSON=$(daemon_qm "$ROOT_KREF" createDelegation "$DEL_ARGS") ok "New delegation created" # --------------------------------------------------------------------------- @@ -305,11 +279,10 @@ ok "New delegation created" # Check if the away device has registered a back-channel HAS_AWAY="false" -CAPS_RAW=$(daemon_exec --quiet queueMessage "[\"$ROOT_KREF\", \"getCapabilities\", []]" 2>/dev/null) || CAPS_RAW="" -if [[ -n "$CAPS_RAW" ]]; then - HAS_AWAY=$(echo "$CAPS_RAW" | node -e " - const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); - const v = JSON.parse(d.body.slice(1)); +CAPS_RESULT=$(daemon_qm --quiet "$ROOT_KREF" getCapabilities 2>/dev/null) || CAPS_RESULT="" +if [[ -n "$CAPS_RESULT" ]]; then + HAS_AWAY=$(echo "$CAPS_RESULT" | node -e " + const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); process.stdout.write(String(v.hasAwayWallet === true)); " 2>&1) || { echo -e " ${YELLOW}Warning: Failed to parse capabilities — cannot auto-push delegation${RESET}" >&2 @@ -321,11 +294,10 @@ fi if [[ "$HAS_AWAY" == "true" ]]; then info "Pushing delegation to away device..." - PUSH_PARAMS=$(KREF="$ROOT_KREF" DEL="$DEL_JSON" OLD="$OLD_IDS" node -e " - const p = JSON.stringify([process.env.KREF, 'pushDelegationToAway', [JSON.parse(process.env.DEL), JSON.parse(process.env.OLD)]]); - process.stdout.write(p); + PUSH_ARGS=$(DEL="$DEL_JSON" OLD="$OLD_IDS" node -e " + process.stdout.write(JSON.stringify([JSON.parse(process.env.DEL), JSON.parse(process.env.OLD)])); ") - PUSH_OUTPUT=$(daemon_exec --quiet queueMessage "$PUSH_PARAMS" --timeout 30 2>&1) && { + PUSH_OUTPUT=$(daemon_qm --quiet "$ROOT_KREF" pushDelegationToAway "$PUSH_ARGS" --timeout 30 2>&1) && { ok "Delegation pushed to away device" } || { echo -e " ${RED}✗${RESET} Push failed — falling back to manual transfer" >&2 @@ -342,13 +314,10 @@ if [[ "$HAS_AWAY" != "true" ]]; then const ids = JSON.parse(process.env.IDS); const lines = []; for (const id of ids) { - const args = JSON.stringify([kref, 'revokeDelegationLocally', [id]]); - const escaped = args.replace(/'/g, \"'\\\\''\" ); - lines.push('yarn ocap daemon exec queueMessage ' + \"'\" + escaped + \"'\"); + lines.push('yarn ocap daemon queueMessage ' + kref + ' revokeDelegationLocally \'[\"' + id + '\"]\''); } - const recvArgs = JSON.stringify([kref, 'receiveDelegation', [JSON.parse(process.env.DEL)]]); - const recvEscaped = recvArgs.replace(/'/g, \"'\\\\''\" ); - lines.push('yarn ocap daemon exec queueMessage ' + \"'\" + recvEscaped + \"'\"); + const del = process.env.DEL.replace(/'/g, \"'\\\\''\" ); + lines.push('yarn ocap daemon queueMessage ' + kref + ' receiveDelegation \'[' + del + ']\''); process.stdout.write(lines.join('\n')); ") From 6a0e51521d461b5cd7b423fc34ff7a18325912e1 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 31 Mar 2026 15:04:15 +0200 Subject: [PATCH 2/9] fix(evm-wallet-experiment): detect prettifySmallcaps error strings in update-limits prettifySmallcaps converts CapData #error objects to strings like "[TypeError: msg]" rather than preserving the {"#error": "msg"} shape. The old grep for '"#error"' would never match the decoded output, silently ignoring revocation failures. Use string-prefix detection instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/update-limits.sh | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/evm-wallet-experiment/scripts/update-limits.sh b/packages/evm-wallet-experiment/scripts/update-limits.sh index 47bff739a..c01412db8 100755 --- a/packages/evm-wallet-experiment/scripts/update-limits.sh +++ b/packages/evm-wallet-experiment/scripts/update-limits.sh @@ -218,23 +218,23 @@ while read -r DEL_ID; do REVOKE_FAILED=$((REVOKE_FAILED + 1)) continue } - # Check if the result is an error object (decoded by prettifySmallcaps). - if echo "$REVOKE_OUTPUT" | grep -q '"#error"'; then + # prettifySmallcaps converts #error objects to strings like "[TypeError: msg]". + # A successful revocation returns a hex userOpHash starting with "0x". + # Detect errors by checking if the decoded value is a "[..." error string. + IS_ERROR=$(echo "$REVOKE_OUTPUT" | node -e " + const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); + process.stdout.write(typeof v === 'string' && v.startsWith('[') ? 'true' : 'false'); + " 2>/dev/null || echo "false") + if [[ "$IS_ERROR" == "true" ]]; then ERR_MSG=$(echo "$REVOKE_OUTPUT" | node -e " - const raw = require('fs').readFileSync('/dev/stdin','utf8').trim(); - try { - const d = JSON.parse(raw); - process.stdout.write(d['#error'] || raw); - } catch (e) { - process.stdout.write(raw || 'Unknown error'); - } + const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); + process.stdout.write(typeof v === 'string' ? v : JSON.stringify(v)); " 2>/dev/null) || ERR_MSG="Unknown error" echo -e " ${RED}✗${RESET} Failed to revoke delegation $DEL_ID" >&2 echo -e " ${DIM}Reason: $ERR_MSG${RESET}" >&2 REVOKE_FAILED=$((REVOKE_FAILED + 1)) continue fi - # The decoded result is the userOpHash string (already unwrapped from CapData). USER_OP_HASH=$(echo "$REVOKE_OUTPUT" | node -e " const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); if (typeof v === 'string') process.stdout.write(v); From b18d8ac69451673a74f39a8b607dbeadacb39134 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 31 Mar 2026 15:09:43 +0200 Subject: [PATCH 3/9] chore(evm-wallet-experiment): address review suggestions - Add missing YELLOW color variable to setup-away.sh - Clarify daemon_qm helper comments (--quiet shift + method capture) - Simplify call()/rawCall() in home-interactive.mjs: close over kernel and rootKref to reduce from 4 positional args to 2 - Replace `any` with `unknown` for parameterized test rejection params in coordinator-vat.test.ts Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/home-interactive.mjs | 68 ++++++++----------- .../scripts/setup-away.sh | 6 +- .../scripts/setup-home.sh | 5 +- .../scripts/update-limits.sh | 5 +- .../src/vats/coordinator-vat.test.ts | 18 +---- 5 files changed, 43 insertions(+), 59 deletions(-) diff --git a/packages/evm-wallet-experiment/scripts/home-interactive.mjs b/packages/evm-wallet-experiment/scripts/home-interactive.mjs index f0728e629..65aebc066 100644 --- a/packages/evm-wallet-experiment/scripts/home-interactive.mjs +++ b/packages/evm-wallet-experiment/scripts/home-interactive.mjs @@ -349,34 +349,6 @@ async function main() { ); const { makeWalletClusterConfig } = await import('../src/cluster-config.ts'); - /** - * Send a message to the coordinator and return the deserialized result. - * - * @param kernel - * @param target - * @param method - * @param callArgs - */ - async function call(kernel, target, method, callArgs = []) { - const result = await kernel.queueMessage(target, method, callArgs); - await waitUntilQuiescent(); - return kunser(result); - } - - /** - * Like call(), but also returns the raw CapData for pasting into other scripts. - * - * @param kernel - * @param target - * @param method - * @param callArgs - */ - async function rawCall(kernel, target, method, callArgs = []) { - const raw = await kernel.queueMessage(target, method, callArgs); - await waitUntilQuiescent(); - return { raw, value: kunser(raw) }; - } - // ----------------------------------------------------------------------- // 3. Create kernel in-process with SQLite persistence // ----------------------------------------------------------------------- @@ -460,6 +432,30 @@ async function main() { await waitUntilQuiescent(); ok(`Subcluster launched — coordinator: ${rootKref}`); + /** + * Send a message to the coordinator and return the deserialized result. + * + * @param method + * @param callArgs + */ + async function call(method, callArgs = []) { + const result = await kernel.queueMessage(rootKref, method, callArgs); + await waitUntilQuiescent(); + return kunser(result); + } + + /** + * Like call(), but also returns the raw CapData for pasting into other scripts. + * + * @param method + * @param callArgs + */ + async function rawCall(method, callArgs = []) { + const raw = await kernel.queueMessage(rootKref, method, callArgs); + await waitUntilQuiescent(); + return { raw, value: kunser(raw) }; + } + // ----------------------------------------------------------------------- // 6. Register MetaMask signer as kernel service → pass to coordinator // ----------------------------------------------------------------------- @@ -470,9 +466,7 @@ async function main() { signer, ); - await call(kernel, rootKref, 'connectExternalSigner', [ - kslot(signerKref, 'metamaskSigner'), - ]); + await call('connectExternalSigner', [kslot(signerKref, 'metamaskSigner')]); ok('External signer connected to coordinator'); // ----------------------------------------------------------------------- @@ -480,9 +474,7 @@ async function main() { // ----------------------------------------------------------------------- info(`Configuring provider (chain ${args.chainId})...`); - await call(kernel, rootKref, 'configureProvider', [ - { chainId: args.chainId, rpcUrl: RPC_URL }, - ]); + await call('configureProvider', [{ chainId: args.chainId, rpcUrl: RPC_URL }]); ok(`Provider configured — ${RPC_URL}`); // ----------------------------------------------------------------------- @@ -500,7 +492,7 @@ async function main() { } const bundlerUrl = `https://api.pimlico.io/v2/${pimlicoSlug}/rpc?apikey=${args.pimlicoKey}`; info('Configuring bundler (Pimlico)...'); - await call(kernel, rootKref, 'configureBundler', [ + await call('configureBundler', [ { bundlerUrl, chainId: args.chainId, @@ -512,7 +504,7 @@ async function main() { // Hybrid smart account — uses EIP-712 typed data for UserOp signing, // fully compatible with external signers (MetaMask). info('Configuring smart account...'); - const saResult = await call(kernel, rootKref, 'createSmartAccount', [ + const saResult = await call('createSmartAccount', [ { chainId: args.chainId, implementation: 'hybrid', @@ -635,7 +627,7 @@ async function main() { // ----------------------------------------------------------------------- info('Issuing OCAP URL for the away device...'); - const ocapUrl = await call(kernel, rootKref, 'issueOcapUrl', []); + const ocapUrl = await call('issueOcapUrl', []); ok('OCAP URL issued'); // ----------------------------------------------------------------------- @@ -873,8 +865,6 @@ ${BOLD} ./packages/evm-wallet-experiment/scripts/setup-away.sh \\ } const { raw: delegationRaw, value: delegation } = await rawCall( - kernel, - rootKref, 'createDelegation', [ { diff --git a/packages/evm-wallet-experiment/scripts/setup-away.sh b/packages/evm-wallet-experiment/scripts/setup-away.sh index 30a0b4697..ec15387bd 100755 --- a/packages/evm-wallet-experiment/scripts/setup-away.sh +++ b/packages/evm-wallet-experiment/scripts/setup-away.sh @@ -155,6 +155,7 @@ DIM='\033[2m' GREEN='\033[0;32m' RED='\033[0;31m' CYAN='\033[0;36m' +YELLOW='\033[0;33m' RESET='\033[0m' info() { echo -e "${CYAN}→${RESET} $*" >&2; } @@ -187,15 +188,16 @@ daemon_exec() { } # Run `ocap daemon queueMessage` (auto-decodes CapData via prettifySmallcaps). -# Usage: daemon_qm [--quiet] [--raw] KREF METHOD [ARGS_JSON] [--timeout N] +# Usage: daemon_qm [--quiet] KREF METHOD [ARGS_JSON] [--timeout N] [--raw] # --quiet suppresses the stderr log line -# --raw outputs raw CapData instead of decoded result +# Remaining args are passed through to the CLI (including --raw, --timeout). daemon_qm() { local quiet=false if [[ "${1:-}" == "--quiet" ]]; then quiet=true shift fi + # $1=kref, $2=method after any --quiet shift local method="${2:-}" local result result=$(node "$OCAP_BIN" daemon queueMessage "$@") diff --git a/packages/evm-wallet-experiment/scripts/setup-home.sh b/packages/evm-wallet-experiment/scripts/setup-home.sh index 51152fbee..d591a4554 100755 --- a/packages/evm-wallet-experiment/scripts/setup-home.sh +++ b/packages/evm-wallet-experiment/scripts/setup-home.sh @@ -199,15 +199,16 @@ daemon_exec() { } # Run `ocap daemon queueMessage` (auto-decodes CapData via prettifySmallcaps). -# Usage: daemon_qm [--quiet] [--raw] KREF METHOD [ARGS_JSON] [--timeout N] +# Usage: daemon_qm [--quiet] KREF METHOD [ARGS_JSON] [--timeout N] [--raw] # --quiet suppresses the stderr log line -# --raw outputs raw CapData instead of decoded result +# Remaining args are passed through to the CLI (including --raw, --timeout). daemon_qm() { local quiet=false if [[ "${1:-}" == "--quiet" ]]; then quiet=true shift fi + # $1=kref, $2=method after any --quiet shift local method="${2:-}" local result result=$(node "$OCAP_BIN" daemon queueMessage "$@") diff --git a/packages/evm-wallet-experiment/scripts/update-limits.sh b/packages/evm-wallet-experiment/scripts/update-limits.sh index c01412db8..faa4c1daf 100755 --- a/packages/evm-wallet-experiment/scripts/update-limits.sh +++ b/packages/evm-wallet-experiment/scripts/update-limits.sh @@ -68,13 +68,16 @@ ok() { echo -e " ${GREEN}✓${RESET} $*" >&2; } fail() { echo -e " ${RED}✗${RESET} $*" >&2; exit 1; } # Run `ocap daemon queueMessage` (auto-decodes CapData via prettifySmallcaps). -# Usage: daemon_qm [--quiet] [--raw] KREF METHOD [ARGS_JSON] [--timeout N] +# Usage: daemon_qm [--quiet] KREF METHOD [ARGS_JSON] [--timeout N] [--raw] +# --quiet suppresses the stderr log line +# Remaining args are passed through to the CLI (including --raw, --timeout). daemon_qm() { local quiet=false if [[ "${1:-}" == "--quiet" ]]; then quiet=true shift fi + # $1=kref, $2=method after any --quiet shift local method="${2:-}" local result result=$(node "$OCAP_BIN" daemon queueMessage "$@") diff --git a/packages/evm-wallet-experiment/src/vats/coordinator-vat.test.ts b/packages/evm-wallet-experiment/src/vats/coordinator-vat.test.ts index 15d787e24..b167c1ec8 100644 --- a/packages/evm-wallet-experiment/src/vats/coordinator-vat.test.ts +++ b/packages/evm-wallet-experiment/src/vats/coordinator-vat.test.ts @@ -1563,11 +1563,7 @@ describe('coordinator-vat', () => { ['a number', 42], ])( 'rejects %s as external signer', - async ( - _label: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - signer: any, - ) => { + async (_label: string, signer: unknown) => { await expect(coordinator.connectExternalSigner(signer)).rejects.toThrow( 'Invalid external signer', ); @@ -1757,11 +1753,7 @@ describe('coordinator-vat', () => { ['a number', 42], ])( 'rejects %s as away wallet reference', - async ( - _label: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ref: any, - ) => { + async (_label: string, ref: unknown) => { await expect(coordinator.registerAwayWallet(ref)).rejects.toThrow( 'Invalid away wallet reference: must be a non-null object', ); @@ -2209,11 +2201,7 @@ describe('coordinator-vat', () => { ['null', null], ])( 'rejects %s as delegate address', - async ( - _label: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - addr: any, - ) => { + async (_label: string, addr: unknown) => { await expect(coordinator.registerDelegateAddress(addr)).rejects.toThrow( 'Invalid delegate address', ); From 19942b926992f8d732074e7cb1a0e3489d84ca47 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 31 Mar 2026 15:11:41 +0200 Subject: [PATCH 4/9] fix(evm-wallet-experiment): detect prettified errors in plugin, remove dead code - Fix error detection in openclaw plugin daemon.ts: prettifySmallcaps converts #error CapData objects to strings like "[TypeError: msg]", so the old object-shape check never matched. Use string-prefix check. - Remove unused json_value() from setup-away.sh. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../evm-wallet-experiment/openclaw-plugin/daemon.ts | 10 ++++------ packages/evm-wallet-experiment/scripts/setup-away.sh | 8 -------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/evm-wallet-experiment/openclaw-plugin/daemon.ts b/packages/evm-wallet-experiment/openclaw-plugin/daemon.ts index 30c0528c5..aea46fe0b 100644 --- a/packages/evm-wallet-experiment/openclaw-plugin/daemon.ts +++ b/packages/evm-wallet-experiment/openclaw-plugin/daemon.ts @@ -146,12 +146,10 @@ async function callWallet(options: WalletCallOptions): Promise { throw new Error(`Wallet ${method} returned non-JSON output`); } - // Handle error objects from vat exceptions (decoded by prettifySmallcaps) - if (decoded !== null && typeof decoded === 'object' && '#error' in decoded) { - const errorMsg = (decoded as Record)['#error']; - throw new Error( - `Wallet ${method} failed: ${typeof errorMsg === 'string' ? errorMsg : JSON.stringify(errorMsg)}`, - ); + // prettifySmallcaps converts #error objects to strings like "[TypeError: msg]". + // Detect these prettified error strings and throw them as proper errors. + if (typeof decoded === 'string' && decoded.startsWith('[')) { + throw new Error(`Wallet ${method} failed: ${decoded}`); } return decoded; diff --git a/packages/evm-wallet-experiment/scripts/setup-away.sh b/packages/evm-wallet-experiment/scripts/setup-away.sh index ec15387bd..2d62777a4 100755 --- a/packages/evm-wallet-experiment/scripts/setup-away.sh +++ b/packages/evm-wallet-experiment/scripts/setup-away.sh @@ -162,14 +162,6 @@ info() { echo -e "${CYAN}→${RESET} $*" >&2; } ok() { echo -e " ${GREEN}✓${RESET} $*" >&2; } fail() { echo -e " ${RED}✗${RESET} $*" >&2; exit 1; } -# Decode JSON output — for strings, output the raw value; for other types, output JSON. -json_value() { - node -e " - const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); - process.stdout.write(typeof v === 'string' ? v : JSON.stringify(v)); - " -} - # Run a daemon exec command and log its output to stderr. # Usage: daemon_exec [--quiet] [--timeout ] # Pass --quiet to suppress the stderr log line (for sensitive params). From b98307beff197fb1a8f6385db04774707c52943f Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 31 Mar 2026 15:14:59 +0200 Subject: [PATCH 5/9] fix(evm-wallet-experiment): use node -e for COMMS_PARAMS and ENTROPY JSON construction Replace ad-hoc shell string interpolation for COMMS_PARAMS (relay address, allowedWsHosts) and ENTROPY with node -e + env vars. The old pattern was vulnerable to JSON injection if RELAY_ADDR contained double-quote characters, and was inconsistent with the rest of the scripts' JSON construction discipline. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../scripts/setup-away.sh | 30 +++++++++---------- .../scripts/setup-home.sh | 25 +++++++--------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/packages/evm-wallet-experiment/scripts/setup-away.sh b/packages/evm-wallet-experiment/scripts/setup-away.sh index 2d62777a4..2adc192c7 100755 --- a/packages/evm-wallet-experiment/scripts/setup-away.sh +++ b/packages/evm-wallet-experiment/scripts/setup-away.sh @@ -234,21 +234,16 @@ if [[ -n "$RELAY_ADDR" ]]; then else info "Initializing remote comms (direct QUIC on port $QUIC_PORT)..." fi -COMMS_PARAMS="{\"directListenAddresses\":[\"/ip4/0.0.0.0/udp/${QUIC_PORT}/quic-v1\"]" -if [[ -n "$RELAY_ADDR" ]]; then - COMMS_PARAMS="${COMMS_PARAMS},\"relays\":[\"${RELAY_ADDR}\"]" - # Extract the relay host (IP or hostname) for the ws:// allowlist. - # Plain ws:// to public IPs is denied by default; allowedWsHosts permits it. - RELAY_HOST=$(echo "$RELAY_ADDR" | node -e " - const addr = require('fs').readFileSync('/dev/stdin','utf8').trim(); - const m = addr.match(/\\/(?:ip4|ip6|dns4|dns6)\\/([^\\/]+)/); - if (m) process.stdout.write(m[1]); - ") - if [[ -n "$RELAY_HOST" ]]; then - COMMS_PARAMS="${COMMS_PARAMS},\"allowedWsHosts\":[\"${RELAY_HOST}\"]" - fi -fi -COMMS_PARAMS="${COMMS_PARAMS}}" +COMMS_PARAMS=$(QUIC="$QUIC_PORT" RELAY="$RELAY_ADDR" node -e " + const p = { directListenAddresses: ['/ip4/0.0.0.0/udp/' + process.env.QUIC + '/quic-v1'] }; + const relay = process.env.RELAY; + if (relay) { + p.relays = [relay]; + const m = relay.match(/\\/(?:ip4|ip6|dns4|dns6)\\/([^\\/]+)/); + if (m) p.allowedWsHosts = [m[1]]; + } + process.stdout.write(JSON.stringify(p)); +") daemon_exec initRemoteComms "$COMMS_PARAMS" >/dev/null ok "Remote comms initialized" @@ -415,7 +410,10 @@ info "Initializing throwaway keyring..." # is unavailable inside vats). The entropy is passed to the keyring vat which uses # it as the private key for the throwaway account. ENTROPY="0x$(node -e "process.stdout.write(require('crypto').randomBytes(32).toString('hex'))")" -daemon_qm --quiet "$ROOT_KREF" initializeKeyring "[{\"type\":\"throwaway\",\"entropy\":\"$ENTROPY\"}]" >/dev/null +INIT_ARGS=$(ENTROPY="$ENTROPY" node -e " + process.stdout.write(JSON.stringify([{ type: 'throwaway', entropy: process.env.ENTROPY }])); +") +daemon_qm --quiet "$ROOT_KREF" initializeKeyring "$INIT_ARGS" >/dev/null ok "Throwaway keyring initialized" info "Verifying accounts..." diff --git a/packages/evm-wallet-experiment/scripts/setup-home.sh b/packages/evm-wallet-experiment/scripts/setup-home.sh index d591a4554..f36600804 100755 --- a/packages/evm-wallet-experiment/scripts/setup-home.sh +++ b/packages/evm-wallet-experiment/scripts/setup-home.sh @@ -253,21 +253,16 @@ if [[ -n "$RELAY_ADDR" ]]; then else info "Initializing remote comms (direct QUIC on port $QUIC_PORT)..." fi -COMMS_PARAMS="{\"directListenAddresses\":[\"/ip4/0.0.0.0/udp/${QUIC_PORT}/quic-v1\"]" -if [[ -n "$RELAY_ADDR" ]]; then - COMMS_PARAMS="${COMMS_PARAMS},\"relays\":[\"${RELAY_ADDR}\"]" - # Extract the relay host (IP or hostname) for the ws:// allowlist. - # Plain ws:// to public IPs is denied by default; allowedWsHosts permits it. - RELAY_HOST=$(echo "$RELAY_ADDR" | node -e " - const addr = require('fs').readFileSync('/dev/stdin','utf8').trim(); - const m = addr.match(/\\/(?:ip4|ip6|dns4|dns6)\\/([^\\/]+)/); - if (m) process.stdout.write(m[1]); - ") - if [[ -n "$RELAY_HOST" ]]; then - COMMS_PARAMS="${COMMS_PARAMS},\"allowedWsHosts\":[\"${RELAY_HOST}\"]" - fi -fi -COMMS_PARAMS="${COMMS_PARAMS}}" +COMMS_PARAMS=$(QUIC="$QUIC_PORT" RELAY="$RELAY_ADDR" node -e " + const p = { directListenAddresses: ['/ip4/0.0.0.0/udp/' + process.env.QUIC + '/quic-v1'] }; + const relay = process.env.RELAY; + if (relay) { + p.relays = [relay]; + const m = relay.match(/\\/(?:ip4|ip6|dns4|dns6)\\/([^\\/]+)/); + if (m) p.allowedWsHosts = [m[1]]; + } + process.stdout.write(JSON.stringify(p)); +") daemon_exec initRemoteComms "$COMMS_PARAMS" >/dev/null ok "Remote comms initialized" From a9e5eb6d1a11fc2f69e856b98a583348abaed6c9 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 31 Mar 2026 15:17:23 +0200 Subject: [PATCH 6/9] fix(evm-wallet-experiment): update openclaw plugin tests for decoded CLI output The new `daemon queueMessage` CLI auto-decodes CapData, so test mocks must return plain JSON instead of CapData-wrapped responses. Also updates spawn argument assertions to match the new CLI arg structure (daemon queueMessage kref method argsJson) and the error test to use prettified error strings ("[Error: msg]") instead of CapData #error objects. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../test/openclaw-plugin.test.ts | 154 +++++++++--------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/packages/evm-wallet-experiment/test/openclaw-plugin.test.ts b/packages/evm-wallet-experiment/test/openclaw-plugin.test.ts index 72c614a5f..f5fd2623a 100644 --- a/packages/evm-wallet-experiment/test/openclaw-plugin.test.ts +++ b/packages/evm-wallet-experiment/test/openclaw-plugin.test.ts @@ -28,16 +28,16 @@ type MockReadable = EventEmitter & { }; /** - * Encode a value as Endo CapData JSON. + * Encode a value as the JSON output of `ocap daemon queueMessage`. * - * @param value - The value to encode. - * @returns JSON string with `body` and `slots`. + * The CLI auto-decodes CapData via prettifySmallcaps, so the output + * is plain JSON (not CapData-wrapped). + * + * @param value - The decoded value the CLI would output. + * @returns JSON string. */ -function makeCapData(value: unknown): string { - return JSON.stringify({ - body: `#${JSON.stringify(value)}`, - slots: [], - }); +function makeCliOutput(value: unknown): string { + return JSON.stringify(value); } /** @@ -126,9 +126,9 @@ describe('openclaw wallet plugin', () => { expect(tools.has('wallet_token_resolve')).toBe(true); }); - it('decodes CapData string response for wallet_balance', async () => { + it('decodes CLI JSON response for wallet_balance', async () => { mockSpawn.mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData('0xde0b6b3a7640000') }), + makeSpawnResult({ stdout: makeCliOutput('0xde0b6b3a7640000') }), ); const tools = setupPlugin(); const tool = tools.get('wallet_balance'); @@ -152,7 +152,7 @@ describe('openclaw wallet plugin', () => { smartAccountAddress: account, }; mockSpawn.mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData(rawCapabilities) }), + makeSpawnResult({ stdout: makeCliOutput(rawCapabilities) }), ); const tools = setupPlugin(); @@ -177,22 +177,22 @@ describe('openclaw wallet plugin', () => { mockSpawn // 1. getAccounts .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData([account]) }), + makeSpawnResult({ stdout: makeCliOutput([account]) }), ) // 2. sendTransaction .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData('0xtxhash') }), + makeSpawnResult({ stdout: makeCliOutput('0xtxhash') }), ) // 3. getCapabilities (best-effort, for chain ID) .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ chainId: 11155111 }), + stdout: makeCliOutput({ chainId: 11155111 }), }), ) // 4. getTransactionReceipt (best-effort, resolve UserOp hash) .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ + stdout: makeCliOutput({ txHash: '0xrealtxhash', userOpHash: '0xtxhash', success: true, @@ -216,26 +216,22 @@ describe('openclaw wallet plugin', () => { ); expect(result.content[0]?.text).toContain('UserOp hash: 0xtxhash'); - const sendCallArgs = mockSpawn.mock.calls[1]?.[1]; - expect(Array.isArray(sendCallArgs)).toBe(true); - const daemonArgs = sendCallArgs as string[]; - const payload = JSON.parse(daemonArgs[3] ?? 'null') as [ - string, - string, - unknown[], - ]; + // New CLI format: ['daemon', 'queueMessage', kref, method, argsJson] + const sendCallArgs = mockSpawn.mock.calls[1]?.[1] as string[]; + expect(sendCallArgs[1]).toBe('queueMessage'); + expect(sendCallArgs[2]).toBe('ko4'); + expect(sendCallArgs[3]).toBe('sendTransaction'); + const sendArgs = JSON.parse(sendCallArgs[4] ?? 'null') as unknown[]; // 0.08 ETH = 80000000000000000 wei = 0x11c37937e080000 - expect(payload).toStrictEqual([ - 'ko4', - 'sendTransaction', - [{ from: account, to: recipient, value: '0x11c37937e080000' }], + expect(sendArgs).toStrictEqual([ + { from: account, to: recipient, value: '0x11c37937e080000' }, ]); }); it('returns error when wallet_send cannot infer a sender', async () => { mockSpawn.mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData([]) }), + makeSpawnResult({ stdout: makeCliOutput([]) }), ); const tools = setupPlugin(); @@ -270,23 +266,18 @@ describe('openclaw wallet plugin', () => { expect(mockSpawn).not.toHaveBeenCalled(); }); - it('surfaces CapData error from vat exception', async () => { - // Simulate: getAccounts succeeds, sendTransaction returns a CapData error + it('surfaces prettified error from vat exception', async () => { + // Simulate: getAccounts succeeds, sendTransaction returns a prettified error. + // prettifySmallcaps converts #error CapData to strings like "[ErrorName: msg]". mockSpawn .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData([account]) }), + makeSpawnResult({ stdout: makeCliOutput([account]) }), ) .mockImplementationOnce(() => makeSpawnResult({ - stdout: JSON.stringify({ - body: `#${JSON.stringify({ - '#error': - 'Bundler RPC error -32521: UserOperation reverted during simulation', - errorId: 'error:liveSlots:v1#70003', - name: 'Error', - })}`, - slots: [], - }), + stdout: makeCliOutput( + '[Error: Bundler RPC error -32521: UserOperation reverted during simulation]', + ), }), ); @@ -313,16 +304,16 @@ describe('openclaw wallet plugin', () => { mockSpawn // 1. getAccounts (to resolve owner) .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData([account]) }), + makeSpawnResult({ stdout: makeCliOutput([account]) }), ) // 2. getTokenBalance .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData('1000000') }), + makeSpawnResult({ stdout: makeCliOutput('1000000') }), ) // 3. getTokenMetadata .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ + stdout: makeCliOutput({ name: 'USD Coin', symbol: 'USDC', decimals: 6, @@ -345,7 +336,7 @@ describe('openclaw wallet plugin', () => { // 1. getTokenMetadata (for decimals) .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ + stdout: makeCliOutput({ name: 'USD Coin', symbol: 'USDC', decimals: 6, @@ -354,18 +345,18 @@ describe('openclaw wallet plugin', () => { ) // 2. sendErc20Transfer .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData('0xtokentxhash') }), + makeSpawnResult({ stdout: makeCliOutput('0xtokentxhash') }), ) // 3. getCapabilities (best-effort) .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ chainId: 11155111 }), + stdout: makeCliOutput({ chainId: 11155111 }), }), ) // 4. getTransactionReceipt (best-effort) .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ txHash: '0xrealtokentx', success: true }), + stdout: makeCliOutput({ txHash: '0xrealtokentx', success: true }), }), ); @@ -388,30 +379,30 @@ describe('openclaw wallet plugin', () => { ); // Verify the sendErc20Transfer daemon call + // New CLI format: ['daemon', 'queueMessage', kref, method, argsJson] const sendCallArgs = mockSpawn.mock.calls[1]?.[1] as string[]; - const payload = JSON.parse(sendCallArgs[3] ?? 'null'); - expect(payload[1]).toBe('sendErc20Transfer'); + expect(sendCallArgs[3]).toBe('sendErc20Transfer'); }); it('waits for a delayed UserOp receipt before showing tx hash', async () => { mockSpawn .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData([account]) }), + makeSpawnResult({ stdout: makeCliOutput([account]) }), ) .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData('0xuserophash') }), + makeSpawnResult({ stdout: makeCliOutput('0xuserophash') }), ) .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ chainId: 11155111 }), + stdout: makeCliOutput({ chainId: 11155111 }), }), ) .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData(null) }), + makeSpawnResult({ stdout: makeCliOutput(null) }), ) .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ + stdout: makeCliOutput({ success: true, receipt: { transactionHash: '0xresolvedtx' }, }), @@ -437,18 +428,18 @@ describe('openclaw wallet plugin', () => { it('reports pending UserOp without tx explorer URL', async () => { mockSpawn .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData([account]) }), + makeSpawnResult({ stdout: makeCliOutput([account]) }), ) .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData('0xpendinguserop') }), + makeSpawnResult({ stdout: makeCliOutput('0xpendinguserop') }), ) .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ chainId: 11155111 }), + stdout: makeCliOutput({ chainId: 11155111 }), }), ) .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData(null) }), + makeSpawnResult({ stdout: makeCliOutput(null) }), ) .mockImplementationOnce(() => makeSpawnResult({ stderr: 'timed out', code: 1 }), @@ -476,7 +467,7 @@ describe('openclaw wallet plugin', () => { mockSpawn.mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ + stdout: makeCliOutput({ name: 'USD Coin', symbol: 'USDC', decimals: 6, @@ -506,7 +497,11 @@ describe('openclaw wallet plugin', () => { mockSpawn.mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ name: 'USD Coin', symbol: 'USDC', decimals: 6 }), + stdout: makeCliOutput({ + name: 'USD Coin', + symbol: 'USDC', + decimals: 6, + }), }), ); @@ -531,21 +526,21 @@ describe('openclaw wallet plugin', () => { // 1. getCapabilities (for chain ID during token resolution) .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ chainId: 1 }), + stdout: makeCliOutput({ chainId: 1 }), }), ) // 2. getAccounts (to resolve owner) .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData([account]) }), + makeSpawnResult({ stdout: makeCliOutput([account]) }), ) // 3. getTokenBalance .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData('1000000') }), + makeSpawnResult({ stdout: makeCliOutput('1000000') }), ) // 4. getTokenMetadata .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ + stdout: makeCliOutput({ name: 'USD Coin', symbol: 'USDC', decimals: 6, @@ -578,9 +573,10 @@ describe('openclaw wallet plugin', () => { expect(result.content[0]?.text).toContain('USDC'); expect(result.content[0]?.text).toContain('raw: 1000000'); // Verify the resolved address was used for daemon calls + // New CLI format: ['daemon', 'queueMessage', kref, method, argsJson] const balanceCallArgs = mockSpawn.mock.calls[2]?.[1] as string[]; - const balancePayload = JSON.parse(balanceCallArgs[3] ?? 'null'); - expect(balancePayload[2][0].token).toBe( + const balanceArgs = JSON.parse(balanceCallArgs[4] ?? 'null') as unknown[]; + expect((balanceArgs[0] as Record).token).toBe( '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', ); // Single fetch call (MetaMask API returns everything in one request) @@ -594,7 +590,7 @@ describe('openclaw wallet plugin', () => { // getCapabilities for chain ID mockSpawn.mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ chainId: 11155111 }), + stdout: makeCliOutput({ chainId: 11155111 }), }), ); @@ -621,7 +617,7 @@ describe('openclaw wallet plugin', () => { mockSpawn.mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ chainId: 1 }), + stdout: makeCliOutput({ chainId: 1 }), }), ); @@ -666,7 +662,7 @@ describe('openclaw wallet plugin', () => { mockSpawn.mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ chainId: 1 }), + stdout: makeCliOutput({ chainId: 1 }), }), ); @@ -767,16 +763,20 @@ describe('openclaw wallet plugin', () => { mockSpawn .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ chainId: 1 }), + stdout: makeCliOutput({ chainId: 1 }), }), ) .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ name: 'Tether', symbol: 'USDT', decimals: 6 }), + stdout: makeCliOutput({ + name: 'Tether', + symbol: 'USDT', + decimals: 6, + }), }), ) .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData(quote) }), + makeSpawnResult({ stdout: makeCliOutput(quote) }), ); const tools = setupPlugin(); @@ -812,25 +812,25 @@ describe('openclaw wallet plugin', () => { mockSpawn .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ chainId: 1 }), + stdout: makeCliOutput({ chainId: 1 }), }), ) .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ decimals: 6, symbol: 'USDT' }), + stdout: makeCliOutput({ decimals: 6, symbol: 'USDT' }), }), ) .mockImplementationOnce(() => - makeSpawnResult({ stdout: makeCapData(swapResult) }), + makeSpawnResult({ stdout: makeCliOutput(swapResult) }), ) .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ chainId: 1 }), + stdout: makeCliOutput({ chainId: 1 }), }), ) .mockImplementationOnce(() => makeSpawnResult({ - stdout: makeCapData({ + stdout: makeCliOutput({ txHash: '0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1', }), From 14035f79ce0214aba55b9cefdcafc45f325aff89 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Tue, 31 Mar 2026 15:25:29 +0200 Subject: [PATCH 7/9] fix(evm-wallet-experiment): distinguish prettified errors from manifest constants prettifySmallcaps converts both #error objects and manifest constants to strings starting with "[": errors become "[TypeError: msg]" while constants become "[undefined]", "[NaN]", etc. Use a regex that checks for the ": " separator to avoid false positives on void method returns. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/evm-wallet-experiment/openclaw-plugin/daemon.ts | 7 ++++--- packages/evm-wallet-experiment/scripts/update-limits.sh | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/evm-wallet-experiment/openclaw-plugin/daemon.ts b/packages/evm-wallet-experiment/openclaw-plugin/daemon.ts index aea46fe0b..109ddcd1d 100644 --- a/packages/evm-wallet-experiment/openclaw-plugin/daemon.ts +++ b/packages/evm-wallet-experiment/openclaw-plugin/daemon.ts @@ -146,9 +146,10 @@ async function callWallet(options: WalletCallOptions): Promise { throw new Error(`Wallet ${method} returned non-JSON output`); } - // prettifySmallcaps converts #error objects to strings like "[TypeError: msg]". - // Detect these prettified error strings and throw them as proper errors. - if (typeof decoded === 'string' && decoded.startsWith('[')) { + // prettifySmallcaps converts #error objects to "[ErrorName: msg]" strings, + // but also converts manifest constants to "[undefined]", "[NaN]", etc. + // Distinguish errors by checking for the ": " separator after the bracket. + if (typeof decoded === 'string' && /^\[.+: /u.test(decoded)) { throw new Error(`Wallet ${method} failed: ${decoded}`); } diff --git a/packages/evm-wallet-experiment/scripts/update-limits.sh b/packages/evm-wallet-experiment/scripts/update-limits.sh index faa4c1daf..ce2db7891 100755 --- a/packages/evm-wallet-experiment/scripts/update-limits.sh +++ b/packages/evm-wallet-experiment/scripts/update-limits.sh @@ -221,12 +221,12 @@ while read -r DEL_ID; do REVOKE_FAILED=$((REVOKE_FAILED + 1)) continue } - # prettifySmallcaps converts #error objects to strings like "[TypeError: msg]". - # A successful revocation returns a hex userOpHash starting with "0x". - # Detect errors by checking if the decoded value is a "[..." error string. + # prettifySmallcaps converts #error objects to "[ErrorName: msg]" strings, + # but also converts manifest constants to "[undefined]", "[NaN]", etc. + # Distinguish errors by the ": " separator (errors always have "Name: msg"). IS_ERROR=$(echo "$REVOKE_OUTPUT" | node -e " const v = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8').trim()); - process.stdout.write(typeof v === 'string' && v.startsWith('[') ? 'true' : 'false'); + process.stdout.write(typeof v === 'string' && /^\[.+: /.test(v) ? 'true' : 'false'); " 2>/dev/null || echo "false") if [[ "$IS_ERROR" == "true" ]]; then ERR_MSG=$(echo "$REVOKE_OUTPUT" | node -e " From da71ee1843975215bafe857d75e41974cbae1baf Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Wed, 1 Apr 2026 01:58:07 +0200 Subject: [PATCH 8/9] fix(kernel-node-runtime): increase MAX_QUEUE test timeout to 120s The "rejects new messages when queue reaches MAX_QUEUE limit" test sends 201 messages while disconnected, restarts a kernel, and resolves all promises. In CI this can take >90s. Bump from NETWORK_TIMEOUT*3 (90s) to NETWORK_TIMEOUT*4 (120s) to avoid flaky timeout failures. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/kernel-node-runtime/test/e2e/remote-comms.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts b/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts index 5e38b7274..671f0cdca 100644 --- a/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts +++ b/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts @@ -636,7 +636,7 @@ describe.sequential('Remote Communications E2E', () => { const newMessage = kunser(newMessageResult); expect(newMessage).toBe('Sequence 999 received'); }, - NETWORK_TIMEOUT * 3, + NETWORK_TIMEOUT * 4, ); }); From 70bf51b042b88d80b609033b846c41aac69713ad Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Wed, 1 Apr 2026 02:07:29 +0200 Subject: [PATCH 9/9] fix(kernel-node-runtime): increase ACK timeout for multi-peer reconnection test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "handles multiple simultaneous reconnections to different peers" test has kernel1 redeem URLs on two peers that restart sequentially through a relay. The default ackTimeoutMs of 2s gives only 8s for URL redemption, which is too tight when two kernels are reconnecting in CI. Use 5s (→ 20s redemption window) for the initiating kernel. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../kernel-node-runtime/test/e2e/remote-comms.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts b/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts index 671f0cdca..2139def9a 100644 --- a/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts +++ b/packages/kernel-node-runtime/test/e2e/remote-comms.test.ts @@ -651,10 +651,15 @@ describe.sequential('Remote Communications E2E', () => { }); let kernel3: Kernel | undefined; + // Use a longer ACK timeout for the initiating kernel — it must redeem + // URLs on two peers that restart sequentially through the relay, so the + // default 2s × (3+1) = 8s redemption window is too tight for CI. + const multiPeerBackoff = { ...testBackoffOptions, ackTimeoutMs: 5_000 }; + try { await kernel1.initRemoteComms({ relays: testRelays, - ...testBackoffOptions, + ...multiPeerBackoff, }); await kernel2.initRemoteComms({ relays: testRelays, @@ -757,7 +762,7 @@ describe.sequential('Remote Communications E2E', () => { } } }, - NETWORK_TIMEOUT * 3, + NETWORK_TIMEOUT * 4, ); });