Add Para as a WalletProvider (ENG-1976)#14
Conversation
Also fix x402 challenge parsing to accept `amount` field (v2 compat).
- tests/providers.test.ts: stub providers (cdp/para/privy) reject with "not yet implemented" and expose no exportPrivateKey; keystore provider auto-creates on first use, round-trips export, honors explicit password, rejects wrong password; requireAccount/getOwnAddress dispatch to the selected provider and --private-key overrides it - tests/x402-protocol.test.ts: lock in the `amount` fallback (x402 v2), precedence of maxAmountRequired, and rejection when both are absent Covers the test gap flagged in the PR #10 review. 50/50 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Exercises the branch added in fc1173d: an invalid `amount` with no `maxAmountRequired` reports accepts[i].amount in the error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… change The Radius RPC change (2026-06) made eth_getBalance return token_balance × per_unit_exchange_rate + raw_native, so the SBC token balance is already included in the native balance. The balance command was summing eth_getBalance + SBC balanceOf on top, double-counting SBC (e.g. reporting $0.20 for a wallet holding only 0.1 SBC). eth_getBalance is now treated as the total; the raw-native (RUSD) line is derived as the remainder via splitAggregateBalance (clamped at zero), assuming the 1 SBC = 1 native unit peg. JSON output gains totalWei and rusdWei now reports the derived native remainder. Note: per the same RPC change, the returned balance may exceed what the network will provision for a single transaction — the CLI does not gate sends on balance (viem's eth_estimateGas path validates instead). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two interop fixes verified end-to-end against a live Radius x402 server (rad-read.replit.app — paid 0.001 SBC on mainnet, content delivered): 1. v2 payment header envelope. The CLI emitted the v1 shape (flat scheme/network, numeric validity bounds) regardless of challenge version. Per coinbase/x402 specs/schemes/exact/scheme_exact_evm.md, v2 echoes the chosen accepts entry verbatim as `accepted`, carries the challenge's `resource`, and stringifies validAfter/validBefore. parseChallenge now retains the raw accepts entry and resource for the echo. v1 output is unchanged. 2. EIP-2612 permit settlement (Radius flavor). SBC does not implement EIP-3009 transferWithAuthorization, so signTransferAuthorization payloads can never settle against it. Radius servers advertise `extra.settlementMethod: "permit-transferFrom"` plus `extra.settlementSpender` in the challenge; the CLI now detects that, reads the owner's permit nonce on-chain, signs an EIP-2612 Permit for the settlement spender (deadline = now + maxTimeoutSeconds), and sends the documented flat envelope with a `kind: "permit-eip2612"` payload (v/r/s split signature). Servers without the extra keep the EIP-3009 path. Tests: v2 envelope round-trip + v1 stability, permit signature recovery to owner, permit header shape. 60/60 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per docs.radiustech.xyz/skills/x402.md and the reference client in radiustechsystems/skills (plugins/radius/skills/x402): - Permit2 dual-signature settlement when the challenge advertises extra.assetTransferMethod: "permit2": an EIP-2612 Permit for the canonical Permit2 contract (sequential nonce from token.nonces, goes in extensions.eip2612GasSponsoring for gas-free allowance setup) plus a Permit2 PermitWitnessTransferFrom for the x402 proxy (random 128-bit nonce, witness binds payTo), sharing one deadline. Typed data taken verbatim from references/permit2-typed-data.template.json. - Challenge detection: official v2 servers carry the challenge in a base64 PAYMENT-REQUIRED response header; body JSON remains the fallback for older servers. - Payment is sent in both PAYMENT-SIGNATURE (official v2) and X-Payment (legacy) headers; receipts read from PAYMENT-RESPONSE before x-payment-response. - v2 envelope keeps flat scheme/network alongside the accepted echo, matching the skill's payload example. Settlement dispatch is now: permit2 (assetTransferMethod) → permit-eip2612 (settlementMethod, verified live against rad-read) → EIP-3009 (standard x402 default). Tests: permit2 signature recovery under the skill typed data, payload structure field-for-field vs the skill example, nonce randomness, canonical addresses. 64/64 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement Para MPC wallet provider using @getpara/server-sdk and @getpara/viem-v2-integration with pregenerated wallets for CLI use.
erikzrekz
left a comment
There was a problem hiding this comment.
Strong foundation — v3 SDK usage is correct (verified against the shipped types), MPC-aware no-export, 0600 perms.
Requesting changes, blocking on:
- Deprecated
createParaViemAccountpositional overload +as anycast (para.ts:201) getAccountignores the session's wallet identity and can sign as the wrong address (para.ts:192–201)- Dead
paraEnvconfig field — written butresolveEnvironment()never reads it (config.ts:131)
Before Para becomes the reference template for cdp/privy, I'd also like the namespaced provider-config seam (config.ts:23) and a SecretStore decision settled — cheap now, expensive after the next two providers copy the current shape.
- Pass session address to createParaViemAccount (fixes wrong-wallet bug)
- Use non-deprecated object overload with scoped cast
- Namespace provider config: providers.para.{apiKey,env}
- Wire resolveEnvironment to read from config (fix dead paraEnv bug)
- Merge dual resolveApiKey into single function with interactive flag
- Move Para SDK to optional peerDependencies with dynamic import
- Add mocked getAccount happy-path test
- Add UX note: pregenerated wallet not yet claimed
Implement CDP wallet provider using @coinbase/cdp-sdk with server-managed EVM accounts. Legacy tx signing via signHash workaround for Radius chain. Optional peer dep with dynamic import, same pattern as Para.
| if (session.accountName) console.log(`Account: ${session.accountName}`); | ||
| } else { | ||
| console.log('Status: not logged in'); | ||
| console.log('Run `radius-cli wallet login` to set up CDP.'); |
There was a problem hiding this comment.
Missing --wallet cdp in the hint.
// current
console.log('Run `radius-cli wallet login` to set up CDP.');
// should be
console.log('Run `radius-cli --wallet cdp wallet login` to set up CDP.');Without the flag, the command defaults to keystore and the user sees an unrelated prompt. Easy fix — same issue existed in Para and was caught before merge.
Exit code contract (can land after Privy)Right now every error path in every provider exits with code Proposing a stable contract before we add a third provider:
This would be set at the top-level error handler in the CLI entrypoint — providers throw typed errors (or a simple With this in place, a health-check loop becomes: radius-cli --wallet cdp wallet status --json
STATUS=$?
if [ $STATUS -eq 2 ]; then
radius-cli --wallet cdp wallet login --account-name agent-01
elif [ $STATUS -ne 0 ]; then
echo "wallet error $STATUS" && exit 1
fiNo string matching, no fragile grep on stderr. Logging the non-zero code also makes it immediately clear which failure mode hit in agent traces. This touches the wallet command's error handler, not individual providers — so it's clean to do in one commit after Privy lands, before we start wiring agents against this CLI in production. |
structured exit codes make agent health-checks much cleaner. Will track as a follow-up! |
- Auto-generate account name when blank (radius-cli-<timestamp>) - Always use name-based lookup, remove address-based fallback - Fix status hint to include --wallet cdp - Remove as any from signTypedData (types match directly)
Summary
Add Para and Coinbase CDP as WalletProvider implementations for radius-cli.
Para (ENG-1976)
@getpara/server-sdkcreateParaViemAccountwith session address pinning~/.radius/config.jsonunder namespacedproviders.paraCDP (ENG-1978)
@coinbase/cdp-sdkgetOrCreateAccountsignHashworkaround (CDP'ssignTransactiondoesn't support legacy txs; Radius uses legacy)toAccount()fortype: "local"compatibilityShared architecture
providers.{para,cdp}.{apiKey,env,...}— new providers add zero lines to config.tsimport()— default install stays lean~/.radius/{para,cdp}-session.jsonwith 0o600 permissionswallet exporthard-errors for both (remote key material not exportable)E2E test results
--wallet <provider> wallet statusRADIUS_WALLET=<provider> wallet statuswallet loginwallet sign "hello"wallet send ... RUSDwallet x402 get ...wallet exportwallet logoutwallet addressDependencies added (optional peer deps)
@getpara/server-sdk@3.0.0— Para MPC wallet@getpara/viem-v2-integration@3.0.0— Para viem adapter@coinbase/cdp-sdk@^1.51.0— Coinbase Developer PlatformUsers install only the provider they need:
CDP legacy transaction note
CDP's
signTransactionreturns "Malformed unsigned EIP-1559 transaction" for Radius's legacy txs. Workaround: serialize the unsigned tx, hash it with keccak256, sign the hash viacdp.evm.sign({ hash }), then reconstruct the signed transaction with r/s/v. This is tested and working on Radius testnet.Follow-up tickets
Test plan
npm test)--walletflag andRADIUS_WALLETenv var