> ## 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.

# Building cross-chain swaps with OneBalance and Privy Tutorial

> Learn the key concepts for building a production-ready chain-abstracted token swap interface using OneBalance API and Privy embedded wallets.

export const LoomVideo = ({loomUrl}) => {
  return <div style={{
    position: 'relative',
    paddingBottom: '56.25%',
    height: 0,
    marginBottom: '1.5rem'
  }}>
      <iframe src={loomUrl} frameBorder="0" webkitallowfullscreen="true" mozallowfullscreen="true" allowFullScreen={true} style={{
    position: 'absolute',
    top: 0,
    left: 0,
    width: '100%',
    height: '100%'
  }} />
    </div>;
};

<LoomVideo loomUrl="https://www.loom.com/embed/64c86fb3e01840248734b22b71dd09b2?sid=f80e9904-aaf5-4868-8bb1-030c76d41596" />

This guide covers the essential concepts and patterns for building a swap interface that abstracts away blockchain complexity.

<Tip>
  Complete demo available at [onebalance-chain-abstracted-swap.vercel.app](https://onebalance-chain-abstracted-swap.vercel.app).
</Tip>

<Card title="Open Source Example" icon="github" href="https://github.com/dzimiks/onebalance-chain-abstracted-swap" horizontal>
  All examples in this guide are 100% free and open-source. Clone the repository to quickly get started.
</Card>

## Core Architecture Overview

A successful chain-abstracted swap application requires understanding these key components:

* **Smart Account Prediction**: Determine account addresses before deployment
* **Quote Management**: Real-time quote fetching with expiration handling
* **Transaction Signing**: EIP-712 typed data signing with Privy
* **Status Monitoring**: Real-time transaction status polling
* **Balance Validation**: Ensure sufficient funds before execution

## Prerequisites

Before diving into building the cross-chain swap interface, you'll need to have the following prerequisites in place:

### Development Environment

* **Node.js 20+**: Our project uses modern TypeScript features. Install using [nvm](https://github.com/nvm-sh/nvm) or download from [nodejs.org](https://nodejs.org/).
* **pnpm**: We'll use pnpm as our package manager for its speed and efficiency. Install it by running `npm install -g pnpm` or follow the [official installation guide](https://pnpm.io/installation).

### Technical Knowledge

* **Next.js App Router**: Understanding of Next.js 15's App Router architecture is essential for this project.
* **React Hooks**: Familiarity with React's useState, useEffect, useCallback, and custom hooks.
* **TypeScript**: Basic knowledge of TypeScript types and interfaces.
* **Tailwind CSS & shadcn/ui**: We'll use these for styling and components.

### Privy Setup

To integrate Privy into your codebase, follow [Privy's Quickstart documentation](https://docs.privy.io/guide/react/quickstart).

Once Privy is set up, ensure you have the following:

* **Privy App ID**
* **PrivyProvider** configured on your client
* User login workflow organized and tested on the client side

## Setting Up the Project

We will use [Next.js 15](https://nextjs.org) with app router & TypeScript for this project, along with [Tailwind CSS](https://tailwindcss.com) for styling and [shadcn/ui](https://ui.shadcn.com) for components. Let's initialize the project:

```bash terminal theme={null}
pnpm dlx shadcn@latest init
```

For wallet connection and transaction signing, we will use [Privy](https://www.privy.io):

```bash terminal theme={null}
pnpm add @tanstack/react-query wagmi viem @privy-io/react-auth
```

### Environment Setup

Make sure to copy `.env.example` into `.env`:

```jsx .env theme={null}
NEXT_PUBLIC_API_URL=https://be.onebalance.io
NEXT_PUBLIC_API_KEY=
NEXT_PUBLIC_PRIVY_APP_ID=
```

<Warning>
  Get your OneBalance API key from [www.onebalance.io](https://www.onebalance.io) and Privy App ID from [console.privy.io](https://console.privy.io)
</Warning>

## 1. Smart Account Integration

### Understanding OneBalance Smart Accounts

OneBalance uses Smart Contract Accounts (SCAs) that can be **predicted** before deployment. This enables:

* Receiving funds before the account exists on-chain
* Seamless transaction execution across chains
* Gas sponsorship and batched operations

### Implementation Pattern

```typescript lib/api/account.ts theme={null}
export const accountApi = {
  predictAddress: async (sessionAddress: string, adminAddress: string) => {
    const response = await apiClient.post('/account/predict-address', {
      sessionAddress,
      adminAddress,
    });
    return response.data?.predictedAddress;
  },
};
```

**Key Concept**: Use the same wallet address for both `sessionAddress` and `adminAddress` for simplicity. The predicted address becomes your user's primary account identifier.

## 2. Quote Management & Lifecycle

### Quote Request Structure

OneBalance quotes follow a specific request format that defines the swap parameters:

```typescript theme={null}
interface QuoteRequest {
  from: {
    account: {
      sessionAddress: string;      // Privy wallet address
      adminAddress: string;        // Same as session for simplicity  
      accountAddress: string;      // Predicted smart account
    };
    asset: { assetId: string };    // e.g., "ob:usdc"
    amount: string;                // Token amount in wei
  };
  to: {
    asset: { assetId: string };    // e.g., "ob:eth"
    account?: string;              // Optional: for transfers to other addresses
  };
}
```

### Quote Lifecycle Management

The quote lifecycle involves several critical stages:

1. **Validation**: Check user balance before requesting quotes
2. **Expiration Handling**: Quotes expire in 30 seconds - implement countdown timers
3. **Auto-refresh**: Automatically fetch new quotes when current ones expire
4. **Debouncing**: Prevent excessive API calls during user input

**Implementation Pattern** (from `useQuotes.ts`):

```typescript theme={null}
// Debounced quote fetching
const debouncedGetQuote = useCallback(
  debounce(async (request) => {
    await getQuote(request);
  }, 1000),
  [getQuote]
);

// Balance validation before quote request
const hasSufficientBalance = (amount: string) => {
  if (!sourceBalance || !selectedAsset || !amount) return false;
  const parsedAmount = parseTokenAmount(amount, selectedAsset.decimals);
  return BigInt(sourceBalance.balance) >= BigInt(parsedAmount);
};
```

## 3. Transaction Signing with Privy

### EIP-712 Typed Data Signing

OneBalance transactions require signing structured data (EIP-712) for each chain operation. The signing process handles multiple operations sequentially.

**Key Implementation** (from `privySigningUtils.ts`):

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

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);
  };

export const signOperation =
  (embeddedWallet: ConnectedWallet) =>
  (operation: ChainOperation): (() => Promise<ChainOperation>) =>
  async () => {
    const signature = await signTypedDataWithPrivy(embeddedWallet)(operation.typedDataToSign);

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

export const signQuote = async (quote: Quote, embeddedWallet: ConnectedWallet) => {
  const signWithEmbeddedWallet = signOperation(embeddedWallet);

  const signedQuote = {
    ...quote,
  };

  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([])
  );
};
```

**Critical Points**:

* Each quote contains multiple `ChainOperation` objects that need individual signatures
* Operations must be signed **sequentially** to avoid nonce conflicts
* The `typedDataToSign` field contains the EIP-712 structure for each operation

## 4. Transaction Execution Flow

### Complete Transaction Workflow

1. **Quote Validation**: Check expiration before execution
2. **Signing**: Sign all required operations using Privy
3. **Execution**: Submit signed quote to OneBalance
4. **Monitoring**: Poll transaction status until completion

**Implementation Pattern**:

```typescript theme={null}
const executeQuote = async () => {
  // 1. Validate quote expiration
  const expirationTime = parseInt(quote.expirationTimestamp) * 1000;
  if (Date.now() > expirationTime) {
    throw new Error('Quote has expired');
  }

  // 2. Sign quote with Privy
  const signedQuote = await signQuote(quote, embeddedWallet);

  // 3. Execute signed quote
  await quotesApi.executeQuote(signedQuote);

  // 4. Start status polling
  startStatusPolling(quote.id);
};
```

## 5. Real-Time Status Monitoring

### Status Polling Implementation

After execution, implement real-time polling to track transaction progress:

```typescript theme={null}
// Polling pattern from useQuotes.ts
const pollTransactionStatus = (quoteId: string) => {
  const interval = setInterval(async () => {
    try {
      const status = await quotesApi.getQuoteStatus(quoteId);
      
      if (status.status === 'COMPLETED' || status.status === 'FAILED') {
        clearInterval(interval);
        handleTransactionComplete(status);
      }
    } catch (error) {
      console.error('Polling error:', error);
      clearInterval(interval);
    }
  }, 1000); // Poll every second
};
```

**Status Types**:

* `PENDING`: Transaction submitted to blockchain
* `IN_PROGRESS`: Being processed across chains
* `COMPLETED`: Successfully completed
* `FAILED`: Transaction failed
* `REFUNDED`: Funds returned to user

## 6. User Experience Enhancements

### Quote Countdown Timer

Show users when quotes will expire:

```typescript theme={null}
// From QuoteCountdown component
import { useState, useEffect, useCallback } from 'react';

export function useCountdown(timestamp: number, onExpire?: () => void) {
  const [timeLeft, setTimeLeft] = useState<number>(0);

  useEffect(() => {
    const calculateTimeLeft = () => {
      const difference = timestamp * 1000 - Date.now();
      return Math.max(0, Math.floor(difference / 1000));
    };

    setTimeLeft(calculateTimeLeft());

    const timer = setInterval(() => {
      const newTimeLeft = calculateTimeLeft();
      setTimeLeft(newTimeLeft);

      if (newTimeLeft < 1) {
        clearInterval(timer);
        onExpire?.();
      }
    }, 1000);

    return () => clearInterval(timer);
  }, [timestamp, onExpire]);

  const formatTime = useCallback((seconds: number) => {
    if (seconds === 0) {
      return 'Expired';
    }

    const secs = seconds % 60;
    return `${secs.toString()}`;
  }, []);

  return {
    timeLeft,
    formattedTime: formatTime(timeLeft),
    isExpired: timeLeft === 0,
  };
}
```

![Quote Details](https://storage.googleapis.com/onebalance-public-assets/docs/guides/chain-abstracted-swap-with-privy/quote-details.png)

## 7. Error Handling & Recovery

### Graceful Error Management

Implement error handling for all failure scenarios:

```typescript theme={null}
// Quote error handling
if (quote?.error) {
  return (
    <Alert variant="destructive">
      <AlertTitle>Quote Error</AlertTitle>
      <AlertDescription>{quote.message}</AlertDescription>
    </Alert>
  );
}

// Transaction failure recovery
const handleTransactionFailure = (error) => {
  // Clear state
  resetQuote();
  
  // Show user-friendly error
  setError(error.message || 'Transaction failed');
  
  // Refresh balances to ensure accuracy
  fetchBalances();
};
```

## 8. API Integration Patterns

### Proxy Pattern for CORS

Use Next.js API routes to handle CORS and secure API keys:

```typescript app/api/[...path]/route.ts theme={null}
import { NextRequest, NextResponse } from 'next/server';
import { API_BASE_URL, API_KEY } from '@/lib/constants';

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ path: string[] }> }
) {
  const { path } = await params;
  const pathString = path.join('/');
  const searchParams = request.nextUrl.searchParams;

  try {
    // Build the API URL with any query parameters
    const apiUrl = new URL(`/api/${pathString}`, API_BASE_URL);
    searchParams.forEach((value, key) => {
      apiUrl.searchParams.append(key, value);
    });

    const response = await fetch(apiUrl.toString(), {
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': API_KEY,
      },
    });

    const data = await response.json();
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json({ message: 'Failed to fetch data', error }, { status: 400 });
  }
}

export async function POST(
  request: NextRequest,
  { params }: { params: Promise<{ path: string[] }> }
) {
  const { path } = await params;
  const pathString = path.join('/');

  try {
    const body = await request.json();

    const response = await fetch(`${API_BASE_URL}/api/${pathString}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': API_KEY,
      },
      body: JSON.stringify(body),
    });

    const data = await response.json();
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json({ message: 'Failed to fetch data', error }, { status: 400 });
  }
}
```

## Key Integration Concepts

### Chain Abstraction Benefits

* **Unified Token Balances**: Users see aggregated balances across all chains
* **Automatic Routing**: OneBalance finds optimal paths for swaps
* **Gas Sponsorship**: No need for users to hold native tokens for gas
* **Network Abstraction**: Users never need to think about which chain they're using

### Production Considerations

1. **Error Boundaries**: Implement React error boundaries for graceful failures
2. **Rate Limiting**: Implement client-side rate limiting for API calls
3. **Cache Management**: Cache asset data and balance information appropriately
4. **Security**: Never expose API keys in client-side code

<Tip>
  The complete implementation demonstrates these patterns in production. Check the [GitHub repository](https://github.com/dzimiks/onebalance-chain-abstracted-swap) for detailed examples and best practices.
</Tip>
