> ## Documentation Index
> Fetch the complete documentation index at: https://docs.onebalance.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Chain-Abstracted Swap

> Learn how a cross-chain token swap works with OneBalance and Privy, eliminating manual bridging, gas fees, and network switching.

Now that we've set up our project with Privy and OneBalance, let's create a user interface that allows users to perform chain-abstracted swaps with just a few clicks. This is where the "wow" factor comes in - users can swap tokens across chains without even knowing they're dealing with multiple networks, gas fees, or bridges.

<Note>
  **Simplified UX**: By combining Privy's seamless wallet experience with OneBalance's chain
  abstraction, users don't need to worry about networks, gas, or bridging.
</Note>

## Type Definitions

First, let's create type definitions for our quotes and operations. Create a new file at `src/lib/types/quote.ts`:

```typescript quote.ts theme={null}
// src/lib/types/quote.ts
export interface Account {
  sessionAddress: string;
  adminAddress: string;
  accountAddress: string;
}

export interface TokenInfo {
  aggregatedAssetId: string;
  amount: string;
  assetType: string | string[];
  fiatValue: never;
}

export interface ChainOperation {
  userOp: {
    sender: string;
    nonce: string;
    callData: string;
    callGasLimit: string;
    verificationGasLimit: string;
    preVerificationGas: string;
    maxFeePerGas: string;
    maxPriorityFeePerGas: string;
    paymaster: string;
    paymasterVerificationGasLimit: string;
    paymasterPostOpGasLimit: string;
    paymasterData: string;
    signature: string;
  };
  typedDataToSign: {
    domain: unknown;
    types: unknown;
    primaryType: string;
    message: unknown;
  };
  assetType: string;
  amount: string;
}

export interface Quote {
  id: string;
  account: Account;
  originToken: TokenInfo;
  destinationToken: TokenInfo;
  expirationTimestamp: string;
  tamperProofSignature: string;
  originChainsOperations: ChainOperation[];
  destinationChainOperation?: ChainOperation;
}

export interface QuoteStatus {
  quoteId: string;
  status: {
    status: 'PENDING' | 'COMPLETED' | 'FAILED' | 'IN_PROGRESS' | 'REFUNDED';
  };
  user: string;
  recipientAccountId: string;
  originChainOperations: {
    hash: string;
    chainId: number;
    explorerUrl: string;
  }[];
  destinationChainOperations: {
    hash: string;
    chainId: number;
    explorerUrl: string;
  }[];
}
```

## Setting Up the Signing Utilities

Now, let's set up some helper functions for signing operations with the Privy wallet. Create a new file at `src/lib/privySigningUtils.ts`:

```typescript privySigningUtils.ts theme={null}
// src/lib/privySigningUtils.ts
import { Address, createWalletClient, custom, Hash } from 'viem';
import { ConnectedWallet } from '@privy-io/react-auth';
import { ChainOperation, Quote } from '@/lib/types/quote';

// Create a function to sign typed data with Privy wallet
export const signTypedDataWithPrivy =
  (embeddedWallet: ConnectedWallet) =>
  async (typedData: any): Promise<Hash> => {
    const provider = await embeddedWallet.getEthereumProvider();
    const walletClient = createWalletClient({
      transport: custom(provider),
      account: embeddedWallet.address as Address,
    });

    return walletClient.signTypedData(typedData);
  };

// Create a function to sign a chain operation
export const signOperation =
  (embeddedWallet: ConnectedWallet) =>
  (operation: ChainOperation): (() => Promise<ChainOperation>) =>
  async () => {
    const signature = await signTypedDataWithPrivy(embeddedWallet)(operation.typedDataToSign);

    return {
      ...operation,
      userOp: { ...operation.userOp, signature },
    };
  };

// Create a function to sign the entire quote
export const signQuote = async (quote: Quote, embeddedWallet: ConnectedWallet) => {
  const signWithEmbeddedWallet = signOperation(embeddedWallet);

  const signedQuote = {
    ...quote,
  };

  // Sign all operations in sequence
  if (quote.originChainsOperations) {
    signedQuote.originChainsOperations = await sequentialPromises(
      quote.originChainsOperations.map(signWithEmbeddedWallet)
    );
  }

  if (quote.destinationChainOperation) {
    signedQuote.destinationChainOperation = await signWithEmbeddedWallet(
      quote.destinationChainOperation
    )();
  }

  return signedQuote;
};

// Helper to run an array of lazy promises in sequence
export const sequentialPromises = (promises: (() => Promise<any>)[]): Promise<any[]> => {
  return promises.reduce<Promise<any[]>>(
    (acc, curr) => acc.then(results => curr().then(result => [...results, result])),
    Promise.resolve([])
  );
};
```

When a user initiates a swap, they'll be prompted to sign the transaction with their Privy wallet:

![Privy Signing Message](https://storage.googleapis.com/onebalance-public-assets/docs/getting-started/sign-message.png)

## Using OneBalance API Functions

For our chain-abstracted swap interface, we'll use the OneBalance API functions we previously implemented in the [setup section](/getting-started/setup#prerequisites):

<Tip>
  This implementation uses three key endpoints:

  * [Get Quote](/api-reference/quotes/get-quote) - Get a quote for cross-chain swaps or transfers
  * [ExecuteQuote](/api-reference/quotes/execute-quote) - Submit a signed quote for execution
  * [Check Status](/api-reference/status/get-quote-status) - Track the transaction status
</Tip>

## Creating the Dashboard Page

Now, let's create a dashboard that allows users to swap USDC for ETH (and vice versa) with built-in transaction status polling:

```tsx dashboard.tsx theme={null}
// src/app/dashboard/page.tsx
'use client';

import { useEffect, useState, useRef, ChangeEvent } from 'react';
import { usePrivy, useWallets } from '@privy-io/react-auth';
import { getQuote, executeQuote, checkTransactionStatus, predictAccountAddress, getAggregatedBalance } from '@/lib/onebalance';
import { Quote } from '@/lib/types/quote';
import { signQuote } from '@/lib/privySigningUtils';
import { formatUnits } from 'viem';
import { useRouter } from 'next/navigation';

export default function Dashboard() {
  const router = useRouter();
  const { user, ready, authenticated, logout } = usePrivy();
  const { wallets } = useWallets();
  const [loading, setLoading] = useState(false);
  const [swapping, setSwapping] = useState(false);
  const [status, setStatus] = useState<any>(null);
  const [accountAddress, setAccountAddress] = useState<string | null>(null);
  const [usdcBalance, setUsdcBalance] = useState<string | null>(null);
  const [ethBalance, setEthBalance] = useState<string | null>(null);
  const [usdcChainBalances, setUsdcChainBalances] = useState<Array<{chainId: string, balance: string, numericBalance: number, assetType: string}>>([]);
  const [ethChainBalances, setEthChainBalances] = useState<Array<{chainId: string, balance: string, numericBalance: number, assetType: string}>>([]);
  const [quote, setQuote] = useState<Quote | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState<boolean>(false);
  const statusPollingRef = useRef<NodeJS.Timeout | null>(null);
  const [isPolling, setIsPolling] = useState(false);
  const [swapAmount, setSwapAmount] = useState('5.00');
  const [estimatedAmount, setEstimatedAmount] = useState<string | null>(null);
  const [fetchingQuote, setFetchingQuote] = useState(false);
  const [swapDirection, setSwapDirection] = useState<'USDC_TO_ETH' | 'ETH_TO_USDC'>('USDC_TO_ETH');
  const [showUsdcChainDetails, setShowUsdcChainDetails] = useState(false);
  const [showEthChainDetails, setShowEthChainDetails] = useState(false);

  // Helper function to get chain name from chain ID
  const getChainName = (chainId: string): string => {
    const chainMap: Record<string, string> = {
      '1': 'Ethereum',
      '137': 'Polygon',
      '42161': 'Arbitrum',
      '10': 'Optimism',
      '8453': 'Base',
      '59144': 'Linea',
      '43114': 'Avalanche'
    };
    
    return chainMap[chainId] || `Chain ${chainId}`;
  };

  const embeddedWallet = wallets.find(wallet => wallet.walletClientType === 'privy');

  // Handle logout and redirect to home page
  const handleLogout = async () => {
    await logout();
    router.push('/');
  };

  // Get OneBalance account address based on Privy wallet
  useEffect(() => {
    async function setupAccount() {
      if (embeddedWallet && embeddedWallet.address) {
        try {
          // For this demo, we'll use the same address as both session and admin
          const predictedAddress = await predictAccountAddress(
            embeddedWallet.address,
            embeddedWallet.address
          );
          setAccountAddress(predictedAddress);
          
          // Get aggregated balance for USDC and ETH
          fetchBalances(predictedAddress);
        } catch (err) {
          console.error('Error setting up account:', err);
          setError('Failed to set up OneBalance account');
        }
      }
    }

    if (ready && authenticated) {
      setupAccount();
    }

    // Clean up polling interval on unmount
    return () => {
      if (statusPollingRef.current) {
        clearInterval(statusPollingRef.current);
      }
    };
  }, [embeddedWallet, ready, authenticated]);

  // Handle swap direction toggle
  const toggleSwapDirection = () => {
    // Reset current quote and estimated amount
    setQuote(null);
    setEstimatedAmount(null);
    
    // Toggle direction
    setSwapDirection(prevDirection => 
      prevDirection === 'USDC_TO_ETH' ? 'ETH_TO_USDC' : 'USDC_TO_ETH'
    );
    
    // Reset swap amount to a sensible default based on direction
    if (swapDirection === 'USDC_TO_ETH') {
      // Switching to ETH -> USDC, set a default ETH amount (0.001 ETH)
      setSwapAmount('0.001');
    } else {
      // Switching to USDC -> ETH, set a default USDC amount (5 USDC)
      setSwapAmount('5.00');
    }
    
    // Fetch a new quote after a brief delay to ensure state is updated
    setTimeout(() => {
      if (accountAddress && embeddedWallet) {
        fetchEstimatedQuote(swapDirection === 'USDC_TO_ETH' ? '0.001' : '5.00');
      }
    }, 300);
  };

  // Handle swap amount change
  const handleSwapAmountChange = async (e: ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    if (isNaN(Number(value)) || Number(value) <= 0) {
      return;
    }
    
    setSwapAmount(value);
    
    // Reset estimated amount when input changes
    setEstimatedAmount(null);
    
    // If we have a valid amount and an account, fetch a new quote
    if (Number(value) > 0 && accountAddress && embeddedWallet) {
      fetchEstimatedQuote(value);
    }
  };

  // Fetch a quote for estimation purposes
  const fetchEstimatedQuote = async (amountStr: string) => {
    if (!accountAddress || !embeddedWallet) return;
    
    try {
      setFetchingQuote(true);
      
      // Convert to smallest unit based on direction
      // USDC has 6 decimals, ETH has 18 decimals
      const amount = swapDirection === 'USDC_TO_ETH'
        ? (parseFloat(amountStr) * 1_000_000).toString() // USDC -> ETH (6 decimals)
        : (parseFloat(amountStr) * 1e18).toString();     // ETH -> USDC (18 decimals)
      
      const quoteRequest = {
        from: {
          account: {
            sessionAddress: embeddedWallet.address,
            adminAddress: embeddedWallet.address,
            accountAddress: accountAddress,
          },
          asset: {
            assetId: swapDirection === 'USDC_TO_ETH' ? 'ob:usdc' : 'ob:eth',
          },
          amount,
        },
        to: {
          asset: {
            assetId: swapDirection === 'USDC_TO_ETH' ? 'ob:eth' : 'ob:usdc',
          },
        },
      };

      const quoteResponse = await getQuote(quoteRequest);
      setQuote(quoteResponse);
      
      // Extract estimated amount from the quote
      if (quoteResponse.destinationToken && quoteResponse.destinationToken.amount) {
        // Format based on direction
        if (swapDirection === 'USDC_TO_ETH') {
          // USDC -> ETH (ETH has 18 decimals)
          const ethAmount = parseFloat(formatUnits(BigInt(quoteResponse.destinationToken.amount), 18)).toFixed(6);
          setEstimatedAmount(ethAmount);
        } else {
          // ETH -> USDC (USDC has 6 decimals)
          const usdcAmount = parseFloat(formatUnits(BigInt(quoteResponse.destinationToken.amount), 6)).toFixed(2);
          setEstimatedAmount(usdcAmount);
        }
      }
    } catch (err) {
      console.error('Error fetching quote for estimation:', err);
      setEstimatedAmount(null);
    } finally {
      setFetchingQuote(false);
    }
  };

  // Fetch USDC and ETH balances
  const fetchBalances = async (address: string) => {
    try {
      const balanceData = await getAggregatedBalance(address);
      
      // Find USDC in the balance data
      const usdcAsset = balanceData.balanceByAggregatedAsset.find(
        (asset: any) => asset.aggregatedAssetId === 'ob:usdc'
      );
      
      // Find ETH in the balance data
      const ethAsset = balanceData.balanceByAggregatedAsset.find(
        (asset: any) => asset.aggregatedAssetId === 'ob:eth'
      );
      
      if (usdcAsset) {
        // Format the balance (USDC has 6 decimals)
        const formattedBalance = parseFloat(formatUnits(BigInt(usdcAsset.balance), 6)).toFixed(2);
        setUsdcBalance(formattedBalance);

        // Extract individual chain balances for USDC
        if (usdcAsset.individualAssetBalances && usdcAsset.individualAssetBalances.length > 0) {
          const chainBalances = usdcAsset.individualAssetBalances
            .map((chainBalance: any) => {
              // Extract chain ID from assetType (format: eip155:CHAIN_ID/...)
              const chainId = chainBalance.assetType.split(':')[1].split('/')[0];
              const formattedBalance = parseFloat(formatUnits(BigInt(chainBalance.balance), 6)).toFixed(2);
              return {
                chainId,
                balance: formattedBalance,
                numericBalance: parseFloat(formattedBalance), // For sorting
                assetType: chainBalance.assetType
              };
            })
            // Filter out zero balances
            .filter((chainBalance: {numericBalance: number}) => chainBalance.numericBalance > 0)
            // Sort by balance in descending order
            .sort((a: {numericBalance: number}, b: {numericBalance: number}) => b.numericBalance - a.numericBalance);
            
          setUsdcChainBalances(chainBalances);
        }
      } else {
        setUsdcBalance('0.00');
        setUsdcChainBalances([]);
      }
      
      if (ethAsset) {
        // Format the balance (ETH has 18 decimals)
        const formattedBalance = parseFloat(formatUnits(BigInt(ethAsset.balance), 18)).toFixed(6);
        setEthBalance(formattedBalance);

        // Extract individual chain balances for ETH
        if (ethAsset.individualAssetBalances && ethAsset.individualAssetBalances.length > 0) {
          const chainBalances = ethAsset.individualAssetBalances
            .map((chainBalance: any) => {
              // Extract chain ID from assetType (format: eip155:CHAIN_ID/...)
              const chainId = chainBalance.assetType.split(':')[1].split('/')[0];
              const formattedBalance = parseFloat(formatUnits(BigInt(chainBalance.balance), 18)).toFixed(6);
              return {
                chainId,
                balance: formattedBalance,
                numericBalance: parseFloat(formattedBalance), // For sorting
                assetType: chainBalance.assetType
              };
            })
            // Filter out zero balances
            .filter((chainBalance: {numericBalance: number}) => chainBalance.numericBalance > 0)
            // Sort by balance in descending order
            .sort((a: {numericBalance: number}, b: {numericBalance: number}) => b.numericBalance - a.numericBalance);
            
          setEthChainBalances(chainBalances);
        }
      } else {
        setEthBalance('0.000000');
        setEthChainBalances([]);
      }
      
      // After getting balances, fetch an initial quote estimate using the default amount
      if (address && embeddedWallet) {
        fetchEstimatedQuote(swapAmount);
      }
    } catch (err) {
      console.error('Error fetching balances:', err);
      setUsdcBalance('0.00');
      setEthBalance('0.000000');
      setUsdcChainBalances([]);
      setEthChainBalances([]);
    }
  };

  // Poll for transaction status
  const startStatusPolling = (quoteId: string) => {
    if (statusPollingRef.current) {
      clearInterval(statusPollingRef.current);
    }
    
    setIsPolling(true);
    
    statusPollingRef.current = setInterval(async () => {
      try {
        const statusData = await checkTransactionStatus(quoteId);
        setStatus(statusData);
        
        // If the transaction is completed or failed, stop polling
        if (statusData.status === 'COMPLETED' || statusData.status === 'FAILED') {
          if (statusPollingRef.current) {
            clearInterval(statusPollingRef.current);
            setIsPolling(false);
          }
          
          // Refresh balances after transaction is completed
          if (accountAddress && statusData.status === 'COMPLETED') {
            fetchBalances(accountAddress);
          }
        }
      } catch (err) {
        console.error('Error polling transaction status:', err);
        if (statusPollingRef.current) {
          clearInterval(statusPollingRef.current);
          setIsPolling(false);
        }
      }
    }, 1000); // Poll every 1 second
  };

  // Request and execute a chain-abstracted swap
  const handleSwap = async () => {
    if (!accountAddress || !embeddedWallet) {
      setError('Wallet not connected or OneBalance account not set up');
      return;
    }

    setLoading(true);
    setSwapping(true);
    setError(null);
    setSuccess(false);

    try {
      // Use already fetched quote if available
      let quoteResponse = quote;
      
      // If no quote available or it's stale, fetch a new one
      if (!quoteResponse) {
        // Convert to smallest unit based on direction
        const amount = swapDirection === 'USDC_TO_ETH'
          ? (parseFloat(swapAmount) * 1_000_000).toString() // USDC -> ETH (6 decimals)
          : (parseFloat(swapAmount) * 1e18).toString();     // ETH -> USDC (18 decimals)
        
        const quoteRequest = {
          from: {
            account: {
              sessionAddress: embeddedWallet.address,
              adminAddress: embeddedWallet.address,
              accountAddress: accountAddress,
            },
            asset: {
              assetId: swapDirection === 'USDC_TO_ETH' ? 'ob:usdc' : 'ob:eth',
            },
            amount,
          },
          to: {
            asset: {
              assetId: swapDirection === 'USDC_TO_ETH' ? 'ob:eth' : 'ob:usdc',
            },
          },
        };

        quoteResponse = await getQuote(quoteRequest);
        setQuote(quoteResponse);
      }
      
      if (!quoteResponse) {
        throw new Error('Failed to get a quote for the swap');
      }
      
      // Step 2: Sign the quote
      const signedQuote = await signQuote(quoteResponse, embeddedWallet);

      // Step 3: Execute the quote
      setLoading(true);
      const executionResponse = await executeQuote(signedQuote);

      // Step 4: Start polling for transaction status
      startStatusPolling(quoteResponse.id);
      setSuccess(true);
      setLoading(false);
    } catch (err: any) {
      console.error('Error in swap process:', err);
      setError(err.message || 'Failed to complete swap');
      setLoading(false);
    } finally {
      setSwapping(false);
    }
  };

  if (!ready || !authenticated) {
    return (
      <div className="flex min-h-screen items-center justify-center">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#FFAB40]"></div>
      </div>
    );
  }

  return (
    <main className="flex min-h-screen flex-col items-center text-black p-4 md:p-24">
      <div className="max-w-lg w-full bg-white p-8 rounded-xl shadow-md">
        <div className="flex justify-between items-center mb-6">
          <h1 className="text-2xl font-bold">OneBalance Dashboard</h1>
          <button
            onClick={handleLogout}
            className="text-sm text-gray-500 hover:text-gray-700"
          >
            Logout
          </button>
        </div>

        <div className="mb-6">
          <div className="text-sm text-gray-500 mb-1">Connected as</div>
          <div className="font-medium truncate">
            {user?.email?.address || embeddedWallet?.address || 'Anonymous'}
          </div>
        </div>

        {/* Account Info Section */}
        <div className="bg-gray-50 p-6 rounded-lg mb-6">
          <h2 className="text-xl font-semibold mb-4">Account Information</h2>
          
          <div className="space-y-3">
            <div>
              <div className="text-gray-500 text-sm mb-1">OneBalance Smart Account</div>
              <div className="font-semibold text-sm break-all">
                {accountAddress || <div className="animate-pulse bg-gray-200 h-4 w-12 rounded"></div>}
              </div>
            </div>
            
            <div className="flex flex-row space-x-4">
              <div className="flex-1">
                <div className="text-gray-500 text-sm mb-1">USDC Balance</div>
                <div className="font-medium text-xl mb-1">
                  {usdcBalance ? `${usdcBalance} USDC` : <div className="animate-pulse bg-gray-200 h-4 w-12 rounded"></div>}
                </div>
                <div className="text-xs text-gray-500 flex items-center cursor-pointer" 
                     onClick={() => setShowUsdcChainDetails(!showUsdcChainDetails)}>
                  {showUsdcChainDetails ? "Hide chain details" : "Show chain details"} 
                  <svg xmlns="http://www.w3.org/2000/svg" className={`h-4 w-4 ml-1 transition-transform ${showUsdcChainDetails ? "rotate-180" : ""}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
                  </svg>
                </div>
                {showUsdcChainDetails && (
                  <div className="mt-2 space-y-1 text-xs">
                    {usdcChainBalances.length > 0 ? (
                      usdcChainBalances.map((chainBalance, idx) => (
                        <div key={`usdc-${idx}`} className="flex justify-between border-b border-gray-100 pb-1">
                          <span>{getChainName(chainBalance.chainId)}</span>
                          <span>{chainBalance.balance} USDC</span>
                        </div>
                      ))
                    ) : (
                      <div className="text-gray-400">No chain data available</div>
                    )}
                  </div>
                )}
              </div>
              
              <div className="flex-1">
                <div className="text-gray-500 text-sm mb-1">ETH Balance</div>
                <div className="font-medium text-xl mb-1">
                  {ethBalance ? `${ethBalance} ETH` : <div className="animate-pulse bg-gray-200 h-4 w-12 rounded"></div>}
                </div>
                <div className="text-xs text-gray-500 flex items-center cursor-pointer"
                     onClick={() => setShowEthChainDetails(!showEthChainDetails)}>
                  {showEthChainDetails ? "Hide chain details" : "Show chain details"}
                  <svg xmlns="http://www.w3.org/2000/svg" className={`h-4 w-4 ml-1 transition-transform ${showEthChainDetails ? "rotate-180" : ""}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
                  </svg>
                </div>
                {showEthChainDetails && (
                  <div className="mt-2 space-y-1 text-xs">
                    {ethChainBalances.length > 0 ? (
                      ethChainBalances.map((chainBalance, idx) => (
                        <div key={`eth-${idx}`} className="flex justify-between border-b border-gray-100 pb-1">
                          <span>{getChainName(chainBalance.chainId)}</span>
                          <span>{chainBalance.balance} ETH</span>
                        </div>
                      ))
                    ) : (
                      <div className="text-gray-400">No chain data available</div>
                    )}
                  </div>
                )}
              </div>
            </div>
          </div>
        </div>

        <div className="bg-gray-50 p-6 rounded-lg mb-6">
          <h2 className="text-xl font-semibold mb-4">Chain-Abstracted Swap</h2>
          <div className="flex items-center justify-between mb-4">
            <div className="text-center p-3 bg-white rounded-md shadow-sm w-5/12">
              <div className="text-gray-500 text-sm mb-1">From</div>
              <div className="font-medium">
                <input 
                  type="text" 
                  value={swapAmount}
                  onChange={handleSwapAmountChange}
                  className="w-20 text-center border-b border-gray-300 focus:outline-none focus:border-[#FFAB40]"
                /> {swapDirection === 'USDC_TO_ETH' ? 'USDC' : 'ETH'}
              </div>
              <div className="text-xs text-gray-400">on any chain</div>
            </div>

            <div 
              className="text-[#FFAB40] cursor-pointer hover:text-[#FF9800] hover:scale-125 transition-all duration-200"
              onClick={toggleSwapDirection}
              title="Reverse swap direction"
            >
              ↔
            </div>

            <div className="text-center p-3 bg-white rounded-md shadow-sm w-5/12">
              <div className="text-gray-500 text-sm mb-1">To</div>
              <div className="font-medium">
                {fetchingQuote ? (
                  <div className="inline-block w-12 h-4">
                    <div className="animate-pulse bg-gray-200 h-4 w-12 rounded"></div>
                  </div>
                ) : estimatedAmount ? (
                  `${estimatedAmount} ${swapDirection === 'USDC_TO_ETH' ? 'ETH' : 'USDC'}`
                ) : (
                  swapDirection === 'USDC_TO_ETH' ? 'ETH' : 'USDC'
                )}
              </div>
              <div className="text-xs text-gray-400">on any chain</div>
            </div>
          </div>

          <button
            onClick={handleSwap}
            disabled={loading || !accountAddress || parseFloat(swapAmount) <= 0}
            className={`w-full py-3 px-4 rounded-lg font-medium transition-all ${
              loading || parseFloat(swapAmount) <= 0
                ? 'bg-gray-300 cursor-not-allowed'
                : 'bg-[#FFAB40] text-white hover:bg-[#FF9800]'
            }`}
          >
            {loading ? (
              <div className="flex items-center justify-center">
                <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
                {swapping ? 'Swapping...' : 'Processing...'}
              </div>
            ) : (
              'Swap Now'
            )}
          </button>

          <div className="mt-3 text-xs text-gray-500 text-center">
            No gas tokens needed - pay fees with your {swapDirection === 'USDC_TO_ETH' ? 'USDC' : 'ETH'} balance!
          </div>
        </div>

        {error && (
          <div className="mb-6 p-4 bg-red-50 text-red-700 rounded-lg">
            <p className="font-medium">Error</p>
            <p className="text-sm">{error}</p>
          </div>
        )}

        {success && (
          <div className="mb-6 p-4 bg-green-50 text-green-700 rounded-lg">
            <p className="font-medium">Success!</p>
            <p className="text-sm">
              Your chain-abstracted swap has been initiated.
              {isPolling && ' Monitoring transaction status...'}
            </p>
          </div>
        )}

        {status && (
          <div className="bg-gray-50 p-6 rounded-lg">
            <h3 className="text-lg font-semibold mb-3">Transaction Status</h3>
            <div className="text-sm space-y-2">
              <div className="flex justify-between">
                <span className="text-gray-500">Status:</span>
                <span className={`font-medium ${
                  status.status === 'COMPLETED' ? 'text-green-600' : 
                  status.status === 'FAILED' ? 'text-red-600' : 'text-yellow-600'
                }`}>
                  {status.status || 'Pending'}
                </span>
              </div>
              {status.originChainOperations?.length > 0 && (
                <div className="flex justify-between">
                  <span className="text-gray-500">Origin Chain:</span>
                  <a
                    href={status.originChainOperations[0].explorerUrl}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="text-blue-500 hover:underline truncate ml-2 max-w-[200px]"
                  >
                    View Transaction
                  </a>
                </div>
              )}
              {status.destinationChainOperations?.length > 0 && (
                <div className="flex justify-between">
                  <span className="text-gray-500">Destination Chain:</span>
                  <a
                    href={status.destinationChainOperations[0].explorerUrl}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="text-blue-500 hover:underline truncate ml-2 max-w-[200px]"
                  >
                    View Transaction
                  </a>
                </div>
              )}
            </div>
          </div>
        )}
      </div>
    </main>
  );
}
```

When the swap is successfully completed, users can see the status of the transaction:

![Successful Swap](https://storage.googleapis.com/onebalance-public-assets/docs/getting-started/successful-swap.png)

## Understanding the Chain-Abstracted Swap Flow

This improved implementation offers several key enhancements over the basic version:

1. **Bidirectional Swapping**

   * Users can swap from USDC → ETH or ETH → USDC
   * The UI dynamically updates based on the selected direction

2. **Real-Time Quote Estimation**

   * When users input an amount, a quote is fetched to show the estimated output
   * This provides transparency about the expected exchange rate

3. **Balance Display**

   * Shows users their current USDC and ETH balances from across all chains
   * Updates balances automatically after a successful swap

4. **Robust Transaction Polling**

   * Implements a proper polling mechanism to track transaction status
   * Handles different transaction states (pending, completed, failed)
   * Automatically stops polling when the transaction completes or fails

5. **Error Handling & Recovery**

   * Provides clear error messages when issues occur
   * Implements clean-up logic to prevent resource leaks (clearing intervals)

6. **Enhanced User Experience**
   * Shows loading indicators during swap operations
   * Provides visual feedback for transaction states
   * Includes links to block explorers to view transactions

## Multichain Asset Visibility

One of the most powerful aspects of this implementation is how it handles multichain asset visibility. When users interact with the application:

* They see their USDC and ETH balances as a **single aggregated total** across all supported chains
* They can select "from" and "to" assets without needing to specify which chain they're on
* The underlying complexity of potentially interacting with multiple blockchains is completely hidden

This creates an abstraction layer where users don't need to think about chains at all - they simply work with tokens as if they existed in a unified space, which dramatically simplifies the user experience.

## Key Technical Implementations

### 1. Transaction Status Polling

```typescript status-polling.ts theme={null}
// Poll for transaction status
const startStatusPolling = (quoteId: string) => {
  if (statusPollingRef.current) {
    clearInterval(statusPollingRef.current);
  }

  setIsPolling(true);

  statusPollingRef.current = setInterval(async () => {
    try {
      const statusData = await checkTransactionStatus(quoteId);
      setStatus(statusData);

      // If the transaction is completed or failed, stop polling
      if (statusData.status.status === 'COMPLETED' || statusData.status.status === 'FAILED') {
        if (statusPollingRef.current) {
          clearInterval(statusPollingRef.current);
          setIsPolling(false);
        }

        // Refresh balances after transaction is completed
        if (accountAddress && statusData.status.status === 'COMPLETED') {
          fetchBalances(accountAddress);
        }
      }
    } catch (err) {
      console.error('Error polling transaction status:', err);
      if (statusPollingRef.current) {
        clearInterval(statusPollingRef.current);
        setIsPolling(false);
      }
    }
  }, 1000); // Poll every 1 second
};
```

<Tip>
  This polling mechanism uses the [Get Execution
  Status](/api-reference/status/get-quote-status) endpoint to track
  transaction progress in real-time.
</Tip>

### 2. Signing with Privy

A critical part of the implementation is securely signing operations using the Privy wallet. The signing process handles:

* Converting the typed data format required by EIP-712
* Managing the signature process using the embedded wallet
* Ensuring all operations in a quote are properly signed
* Handling both origin and destination chain operations

```typescript swap-flow.ts theme={null}
// Example of the signing flow
const handleSwap = async () => {
  // ... other code

  // Get the quote from OneBalance
  const quoteResponse = await getQuote(quoteRequest);

  // Sign the quote using the Privy wallet
  const signedQuote = await signQuote(quoteResponse, embeddedWallet);

  // Execute the signed quote
  await executeQuote(signedQuote);

  // ... other code
};
```

### 3. Balance Checking Across Chains

```typescript balance-checker.ts theme={null}
// Fetch USDC and ETH balances
const fetchBalances = async (address: string) => {
  try {
    const balanceData = await getAggregatedBalance(address);

    // Find USDC in the balance data
    const usdcAsset = balanceData.balanceByAggregatedAsset.find(
      (asset: any) => asset.aggregatedAssetId === 'ob:usdc'
    );

    // Find ETH in the balance data
    const ethAsset = balanceData.balanceByAggregatedAsset.find(
      (asset: any) => asset.aggregatedAssetId === 'ob:eth'
    );

    if (usdcAsset) {
      // Format the balance (USDC has 6 decimals)
      const formattedBalance = parseFloat(formatUnits(BigInt(usdcAsset.balance), 6)).toFixed(2);
      setUsdcBalance(formattedBalance);
    }

    if (ethAsset) {
      // Format the balance (ETH has 18 decimals)
      const formattedBalance = parseFloat(formatUnits(BigInt(ethAsset.balance), 18)).toFixed(6);
      setEthBalance(formattedBalance);
    }
  } catch (err) {
    console.error('Error fetching balances:', err);
  }
};
```

<Tip>
  This function leverages the [Aggregated
  Balance](/api-reference/balances/aggregated-balance) endpoint to
  retrieve all token balances across multiple chains with a single API call.
</Tip>

## What Makes This Implementation Powerful?

1. **Chain Abstraction**

   * Users can swap tokens across chains without even knowing which chains are involved
   * The UI treats USDC and ETH as unified assets regardless of their underlying chains

2. **Robust Error Handling**

   * Handles API errors gracefully
   * Provides clear feedback to users
   * Implements proper cleanup of resources

3. **Real-Time Transaction Tracking**

   * Users can see the status of their transactions as they progress
   * Provides links to block explorers for deeper inspection
   * Automatically refreshes balances when transactions complete

4. **Bidirectional Swapping**

   * Users can swap in either direction (USDC → ETH or ETH → USDC)
   * The UI adapts dynamically to the selected direction

5. **Gas Abstraction**
   * Users can pay transaction fees with the token they're swapping
   * No need to hold native gas tokens on each chain

## Next Steps

Now that you've seen how to create a chain-abstracted swap interface with OneBalance, you can explore the complete working implementation:

<CardGroup cols={2}>
  <Card title="Clone the GitHub Repository" icon="github" href="https://github.com/OneBalance-io/onebalance-privy-demo">
    Get the full working code example to start building immediately
  </Card>

  <Card title="Explore the API Reference" icon="code" href="/api-reference/introduction">
    Discover all available endpoints in our API documentation
  </Card>
</CardGroup>
