Skip to content

feat(wallet): Privy server wallets as the privy signer backend - #729

Closed
fengtality wants to merge 5 commits into
feat/signer-seamfrom
feat/privy-signer
Closed

fengtality wants to merge 5 commits into
feat/signer-seamfrom
feat/privy-signer

Conversation

@fengtality

@fengtality fengtality commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Privy server wallets as the privy signer backend on the signer seam (#673, base feat/signer-seam). Privy holds the key and signs; Gateway builds and sends. Every execute route works for a privy wallet with no connector change, because the seam's chokepoints get the signer from the registry.

Replaces the closed #649 and #618, using Privy's current REST API (@privy-io/node 0.36 was read as the spec).

What changes

  • src/wallet/privy/privy-client.ts is a small REST client for GET /v1/wallets/{id}, POST /v1/wallets and POST /v1/wallets/{id}/rpc (signTransaction, eth_signTransaction).
    • Credentials come only from PRIVY_APP_ID, PRIVY_APP_SECRET and PRIVY_AUTHORIZATION_KEY in the environment. There is nothing in conf, the templates or request bodies.

    • The privy-authorization-signature matches the SDK. The payload is RFC 8785 {version, method, url, body, headers}, signed with ECDSA P-256 / SHA-256, low-S, DER. It was checked byte for byte against formatRequestForAuthorizationSignature, and the signatures verify with noble's lowS check. A privy-request-expiry (15 min) is signed in.

    • The wallet id must match ^[a-z0-9]{16,64}$ before it reaches a URL, and it is still encodeURIComponent-ed. This is the SSRF fix from feat / Add execute-tx endpoint with Privy wallet integration #618.

    • Privy errors map to clear Gateway errors:

      Privy result Gateway status
      policy violation 403
      missing or wrong authorization signature 403, with a hint to set PRIVY_AUTHORIZATION_KEY
      unknown wallet 404
      rate limit 429
      other 4xx 400
      5xx, or unreachable 502
      missing credentials 503

      Privy's error text is not returned and is logged only at debug level.

    • canonicalize (CJS, about 40 lines) is the only new dependency. The SDK itself is not used: it loads ESM-only jose, which Jest cannot load.

  • src/wallet/signers/privy.ts registers in signerBackends as privy.
    • Solana: it wraps the seam's message in a transaction with empty signature slots and has Privy sign it. It checks that Privy returned the same message bytes, then returns only the wallet's signature, which the seam places and verifies. Partial signing next to ephemeral co-signers works.
    • Ethereum: the populated transaction goes to eth_signTransaction with minimal hex quantities (0x3). The chokepoint checks the sender and fields before it broadcasts.
    • No caip2 or chain id is hard-coded.
  • Routes:
    • POST /wallet/add-privy {chain, privyWalletId, setDefault?} reads the wallet from Privy, checks its chain type and address, and stores {address, privyWalletId, addedAt} in conf/wallets/{chain}/privy-wallets.json.
    • POST /wallet/create-privy {chain, setDefault?} creates a wallet owned by the authorization key's public key, then adds it.
    • GET /wallet lists Privy wallets with signer: 'privy', and DELETE /wallet/remove removes the record. The wallet itself stays in Privy.
  • docs/signer-seam.md has a new Privy section. Operation ids, the route list and openapi.json are updated.

Scope

One authorization key per Gateway, for Gateway and Hummingbot API users. Wallets that another app signs with its own Privy session signers are not designed for here.

Tests

Privy is stubbed at the HTTP edge only (global.fetch). The stub checks the Basic auth header and verifies the authorization signature against the generated key.

  • privy-seam.test.ts: a privy wallet record goes through the real registry and the real Solana and Ethereum chokepoints. Privy signs, the result is broadcast, and the sent transaction verifies. A policy denial is a 403 and nothing is sent.
  • privy-signer.test.ts: co-signer signing, a message changed by Privy is rejected, a non-signer wallet is refused without a Privy call, and EVM field mapping.
  • privy-client.test.ts: request shape, request signature, wallet id validation (no request is made), the error mapping, and missing credentials.
  • privy-wallet.routes.test.ts: add, list, sign-kind, remove; wrong chain; 404; malformed id; address already local; create.

Local evidence (stacked PR, so the ci workflow does not run). Each check ran on the final commit:

  • pnpm build passes.
  • pnpm lint: 0 errors. The 36 warnings are all in files this PR does not touch.
  • pnpm test -w 3: 204 suites and 1916 tests pass.
  • pnpm generate:openapi was run.

Not run live: Privy credentials were not given, so no live Privy swap was run.

🤖 Generated with Claude Code

… error mapping

Credentials come from PRIVY_APP_ID, PRIVY_APP_SECRET and PRIVY_AUTHORIZATION_KEY only.
The authorization signature matches @privy-io/node (RFC 8785 payload, P-256 low-S DER).
Wallet ids are validated before they reach a URL. Privy errors map to 4xx; Privy's
error text is never returned to the caller.
…te-privy routes

The registry maps the 'privy' signer kind to the Privy backend, so every execute route
signs through Privy for a privy wallet with no connector change. Solana: the backend
wraps the seam's message in a transaction, has Privy sign it, checks the returned
message is unchanged and returns only the wallet's signature. Ethereum: the populated
transaction goes to eth_signTransaction and the chokepoint checks it before sending.
Privy wallet records (id and address only) are in conf/wallets/{chain}/privy-wallets.json.
@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge; no new actionable issues remain.

Summary

Adds Privy-backed signing for Solana and Ethereum, plus routes to add and create Privy wallets.

  • Privy wallets can sign Gateway transactions on Solana and Ethereum.
  • Callers can add existing Privy wallets or create new ones through Gateway.
  • Wallet list updates keep concurrent changes instead of overwriting them.

Diagram

sequenceDiagram
  participant Client
  participant Gateway
  participant Privy
  participant Chain
  Client->>Gateway: Execute using a registered Privy wallet
  Gateway->>Gateway: Build transaction
  Gateway->>Privy: Request signature
  Privy-->>Gateway: Signed transaction
  Gateway->>Gateway: Check message, fields, and signature
  Gateway->>Chain: Broadcast transaction
  Chain-->>Gateway: Transaction result
  Gateway-->>Client: Result
Loading

Reviews (4) · Last reviewed commit: "fix(wallet): every wallet list store cha..." · Reviewed by Greptile

Comment thread src/wallet/privy/privy-client.ts Resolved
Comment thread test/wallet/privy/privy-http-stub.ts Resolved
Comment thread src/wallet/utils.ts Resolved
…02; SDK payload fixture

updateListStore runs read-change-write on one list store with no other update to the
same file in between; add-privy and DELETE /wallet/remove use it. A failure while
reading Privy's response body is a 502 like a failed request. The authorization
signature test now verifies against payload bytes taken from @privy-io/node.
… name add-privy in the 404

add-external and the hardware save (now upsertHardwareWallet) read, change and write
their list under the same per-file lock as add-privy and removal, so concurrent saves
keep every entry. The 404 for an unknown wallet names POST /wallet/add-privy.
fengtality added a commit that referenced this pull request Oct 20, 2026
…and Privy wallets (#673, #682)

Every Gateway route that sends a transaction gets a build-* twin that
returns the unsigned transaction, and POST /chains/{chain}/submit
broadcasts what the caller signed. Every execute route signs through one
signer seam per chain, with local, hardware, external and Privy wallets
behind it. This is the build half of the old #721 and the content of
#727, #730, #728, #732 and #729, on top of the launch connectors.

Solana build mode (from #721)
- A build-* route runs the connector's own code and captures the
  transaction at Solana.sendAndConfirmTransactionForWallet instead of
  signing it: simulated, compute budget sized, fresh blockhash, ephemeral
  keypairs partial-signed, fee-payer slot empty. A broadcast attempted
  during a build throws; nothing is sent.
- Routes: router build-swap and build-quote; clmm build-swap, build-open,
  build-add, build-remove, build-collect-fees, build-close and
  build-create-pool; amm build-swap, build-add, build-remove and
  build-create-pool; launch build-swap and build-create-token;
  POST /chains/{chain}/build-transaction for caller instructions; and
  POST /chains/{chain}/submit, which re-merges the ephemeral signatures and
  verifies every signature and the blockhash before it broadcasts.
- The build shape on every build route: extraInstructions, feePayer,
  format (transaction or instructions), memo, references (Solana Pay) and
  priority (normal, high, urgent).
- platformFee on builds, paid to a recipient the caller names (Jupiter
  through its feeAccount, other venues by a transfer after the
  operation). Router quotes take platformFeeBps.
- pancakeswap-sol build routes are refused by name. poll reports
  currentBlockHeight beside the slot.
- Build routes are behind the API-token gate with their execute routes;
  submit stays public like poll.

Build coverage (#727)
- One build response for an operation of several transactions; a wide
  Meteora DLMM open, add, remove, close or collect builds as one response.
- collect-fees and build-collect-fees for Meteora DAMM v2.
- build-claim-creator-fees and build-migrate; creator-fee claims on
  meteora and raydium.
- pump: a dev-buy create names its lookup table, or has one made for it;
  each new lookup table gets its own slot, and a create after a new table
  compiles with the build's fee payer.
- build-transaction takes feePayer, memo and priority. A legacy build's
  compute is sized against the RPC's own blockhash.
- raydium AMM v4 add gives the non-limiting side its slippage headroom.

Signer seam (#730)
- The signer seam interface and the wallet record. Every Solana and EVM
  transaction is signed through it.
- External wallets: Gateway holds no key, and a route that would sign for
  one refuses and names its build twin.
- The key-handling helpers the seam replaced are deleted, with the old
  ledger helpers.
- The route-coverage test: every route that sends a transaction is listed
  with its build twin, and only the seam signs.

EVM build and submit (#728)
- build-* twins and submit for every EVM transaction route. A build
  returns the unsigned transaction with the approvals it needs first.
- Tests for build mode, approvals, submit and the EVM build routes.
- The uniswap execute-quote Permit2 check moved to
  Ethereum.requirePermit2Allowance, so its own test
  (executeQuote-permit2-expiration.test.ts) is deleted; the EVM build-mode
  tests cover the Permit2 path with the max uint48 expiration.

The last build twins (#732)
- Solana build-wrap and build-unwrap, and launch build-quote. The route
  table has no gaps.

Privy wallets (#729)
- A Privy server-wallet REST client with request signing and error
  mapping.
- The privy signer backend on the seam, and the add-privy and
  create-privy routes.
- EVM quantities go to Privy as minimal hex. Wallet list stores change
  through updateListStore, one update at a time.
- The Privy seam test calls the EVM chokepoint the way #728 left it.

openapi.json is regenerated.

Closes #682
@fengtality

Copy link
Copy Markdown
Contributor Author

Consolidated into the single draft build-and-submit PR #734.

@fengtality fengtality closed this Oct 20, 2026
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.

1 participant