Skip to content

feat: Privy server wallet integration (policies-ready, official SDK)#649

Draft
fengtality wants to merge 6 commits into
developmentfrom
feat/privy-wallets
Draft

feat: Privy server wallet integration (policies-ready, official SDK)#649
fengtality wants to merge 6 commits into
developmentfrom
feat/privy-wallets

Conversation

@fengtality

@fengtality fengtality commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Productionizes the Privy wallet integration from #618 as a fresh, focused branch. The execute-tx endpoints from that PR are not included; this PR keeps and modernizes the Privy wallet layer for both Solana and Ethereum, and threads Privy signing through every connector execution path.

Privy core (commit 1)

  • Official SDK: replaces the hand-rolled REST client (deprecated auth.privy.io endpoint) with @privy-io/node, lazy-loaded so its ESM-only deps don't affect startup or Jest.
  • Authorization keys: apiKeys.privyAuthorizationKey — signing requests carry a P-256 authorization signature; with an owner set on wallets/policies, the app secret alone cannot sign or strip policies.
  • Sign-only flow: Gateway builds and broadcasts on its own RPC; Privy only signs. No CAIP-2 coupling (chain identity is in the blockhash / chain_id).
  • Round-trip verification: Solana compares signed message bytes and requires the wallet's signature; Ethereum parses returned RLP and verifies from/to/value/data/nonce/chainId.
  • Policy-aware registration: POST /wallet/add-privy returns policyIds/hasOwner/warnings.

Connector integration + policy lifecycle (commit 2)

  • Unified wallet-type signing seam: Solana.getWalletType/prepareWallet/signTransactionByType and Ethereum.getWalletType/getSigner. Connectors no longer branch on a hardware boolean — signing routes by wallet type (local / hardware / privy).
  • All connectors covered: Raydium (AMM+CLMM), Jupiter, PancakeSwap-Sol (previously local-only — gains hardware support too), Uniswap (AMM+CLMM+router), PancakeSwap EVM, 0x, plus chain-level approve/wrap/unwrap. EVM paths ride the regular ethers Contract flow via PrivyEvmSigner.
  • Read-path fixes: balances/allowances use by-address reads for Privy wallets (no key file on disk).
  • POST /wallet/privy-policy: creates an allowlist policy (EVM to addresses / Solana program IDs) owned by the configured authorization key; optionally attaches it to a wallet.
  • Passphrase challenge on add-privy/remove-privy, mirroring show-private-key.
  • scripts/test-privy-live.js for live verification against real credentials.

Known platform constraint (verified against Privy docs + OpenAPI spec, June 2026)

Solana policy conditions support program-ID allowlists and parsed System/Token-program instruction fields only — pool-account pinning is not expressible on Solana (a wallet allowlisted to the Raydium CLMM program can use any Raydium CLMM pool). On EVM, per-pool restriction IS possible via ethereum_transaction.to + ethereum_calldata ABI conditions.

Test plan

  • 996 tests pass (pnpm test); 25 new Privy tests (routes, policy creation, round-trip verification accept/tamper/wrong-key)
  • pnpm build, pnpm typecheck, pnpm lint clean
  • Live signing spike against a policy-restricted wallet (scripts/test-privy-live.js)

🤖 Generated with Claude Code

fengtality and others added 2 commits June 10, 2026 11:19
…ware registration

Productionizes the Privy wallet integration from PR #618, dropping the
execute-tx endpoints and rebuilding the client layer on the official SDK:

- Replace hand-rolled REST client (deprecated auth.privy.io endpoint, Basic
  auth only) with PrivyService wrapping @privy-io/node, loaded lazily so its
  ESM-only deps don't affect startup or Jest
- Support authorization keys (apiKeys.privyAuthorizationKey): requests are
  P-256 signed when wallets/policies have an owner, so the app secret alone
  cannot bypass policies
- Sign-only flow: Gateway builds and broadcasts transactions on its own RPC
  connections; Privy only signs (no caip2/network coupling)
- Round-trip verification in both signers: Solana compares signed message
  bytes and checks the wallet's signature; Ethereum parses the returned RLP
  and verifies from/to/value/data/nonce/chainId before broadcast
- add-privy response surfaces policyIds/hasOwner and warns when a wallet has
  no policy (can sign anything) or no owner (app secret can remove policies)
- GET /wallet now lists privyWalletAddresses alongside hardware wallets
- Read credentials at call time so runtime config updates take effect;
  sanitize Privy error bodies out of API responses (full detail at debug)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n endpoint

Connector integration (parity with hardware wallets, now unified):
- Solana chain class gains getWalletType/prepareWallet/signTransactionByType;
  Raydium, PancakeSwap-Sol, Jupiter, and wrap/unwrap routes route signing by
  wallet type (local keypair / Ledger / Privy) instead of a hardware boolean
- PancakeSwap-Sol CLMM routes were local-only; they now support hardware and
  Privy wallets via the chain-level helpers
- Ethereum chain class gains getWalletType/getSigner; all EVM write paths
  (approve, wrap, unwrap, Uniswap/PancakeSwap/0x execute and liquidity routes)
  obtain their signer via getSigner so Privy wallets ride the regular ethers
  Contract flow through PrivyEvmSigner (now exposing .address for Wallet
  drop-in compatibility)
- Balance and allowance reads use by-address paths for Privy wallets (no
  local key file to load)
- Jupiter buildSwapTransactionForHardwareWallet renamed to
  buildUnsignedSwapTransaction (it serves any externally-signed wallet)

Policy lifecycle and hardening:
- POST /wallet/privy-policy creates a Privy policy (EVM to-address allowlist /
  Solana program-ID allowlist) owned by the configured authorization key, and
  optionally attaches it to a wallet
- PrivyService derives the P-256 public key from privyAuthorizationKey to set
  policy ownership; without the key the response warns that the app secret
  alone can modify the policy
- add-privy/remove-privy now require the gateway passphrase, mirroring
  show-private-key
- scripts/test-privy-live.js: live credential check, wallet inspection, and
  optional sign test against a policy-restricted wallet

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/wallet/routes/createPrivyPolicy.ts Fixed
Addresses the js/missing-rate-limiting alert on POST /wallet/privy-policy:
- Explicit per-route rate limit (10/min) on top of the global limiter
- Require the gateway passphrase, consistent with add-privy/remove-privy
  (creating or attaching a policy changes a wallet's signing scope)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@fengtality fengtality added this to the v2.16 milestone Jun 11, 2026
fengtality and others added 3 commits June 10, 2026 18:08
axios isn't a dependency; Node 22+ fetch is. Privy's GET /v1/wallets
returns HTTP 500 when passed a bare limit query param - call it plain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verified live: policy denials return HTTP 400 with body
{"error":"RPC request denied due to policy violation","code":"policy_violation"},
and signing without the owner's authorization key returns HTTP 401 with a
"Missing privy-authorization-signature header" error. Surface both as
actionable error messages instead of the generic sanitized failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Privy app secret and authorization (owner) key are secrets; the owner key
in particular is the root of trust for policy enforcement. Reading them from
conf/apiKeys.yml put both on the same host that builds transactions, so a host
compromise could read the owner key and rewrite the wallet policy at will,
nullifying it entirely.

Move both to environment variables (GATEWAY_PRIVY_APP_SECRET,
GATEWAY_PRIVY_AUTHORIZATION_KEY) so they are never read from disk config and can
be injected at runtime by a secret manager. Only the non-sensitive app ID
remains in conf. This is step one; the owner key should ultimately live off the
transaction-building host (remote signer / HSM / multi-key owner quorum).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants