# Bridge Kit Quickstart Guide

Welcome to the Bridge Kit! This guide will help you understand the ecosystem and get started with cross-chain USDC bridging quickly.

## Table of Contents

- [What is the App Kit Ecosystem?](#what-is-the-app-kit-ecosystem)
- [Bring Your Own Infrastructure](#bring-your-own-infrastructure)
- [Architecture Overview](#architecture-overview)
- [Quick Setup](#quick-setup)
- [Understanding Bridge Parameters](#understanding-bridge-parameters)
- [Examples](#examples)
- [Event Handling](#event-handling)
- [Supported Chains](#supported-chains)
- [Error Handling](#error-handling)
- [Retry Guide: Resuming Failed Transfers](#retry-guide-resuming-failed-transfers)
- [Troubleshooting](#troubleshooting)
- [Best Practices](#best-practices)
- [Next Steps](#next-steps)

## What is the App Kit Ecosystem?

The App Kit ecosystem is Circle's open-source initiative to streamline stablecoin development. Our SDKs are designed to be easy to use correctly and hard to misuse, with cross-framework compatibility (viem, ethers, solana-web3 etc.) that integrates cleanly into any stack. While opinionated with sensible defaults, they provide escape hatches when you need full control. The pluggable architecture ensures flexible implementation, and all kits are interoperable—allowing you to compose them together for a wide range of use cases.

**This Bridge Kit specifically focuses on cross-chain stablecoin bridging.** Its goal is to abstract away the complexity of cross-chain bridging while maintaining security, type safety, and developer experience.

The Bridge Kit can have any bridging provider plugged in, by implementing your own `BridgingProvider`, but comes by default with full CCTPv2 support.

## Bring Your Own Infrastructure

The Bridge Kit is designed to integrate seamlessly into your existing development infrastructure. Already have a Viem setup? Perfect! Simply pass your pre-configured clients to the `ViemAdapter` and instantly unlock all EVM chains.

We put developers first by ensuring:

- **🔧 Stack compatibility**: Use your existing Viem clients and configuration
- **⚡ Instant integration**: No need to migrate to completely new infrastructure
- **🎯 Meaningful defaults**: We provide helpful utilities to quickly initialize clients when needed
- **🚀 Future-ready support**: Moving forward, we'll support more EVM frameworks like Ethers and Web3.js and other non-EVM frameworks to broaden compatibility

## Understanding Address Context Modes

Before diving into the examples, it's important to understand the two address context modes that determine how your adapter handles wallet addresses:

### 🏠 **User-Controlled** (Default - Recommended for Most Use Cases)

**What it means**: The adapter automatically manages the wallet address. You don't need to specify an address for operations - it's derived from your wallet/private key.

**When to use**:

- 🔑 **Private key wallets** - Server-side applications, scripts, bots
- 🌐 **Browser wallets** - MetaMask, Coinbase Wallet, WalletConnect
- 👤 **Single address scenarios** - One wallet, one address per chain
- 🚀 **Getting started** - Simplest setup, less boilerplate

**How it works**: The adapter calls `getAddress()` automatically when needed. You never pass address parameters.

```typescript
// User-controlled example - address resolved automatically
const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY,
  // addressContext: 'user-controlled' is the default
})

// Usage - no address needed, it's automatic!
await kit.bridge({
  from: { adapter, chain: 'Ethereum' }, // Address auto-resolved
  to: { adapter, chain: 'Base' }, // Address auto-resolved
  amount: '100',
})
```

### 🏢 **Developer-Controlled** (For Enterprise/Multi-Address Systems)

**What it means**: You must explicitly specify which address to use for each operation. The adapter doesn't assume which address you want to use.

**When to use**:

- 🏦 **Enterprise custody** - Fireblocks, Coinbase, Circle Wallets
- 🔐 **Multi-signature wallets** - Different signers for different operations
- 📊 **Multi-address management** - One provider, many addresses/vaults
- 🎯 **Explicit control** - You want to specify exactly which address per operation

**How it works**: You must pass an `address` parameter for every operation. TypeScript will enforce this at compile-time.

```typescript
// Developer-controlled example - explicit address control
const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY,
  capabilities: {
    addressContext: 'developer-controlled',
  },
})

// Usage - address required for each operation
await kit.bridge({
  from: {
    adapter,
    chain: 'Ethereum',
    address: '0x123...', // Required! TypeScript error without this
  },
  to: {
    adapter,
    chain: 'Base',
    address: '0x456...', // Can be different address
  },
  amount: '100',
})
```

### 🤔 **How to Choose?**

**Start with User-Controlled** unless you specifically need developer-controlled features:

| Scenario               | Recommended Mode       | Why                                         |
| ---------------------- | ---------------------- | ------------------------------------------- |
| Private key script     | `user-controlled`      | Simpler API, one address per key            |
| MetaMask integration   | `user-controlled`      | Browser wallet = one connected address      |
| Fireblocks integration | `developer-controlled` | Multiple vaults, explicit vault selection   |
| Multi-sig wallet       | `developer-controlled` | Different signers, explicit control         |
| Getting started        | `user-controlled`      | Less complexity, easier to learn            |
| Production dApp        | `user-controlled`      | Most dApps use one address per user         |
| Enterprise custody     | `developer-controlled` | Multiple addresses, compliance requirements |

**Rule of thumb**: If you're managing one address per adapter instance → use `user-controlled`. If you're managing multiple addresses through one provider → use `developer-controlled`.

## Architecture Overview

The ecosystem consists of three main components:

### 1. **Adapter** - Your Blockchain Interface

A `Adapter` is an abstraction that handles all blockchain-specific operations for a particular network. Think of it as your "wallet + connection" for a specific blockchain.

**Available Adapters:**

- [**`ViemAdapter`**](https://www.npmjs.com/package/@circle-fin/adapter-viem-v2) - For all EVM-compatible chains (Ethereum, Base, Arbitrum, Polygon, etc.)
- [**`SolanaAdapter`**](https://www.npmjs.com/package/@circle-fin/adapter-solana) - For Solana blockchain
- More coming soon 🚀

#### What does an Adapter do?

- 🔑 **Wallet operations**: Get addresses, sign transactions
- ⛽ **Gas management**: Estimate fees, calculate transaction costs
- 🔗 **Chain interaction**: Prepare, simulate, and execute transactions
- 📍 **Chain identification**: Know which blockchain you're operating on

### 2. **Provider** - Your Transfer Protocol

A `Provider` implements a specific bridging protocol and defines which chains and tokens it supports.

**Available Providers:**

- **`CCTPV2BridgingProvider`**(<https://www.npmjs.com/package/@circle-fin/provider-cctp-v2>) - Circle's Cross-Chain Transfer Protocol v2 (CCTPv2)

#### What does a Provider do?

- 🛣️ **Route validation**: Check if a transfer path is supported
- 💰 **Cost estimation**: Calculate gas and protocol fees
- 🔄 **Transfer execution**: Handle the complete cross-chain flow
- 📊 **Progress tracking**: Monitor transfer status and confirmations

> **Note:** The Bridge Kit uses the `CCTPV2BridgingProvider` by default. Most developers will not need to think about or interact with any provider-specific logic unless they are building a custom provider, or until there are multiple providers to switch between.

### 3. **Kit** - Your Developer Interface

The `BridgeKit` orchestrates Adapters and Providers to create a unified, type-safe API.

#### What does the Kit do?

- 🎯 **Auto-routing**: Automatically selects the right provider for your transfer
- ✅ **Validation**: Comprehensive parameter validation with helpful error messages
- 🔧 **Resolution**: Automatically resolves chain definitions and addresses
- 📡 **Events**: Real-time progress updates and error handling

## Quick Setup

### Installation

```bash
npm install @circle-fin/bridge-kit @circle-fin/adapter-viem-v2
# or
yarn add @circle-fin/bridge-kit @circle-fin/adapter-viem-v2
```

### 🚀 Easiest Setup with Factory Methods (Recommended)

The factory methods make getting started incredibly simple. Plus, you can create just **one adapter** and use it across different chains!

#### User-Controlled Setup (Most Common)

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'

// Initialize the kit (CCTPv2 provider included by default)
const kit = new BridgeKit()

// Create ONE adapter that can work across chains!
// Uses 'user-controlled' by default - addresses resolved automatically
const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})

// Get cost estimate first
const estimate = await kit.estimate({
  from: { chain: 'Ethereum' },
  to: { chain: 'Base' },
  amount: '10.50',
})
console.log('Estimated fees:', estimate)

// Execute a transfer - same adapter, different chains!
// No addresses needed - they're resolved automatically from your private key
const result = await kit.bridge({
  from: { adapter, chain: 'Ethereum' }, // Source chain: Ethereum
  to: { adapter, chain: 'Base' }, // Destination chain: Base
  amount: '10.50', // 10.50 USDC
})
```

#### Developer-Controlled Setup (Enterprise/Multi-Address)

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'

const kit = new BridgeKit()

// Create adapter with explicit address control
const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
  capabilities: {
    addressContext: 'developer-controlled', // Explicit address control
  },
})

// Execute transfer with explicit addresses
// TypeScript will require address fields - compile error if missing!
const result = await kit.bridge({
  from: {
    adapter,
    chain: 'Ethereum',
    address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', // Required!
  },
  to: {
    adapter,
    chain: 'Base',
    address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', // Can be different
  },
  amount: '10.50',
})
```

### 🏭 Production Considerations

> **⚠️ Important for Production**: The factory methods use Viem's default public RPC endpoints, which may have rate limits and lower reliability. For production applications, we strongly recommend using dedicated RPC providers like Alchemy, Infura, or QuickNode.

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { createPublicClient, http } from 'viem'

const kit = new BridgeKit()

// Production-ready setup with custom RPC endpoints
const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
  capabilities: {
    addressContext: 'user-controlled', // Explicit for clarity in production
    supportedChains: [Ethereum, Base], // Restrict to your supported chains
  },
  getPublicClient: ({ chain }) =>
    createPublicClient({
      chain,
      transport: http(
        `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
        {
          retryCount: 3,
          timeout: 10000,
        },
      ),
    }),
})

// Same simple usage, but with production-grade infrastructure
const result = await kit.bridge({
  from: { adapter, chain: 'Ethereum' },
  to: { adapter, chain: 'Base' },
  amount: '10.50',
})
```

### 🌐 Browser/Provider Support

For browser environments with wallet providers like MetaMask:

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromProvider } from '@circle-fin/adapter-viem-v2'

const kit = new BridgeKit()

// Create adapters from browser wallet providers
// Browser wallets are typically user-controlled (one connected address)
const adapter = await createViemAdapterFromProvider({
  provider: window.ethereum,
  capabilities: {
    addressContext: 'user-controlled', // Browser wallets = user-controlled
  },
})

// Execute a transfer
const result = await kit.bridge({
  from: { adapter, chain: 'Ethereum' },
  to: { adapter, chain: 'Base' },
  amount: '10.50',
})
```

### 🔧 Advanced Setup (Custom Configuration)

For advanced users who need custom RPC endpoints or client configuration:

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { ViemAdapter } from '@circle-fin/adapter-viem-v2'
import { createPublicClient, createWalletClient, http } from 'viem'
import { mainnet, base } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'

const kit = new BridgeKit()
const account = privateKeyToAccount(process.env.PRIVATE_KEY)

// Create adapters with custom configuration
const ethereumAdapter = new ViemAdapter({
  publicClient: createPublicClient({
    chain: mainnet,
    transport: http('https://your-custom-rpc.com'),
  }),
  walletClient: createWalletClient({
    account,
    chain: mainnet,
    transport: http('https://your-custom-rpc.com'),
  }),
})

const baseAdapter = new ViemAdapter({
  publicClient: createPublicClient({ chain: base, transport: http() }),
  walletClient: createWalletClient({ account, chain: base, transport: http() }),
})

// Execute a transfer
const result = await kit.bridge({
  from: { adapter: ethereumAdapter, chain: 'Ethereum' },
  to: { adapter: baseAdapter, chain: 'Base' },
  amount: '10.50',
})
```

## Understanding Bridge Parameters

The `BridgeParams` type is designed to be flexible and developer-friendly, with built-in type safety based on your adapter's address context:

```typescript
interface BridgeParams {
  from: AdapterContext // Source wallet and chain
  to: BridgeDestination // Destination wallet/address and chain
  amount: string // Amount to transfer (e.g., '10.50')
  token?: 'USDC' // Optional, defaults to 'USDC'
  config?: BridgeConfig // Optional, defaults to FAST transfer
}
```

### ⚡ **Type Safety Based on Address Context**

The shape of `AdapterContext` changes based on your adapter's `addressContext` setting:

#### User-Controlled Adapters

```typescript
// User-controlled - address forbidden (TypeScript error if provided)
const userControlledContext = {
  adapter: userAdapter,
  chain: 'Ethereum',
  // address: '0x...' // ❌ TypeScript error - not allowed!
}
```

#### Developer-Controlled Adapters

```typescript
// Developer-controlled - address required (TypeScript error if missing)
const devControlledContext = {
  adapter: devAdapter,
  chain: 'Ethereum',
  address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', // ✅ Required!
}
```

This compile-time validation prevents common mistakes and ensures you're using the right pattern for your adapter type.

### Configuration Types Explained

#### 1. **AdapterContext** - Your Transfer Endpoint

Represents where funds come from or go to:

```typescript
type AdapterContext = { adapter: Adapter; chain: ChainIdentifier }
```

**Chain-agnostic adapter creation**:

```typescript
const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
  chain: 'Ethereum',
})
```

**Chain specification is required**:

```typescript
const adapterContext = { adapter, chain: 'Ethereum' }
```

#### 2. **BridgeDestination** - Where Funds Go

Can be either a simple `AdapterContext` or include a custom recipient address:

```typescript
type BridgeDestination = AdapterContext | BridgeDestinationWithAddress

interface BridgeDestinationWithAddress {
  adapter: Adapter // Adapter for the destination chain
  chain: ChainIdentifier // Chain identifier
  recipientAddress: string // Custom recipient address
}
```

**Send to adapter's own address**:

```typescript
const destination = { adapter, chain: 'Base' }
```

**Send to different address**:

```typescript
const destination = {
  adapter,
  chain: 'Base',
  recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
}
```

### Address Resolution - Automatic vs Manual

**When you use AdapterContext:**

```typescript
await kit.bridge({
  from: { adapter: ethereumAdapter, chain: 'Ethereum' },
  to: { adapter: baseAdapter, chain: 'Base' },
  amount: '10.50',
})
```

- The address is automatically derived from `adapter.getAddress()`
- The chain is explicitly specified for clarity
- Funds are sent from/to the adapter's own address

**When you specify custom recipient address:**

```typescript
await kit.bridge({
  from: { adapter: ethereumAdapter, chain: 'Ethereum' },
  to: {
    adapter: baseAdapter,
    chain: 'Base',
    recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
  },
  amount: '10.50',
})
```

- Uses the explicit address provided for the recipient
- Chain must be specified along with adapter
- Useful for sending to different recipients or custodial services

### Chain Specification - Multiple Formats

You can specify chains in multiple ways:

```typescript
// 1. Use the exported chain definition (recommended)
import { Ethereum } from '@circle-fin/bridge-kit/chains'
{ adapter: ethereumAdapter, chain: Ethereum }

// 2. Use the blockchain enum
import { Blockchain } from '@circle-fin/bridge-kit/chains'
{ adapter: ethereumAdapter, chain: Blockchain.Ethereum }

// 3. Use a string literal
{ adapter: ethereumAdapter, chain: 'Ethereum' }

// Note: Chain specification is required for clarity
```

## Transfer Configuration

Customize your transfer behavior with the `config` parameter:

```typescript
interface BridgeConfig {
  transferSpeed: 'FAST' | 'SLOW' // Default: 'FAST'
  /**
   * The maximum bridging fee you're willing to pay in human-readable format.
   * For example: "1" for 1 USDC, "0.5" for 0.5 USDC.
   * You should only set this parameter if speed is set to FAST.
   * If this value ends up being less than the protocol fee, the bridge flow will be executed as a SLOW transfer.
   */
  maxFee?: string // Optional: maximum fee (e.g., "1" for 1 USDC)
}
```

- **FAST**: Optimized for speed with potentially higher fees
- **SLOW**: Optimized for cost with zero transfer fees but longer confirmation times

## Custom Fees

### Understanding How Custom Fees Work

Custom fees allow you to charge developer fees on cross-chain USDC transfers. **The custom fee is added on top of the transfer amount.** The wallet signs for `amount + customFee`, the custom fee is split (10% to Circle, 90% to your `recipientAddress`) on the source chain, and the entire transfer amount continues through CCTPv2 unchanged.

### Step-by-Step Example: 1,000 USDC Transfer with 10 USDC Custom Fee

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'

const kit = new BridgeKit()
const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})

// User wants to transfer 1,000 USDC
// You want to charge a 10 USDC custom fee
await kit.bridge({
  from: { adapter, chain: 'Ethereum' },
  to: { adapter, chain: 'Base' },
  amount: '1000', // ← Amount forwarded to CCTPv2
  config: {
    customFee: {
      value: '10', // ← Additional debit on top of the transfer amount
      recipientAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0',
    },
  },
})
```

#### Breakdown of What Happens:

| Stage                                 | Amount     | Description                                        |
| ------------------------------------- | ---------- | -------------------------------------------------- |
| **Wallet debit**                      | 1,010 USDC | User signs for transfer (1,000) + custom fee (10)  |
| **Custom fee → Circle (10%)**         | 1 USDC     | Automatically routed to Circle on the source chain |
| **Custom fee → Your recipient (90%)** | 9 USDC     | Sent to `0x742d35Cc...bEb0`                        |
| **Forwarded to CCTPv2**               | 1,000 USDC | Transfer amount proceeds unchanged                 |
| **CCTPv2 fee (FAST 1 bps example)**   | -0.1 USDC  | Protocol fee taken from the transfer amount        |
| **Destination receives**              | 999.9 USDC | Amount minted on Base                              |

**Summary:**

- **Circle receives:** 10% of the custom fee (1 USDC in this example).
- **You receive:** 90% of the custom fee (9 USDC).
- **User receives:** Transfer amount minus the CCTPv2 protocol fee (999.9 USDC).
- **Important:** Circle only participates in the 10% share when a custom fee is present. If you do not configure a custom fee for a transfer, Circle does not collect any additional amount.
- **Total user debit:** 1,010.1 USDC (1,000 transfer + 10 custom fee + 0.1 protocol fee).

### Per-Transfer Custom Fee

Add a one-off fee directly inside `bridge` parameters:

```typescript
await kit.bridge({
  from: { adapter, chain: 'Ethereum' },
  to: { adapter, chain: 'Base' },
  amount: '100',
  config: {
    customFee: {
      value: '1', // 1 USDC fee
      recipientAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0',
    },
  },
})

// Result (Fast Transfer):
// - Wallet signs for 101 USDC (100 transfer + 1 custom fee)
// - Custom fee split: 0.1 USDC to Circle, 0.9 USDC to your recipientAddress wallet
// - 100 USDC forwarded to CCTPv2
// - CCTPv2 fee: 0.01 USDC (1 bps)
// - User receives: 99.99 USDC
```

### Kit-Level Fee Policy

Set a global fee policy for all transfers:

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'

const kit = new BridgeKit()

kit.setCustomFeePolicy({
  computeFee: (params) => {
    const amount = parseFloat(params.amount)

    // Example: Charge 1% fee
    const feePercentage = 0.01
    const fee = amount * feePercentage

    // Return human-readable fee (e.g., '10' for 10 USDC)
    return fee.toFixed(6)
  },
  resolveFeeRecipientAddress: (feePayoutChain) => {
    // Return address for the source chain
    if (feePayoutChain.type === 'solana') {
      return 'YourSolanaAddressBase58Encoded...'
    }
    return '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0'
  },
})

// Now all transfers automatically include the custom fee
await kit.bridge({
  from: { adapter, chain: 'Ethereum' },
  to: { adapter, chain: 'Base' },
  amount: '1000',
})
// Custom fee (10 USDC) calculated and charged automatically
```

> **Note**: The `calculateFee` function is deprecated. Use `computeFee` instead, which receives human-readable amounts (e.g., `'100'` for 100 USDC) rather than smallest-unit amounts.

## Examples

### Example 1: EVM to EVM Transfer (Ethereum → Base)

#### Standard User-Controlled Transfer

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'

const kit = new BridgeKit()

// Create ONE adapter that can work across chains
// Uses user-controlled by default - addresses resolved automatically
const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})

async function evmToEvmTransfer() {
  try {
    // Get cost estimate first
    // Note: this is a completely optional pattern - you do not need to estimate the transaction costs ahead of time, but it can be useful if you want to see the gas fees and provider fees ahead of time
    const estimate = await kit.estimate({
      from: { chain: 'Ethereum' },
      to: { chain: 'Base' },
      amount: '25.0',
      config: { transferSpeed: 'FAST' },
    })

    console.log('Estimated gas fees:', estimate.gasFees)
    console.log('Protocol fees:', estimate.fees)

    // Execute the transfer - same adapter, different chains
    // No addresses needed - they're resolved automatically from your private key
    const result = await kit.bridge({
      from: { adapter, chain: 'Ethereum' },
      to: { adapter, chain: 'Base' },
      amount: '25.0',
      config: { transferSpeed: 'FAST' },
    })

    console.log('Transfer completed!')
    console.log('Steps:', result.steps)
    console.log(
      'Source tx:',
      result.steps.find((s) => s.name === 'depositForBurn')?.txHash,
    )
    console.log(
      'Destination tx:',
      result.steps.find((s) => s.name === 'mint')?.txHash,
    )
  } catch (error) {
    console.error('Transfer failed:', error)
  }
}
```

#### Enterprise Developer-Controlled Transfer

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'

const kit = new BridgeKit()

// Create adapter with explicit address control for enterprise use
const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
  capabilities: {
    addressContext: 'developer-controlled', // Enterprise/multi-address mode
  },
})

async function enterpriseEvmToEvmTransfer() {
  try {
    // Execute transfer with explicit address control
    // TypeScript enforces that addresses are provided
    const result = await kit.bridge({
      from: {
        adapter,
        chain: 'Ethereum',
        address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', // Vault A
      },
      to: {
        adapter,
        chain: 'Base',
        address: '0x123d35Cc6634C0532925a3b844Bc454e4438f123', // Vault B (different!)
      },
      amount: '25.0',
      config: { transferSpeed: 'FAST' },
    })

    console.log('Enterprise transfer completed!')
    console.log('From vault:', result.source.address)
    console.log('To vault:', result.destination.address)
  } catch (error) {
    console.error('Transfer failed:', error)
  }
}
```

### Example 2: EVM to Non-EVM Transfer (Ethereum → Solana)

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { SolanaAdapter } from '@circle-fin/adapter-solana'
import { Connection, Keypair } from '@solana/web3.js'
import bs58 from 'bs58'

const kit = new BridgeKit()

// Create EVM adapter (Ethereum) using factory method
const ethereumAdapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
  chain: 'Ethereum',
})

// Create Solana adapter
const solanaConnection = new Connection('https://api.mainnet-beta.solana.com')
const solanaKeypair = Keypair.fromSecretKey(
  bs58.decode(process.env.SOLANA_PRIVATE_KEY),
)
const solanaAdapter = new SolanaAdapter({
  connection: solanaConnection,
  signer: solanaKeypair,
})

async function evmToSolanaTransfer() {
  try {
    // Cross-chain bridge from Ethereum to Solana
    const result = await kit.bridge({
      from: { adapter: ethereumAdapter, chain: 'Ethereum' },
      to: { adapter: solanaAdapter, chain: 'Solana' },
      amount: '50.0',
      config: { transferSpeed: 'SLOW' }, // Use slow for lower fees
    })

    console.log('Cross-chain bridge completed!')
    console.log(
      'Ethereum tx:',
      result.steps.find((s) => s.name === 'depositForBurn')?.txHash,
    )
    console.log(
      'Solana tx:',
      result.steps.find((s) => s.name === 'mint')?.txHash,
    )
  } catch (error) {
    console.error('Transfer failed:', error)
  }
}
```

### Example 3: Advanced Usage with Custom Addresses

**This pattern is useful when you need to bridge funds to a wallet address that is different from the wallet that is signing the transactions.**

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'

const kit = new BridgeKit()

// Create ONE adapter that can work across chains
const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
  chain: 'Ethereum',
})

// Send from your address to a different recipient on another chain
const result = await kit.bridge({
  from: { adapter, chain: 'Ethereum' }, // Source: your Ethereum address
  to: {
    adapter, // Same adapter, different chain
    chain: 'Base',
    recipientAddress: '0xRecipientAddress', // Send to a different address
  },
  amount: '100.0',
})

// More explicit configuration with custom settings
const result2 = await kit.bridge({
  from: { adapter, chain: 'Ethereum' },
  to: {
    adapter,
    chain: 'Base',
    recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
  },
  amount: '100.0',
  config: { transferSpeed: 'FAST', maxFee: '1' }, // 1 USDC maximum fee
})
```

> **💡 Pro Tip**: The single adapter pattern is especially powerful when you need to send to custom addresses. You can use the same adapter for both source and destination while specifying different addresses and chains for each side of the transfer!

## Event Handling

Events allow you to subscribe to different parts of the briding lifecycle and respond to them however you want. Events match up 1-1 to actions that are taken by the Bridge Kit: **'approve', 'burn', 'fetchAttestation', and 'mint'** are the events you can subscribe to, or you can use **'\*'** to subscribe to all events.

Each event can also be subscribed to multiple times with different callbacks.

Monitor transfer progress in real-time:

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'

const kit = new BridgeKit()

// Listen to all events
kit.on('*', (event) => {
  console.log(`[${event.method}] ${event.protocol}:`, event.values)
})

// Listen to specific events
kit.on('approve', (event) => {
  console.log('Approval completed:', event.values.txHash)
})

kit.on('burn', (event) => {
  console.log('Burn completed:', event.values.txHash)
})

kit.on('fetchAttestation', (event) => {
  console.log('Attestation received:', event.values.data)
})

kit.on('mint', (event) => {
  console.log('Mint completed:', event.values.txHash)
})
```

## Supported Chains

The Bridge Kit supports chains through Circle's Cross-Chain Transfer Protocol v2 (CCTPv2), enabling **1300 total bridge routes** across networks:

### Mainnet Chains (26 chains = 650 routes)

> **Note**: The Bridge Kit stays in lockstep with CCTPv2 development. As Circle adds new chains to CCTPv2, they automatically become available in the Bridge Kit.

**Arbitrum**, **Arc**, **Avalanche**, **Base**, **Codex**, **Cronos**, **Edge**, **Ethereum**, **HyperEVM**, **Injective**, **Ink**, **Linea**, **Monad**, **Morph**, **OP Mainnet**, **Pharos**, **Plasma**, **Plume**, **Polygon PoS**, **Sei**, **Solana**, **Sonic**, **Unichain**, **World Chain**, **XDC**, **X Layer**

### Testnet Chains (26 chains = 650 routes)

**Arc Testnet**, **Arbitrum Sepolia**, **Avalanche Fuji**, **Base Sepolia**, **Codex Testnet**, **Cronos Testnet**, **Edge Testnet**, **Ethereum Sepolia**, **HyperEVM Testnet**, **Injective Testnet**, **Ink Testnet**, **Linea Sepolia**, **Monad Testnet**, **Morph Testnet**, **OP Sepolia**, **Pharos Atlantic**, **Plasma Testnet**, **Plume Testnet**, **Polygon PoS Amoy**, **Sei Testnet**, **Solana Devnet**, **Sonic Testnet**, **Unichain Sepolia**, **World Chain Sepolia**, **XDC Apothem**, **X Layer Testnet**

## Error Handling

The Bridge Kit uses the following error handling approach designed for developer control:

### Hard Errors (Thrown)

The kit only throws exceptions for "hard errors" that prevent execution:

- **Validation errors**: Invalid parameters, unsupported routes, malformed data
- **Configuration errors**: Missing required settings, invalid chain configurations
- **Authentication errors**: Invalid signatures, insufficient permissions

### Soft Errors (Returned)

For recoverable issues during transfer execution, the kit returns a result object with partial success:

- **Balance insufficient**: Returns successful steps completed before failure
- **Network errors**: Provides transaction hashes and step details for manual recovery
- **Timeout issues**: Returns progress made and allows for retry logic

This approach gives you full control over recovery scenarios while preventing unexpected crashes.

> **Note**: You can resume actionable failures using `BridgeKit.retry(result, context)`. See the Retry Guide below.

## Retry Guide: Resuming Failed Transfers

Use `kit.retry(result, context)` to resume failed or incomplete transfers when the failure is actionable (e.g., transient network issues, dropped/repriced transactions, or a failed step in a multi-step flow). The kit delegates retry to the original provider (CCTPv2 supports actionable retries) and continues from the correct step.

### Method signature

```typescript
retry<
  TFromAdapterCapabilities extends AdapterCapabilities,
  TToAdapterCapabilities extends AdapterCapabilities
>(
  result: BridgeResult,
  context: RetryContext<TFromAdapterCapabilities, TToAdapterCapabilities>
): Promise<BridgeResult>
```

### When to retry vs manual intervention

- Retry: transient RPC/network errors; gas repricing; step failed but prior steps succeeded.
- Manual: insufficient funds; wrong recipient; unsupported route; errors indicating non-actionable state.

### Example A: EVM → EVM retry (Ethereum Sepolia → Base Sepolia)

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'

const kit = new BridgeKit()
const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})

// Start a transfer that may fail
const result = await kit.bridge({
  from: { adapter, chain: 'Ethereum_Sepolia' },
  to: { adapter, chain: 'Base_Sepolia' },
  amount: '1',
})

if (result.state === 'error') {
  try {
    const retryResult = await kit.retry(result, { from: adapter, to: adapter })
    console.log('Retry state:', retryResult.state)
    console.log('Steps:', retryResult.steps)
  } catch (error) {
    console.error('Retry failed:', error)
  }
}
```

### Example B: EVM → Solana retry (Ethereum → Solana Devnet)

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { createSolanaAdapterFromPrivateKey } from '@circle-fin/adapter-solana'

const kit = new BridgeKit()
const evm = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})
const sol = createSolanaAdapterFromPrivateKey({
  privateKey: process.env.SOL_PRIVATE_KEY as string,
})

const result = await kit.bridge({
  from: { adapter: evm, chain: 'Ethereum' },
  to: { adapter: sol, chain: 'Solana' },
  amount: '1',
})

// Check if the bridge failed and retry if needed
if (result.state === 'error') {
  try {
    const retryResult = await kit.retry(result, { from: evm, to: sol })
    console.log('Retry state:', retryResult.state)
  } catch (error) {
    console.error('Retry failed:', error)
  }
}
```

### Limitations

- Only actionable failures can be retried; some failures require user action first.
- Provide valid adapters for both `from` and `to` contexts.

### Common Retry Scenarios

#### 1) Transient RPC/network timeout (use backoff)

```typescript
import { BridgeKit, BridgeResult } from '@circle-fin/bridge-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'

const kit = new BridgeKit()
const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})

async function retryWithBackoff(result: BridgeResult) {
  let attempt = 0
  const maxAttempts = 5
  const baseDelayMs = 1000

  while (attempt < maxAttempts) {
    try {
      return await kit.retry(result, { from: adapter, to: adapter })
    } catch (error) {
      attempt++
      if (attempt >= maxAttempts) throw error
      const delay = baseDelayMs * 2 ** (attempt - 1)
      await new Promise((r) => setTimeout(r, delay))
    }
  }
}
```

#### 2) Burn succeeded, attestation pending (wait, then retry)

```typescript
// If failure indicates attestation delay, wait/poll before retrying
if (result.state === 'error') {
  const burnStep = result.steps.find((s) => s.name === 'burn')
  const attestationStep = result.steps.find(
    (s) => s.name === 'fetchAttestation',
  )

  if (burnStep?.state === 'success' && attestationStep?.state !== 'success') {
    // Wait a bit for attestation availability (example: 30 seconds)
    await new Promise((r) => setTimeout(r, 30_000))
  }

  const retryResult = await kit.retry(result, { from: adapter, to: adapter })
  console.log('Retry state:', retryResult.state)
}
```

#### 3) Mint failed due to gas (EVM gas repricing), then retry

```typescript
// If the mint step failed on destination chain (EVM), reprice and retry
const mintStep = result.steps.find((s) => s.name === 'mint')
if (result.state === 'error' && mintStep?.state === 'error') {
  // Recreate destination adapter with updated gas settings as needed
  const dest = createViemAdapterFromPrivateKey({
    privateKey: process.env.PRIVATE_KEY as `0x${string}`,
    // Provide a higher-priority RPC or adjust client gas policies here if supported by your setup
  })

  const retryResult = await kit.retry(result, { from: adapter, to: dest })
  console.log('Retry state:', retryResult.state)
}
```

### Performance and best practices

- Backoff on transient failures (2s, 4s, 8s...).
- Reprice gas sensibly when congestion is high.
- Persist `result.steps` and transaction hashes for observability.
- Log and monitor both source and destination tx hashes.

## Troubleshooting

### Common Issues and Solutions

#### "Route not supported" Error

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'

const kit = new BridgeKit()

// Check if your transfer route is supported
const isSupported = kit.supportsRoute('Ethereum', 'Base', 'USDC')
if (!isSupported) {
  console.log('This route is not available through CCTPv2')
}
```

**Solution**: Verify both chains are in the [supported chains list](#supported-chains). The kit currently, by default, only supports routes available through Circle's CCTPv2

#### "Insufficient balance" Error

**Check your USDC balance before transfer:**

> **💡 Coming Soon**: The Kit will soon provide built-in balance checking methods. For now, you can check balances manually using the examples below.

```typescript
// On EVM chains (using viem)
import { createPublicClient, http, getContract } from 'viem'
import { mainnet } from 'viem/chains'
import { formatUnits } from 'viem'
import { Ethereum } from '@circle-fin/bridge-kit/chains'

const publicClient = createPublicClient({ chain: mainnet, transport: http() })
const usdcContract = getContract({
  // Use the chain definitions from the kit to get the correct USDC address
  address: Ethereum.usdcAddress,
  abi: [
    {
      name: 'balanceOf',
      type: 'function',
      inputs: [{ name: 'account', type: 'address' }],
      outputs: [{ name: '', type: 'uint256' }],
    },
  ],
  publicClient,
})

const usdcBalance = await usdcContract.read.balanceOf([walletAddress])
console.log('USDC Balance:', formatUnits(usdcBalance, 6))

// On Solana (check ATA balance)
import { Connection } from '@solana/web3.js'

const connection = new Connection('https://api.mainnet-beta.solana.com')
const ataBalance = await connection.getTokenAccountBalance(ataAddress)
console.log('USDC Balance:', ataBalance.value.uiAmount)
```

#### Network RPC Issues

**Symptoms**: Timeouts, connection errors, or slow responses

**Solutions**:

- Use reliable RPC endpoints (Alchemy, Infura, QuickNode)
- Implement retry logic for network calls
- Consider multiple RPC fallbacks

```typescript
import { createPublicClient, http, fallback } from 'viem'
import { mainnet } from 'viem/chains'

const publicClient = createPublicClient({
  chain: mainnet,
  transport: fallback([
    http('https://eth-mainnet.g.alchemy.com/v2/your-key'),
    http('https://mainnet.infura.io/v3/your-key'),
    http(), // Default public RPC as fallback
  ]),
})
```

#### Transaction Stuck or Failed

**For EVM chains**: Check transaction on the block explorer using the `txHash` from the result

**For Solana**: Use Solana Explorer or SolScan

**Recovery**: If a transfer fails mid-process, check the returned `result.steps` to see what completed successfully. See [Recovery from Soft Errors](#recovery-from-soft-errors) for detailed recovery patterns.

### Recovery from Soft Errors

When transfers encounter soft errors (network congestion, insufficient gas, RPC timeouts), you can recover by using the CCTPv2BridgingProvider directly to complete the remaining steps. The BridgeKit's transfer result contains enough information to resume from any point.

> **Note**: You can resume actionable failures using `BridgeKit.retry(result, context)` to automatically continue from the correct step. For non-actionable cases, use the manual recovery patterns below.

#### Understanding Transfer State

Every CCTPv2 transfer follows these steps:

1. **Approval** - Allow the contract to spend USDC
2. **DepositForBurn** - Burns USDC on source chain, generates attestation
3. **FetchAttestation** - Wait for Circle to sign the burn proof
4. **Mint** - Mint USDC on destination chain using the attestation

> **⚠️ Important**: The `result.source` and `result.destination` from `BridgeResult` only contain `address` and `blockchain` properties. To use provider methods for recovery, you must reconstruct full wallet contexts with `adapter` and `chain` properties as shown in the examples below.

```typescript
import { BridgeKit } from '@circle-fin/bridge-kit'
import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'

// Start a transfer that might fail
const kit = new BridgeKit()
const result = await kit.bridge({
  from: sourceAdapter,
  to: destAdapter,
  amount: '100.0',
})

// Check which steps completed successfully
console.log('Transfer state:', result.state)
console.log('Steps:', result.steps)

/*
Expected output for partial failure:
result.state: 'error'
result.steps: [
  { name: 'approve', state: 'success', txHash: '0x123...' },
  { name: 'depositForBurn', state: 'success', txHash: '0x456...' },
  { name: 'fetchAttestation', state: 'error', error: 'Network timeout' },
  // mint step won't be present since fetchAttestation failed
]
*/

// Helper function to find specific steps
const getStep = (stepName: string) =>
  result.steps.find((step) => step.name === stepName)
const approveStep = getStep('approve')
const burnStep = getStep('depositForBurn')
const attestationStep = getStep('fetchAttestation')
const mintStep = getStep('mint')
```

#### Recovery Pattern 1: Failed Attestation Fetch

If the attestation fetch fails due to network issues but the burn completed, you can retry just the fetchAttestation step:

> **Note**: In the future, we will be adding capabilities to continue from a certain point in the bridging lifecycle to accomodate soft error cases like this.

```typescript
import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'

// Create provider instance for manual recovery
const provider = new CCTPV2BridgingProvider()

// Use existing contexts or reconstruct wallet contexts from the result and adapters
const sourceContext = {
  adapter: sourceAdapter,
  address: result.source.address,
  chain: 'Ethereum',
}

const destContext = {
  adapter: destAdapter,
  address: result.destination.address,
  chain: 'Base',
}

// Check if fetchAttestation specifically failed
const attestationStep = result.steps.find(
  (step) => step.name === 'fetchAttestation',
)
const burnStep = result.steps.find((step) => step.name === 'depositForBurn')

if (burnStep?.state === 'success' && attestationStep?.state === 'error') {
  console.log('Burn completed but attestation fetch failed. Retrying...')

  try {
    // Retry fetching the attestation using the burn transaction hash
    const attestationResult = await provider.fetchAttestation(
      sourceContext, // proper wallet context with adapter
      burnStep.txHash,
    )
    console.log('Attestation fetch retry successful')

    // Continue with minting using the attestation data
    const mintRequest = await provider.mint(
      sourceContext, // proper source context
      destContext, // proper destination context
      attestationResult, // attestation data
    )

    // Execute the mint transaction
    if (mintRequest.type !== 'noop') {
      const mintTxHash = await mintRequest.execute()
      const mintReceipt = await destAdapter.waitForTransaction(mintTxHash)
      console.log('Recovery completed:', mintTxHash)
    }
  } catch (error) {
    console.error('Attestation retry failed:', error)

    // Implement exponential backoff for attestation fetching
    const maxRetries = 5
    let retryCount = 0
    const baseDelay = 10000 // Start with 10 seconds

    while (retryCount < maxRetries) {
      try {
        const delay = baseDelay * Math.pow(2, retryCount)
        console.log(`Retry ${retryCount + 1}/${maxRetries} in ${delay}ms...`)
        await new Promise((resolve) => setTimeout(resolve, delay))

        const attestationResult = await provider.fetchAttestation(
          sourceContext,
          burnStep.txHash,
        )
        console.log('Attestation fetched on retry', retryCount + 1)
        break
      } catch (retryError) {
        retryCount++
        if (retryCount === maxRetries) {
          console.error('All attestation retries failed')
        }
      }
    }
  }
} else if (!burnStep || burnStep.state !== 'success') {
  console.error(
    'Cannot retry attestation: burn step not completed successfully',
  )
} else if (!attestationStep) {
  console.error('Attestation step not found in transfer result')
}
```

#### Recovery Pattern 2: Failed Mint Step

If minting fails due to gas issues or network problems:

> **Note**: In the future, we will be adding capabilities to continue from a certain point in the bridging lifecycle to accomodate soft error cases like this.

```typescript
import { parseGwei } from 'viem'
import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'

const provider = new CCTPV2BridgingProvider()

// Use existing contexts or reconstruct wallet contexts from the result and adapters
const sourceContext = {
  adapter: sourceAdapter,
  address: result.source.address,
  chain: 'Ethereum'

const destContext = {
  adapter: destAdapter,
  address: result.destination.address,
  chain: 'Base''
}

// Check if attestation was fetched successfully but mint failed
const attestationStep = result.steps.find(
  (step) => step.name === 'fetchAttestation',
)
const mintStep = result.steps.find((step) => step.name === 'mint')

if (attestationStep?.state === 'success' && mintStep?.state === 'error') {
  try {
    // Retry the mint with the attestation data
    const mintRequest = await provider.mint(
      sourceContext,
      destContext,
      attestationStep.data, // The attestation data from the successful step
    )

    // Execute with custom gas settings for the retry
    if (mintRequest.type !== 'noop') {
      // For EVM chains, you can modify gas settings on the adapter
      if (result.destination.blockchain === 'evm') {
        // Override gas price for the retry (EVM chains)
        destAdapter.walletClient.gasPrice = parseGwei('25') // Higher gas price
      }

      const retryTxHash = await mintRequest.execute()
      const receipt = await destAdapter.waitForTransaction(retryTxHash)
      console.log('Mint retry successful:', retryTxHash)
    }
  } catch (error) {
    console.error('Mint retry failed:', error)
  }
}
```

#### Recovery Best Practices

1. **Save Transfer State**: Always persist the transfer result for recovery
2. **Check Step Status**: Verify which steps completed before attempting recovery
3. **Use Appropriate Timeouts**: Give network operations enough time to complete
4. **Implement Exponential Backoff**: For retry logic, use increasing delays
5. **Monitor Gas Prices**: Adjust gas settings during network congestion

```typescript
// Example: Robust retry with exponential backoff
async function retryWithBackoff<T>(
  operation: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000,
): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation()
    } catch (error) {
      if (attempt === maxRetries) throw error

      const delay = baseDelay * Math.pow(2, attempt - 1)
      console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`)
      await new Promise((resolve) => setTimeout(resolve, delay))
    }
  }
  throw new Error('All retry attempts failed')
}

// Usage for resilient attestation fetching (requires sourceContext from above)
const attestation = await retryWithBackoff(
  () => provider.fetchAttestation(sourceContext, burnTxHash),
  5, // 5 retries
  2000, // Start with 2 second delay
)
```

### Debugging Tips

1. **Monitor gas prices**: High network congestion can cause failures
2. **Test on testnets first**: Always test your integration on testnets before mainnet
3. **Use block explorers**: Always verify transaction status on-chain
4. **Save intermediate results**: Persist transfer state for recovery scenarios

## Best Practices

1. **Always estimate first**: Use `kit.estimate()` to show costs before transfers
2. **Ensure sufficient gas tokens**: Verify you have enough native tokens (ETH, MATIC, AVAX, etc.) on both source and destination chains for transaction fees. The kit currently doesn't check gas balances pre-initialization - this convenience feature will be added in the future
3. **Handle errors gracefully**: Network issues and validation errors are common
4. **Monitor events**: Use event listeners for real-time progress updates
5. **Validate inputs**: The kit validates automatically, but client-side validation improves UX

## Next Steps

- **Explore Examples**: Check out the [examples directory](https://github.com/crcl-main/stablecoin-kits-private/tree/main/examples) for more detailed implementations
- **Join the community**: Connect with other developers on [discord](https://discord.com/invite/buildoncircle) building on Circle's stablecoin infrastructure

Ready to start bridging? Let's make cross-chain bridging as easy as a single function call! 🌉
