feat(public-api): normalize transaction data format with discriminated union#11738
feat(public-api): normalize transaction data format with discriminated union#11738
Conversation
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (1)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis pull request introduces a comprehensive multi-chain transaction data handling system by defining discriminated union schemas for EVM, Solana, UTXO, and Cosmos chains. The changes restructure quote step responses to use a unified TransactionData type and implement extraction logic for transaction-specific metadata across multiple blockchain networks, including deposit address resolution for inbound flows. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant QuoteAPI as Quote API<br/>(getQuote)
participant DepositResolver as Deposit Address<br/>Resolver
participant TxExtractor as Transaction Data<br/>Extractor
participant SwapWidget as Swap Widget
Client->>QuoteAPI: Request swap quote
QuoteAPI->>DepositResolver: resolveDepositContext<br/>(for UTXO/Cosmos)
DepositResolver->>DepositResolver: fetchInboundAddress
DepositResolver-->>QuoteAPI: depositContext<br/>(memo, address)
QuoteAPI->>TxExtractor: extractTransactionData<br/>(step, chain type)
TxExtractor->>TxExtractor: Chain-specific extraction<br/>(EVM/Solana/UTXO/Cosmos)
TxExtractor-->>QuoteAPI: TransactionData<br/>(discriminated union)
QuoteAPI-->>Client: QuoteResponse<br/>with steps[].transactionData
Client->>SwapWidget: Pass quote response
SwapWidget->>SwapWidget: Validate transactionData.type
SwapWidget->>SwapWidget: Route to chain handler<br/>(EVM/Solana/UTXO/Cosmos)
SwapWidget->>SwapWidget: Extract & prepare<br/>transaction fields
SwapWidget-->>Client: Ready for signing/broadcast
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…d union Introduces a consistent TransactionData union type for executable quotes, eliminating the need for consumers to handle swapper-specific metadata fields. Phase 1 covers EVM swappers: 0x, Portals, Bebop, ButterSwap, and Relay. BREAKING CHANGE: removed raw 'quote' field from QuoteResponse
Phase 2: Adds extractSolanaTransactionData for Jupiter and Relay-Solana swappers. Converts Solana TransactionInstruction to serializable format with base64 data.
…d deposit-based swappers
Update TransactionData to discriminated union format and QuoteResponse structure to match the new public-api multi-chain transaction support. Co-Authored-By: Claude <noreply@anthropic.com>
…ess failure - Remove unused _sellAssetId parameter from buildApprovalInfo - Add signatureRequired to CowswapOrderData in swap-widget types - Return 503 error when deposit address fetch fails for UTXO/Cosmos swaps
Update widget swap execution to use the discriminated union transaction data format from the public API instead of the old swapper-internal metadata fields. Bump @shapeshiftoss/caip to 8.16.7 for tonAssetId export and exclude workspace packages from Vite pre-bundling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9ba4996 to
827b9ac
Compare
Remove dead CowswapOrderData types, extract deposit context into a pure helper function, scope deposit context to first step only, log fetchInboundAddress errors, and apply permit2 as a swapper-agnostic post-processing step. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…a ALT support - Move TransactionData discriminated union types to packages/types/src/api.ts as single source of truth, eliminating duplication across public-api and swap-widget - Remove redundant type assertions in SwapWidget after discriminated union narrowing - Add Solana address lookup table (ALT) support using VersionedTransaction instead of legacy Transaction, enabling Jupiter swaps that require ALTs - Include Permit2SignatureRequired on shared EvmTransactionData type Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/public-api/src/docs/openapi.ts (1)
122-139:⚠️ Potential issue | 🟡 Minor
QuoteResponseSchemais missingapprovalandnetworkFeeCryptoBaseUnitfields.The
QuoteResponsetype inpackages/public-api/src/types.ts(lines 108-123) includesapproval: ApprovalInfoandnetworkFeeCryptoBaseUnit: string | undefined, but the OpenAPI schema here omits both. This means the API documentation won't accurately describe the response shape, and consumers relying on the OpenAPI spec will be surprised by these fields.Proposed fix
const QuoteResponseSchema = registry.register( 'QuoteResponse', z.object({ quoteId: z.string().uuid(), swapperName: z.string().openapi({ example: '0x' }), rate: z.string().openapi({ example: '0.995' }), sellAsset: AssetSchema, buyAsset: AssetSchema, sellAmountCryptoBaseUnit: z.string(), buyAmountBeforeFeesCryptoBaseUnit: z.string(), buyAmountAfterFeesCryptoBaseUnit: z.string(), affiliateBps: z.string().openapi({ example: '10' }), slippageTolerancePercentageDecimal: z.string().optional().openapi({ example: '0.01' }), + networkFeeCryptoBaseUnit: z.string().optional(), steps: z.array(QuoteStepSchema), + approval: z.object({ + isRequired: z.boolean(), + spender: z.string(), + approvalTx: z.object({ + to: z.string(), + data: z.string(), + value: z.string(), + }).optional(), + }), expiresAt: z.number(), }), )
🤖 Fix all issues with AI agents
In `@packages/public-api/src/routes/quote.ts`:
- Around line 179-199: The extractSolanaTransactionData function currently maps
instructions without any error handling; wrap the instruction
mapping/serialization in a try-catch inside extractSolanaTransactionData (or
refactor it to return a Result/Ok|Err) so any Buffer.from or .toBase58()
failures are caught, validate that step.solanaTransactionMetadata.instructions
is an array and each ix has expected fields before mapping, and on failure log a
structured error including context (swapper name from step.swapperName or
equivalent, step index from step.index, and instruction count) via the existing
logger and then return undefined or an Err result; ensure the returned shape
still matches SolanaTransactionData on success.
🧹 Nitpick comments (1)
packages/public-api/src/routes/quote.ts (1)
256-279: Consider usingCHAIN_NAMESPACEconstants instead of string literals.The chain namespace comparisons use raw strings (
'eip155','solana','bip122','cosmos'). While TypeScript will catch type-level mismatches, importing and usingCHAIN_NAMESPACEfrom@shapeshiftoss/caip(already available in the codebase) would be more consistent and resilient.Proposed refactor
+import { CHAIN_NAMESPACE, fromChainId } from '@shapeshiftoss/caip' + const extractTransactionData = ( step: TradeQuoteStep, context: DepositExtractionContext = {}, ): TransactionData | undefined => { const { chainNamespace } = fromChainId(step.sellAsset.chainId) - if (chainNamespace === 'eip155') { + if (chainNamespace === CHAIN_NAMESPACE.Evm) { return extractEvmTransactionData(step) } - if (chainNamespace === 'solana') { + if (chainNamespace === CHAIN_NAMESPACE.Solana) { return extractSolanaTransactionData(step) } - if (chainNamespace === 'bip122') { + if (chainNamespace === CHAIN_NAMESPACE.Utxo) { return extractUtxoTransactionData(step, context) } - if (chainNamespace === 'cosmos') { + if (chainNamespace === CHAIN_NAMESPACE.CosmosSdk) { return extractCosmosTransactionData(step, context) }Note: The same pattern applies to
buildApprovalInfoat line 284 andresolveDepositContextat line 330.
NeOMakinG
left a comment
There was a problem hiding this comment.
Couldn't test with the actual widget because of some issues in my local env, but I did try a few calls to the API locally and I'm more than happy with this one, I'll get it in so I can use it with the widget polish PR and fix anything I'll find later if needed
Example of call:
{
"quoteId": "233c8412-9606-4adf-90c9-cdbdedf2a2b7",
"swapperName": "Relay",
"rate": "0.02838925",
"sellAsset": {
"assetId": "eip155:1/slip44:60",
"chainId": "eip155:1",
"symbol": "ETH",
"name": "Ethereum",
"networkName": "Ethereum",
"precision": 18,
"color": "#5C6BC0",
"icon": "https://rawcdn.githack.com/trustwallet/assets/32e51d582a890b3dd3135fe3ee7c20c2fd699a6d/blockchains/ethereum/info/logo.png",
"explorer": "https://etherscan.io",
"explorerAddressLink": "https://etherscan.io/address/",
"explorerTxLink": "https://etherscan.io/tx/",
"relatedAssetKey": "eip155:1/slip44:60"
},
"buyAsset": {
"assetId": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"chainId": "bip122:000000000019d6689c085ae165831e93",
"symbol": "BTC",
"name": "Bitcoin",
"networkName": "Bitcoin",
"precision": 8,
"color": "#FF9800",
"icon": "https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/bitcoin/info/logo.png",
"explorer": "https://live.blockcypher.com",
"explorerAddressLink": "https://live.blockcypher.com/btc/address/",
"explorerTxLink": "https://live.blockcypher.com/btc/tx/",
"relatedAssetKey": null
},
"sellAmountCryptoBaseUnit": "1000000000000000000",
"buyAmountBeforeFeesCryptoBaseUnit": "2863019",
"buyAmountAfterFeesCryptoBaseUnit": "2838925",
"affiliateBps": "55",
"slippageTolerancePercentageDecimal": "0.02",
"networkFeeCryptoBaseUnit": "1034391166656",
"steps": [
{
"sellAsset": {
"assetId": "eip155:1/slip44:60",
"chainId": "eip155:1",
"symbol": "ETH",
"name": "Ethereum",
"networkName": "Ethereum",
"precision": 18,
"color": "#5C6BC0",
"icon": "https://rawcdn.githack.com/trustwallet/assets/32e51d582a890b3dd3135fe3ee7c20c2fd699a6d/blockchains/ethereum/info/logo.png",
"explorer": "https://etherscan.io",
"explorerAddressLink": "https://etherscan.io/address/",
"explorerTxLink": "https://etherscan.io/tx/",
"relatedAssetKey": "eip155:1/slip44:60"
},
"buyAsset": {
"assetId": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"chainId": "bip122:000000000019d6689c085ae165831e93",
"symbol": "BTC",
"name": "Bitcoin",
"networkName": "Bitcoin",
"precision": 8,
"color": "#FF9800",
"icon": "https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/bitcoin/info/logo.png",
"explorer": "https://live.blockcypher.com",
"explorerAddressLink": "https://live.blockcypher.com/btc/address/",
"explorerTxLink": "https://live.blockcypher.com/btc/tx/",
"relatedAssetKey": null
},
"sellAmountCryptoBaseUnit": "1000000000000000000",
"buyAmountAfterFeesCryptoBaseUnit": "2838925",
"allowanceContract": "0x4cd00e387622c35bddb9b4c962c136462338bc31",
"estimatedExecutionTimeMs": 220000,
"source": "Relay",
"transactionData": {
"type": "evm",
"chainId": 1,
"to": "0x4cd00e387622c35bddb9b4c962c136462338bc31",
"data": "0x49290c1c000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045f4d1093afcc5f9eb1512ab8b626eab429fb2187bb3dafeef2d8aabc55d964207",
"value": "1000000000000000000",
"gasLimit": "32697"
}
}
],
"approval": {
"isRequired": false,
"spender": ""
},
"expiresAt": 1771239369423
}
Description
Normalizes the public API's transaction data format from a flat, EVM-only structure into a discriminated union (
typefield) supporting EVM, Solana, UTXO (PSBT and deposit-based), and Cosmos chains.Key changes:
transactionData— adds atypefield (evm,solana,utxo_psbt,utxo_deposit,cosmos) enabling clean type narrowing in consumers@shapeshiftoss/types—TransactionDataand all variants live inpackages/types/src/api.tsas a single source of truth, consumed by bothpublic-apiandswap-widgetquote: TradeQuote | TradeRatefield was removed fromQuoteResponse, preventing exposure of internal swapper types to API consumersVersionedTransactionwith address lookup tables instead of legacyTransaction, enabling Jupiter swaps that require ALTssignatureRequiredfor swappers that need Permit2 approvalBefore / After
Before — consumers had to probe swapper-specific fields:
After — a single normalized field with discriminated union narrowing:
Transaction Data Type Reference
EvmTransactionDataevmchainId,to,data,value,gasLimit?,signatureRequired?SolanaTransactionDatasolanainstructions[],addressLookupTableAddresses[]UtxoPsbtTransactionDatautxo_psbtpsbt,opReturnData?UtxoDepositTransactionDatautxo_depositdepositAddress,memo,valueCosmosTransactionDatacosmoschainId,to,value,memo?Permit2 Flow
When
signatureRequiredis present on anevmtransaction, the consumer must:signatureRequired.eip712with the user's walletCurrently only
type: 'permit2'is supported, used by swappers like 0x.Multi-hop Behavior
Deposit context (memo + inbound address) is only resolved and applied to step 0 of multi-hop quotes. Later steps are internal to the swapper protocol and don't require consumer-side transaction building.
Risk
Medium risk. Changes the public API response shape (breaking change for existing consumers) and modifies on-chain transaction building for Solana (VersionedTransaction).
Testing
Engineering
cd packages/public-api && npx tsc --noEmit— verify no type errorscd packages/swap-widget && npx tsc --noEmit— verify no type errorsyarn workspace @shapeshiftoss/types build— verify clean buildtransactionData.type === 'evm'withchainId,to,data,valuetransactionData.type === 'solana'with serialized instructions and ALT addressestransactionData.type === 'utxo_psbt'with PSBT datatransactionData.type === 'utxo_deposit'with deposit address and memotransactionData.type === 'cosmos'with deposit address and memoOperations
Screenshots (if applicable)
N/A - API and type changes only
Summary by CodeRabbit
Release Notes
New Features
Chores