Skip to main content

Overview

Swap mode provides a flexible interface for cross-chain token exchanges. Users control both input and output selections, with the widget handling optimal routing across bridges and DEXs. Trade Types: Supports both EXACT_INPUT and EXACT_OUTPUT trade types.

Configuration

Required Props

PropTypeDescription
mode"swap"Sets the widget to swap mode

Optional Props

PropTypeDescription
fromChainIdnumberPre-select source chain
fromTokenstringPre-select source token
toChainIdnumberPre-select destination chain
toTokenstringPre-select destination token
slippageTolerancestring | numberSlippage tolerance (default: 0.5%)
quoteProviderQuoteProviderQuote provider selection

Widget Implementation

Basic Swap Widget

Minimal configuration - users select everything:
import { TrailsWidget } from '0xtrails/widget'

<TrailsWidget
  apiKey="YOUR_API_KEY"
  mode="swap"
  onCheckoutComplete={({ sessionId }) => {
    console.log('Swap completed:', sessionId)
  }}
>
  <button>Swap Tokens</button>
</TrailsWidget>

Pre-configured Swap

Pre-select chains and tokens:
<TrailsWidget
  apiKey="YOUR_API_KEY"
  mode="swap"
  fromChainId={1} // Ethereum
  fromToken="USDC"
  toChainId={8453} // Base
  toToken="ETH"
  slippageTolerance="0.01" // 1%
  onCheckoutComplete={({ sessionId }) => {
    console.log('Swap completed:', sessionId)
  }}
>
  <button>Swap USDC to ETH</button>
</TrailsWidget>

Custom Quote Provider

Specify routing preference:
<TrailsWidget
  apiKey="YOUR_API_KEY"
  mode="swap"
  quoteProvider="cctp" // Use Circle's CCTP for USDC
  onCheckoutComplete={({ sessionId }) => {
    console.log('CCTP swap completed:', sessionId)
  }}
>
  <button>Swap via CCTP</button>
</TrailsWidget>

Headless Implementation with useQuote

For custom UI implementations, use the useQuote hook directly:

Basic useQuote Implementation

import { useQuote, TradeType } from '0xtrails'
import { useWalletClient, useAccount } from 'wagmi'
import { useState, useEffect } from 'react'

export const CustomSwap = () => {
  const { data: walletClient } = useWalletClient()
  const { address } = useAccount()
  
  const { quote, swap, isLoadingQuote, quoteError, refetchQuote } = useQuote({
    walletClient,
    fromTokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC on Ethereum
    fromChainId: 1,
    toTokenAddress: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', // USDC on Base
    toChainId: 8453,
    swapAmount: '1000000', // 1 USDC (6 decimals)
    tradeType: TradeType.EXACT_INPUT,
    toRecipient: address,
    slippageTolerance: '0.005', // 0.5%
    onStatusUpdate: (states) => {
      console.log('Transaction status:', states)
    },
  })
  
  // Refresh quotes every 30 seconds
  useEffect(() => {
    const interval = setInterval(() => {
      refetchQuote?.()
    }, 30000)
    return () => clearInterval(interval)
  }, [refetchQuote])
  
  const handleSwap = async () => {
    if (!swap) return
    
    try {
      const result = await swap()
      console.log('Swap result:', result)
    } catch (error) {
      console.error('Swap failed:', error)
    }
  }
  
  if (isLoadingQuote) return <div>Loading quote...</div>
  if (quoteError) return <div>Error: {String(quoteError)}</div>
  if (!quote) return <div>No quote available</div>
  
  return (
    <div>
      <h3>Quote</h3>
      <p>From: {quote.originAmountFormatted} {quote.originToken.symbol}</p>
      <p>To: {quote.destinationAmountFormatted} {quote.destinationToken.symbol}</p>
      <p>Fee: {quote.totalFeeAmountUsdDisplay}</p>
      <p>Rate: 1 {quote.originToken.symbol} = {quote.destinationTokenRate} {quote.destinationToken.symbol}</p>
      <p>Est. Time: {quote.completionEstimateSeconds}s</p>
      <button onClick={handleSwap}>Execute Swap</button>
    </div>
  )
}

