Skip to content

Add Para as a WalletProvider (ENG-1976)#14

Open
AkasshP wants to merge 12 commits into
mainfrom
eng-1976/para-wallet-provider
Open

Add Para as a WalletProvider (ENG-1976)#14
AkasshP wants to merge 12 commits into
mainfrom
eng-1976/para-wallet-provider

Conversation

@AkasshP
Copy link
Copy Markdown

@AkasshP AkasshP commented Jun 3, 2026

Summary

Add Para and Coinbase CDP as WalletProvider implementations for radius-cli.

Para (ENG-1976)

  • Login via email with pregenerated wallets using @getpara/server-sdk
  • MPC signing via createParaViemAccount with session address pinning
  • API key prompted on first login, saved to ~/.radius/config.json under namespaced providers.para

CDP (ENG-1978)

  • Login with CDP credentials (API Key ID, API Key Secret, Wallet Secret) using @coinbase/cdp-sdk
  • Server-managed EVM accounts with getOrCreateAccount
  • Legacy tx signing via signHash workaround (CDP's signTransaction doesn't support legacy txs; Radius uses legacy)
  • Wrapped with viem toAccount() for type: "local" compatibility

Shared architecture

  • Namespaced provider config: providers.{para,cdp}.{apiKey,env,...} — new providers add zero lines to config.ts
  • Optional peer dependencies with dynamic import() — default install stays lean
  • Session files at ~/.radius/{para,cdp}-session.json with 0o600 permissions
  • wallet export hard-errors for both (remote key material not exportable)

E2E test results

Command Para CDP
--wallet <provider> wallet status pass pass
RADIUS_WALLET=<provider> wallet status pass pass
wallet login pass (email + API key) pass (3 credentials)
wallet sign "hello" pass (MPC signature) pass (MPC signature)
wallet send ... RUSD pass pass (signHash workaround)
wallet x402 get ... pass (EIP-712) pass (EIP-712)
wallet export hard-error hard-error
wallet logout pass pass
wallet address pass pass

Dependencies 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 Platform

Users install only the provider they need:

# For Para
npm install @getpara/server-sdk @getpara/viem-v2-integration

# For CDP
npm install @coinbase/cdp-sdk

CDP legacy transaction note

CDP's signTransaction returns "Malformed unsigned EIP-1559 transaction" for Radius's legacy txs. Workaround: serialize the unsigned tx, hash it with keccak256, sign the hash via cdp.evm.sign({ hash }), then reconstruct the signed transaction with r/s/v. This is tested and working on Radius testnet.

Follow-up tickets

  • SecretStore seam for key-equivalent secrets (userShare, etc.) — discussed in review, tracked separately

Test plan

  • 84/84 unit tests pass (npm test)
  • E2E: Para login/sign/send/x402/export/logout
  • E2E: CDP login/sign/send/x402/export/logout
  • E2E: Provider selection via --wallet flag and RADIUS_WALLET env var
  • E2E: CDP signTransaction on Radius testnet (legacy tx via signHash)

AkasshP and others added 9 commits May 29, 2026 12:53
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.
@AkasshP AkasshP changed the base branch from eng-1974/wallet-provider-abstraction to main June 3, 2026 22:38
@AkasshP AkasshP requested a review from erikzrekz June 3, 2026 22:45
Copy link
Copy Markdown

@erikzrekz erikzrekz left a comment

Choose a reason for hiding this comment

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

Strong foundation — v3 SDK usage is correct (verified against the shipped types), MPC-aware no-export, 0600 perms.

Requesting changes, blocking on:

  1. Deprecated createParaViemAccount positional overload + as any cast (para.ts:201)
  2. getAccount ignores the session's wallet identity and can sign as the wrong address (para.ts:192–201)
  3. Dead paraEnv config field — written but resolveEnvironment() 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.

Comment thread src/lib/providers/para.ts
Comment thread src/lib/providers/para.ts
Comment thread src/lib/providers/para.ts
Comment thread src/lib/providers/para.ts
Comment thread src/lib/providers/para.ts Outdated
Comment thread src/lib/config.ts Outdated
Comment thread src/lib/config.ts Outdated
Comment thread tests/providers.test.ts
Comment thread tests/providers.test.ts
Comment thread package.json Outdated
AkasshP added 2 commits June 3, 2026 22:38
- 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.
Comment thread src/lib/providers/cdp.ts Outdated
Comment thread src/lib/providers/cdp.ts Outdated
Comment thread src/lib/providers/cdp.ts Outdated
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.');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread src/lib/providers/cdp.ts Outdated
@erikzrekz
Copy link
Copy Markdown

Exit code contract (can land after Privy)

Right now every error path in every provider exits with code 1. That makes it impossible for an agent to distinguish "not logged in" from "credentials missing" from "SDK not installed" without parsing stderr — which breaks the moment error message wording changes.

Proposing a stable contract before we add a third provider:

Code Meaning
0 success
1 usage error / bad input
2 not logged in (no session)
3 credentials not configured
4 optional SDK not installed

This would be set at the top-level error handler in the CLI entrypoint — providers throw typed errors (or a simple { code: number, message: string } wrapper), and the handler maps them to exit codes.

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
fi

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

@AkasshP
Copy link
Copy Markdown
Author

AkasshP commented Jun 4, 2026

Exit code contract (can land after Privy)

Right now every error path in every provider exits with code 1. That makes it impossible for an agent to distinguish "not logged in" from "credentials missing" from "SDK not installed" without parsing stderr — which breaks the moment error message wording changes.

Proposing a stable contract before we add a third provider:

Code Meaning
0 success
1 usage error / bad input
2 not logged in (no session)
3 credentials not configured
4 optional SDK not installed
This would be set at the top-level error handler in the CLI entrypoint — providers throw typed errors (or a simple { code: number, message: string } wrapper), and the handler maps them to exit codes.

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
fi

No 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants