Repository navigation
feat / Add execute-tx endpoint with Privy wallet integration - #618
fengtality wants to merge 6 commits into
Conversation
Adds a generic execute-tx endpoint for executing transaction payloads from external APIs (like USDM). Includes full Privy server wallet integration for both Solana and Ethereum chains. New features: - POST /chains/solana/execute-tx endpoint - POST /chains/ethereum/execute-tx endpoint - Privy wallet management (add/remove routes) - Support for serialized transactions, instructions arrays, and single instruction (ix) format for USDM compatibility - Hardware wallet (Ledger), Privy, and local wallet signing Files created: - src/chains/solana/routes/execute-tx.ts - src/chains/ethereum/routes/execute-tx.ts - src/schemas/execute-tx-schema.ts - src/wallet/privy/privy-client.ts - src/wallet/privy/privy-evm-signer.ts - src/wallet/privy/privy-solana-signer.ts - src/wallet/routes/addPrivyWallet.ts - src/wallet/routes/removePrivyWallet.ts - test/chains/solana/routes/execute-tx.test.ts Files modified: - src/app.ts (register sensible globally) - src/chains/solana/schemas.ts (add execute-tx schemas) - src/chains/solana/solana.ts (add isPrivyWallet method) - src/chains/ethereum/ethereum.ts (add isPrivyWallet method) - src/wallet/utils.ts (add Privy wallet utilities) - src/templates/apiKeys.yml (add Privy credentials) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Addresses CodeQL security finding by: - Adding validateWalletId() function to validate wallet IDs contain only alphanumeric characters, hyphens, and underscores - Rejecting wallet IDs with path traversal or URL manipulation chars - Adding encodeURIComponent() as additional defense in depth - Adding length limit to prevent abuse Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The @fastify/sensible plugin returns error names like 'InternalServerError' instead of 'Internal Server Error'. Updated test to accept both formats. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
@rapcmia this PR needs more work before it's ready. Moving to v2.15 |
fengtality
left a comment
There was a problem hiding this comment.
Self-review notes captured during a pre-undraft pass — posting publicly so the conversation is preserved. Do not undraft until the signing-authority bugs (Critical #1–#2) are fixed.
🔴 Critical security (blocking for undrafting)
1. chains/ethereum/routes/execute-tx.ts:171-176 — skipSign=true broadcasts caller-supplied raw bytes with no authentication.
Gateway has no per-request auth beyond TLS. Any caller reaching the port can POST { serializedTx: "0x...", skipSign: true } and the route calls ethereum.provider.sendTransaction(request.serializedTx) immediately. walletAddress is read for config defaulting but never compared to parsedTx.from. An attacker can broadcast arbitrary signed bytes for any registered wallet.
Fix: when skipSign=true, parse the serialized tx and assert parsedTx.from.toLowerCase() === walletAddress.toLowerCase() before broadcasting. If walletAddress is missing, reject the request.
2. skipSign=false re-sign path doesn't validate walletAddress vs parsedTx.from / feePayer.
Both chains/ethereum/routes/execute-tx.ts:195-210 and chains/solana/routes/execute-tx.ts:491-527 accept a serializedTx, discard the existing signature, and re-sign with the key matching walletAddress — without asserting that the original tx's from (Ethereum) or feePayer (Solana) matches walletAddress. Attacker can submit serializedTx with arbitrary to/data/feePayer and walletAddress = victim_wallet; Gateway signs with the victim's key.
Fix: after deserialization, assert parsedTx.from === walletAddress (Ethereum) and tx.feePayer?.toBase58() === walletAddress (Solana), or override these to the gateway-controlled address.
3. wallet/privy/privy-client.ts:1124, 1147, 1194 — Solana caip2 hardcoded to mainnet.
signSolanaTransaction, signAndSendSolanaTransaction, and Ethereum signMessage all hardcode caip2: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' (Solana mainnet) and caip2: 'eip155:1' (Ethereum mainnet) regardless of the wallet's actual network. Devnet wallets request mainnet signatures (Privy signs for mainnet → devnet rejects blockhash → confusing failure). On Polygon/Base/Arbitrum the signMessage produces wrong-chain signatures.
Fix: thread network into PrivySolanaSigner / PrivyClient, derive caip2 from network at call time.
4. No round-trip verification of Privy-returned signed bytes.
signSolanaTransaction and signEthereumTransaction deserialize and broadcast whatever Privy returns without checking that the returned bytes encode the same transaction intent (same instructions / to / value / data). Standard HSM pattern is: strip signatures from returned tx, re-serialize unsigned portion, compare byte-for-byte to the original input. If Privy were MITMed or compromised, Gateway would broadcast manipulated payloads.
🟡 Important
chains/solana/routes/execute-tx.ts:656-680—priorityFeePerCU,computeUnits,addressLookupTablesare accepted in the schema but the route reads none of them. NoComputeBudgetProgram.setComputeUnitPrice, nosetComputeUnitLimit, no lookup-table resolution. Implement or drop from the schema.wallet/privy/privy-client.ts:1066-1068— Privy error body echoed into gateway error message + logs. Privy responses can include wallet addresses, internal trace IDs. Sanitize: log full text atdebuglevel, expose only status + fixed message to callers.chains/solana/routes/execute-tx.ts:561-563— catch block callshandleSolanaTransactionErrorbut has no explicitthrowafter. If the helper doesn't throw for an unrecognized error type, the function returnsundefinedand the route 200s with empty body. Add an explicitthrowafter the helper.wallet/routes/addPrivyWallet.ts— no passphrase challenge onPOST /wallet/add-privy. Registering a Privy wallet grants signing authority; this should require the gateway passphrase consistent withaddWallet.
🟢 Nits
wallet/wallet.routes.tsstill registerssensiblelocally (line ~16) even thoughapp.tsregisters it globally — partial cleanup. Same pattern was removed elsewhere; finish here.wallet/privy/privy-evm-signer.ts:1355setstx.from = this._addressbut ethers'sendTransactionignoresfromon the wallet object — harmless but misleading.getPrivyClient()caches credentials at construction. If credentials are updated viaPUT /configat runtime, the stale instance keeps the old keys. Consider reading viaConfigManagerV2.get()at call time.
To undraft
from/feePayerassertion on both chains, bothskipSignpaths- Solana + Ethereum caip2: derive from
network, drop the hardcodes - Round-trip verification of Privy-signed bytes (strip-resign-compare)
- Implement or remove
priorityFeePerCU/computeUnits/addressLookupTables - Add Ethereum
execute-txtests — Solana has 522 LoC of coverage; Ethereum has zero - Add at least one
privy-client.tspath test (Solana suite only exercises the signer stub) - Sanitize Privy error logs / responses
- Add the missing
throwin the Solana catch - Document or implement the passphrase model for
add-privy
Summary
execute-txendpoint for executing transaction payloads from external APIs (like USDM)New Endpoints
Execute Transaction
POST /chains/solana/execute-tx- Execute Solana transactionsPOST /chains/ethereum/execute-tx- Execute Ethereum transactionsPrivy Wallet Management
POST /wallet/add-privy- Register a Privy walletDELETE /wallet/remove-privy- Unregister a Privy walletFeatures
Request Format (Solana)
{ "network": "mainnet-beta", "walletAddress": "...", "ix": { "keys": [{"pubkey": "...", "isSigner": true, "isWritable": true}], "programId": "...", "data": "base64..." } }Or with instructions array:
{ "instructions": [...] }Or with serialized transaction:
{ "serializedTx": "base64..." }Test plan
🤖 Generated with Claude Code