Trade Types

EXACT_INPUT

User specifies exact input amount; output amount varies:
const { quote } = useQuote({
  // ... other props
  swapAmount: '100000000', // 100 USDC exactly
  tradeType: TradeType.EXACT_INPUT,
})
// User sends exactly 100 USDC, receives ~0.039 ETH (varies with price)

EXACT_OUTPUT

User specifies exact output amount; input amount varies:
const { quote } = useQuote({
  // ... other props
  swapAmount: '100000000000000000', // 0.1 ETH exactly
  tradeType: TradeType.EXACT_OUTPUT,
})
// User receives exactly 0.1 ETH, sends ~256 USDC (varies with price)

Quote Types

type UseQuoteProps = {
  walletClient?: WalletClient
  fromTokenAddress?: string | null
  fromChainId?: number | null
  toTokenAddress?: string | null
  toChainId?: number | null
  swapAmount?: string | bigint
  toRecipient?: string | null
  tradeType?: TradeType | null
  slippageTolerance?: string | number | null
  onStatusUpdate?: (txs: TransactionState[]) => void | null
}

type UseQuoteReturn = {
  quote: Quote | null
  swap: (() => Promise<SwapReturn | null>) | null
  isLoadingQuote: boolean
  quoteError: unknown
  refetchQuote: (() => Promise<void>) | null
}

Quote Refresh Strategy

Quotes can become stale. Implement refresh logic:
// Refresh every 30 seconds
useEffect(() => {
  const interval = setInterval(() => {
    refetchQuote?.()
  }, 30000)
  return () => clearInterval(interval)
}, [refetchQuote])

// Refresh on window focus
useEffect(() => {
  const handleFocus = () => refetchQuote?.()
  window.addEventListener('focus', handleFocus)
  return () => window.removeEventListener('focus', handleFocus)
}, [refetchQuote])

Event Handling

Widget Events

<TrailsWidget
  apiKey="YOUR_API_KEY"
  mode="swap"
  onCheckoutStart={({ sessionId }) => {
    console.log('Swap started:', sessionId)
  }}
  onCheckoutComplete={({ sessionId }) => {
    console.log('Swap completed:', sessionId)
  }}
  onCheckoutError={({ sessionId, error }) => {
    console.error('Swap failed:', error)
  }}
/>

useQuote Transaction Status

const { quote, swap } = useQuote({
  // ... other props
  onStatusUpdate: (states) => {
    states.forEach(state => {
      console.log(`${state.chainId}: ${state.status}`)
      // States: 'pending', 'executing', 'completed', 'failed'
    })
  },
})

Error Handling

import { getIsUserRejectionError, InsufficientBalanceError } from '0xtrails'

const handleSwap = async () => {
  try {
    await swap()
  } catch (error) {
    if (getIsUserRejectionError(error)) {
      console.log('User rejected transaction')
    } else if (error instanceof InsufficientBalanceError) {
      console.error('Insufficient balance')
    } else {
      console.error('Swap failed:', error)
    }
  }
}

Use Cases

  • Cross-chain Token Exchange: Swap any token to any other token across chains
  • Portfolio Rebalancing: Shift holdings between chains and tokens
  • Yield Optimization: Move assets to chains with better yields
  • Gas Token Acquisition: Get native tokens for new chains
  • Arbitrage: Take advantage of price differences across chains

Technical Notes

  • Quotes automatically factor in gas costs and fees
  • Multiple liquidity sources are queried for optimal routing
  • Supports both same-chain and cross-chain swaps
  • Native token wrapping/unwrapping is handled automatically
  • Slippage protection is built-in

See Also