Skip to content

feat(public-api): normalize transaction data format with discriminated union#11738

Merged
NeOMakinG merged 13 commits intodevelopfrom
feat/normalize-api-data
Feb 16, 2026
Merged

feat(public-api): normalize transaction data format with discriminated union#11738
NeOMakinG merged 13 commits intodevelopfrom
feat/normalize-api-data

Conversation

@0xApotheosis
Copy link
Copy Markdown
Member

@0xApotheosis 0xApotheosis commented Jan 20, 2026

Description

Normalizes the public API's transaction data format from a flat, EVM-only structure into a discriminated union (type field) supporting EVM, Solana, UTXO (PSBT and deposit-based), and Cosmos chains.

Screenshot 2026-02-16 at 6 27 39 pm

Key changes:

  • Discriminated union for transactionData — adds a type field (evm, solana, utxo_psbt, utxo_deposit, cosmos) enabling clean type narrowing in consumers
  • Shared types in @shapeshiftoss/typesTransactionData and all variants live in packages/types/src/api.ts as a single source of truth, consumed by both public-api and swap-widget
  • Removes leaked internal state — the quote: TradeQuote | TradeRate field was removed from QuoteResponse, preventing exposure of internal swapper types to API consumers
  • Multi-chain transaction support — adds Solana instruction serialization, UTXO PSBT/deposit flows, Cosmos memo-based deposits, and Chainflip/NearIntents deposit-based EVM transactions
  • Solana ALT support — swap widget now uses VersionedTransaction with address lookup tables instead of legacy Transaction, enabling Jupiter swaps that require ALTs
  • Deposit address resolution — THORChain/MAYAChain UTXO and Cosmos swaps now resolve inbound vault addresses server-side
  • Permit2 EIP-712 signatures — EVM transaction data includes signatureRequired for swappers that need Permit2 approval

Before / After

Before — consumers had to probe swapper-specific fields:

const transactionData =
  quoteResponse.transactionData ??
  outerStep?.transactionData ??
  outerStep?.relayTransactionMetadata ??
  outerStep?.butterSwapTransactionMetadata ??
  innerStep?.transactionData ??
  innerStep?.relayTransactionMetadata ??
  innerStep?.butterSwapTransactionMetadata

const to = transactionData.to as string
const data = transactionData.data as string
const value = transactionData.value ?? '0'

// Solana required reaching into internal quote
const solanaTransactionMetadata = (innerStep as any)?.solanaTransactionMetadata

After — a single normalized field with discriminated union narrowing:

const transactionData = quoteResponse.steps[0]?.transactionData

switch (transactionData.type) {
  case 'evm':
    // transactionData.chainId, .to, .data, .value, .gasLimit, .signatureRequired
    break
  case 'solana':
    // transactionData.instructions, .addressLookupTableAddresses
    break
  case 'utxo_psbt':
    // transactionData.psbt, .opReturnData
    break
  case 'utxo_deposit':
    // transactionData.depositAddress, .memo, .value
    break
  case 'cosmos':
    // transactionData.chainId, .to, .value, .memo
    break
}

Transaction Data Type Reference

Type Discriminant Fields Used By
EvmTransactionData evm chainId, to, data, value, gasLimit?, signatureRequired? 0x, Portals, Bebop, ButterSwap, Relay (EVM), Chainflip (EVM deposit), NearIntents (EVM deposit)
SolanaTransactionData solana instructions[], addressLookupTableAddresses[] Jupiter
UtxoPsbtTransactionData utxo_psbt psbt, opReturnData? Relay (BTC PSBT)
UtxoDepositTransactionData utxo_deposit depositAddress, memo, value THORChain, MAYAChain, Chainflip (BTC deposit)
CosmosTransactionData cosmos chainId, to, value, memo? THORChain, MAYAChain (Cosmos-SDK chains)

Permit2 Flow

When signatureRequired is present on an evm transaction, the consumer must:

  1. Sign the EIP-712 typed data from signatureRequired.eip712 with the user's wallet
  2. Include the signature in the subsequent transaction

Currently 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).

Protocols affected: THORChain, MAYAChain, Chainflip, NearIntents, Jupiter, Relay, 0x, Portals, Bebop, ButterSwap
Transaction types: EVM contract calls, EVM deposits, UTXO PSBT, UTXO deposits, Cosmos deposits, Solana versioned transactions

Testing

Engineering

  1. Public API types: cd packages/public-api && npx tsc --noEmit — verify no type errors
  2. Swap widget types: cd packages/swap-widget && npx tsc --noEmit — verify no type errors
  3. Types package: yarn workspace @shapeshiftoss/types build — verify clean build
  4. EVM swaps (0x, Portals, Bebop, ButterSwap, Relay): verify transactionData.type === 'evm' with chainId, to, data, value
  5. Solana swaps (Jupiter): verify transactionData.type === 'solana' with serialized instructions and ALT addresses
  6. UTXO PSBT swaps (Relay BTC): verify transactionData.type === 'utxo_psbt' with PSBT data
  7. UTXO deposit swaps (THORChain/MAYAChain/Chainflip BTC): verify transactionData.type === 'utxo_deposit' with deposit address and memo
  8. Cosmos deposit swaps (THORChain/MAYAChain ATOM): verify transactionData.type === 'cosmos' with deposit address and memo
  9. Chainflip BTC deposits: confirm empty memo is correct (Chainflip uses unique deposit channels, no OP_RETURN needed)
  10. Swap widget Solana: test Jupiter swap through widget to verify VersionedTransaction + ALT support works end-to-end

