# Unified Balance Kit

<div align="center">

[![npm version](https://badge.fury.io/js/@circle-fin%2Funified-balance-kit.svg)](https://badge.fury.io/js/@circle-fin%2Funified-balance-kit)
[![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Discord](https://img.shields.io/discord/473781666251538452?label=Discord&logo=discord)](https://discord.com/invite/buildoncircle)

**A strongly-typed SDK for cross-chain USDC deposits, spending, and balance queries**

_Move USDC across chains with simple method calls—no manual bridging, no liquidity pre-positioning, no on-chain delays_

</div>

## Table of Contents

- [Unified Balance Kit](#unified-balance-kit)
  - [Table of Contents](#table-of-contents)
  - [Overview](#overview)
    - [Why Unified Balance Kit?](#why-unified-balance-kit)
  - [Architecture Flow](#architecture-flow)
  - [Installation](#installation)
    - [Adapters](#adapters)
  - [Quick Start](#quick-start)
    - [🚀 Easiest Setup: Deposit and Spend](#-easiest-setup-deposit-and-spend)
    - [🎯 Send to Different Address](#-send-to-different-address)
    - [📊 Query Balances](#-query-balances)
    - [💰 Cost Estimation](#-cost-estimation)
  - [Configuration](#configuration)
    - [Kit Context](#kit-context)
    - [Spend Parameters](#spend-parameters)
    - [Balance Parameters](#balance-parameters)
  - [Custom Fees](#custom-fees)
  - [Error Handling](#error-handling)
  - [Retrying Failed Mints](#retrying-failed-mints)
  - [API Reference](#api-reference)
    - [Core Methods](#core-methods)
    - [Functional vs Class API](#functional-vs-class-api)
  - [Development](#development)
    - [Building](#building)
    - [Testing](#testing)
    - [Local Development](#local-development)
  - [Community & Support](#community--support)
  - [License](#license)

## Overview

The Stablecoin Kit ecosystem is Circle's open-source effort to streamline stablecoin development with SDKs that are easy to use correctly and hard to misuse. Kits are cross-framework (viem, ethers, @solana/web3) and integrate cleanly into any stack. They're opinionated with sensible defaults, but offer escape hatches for full control. A pluggable architecture makes implementation flexible, and all kits are interoperable, so they can be composed to suit a wide range of use cases.

The Unified Balance Kit provides a high-level abstraction for cross-chain USDC operations via Circle's Gateway protocol. Deposit, spend (mint), and query balances across multiple chains with simple method calls—no manual bridging steps, no liquidity pre-positioning, and no on-chain delays.

### Why Unified Balance Kit?

- **🌐 Unified balance model**: Single abstraction for deposits, spends, and balance queries across chains
- **⚡ Instant cross-chain moves**: Pull from multiple source chains and mint on destination in one flow
- **🔧 Bring your own adapters**: Use viem, ethers, or @solana/web3.js adapters
- **🔒 Production-ready**: Leverages Circle's Gateway v1 with attestations and deterministic flows
- **🚀 Developer experience**: Complete TypeScript support, comprehensive validation, and structured errors
- **📦 Multi-source spends**: Allocate amounts from multiple chains in a single spend operation
- **🛡️ Robust error handling**: Structured KitError with recoverability and retry support
- **📡 Event monitoring**: Track lifecycle events for deposits, spends, and balance operations

## Architecture Flow

The Unified Balance Kit follows a three-layer architecture designed for flexibility and type safety:

```text
┌─────────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│  Unified Balance    │────│     Provider     │────│   Adapter       │
│  Kit (Orchestrator) │    │  (Gateway v1)    │    │ (Blockchain)    │
└─────────────────────┘    └──────────────────┘    └─────────────────┘
```

1. **Adapter**: Handles blockchain-specific operations (wallets, transactions, gas) and enables you to use whatever framework you're comfortable with (viem, ethers, @solana/web3.js)
2. **Provider**: Implements the Gateway protocol (currently Gateway v1)
3. **Unified Balance Kit**: Orchestrates adapters and providers with validation and routing

This separation ensures that each component has a single responsibility while maintaining seamless integration across the entire cross-chain lifecycle.

## Installation

```bash
npm install @circle-fin/unified-balance-kit
# or
yarn add @circle-fin/unified-balance-kit
```

### Adapters

Choose the appropriate adapter for your target chains:

```bash
# For EVM chains (Ethereum, Base, Arbitrum, etc.)
npm install @circle-fin/adapter-viem-v2 viem
# or
yarn add @circle-fin/adapter-viem-v2 viem

# For EVM chains using Ethers.js
npm install @circle-fin/adapter-ethers-v6
# or
yarn add @circle-fin/adapter-ethers-v6

# For Solana
npm install @circle-fin/adapter-solana @solana/web3.js
# or
yarn add @circle-fin/adapter-solana @solana/web3.js
```

## Quick Start

### 🚀 Easiest Setup: Deposit and Spend

**Best for**: Getting started quickly, simple deposits and cross-chain spends

```typescript
import {
  createUnifiedBalanceKitContext,
  deposit,
  spend,
} from '@circle-fin/unified-balance-kit'
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'

// Create context with default Gateway v1 provider
const context = createUnifiedBalanceKitContext()

// Create adapter that works across chains
const adapter = createViemAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})

// Deposit USDC on Ethereum
await deposit(context, {
  from: { adapter, chain: 'Ethereum' },
  amount: '100',
})

// Spend (mint) USDC on Base by pulling from Ethereum
const result = await spend(context, {
  amount: '50',
  from: {
    adapter,
    allocations: { amount: '50', chain: 'Ethereum' },
  },
  to: { adapter, chain: 'Base' },
})

console.log('Spend tx:', result.txHash, result.explorerUrl)
```

### 🎯 Send to Different Address

**Best for**: Sending funds to someone else's wallet, custodial services

Use `recipientAddress` when the recipient is different from your adapter's address:

```typescript
const result = await spend(context, {
  amount: '50',
  from: {
    adapter,
    allocations: { amount: '50', chain: 'Ethereum' },
  },
  to: {
    adapter,
    chain: 'Base',
    recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
  },
})
```

### 📊 Query Balances

**Best for**: Displaying aggregated or per-chain USDC balances

```typescript
import {
  createUnifiedBalanceKitContext,
  getBalances,
} from '@circle-fin/unified-balance-kit'

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

// Get confirmed balances across all supported chains
const balances = await getBalances(context, {
  sources: { adapter },
})

console.log('Confirmed:', balances.totalConfirmedBalance)

// Include pending deposits in the result
const withPending = await getBalances(context, {
  sources: { adapter },
  includePending: true,
})

console.log('Confirmed:', withPending.totalConfirmedBalance)
console.log('Pending:', withPending.totalPendingBalance)
```

### 💰 Cost Estimation

**Best for**: Showing users fees upfront before spending

```typescript
import {
  createUnifiedBalanceKitContext,
  estimateSpend,
} from '@circle-fin/unified-balance-kit'

const context = createUnifiedBalanceKitContext()

const estimate = await estimateSpend(context, {
  amount: '100',
  from: {
    adapter,
    allocations: { amount: '100', chain: 'Ethereum' },
  },
  to: { adapter, chain: 'Base' },
})

console.log('Estimated fees:', estimate.fees)
```

## Configuration

### Kit Context

The kit uses a context object that holds providers. Create it with `createUnifiedBalanceKitContext`:

```typescript
// Default: Gateway v1 provider
const context = createUnifiedBalanceKitContext()

// Or with additional providers
const customContext = createUnifiedBalanceKitContext({
  providers: [myCustomProvider],
})
```

### Spend Parameters

```typescript
// Single source allocation
const spendParams = {
  amount: '100',
  from: {
    adapter,
    allocations: { amount: '100', chain: 'Ethereum' },
  },
  to: { adapter, chain: 'Base' },
}

// Multi-source allocation (pull from multiple chains)
const multiSourceSpend = {
  amount: '100',
  from: {
    adapter,
    allocations: [
      { amount: '50', chain: 'Ethereum' },
      { amount: '50', chain: 'Base' },
    ],
  },
  to: { adapter, chain: 'Avalanche' },
}
```

### Balance Parameters

```typescript
// By adapter (wallet-controlled)
const balances = await getBalances(context, {
  sources: { adapter },
})

// By address (read-only, no adapter needed)
const balancesByAddress = await getBalances(context, {
  sources: { address: '0x...', chains: ['Ethereum', 'Base'] },
})
```

## Custom Fees

The kit supports custom fees on spend operations. Fees are added on top of the transfer amount. Use the class API with `setCustomFeePolicy` for dynamic fee calculation, or pass `config.customFee` per spend:

```typescript
import { UnifiedBalanceKit } from '@circle-fin/unified-balance-kit'

const kit = new UnifiedBalanceKit()

// Kit-level policy (class API)
kit.setCustomFeePolicy({
  computeFee: (params) => {
    const total = parseFloat(params.amount)
    return (total * 0.01).toFixed(6) // 1%
  },
  resolveFeeRecipientAddress: (feePayoutChain) => {
    return feePayoutChain.type === 'solana'
      ? 'SolanaAddressBase58...'
      : '0xEvmAddress...'
  },
})

// Or per-spend override (works with both APIs)
await spend(context, {
  amount: '100',
  from: { adapter, allocations: { amount: '100', chain: 'Ethereum' } },
  to: { adapter, chain: 'Base' },
  config: {
    customFee: {
      value: '1.0',
      recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
    },
  },
})
```

## Error Handling

The kit uses structured `KitError` instances with consistent properties:

- **code**: Numeric error code (e.g., 1001, 5001)
- **name**: Human-readable ID (e.g., `INPUT_VALIDATION_FAILED`, `ONCHAIN_TRANSACTION_REVERTED`)
- **type**: Error category (`INPUT`, `BALANCE`, `ONCHAIN`, `RPC`, `NETWORK`)
- **recoverability**: `FATAL`, `RETRYABLE`, or `RESUMABLE`
- **message**: User-friendly explanation
- **cause.trace**: Additional context for debugging

```typescript
import {
  createUnifiedBalanceKitContext,
  spend,
  KitError,
  isKitError,
  getErrorCode,
} from '@circle-fin/unified-balance-kit'

const context = createUnifiedBalanceKitContext()

try {
  await spend(context, params)
} catch (error) {
  if (isKitError(error)) {
    console.error(`Error ${error.code}: ${error.name}`, error.message)
    if (error.recoverability === 'RESUMABLE' && error.cause?.trace) {
      // Handle retry (see Retrying Failed Mints)
    }
  }
  throw error
}
```

## Retrying Failed Mints

When the on-chain mint step fails after the transfer was committed (funds locked), the kit throws a `KitError` with `recoverability: 'RESUMABLE'` and `attestation`/`signature` in `cause.trace`. Use `config.retry` to reattempt:

```typescript
try {
  const result = await spend(context, params)
  console.log('Success:', result.txHash)
} catch (error) {
  if (
    error instanceof KitError &&
    error.recoverability === 'RESUMABLE' &&
    error.cause?.trace
  ) {
    const { attestation, signature } = error.cause.trace as {
      attestation: string
      signature: string
    }
    // Retry with the attestation
    const result = await spend(context, {
      ...params,
      config: { retry: { attestation, signature } },
    })
    console.log('Retry success:', result.txHash)
  } else {
    throw error
  }
}
```

## API Reference

### Core Methods

| Method                                | Description                                                        |
| ------------------------------------- | ------------------------------------------------------------------ |
| `deposit(context, params)`            | Deposit USDC into the caller's account on a chain                  |
| `depositFor(context, params)`         | Deposit USDC into another account                                  |
| `spend(context, params)`              | Spend (mint) USDC on a destination chain by pulling from source(s) |
| `estimateSpend(context, params)`      | Get fee estimate before spending                                   |
| `getBalances(context, params)`        | Query aggregated and per-chain balances                            |
| `getSupportedChains(context, token?)` | Get chains supported by configured providers                       |
| `addDelegate(context, params)`        | Add a delegate for the account                                     |
| `removeDelegate(context, params)`     | Remove a delegate                                                  |
| `getDelegateStatus(context, params)`  | Check delegate status: `'none'`, `'pending'`, or `'ready'`         |
| `initiateRemoveFund(context, params)` | Initiate withdrawal from Gateway                                   |
| `removeFund(context, params)`         | Complete withdrawal (mint on destination)                          |

#### Delegate status and finality

After calling `addDelegate`, the delegate may not be immediately usable for `spend` on
chains with slow finality (e.g. Ethereum, Base, Arbitrum). `getDelegateStatus` returns
a tri-state that reflects Gateway's finality view:

```typescript
const status = await kit.getDelegateStatus({ from, delegateAddress })
if (status === 'ready') { await kit.spend(...) }
if (status === 'pending') { /* poll until 'ready' */ }
```

- `'none'` — not a delegate on-chain
- `'pending'` — delegate set on-chain but Gateway hasn't finalized it yet; spend will fail
- `'ready'` — finalized at Gateway; spend will succeed

### Functional vs Class API

**Functional API** (recommended):

```typescript
import {
  createUnifiedBalanceKitContext,
  deposit,
  spend,
  getBalances,
} from '@circle-fin/unified-balance-kit'

const context = createUnifiedBalanceKitContext()
await deposit(context, { from: { adapter, chain: 'Ethereum' }, amount: '100' })
const result = await spend(context, {
  amount: '50',
  from: { adapter, allocations: { amount: '50', chain: 'Ethereum' } },
  to: { adapter, chain: 'Base' },
})
const balances = await getBalances(context, { sources: { adapter } })
```

**Class API**:

```typescript
import { UnifiedBalanceKit } from '@circle-fin/unified-balance-kit'

const kit = new UnifiedBalanceKit()

kit.on('gateway.deposit.succeeded', (payload) => {
  console.log('Deposit succeeded:', payload.data)
})

kit.on('gateway.spend.succeeded', (payload) => {
  console.log('Spend succeeded:', payload.data.txHash)
})

await kit.deposit({ from: { adapter, chain: 'Ethereum' }, amount: '100' })
const result = await kit.spend({
  amount: '50',
  from: { adapter, allocations: { amount: '50', chain: 'Ethereum' } },
  to: { adapter, chain: 'Base' },
})
```

## Development

### Building

```bash
# From the root of the monorepo
nx build @circle-fin/unified-balance-kit
```

### Testing

```bash
# From the root of the monorepo
nx test @circle-fin/unified-balance-kit
```

### Local Development

```bash
# Install dependencies
yarn install

# Build all packages
yarn build

# Build the unified-balance-kit specifically
nx build @circle-fin/unified-balance-kit

# Run tests
nx test @circle-fin/unified-balance-kit
```

## Community & Support

- **💬 Discord**: [Join our community](https://discord.com/invite/buildoncircle)

## License

This project is licensed under the Apache 2.0 License. Contact [support](https://help.circle.com/s/submit-ticket) for details.

---

<div align="center">

**Ready to build cross-chain USDC apps?**

[Join Discord](https://discord.com/invite/buildoncircle)

_Built with ❤️ by Circle_

</div>
