Skip to content

feat / Add execute-tx endpoint with Privy wallet integration - #618

Closed
fengtality wants to merge 6 commits into
developmentfrom
feat/execute-tx-privy-integration
Closed

fengtality wants to merge 6 commits into
developmentfrom
feat/execute-tx-privy-integration

Conversation

@fengtality

Copy link
Copy Markdown
Contributor

Summary

  • 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
  • Supports multiple wallet types: local, hardware (Ledger), and Privy

New Endpoints

Execute Transaction

  • POST /chains/solana/execute-tx - Execute Solana transactions
  • POST /chains/ethereum/execute-tx - Execute Ethereum transactions

Privy Wallet Management

  • POST /wallet/add-privy - Register a Privy wallet
  • DELETE /wallet/remove-privy - Unregister a Privy wallet

Features

  • Multiple input formats: Accepts serialized transactions, instructions arrays, or single instruction (ix) for USDM compatibility
  • Wallet type auto-detection: Automatically detects wallet type (local, hardware, Privy) and uses appropriate signing method
  • Transaction simulation: Simulates transactions before sending to catch errors early
  • Comprehensive error handling: Proper error codes for insufficient funds, timeouts, etc.

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

  • Unit tests for execute-tx route (22 tests passing)
  • Build passes
  • Manual testing with USDM API integration
  • Test Privy wallet signing

🤖 Generated with Claude Code

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>
Comment thread src/wallet/privy/privy-client.ts Fixed
fengtality and others added 2 commits March 25, 2026 22:07
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
rapcmia requested a review from nikspz March 30, 2026 02:06
@rapcmia rapcmia moved this to Backlog in Pull Request Board Mar 30, 2026
@rapcmia rapcmia added this to the v2.14 milestone Mar 30, 2026
@rapcmia rapcmia changed the title feat: Add execute-tx endpoint with Privy wallet integration feat/ Add execute-tx endpoint with Privy wallet integration Mar 30, 2026
@rapcmia rapcmia changed the title feat/ Add execute-tx endpoint with Privy wallet integration feat / Add execute-tx endpoint with Privy wallet integration Mar 30, 2026
@fengtality fengtality removed this from the v2.14 milestone Mar 31, 2026
@fengtality
fengtality marked this pull request as draft March 31, 2026 17:29
@fengtality

Copy link
Copy Markdown
Contributor Author

@rapcmia this PR needs more work before it's ready. Moving to v2.15

@nikspz nikspz moved this from Backlog to In Discussion in Pull Request Board Apr 3, 2026
@rapcmia
rapcmia removed the request for review from nikspz May 12, 2026 07:11

@fengtality fengtality left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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, addressLookupTables are accepted in the schema but the route reads none of them. No ComputeBudgetProgram.setComputeUnitPrice, no setComputeUnitLimit, 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 at debug level, expose only status + fixed message to callers.
  • chains/solana/routes/execute-tx.ts:561-563 — catch block calls handleSolanaTransactionError but has no explicit throw after. If the helper doesn't throw for an unrecognized error type, the function returns undefined and the route 200s with empty body. Add an explicit throw after the helper.
  • wallet/routes/addPrivyWallet.ts — no passphrase challenge on POST /wallet/add-privy. Registering a Privy wallet grants signing authority; this should require the gateway passphrase consistent with addWallet.

🟢 Nits

  • wallet/wallet.routes.ts still registers sensible locally (line ~16) even though app.ts registers it globally — partial cleanup. Same pattern was removed elsewhere; finish here.
  • wallet/privy/privy-evm-signer.ts:1355 sets tx.from = this._address but ethers' sendTransaction ignores from on the wallet object — harmless but misleading.
  • getPrivyClient() caches credentials at construction. If credentials are updated via PUT /config at runtime, the stale instance keeps the old keys. Consider reading via ConfigManagerV2.get() at call time.

To undraft

  1. from / feePayer assertion on both chains, both skipSign paths
  2. Solana + Ethereum caip2: derive from network, drop the hardcodes
  3. Round-trip verification of Privy-signed bytes (strip-resign-compare)
  4. Implement or remove priorityFeePerCU / computeUnits / addressLookupTables
  5. Add Ethereum execute-tx tests — Solana has 522 LoC of coverage; Ethereum has zero
  6. Add at least one privy-client.ts path test (Solana suite only exercises the signer stub)
  7. Sanitize Privy error logs / responses
  8. Add the missing throw in the Solana catch
  9. Document or implement the passphrase model for add-privy

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.

4 participants