Operations

  • Test EVM swap via swap widget (ETH → USDC)
  • Test BTC swap via swap widget (BTC → ETH via THORChain)
  • Test Solana swap via swap widget (SOL → USDC via Jupiter)
  • Verify API documentation at /docs reflects new transaction data schema

Screenshots (if applicable)

N/A - API and type changes only

Summary by CodeRabbit

Release Notes

New Features

  • Extended transaction data support for multiple blockchain networks (EVM, Solana, UTXO, Cosmos) with standardized formatting
  • Added deposit address resolution capability for inbound transfer flows
  • Enhanced quote responses with detailed, chain-specific transaction information

Chores

  • Updated internal dependency resolution to use monorepo workspace references

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 20, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
Core Transaction Data Types
packages/types/src/api.ts, packages/types/src/index.ts
Introduces 8 new exported types: Permit2SignatureRequired, EvmTransactionData, SolanaTransactionData, UtxoPsbtTransactionData, UtxoDepositTransactionData, UtxoTransactionData, CosmosTransactionData, and TransactionData as a discriminated union.
Public API Schema Definitions
packages/public-api/src/docs/openapi.ts
Adds Zod schema definitions for all transaction data types and replaces QuoteStep's inline transactionData with a reference to the new TransactionDataSchema union.
Quote Route Implementation
packages/public-api/src/routes/quote.ts
Implements multi-chain transaction data extraction logic including deposit address resolution (fetchInboundAddress), DepositExtractionContext management, and chain-specific extraction handlers for EVM, Solana, UTXO, and Cosmos with namespace-aware metadata routing.
Public API Type Exports
packages/public-api/src/types.ts
Re-exports transaction data types from @shapeshiftoss/types; restructures ApiQuoteStep to use TransactionData; removes quote field from QuoteResponse.
Swap Widget Dependencies
packages/swap-widget/package.json, packages/swap-widget/vite.config.ts
Switches @shapeshiftoss/caip and @shapeshiftoss/utils to workspace resolution; adds @shapeshiftoss/types workspace dependency; excludes caip and utils from Vite optimizeDeps.
Swap Widget Type Definitions
packages/swap-widget/src/types/index.ts
Re-exports transaction data types from @shapeshiftoss/types; replaces QuoteResponse with flattened structure containing steps array of ApiQuoteStep; removes legacy QuoteStep metadata fields.
Swap Widget Transaction Handling
packages/swap-widget/src/components/SwapWidget.tsx
Refactors transaction data extraction to use single source (quoteResponse.steps?.[0]?.transactionData); adds explicit type validation per chain (evm, solana, utxo); updates Solana flow to support VersionedTransaction with address lookup tables; adds strict type guards before proceeding with chain-specific logic.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

high risk

Suggested reviewers

  • NeOMakinG

Poem

🐰 The rabbit hops through chains so bright,
EVM, Solana, UTXO in sight,
With schemas stacked and types aligned,
A multi-chain dance, perfectly designed!
From deposit to transaction, all in place,
We've built a swap with style and grace!

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main objective: normalizing transaction data format with a discriminated union pattern across multiple chains.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into develop

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/normalize-api-data

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

0xApotheosis and others added 8 commits February 16, 2026 17:03
…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.
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>
@0xApotheosis 0xApotheosis force-pushed the feat/normalize-api-data branch from 9ba4996 to 827b9ac Compare February 16, 2026 06:27
0xApotheosis and others added 4 commits February 16, 2026 17:59
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>
@0xApotheosis 0xApotheosis marked this pull request as ready for review February 16, 2026 07:32
@0xApotheosis 0xApotheosis requested a review from a team as a code owner February 16, 2026 07:32
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

QuoteResponseSchema is missing approval and networkFeeCryptoBaseUnit fields.

The QuoteResponse type in packages/public-api/src/types.ts (lines 108-123) includes approval: ApprovalInfo and networkFeeCryptoBaseUnit: 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 using CHAIN_NAMESPACE constants instead of string literals.

The chain namespace comparisons use raw strings ('eip155', 'solana', 'bip122', 'cosmos'). While TypeScript will catch type-level mismatches, importing and using CHAIN_NAMESPACE from @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 buildApprovalInfo at line 284 and resolveDepositContext at line 330.

Comment thread packages/public-api/src/routes/quote.ts
Copy link
Copy Markdown
Collaborator

@NeOMakinG NeOMakinG left a comment

Choose a reason for hiding this comment

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

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
}

@NeOMakinG NeOMakinG enabled auto-merge (squash) February 16, 2026 10:56
@NeOMakinG NeOMakinG merged commit bcfa112 into develop Feb 16, 2026
4 checks passed
@NeOMakinG NeOMakinG deleted the feat/normalize-api-data branch February 16, 2026 11:09
@coderabbitai coderabbitai Bot mentioned this pull request Feb 16, 2026
1 task
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