@circle-fin/unified-balance-kit

中文文档
1.10.1发布于 2026/11/06TypeScriptESM + CJS近 30 天下载 3.3万

SDK for cross-chain USDC deposits, spending, and balance queries

原始使用文档
文档目录28 节
  1. Table of Contents
  2. Overview
  3. Why Unified Balance Kit?
  4. Architecture Flow
  5. Installation
  6. Adapters
  7. Quick Start
  8. 🚀 Easiest Setup: Deposit and Spend
  9. 🎯 Send to Different Address
  10. 📊 Query Balances
  11. 💰 Cost Estimation
  12. Configuration
  13. Kit Context
  14. Spend Parameters
  15. Balance Parameters
  16. Custom Fees
  17. Fee recipients across chain types
  18. Error Handling
  19. Retrying Failed Mints
  20. API Reference
  21. Core Methods
  22. Functional vs Class API
  23. Development
  24. Building
  25. Testing
  26. Local Development
  27. Community & Support
  28. License

Unified Balance Kit

npm version TypeScript License Discord

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 for normal spend flows

Table of Contents

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 for normal spend flows.

Use spend for normal movement out of a Unified Balance. removeFund is a trustless recovery path for situations where the normal spend flow is unavailable, and it requires a 7-day withdrawal delay before funds can be removed.

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:

┌─────────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│  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

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

Adapters

Choose the appropriate adapter for your target chains:

# 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

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:

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

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

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:

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

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

Spend Parameters

// 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

// 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:

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: (destinationChain) => {
    return destinationChain.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',
    },
  },
})

Fee recipients across chain types

spend() can pull funds from multiple source chains (e.g. combining an EVM and a Solana source), but every fee payout in a spend mints to the same single destination chain — the one you pass as to. So destinationChain above is always the spend's destination, never a source chain, and exactly one recipient address (valid on that destination) is resolved per spend.

If you spend to different destination chain types across calls (some spends to an EVM chain, others to Solana), configure setFeeRecipients once with an address for each chain type instead of writing chain-branching logic yourself. setFeeRecipients only affects which recipient address is used — you still need setCustomFeePolicy's computeFee to determine the fee amount. Calling setFeeRecipients without ever calling setCustomFeePolicy throws at spend time (there is no computeFee to determine an amount):

kit.setCustomFeePolicy({
  computeFee: (params) => (parseFloat(params.amount) * 0.01).toFixed(6), // 1%
})
kit.setFeeRecipients({
  evm: '0xEvmAddress...',
  solana: 'SolanaAddressBase58...',
})

At spend time the kit maps the resolved destination chain to its chain type ('evm' or 'solana' today) and looks up the matching entry. setFeeRecipients takes priority over resolveFeeRecipientAddress for any chain type it has an entry for — the callback is never invoked for that type. If the map is missing an entry for the resolved destination's chain type and a resolveFeeRecipientAddress callback is also configured, the kit falls back to that callback for the missing type (a warning is logged so the fallback isn't a silent surprise). The spend throws before any fee collection is attempted only if neither the map nor the callback can resolve an address for the destination's chain type. Existing integrations using only resolveFeeRecipientAddress or a single-string recipientAddress continue to work unchanged.

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

On Solana, the mint does not create recipient token accounts. Both errors below list the missing accounts in cause.trace.missingTokenAccounts. If the missing account belongs to the Circle fee recipient, contact Circle support instead.

  • Before the transfer (non-forwarder spends and estimateSpend): spend checks the recipient token accounts before any transfer is submitted. A missing account throws a VALIDATION_FAILED KitError that is FATAL. Create the accounts, then call spend again.
  • At mint time (for example on config.retry): a missing account throws a VALIDATION_FAILED KitError that is RESUMABLE. Create the accounts before retrying with config.retry; retrying first fails with the same error.
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, missingTokenAccounts } = error.cause
      .trace as {
      attestation: string
      signature: string
      missingTokenAccounts?: { tokenAccount: string; mint: string }[]
    }
    if (missingTokenAccounts?.length) {
      // Solana only: create the listed token accounts first, then retry
      // with the same attestation. Retrying now fails the same way.
      throw error
    }
    // 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

MethodDescription
deposit(context, params)Deposit a supported token into the caller's account on a chain
depositFor(context, params)Deposit a supported token into another account
spend(context, params)Spend a supported token on a destination chain from source(s)
estimateSpend(context, params)Get fee estimate before spending
getBalances(context, params)Query aggregated and per-chain balances
getSupportedChains(context, token?, options?)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)Start recovery-only fund removal and the 7-day delay
removeFund(context, params)Complete recovery-only fund removal after the 7-day delay

Use spend for normal movement out of a Unified Balance. Use removeFund only as a trustless recovery path when the normal spend flow is unavailable.

Gateway supports USDC, cirBTC, and EURC where each token has a configured address. Check getSupportedChains(context, token) before choosing a chain. FAST cross-chain deposits use CCTP v2 and accept USDC only.

Delegate permissions are per token on EVM and Solana. A delegate authorized for USDC cannot spend EURC until it is separately authorized for EURC. For an EURC spend to Solana, the recipient and any custom fee recipient must already have an EURC associated token account. The SDK does not create these accounts. Create them before spending. If one is missing, spend fails before anything is signed; create the account and call spend again.

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:

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):

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:

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

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

Testing

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

Local Development

# 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

License

This project is licensed under the Apache 2.0 License. Contact support for details.


Ready to build cross-chain USDC apps?

Join Discord

Built with ❤️ by Circle

版本与发布记录

@circle-fin/unified-balance-kit 共发布 23 个版本,首次发布于 2026/04/22;过去 12 个月发布了 23 个版本。

版本发布日期距今
1.10.1latest2026/11/063 天前
1.10.02026/10/3110 天前
1.9.02026/10/2317 天前
1.8.22026/10/2119 天前
1.8.12026/10/1624 天前
1.8.02026/09/301 个月前

关于 @circle-fin/unified-balance-kit 的常见问题

@circle-fin/unified-balance-kit 是什么?

SDK for cross-chain USDC deposits, spending, and balance queries

如何安装 @circle-fin/unified-balance-kit?

在项目目录运行 npm install @circle-fin/unified-balance-kit,或使用 pnpm add / yarn add / bun add。

@circle-fin/unified-balance-kit 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@circle-fin/unified-balance-kit 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@circle-fin/unified-balance-kit 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@circle-fin/unified-balance-kit 可以商用吗?

@circle-fin/unified-balance-kit 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@circle-fin/unified-balance-kit 的下载量有多少?

根据 npm 官方下载统计,@circle-fin/unified-balance-kit 在 2026/10/10 – 2026/11/08 这 30 天内被下载 33,363 次,日均约 1,112 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@circle-fin/unified-balance-kit 还在维护吗?

最新版本 1.10.1 发布于 2026/11/06(3 天前);至今共发布 23 个版本,其中过去 12 个月发布了 23 个;首次发布于 2026/04/22。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。