@circle-fin/bridge-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/QUICKSTART.md +1147 -0
- package/README.md +478 -0
- package/chains.cjs.js +2250 -0
- package/chains.cjs.js.map +1 -0
- package/chains.d.ts +1088 -0
- package/chains.mjs +2225 -0
- package/chains.mjs.map +1 -0
- package/index.cjs.js +4862 -0
- package/index.cjs.js.map +1 -0
- package/index.d.ts +4048 -0
- package/index.mjs +4858 -0
- package/index.mjs.map +1 -0
- package/package.json +46 -0
package/QUICKSTART.md
ADDED
|
@@ -0,0 +1,1147 @@
|
|
|
1
|
+
# Bridge Kit Quickstart Guide
|
|
2
|
+
|
|
3
|
+
Welcome to the Bridge Kit! This guide will help you understand the ecosystem and get started with cross-chain USDC bridging quickly.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [What is the Stablecoin Kit Ecosystem?](#what-is-the-stablecoin-kit-ecosystem)
|
|
8
|
+
- [Bring Your Own Infrastructure](#bring-your-own-infrastructure)
|
|
9
|
+
- [Architecture Overview](#architecture-overview)
|
|
10
|
+
- [Quick Setup](#quick-setup)
|
|
11
|
+
- [Understanding Bridge Parameters](#understanding-bridge-parameters)
|
|
12
|
+
- [Examples](#examples)
|
|
13
|
+
- [Event Handling](#event-handling)
|
|
14
|
+
- [Supported Chains](#supported-chains)
|
|
15
|
+
- [Error Handling](#error-handling)
|
|
16
|
+
- [Troubleshooting](#troubleshooting)
|
|
17
|
+
- [Best Practices](#best-practices)
|
|
18
|
+
- [Next Steps](#next-steps)
|
|
19
|
+
|
|
20
|
+
## What is the Stablecoin Kit Ecosystem?
|
|
21
|
+
|
|
22
|
+
The Stablecoin 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.
|
|
23
|
+
|
|
24
|
+
**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.
|
|
25
|
+
|
|
26
|
+
The Bridge Kit can have any bridging provider plugged in, by implementing your own `BridgingProvider`, but comes by default with full CCTPv2 support.
|
|
27
|
+
|
|
28
|
+
## Bring Your Own Infrastructure
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
We put developers first by ensuring:
|
|
33
|
+
|
|
34
|
+
- **π§ Stack compatibility**: Use your existing Viem clients and configuration
|
|
35
|
+
- **β‘ Instant integration**: No need to migrate to completely new infrastructure
|
|
36
|
+
- **π― Meaningful defaults**: We provide helpful utilities to quickly initialize clients when needed
|
|
37
|
+
- **π Future-ready support**: Moving forward, we'll support more EVM frameworks like Ethers and Web3.js and other non-EVM frameworks to broaden compatibility
|
|
38
|
+
|
|
39
|
+
## Understanding Address Context Modes
|
|
40
|
+
|
|
41
|
+
Before diving into the examples, it's important to understand the two address context modes that determine how your adapter handles wallet addresses:
|
|
42
|
+
|
|
43
|
+
### π **User-Controlled** (Default - Recommended for Most Use Cases)
|
|
44
|
+
|
|
45
|
+
**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.
|
|
46
|
+
|
|
47
|
+
**When to use**:
|
|
48
|
+
|
|
49
|
+
- π **Private key wallets** - Server-side applications, scripts, bots
|
|
50
|
+
- π **Browser wallets** - MetaMask, Coinbase Wallet, WalletConnect
|
|
51
|
+
- π€ **Single address scenarios** - One wallet, one address per chain
|
|
52
|
+
- π **Getting started** - Simplest setup, less boilerplate
|
|
53
|
+
|
|
54
|
+
**How it works**: The adapter calls `getAddress()` automatically when needed. You never pass address parameters.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// User-controlled example - address resolved automatically
|
|
58
|
+
const adapter = createAdapterFromPrivateKey({
|
|
59
|
+
privateKey: process.env.PRIVATE_KEY,
|
|
60
|
+
// addressContext: 'user-controlled' is the default
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Usage - no address needed, it's automatic!
|
|
64
|
+
await kit.bridge({
|
|
65
|
+
from: { adapter, chain: 'Ethereum' }, // Address auto-resolved
|
|
66
|
+
to: { adapter, chain: 'Base' }, // Address auto-resolved
|
|
67
|
+
amount: '100',
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### π’ **Developer-Controlled** (For Enterprise/Multi-Address Systems)
|
|
72
|
+
|
|
73
|
+
**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.
|
|
74
|
+
|
|
75
|
+
**When to use**:
|
|
76
|
+
|
|
77
|
+
- π¦ **Enterprise custody** - Fireblocks, Coinbase, Circle Wallets
|
|
78
|
+
- π **Multi-signature wallets** - Different signers for different operations
|
|
79
|
+
- π **Multi-address management** - One provider, many addresses/vaults
|
|
80
|
+
- π― **Explicit control** - You want to specify exactly which address per operation
|
|
81
|
+
|
|
82
|
+
**How it works**: You must pass an `address` parameter for every operation. TypeScript will enforce this at compile-time.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// Developer-controlled example - explicit address control
|
|
86
|
+
const adapter = createAdapterFromPrivateKey({
|
|
87
|
+
privateKey: process.env.PRIVATE_KEY,
|
|
88
|
+
capabilities: {
|
|
89
|
+
addressContext: 'developer-controlled',
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Usage - address required for each operation
|
|
94
|
+
await kit.bridge({
|
|
95
|
+
from: {
|
|
96
|
+
adapter,
|
|
97
|
+
chain: 'Ethereum',
|
|
98
|
+
address: '0x123...', // Required! TypeScript error without this
|
|
99
|
+
},
|
|
100
|
+
to: {
|
|
101
|
+
adapter,
|
|
102
|
+
chain: 'Base',
|
|
103
|
+
address: '0x456...', // Can be different address
|
|
104
|
+
},
|
|
105
|
+
amount: '100',
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### π€ **How to Choose?**
|
|
110
|
+
|
|
111
|
+
**Start with User-Controlled** unless you specifically need developer-controlled features:
|
|
112
|
+
|
|
113
|
+
| Scenario | Recommended Mode | Why |
|
|
114
|
+
| ---------------------- | ---------------------- | ------------------------------------------- |
|
|
115
|
+
| Private key script | `user-controlled` | Simpler API, one address per key |
|
|
116
|
+
| MetaMask integration | `user-controlled` | Browser wallet = one connected address |
|
|
117
|
+
| Fireblocks integration | `developer-controlled` | Multiple vaults, explicit vault selection |
|
|
118
|
+
| Multi-sig wallet | `developer-controlled` | Different signers, explicit control |
|
|
119
|
+
| Getting started | `user-controlled` | Less complexity, easier to learn |
|
|
120
|
+
| Production dApp | `user-controlled` | Most dApps use one address per user |
|
|
121
|
+
| Enterprise custody | `developer-controlled` | Multiple addresses, compliance requirements |
|
|
122
|
+
|
|
123
|
+
**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`.
|
|
124
|
+
|
|
125
|
+
## Architecture Overview
|
|
126
|
+
|
|
127
|
+
The ecosystem consists of three main components:
|
|
128
|
+
|
|
129
|
+
### 1. **Adapter** - Your Blockchain Interface
|
|
130
|
+
|
|
131
|
+
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.
|
|
132
|
+
|
|
133
|
+
**Available Adapters:**
|
|
134
|
+
|
|
135
|
+
- [**`ViemAdapter`**](https://www.npmjs.com/package/@circle-fin/adapter-viem-v2) - For all EVM-compatible chains (Ethereum, Base, Arbitrum, Polygon, etc.)
|
|
136
|
+
- [**`SolanaAdapter`**](https://www.npmjs.com/package/@circle-fin/adapter-solana) - For Solana blockchain
|
|
137
|
+
- More coming soon π
|
|
138
|
+
|
|
139
|
+
#### What does an Adapter do?
|
|
140
|
+
|
|
141
|
+
- π **Wallet operations**: Get addresses, sign transactions
|
|
142
|
+
- β½ **Gas management**: Estimate fees, calculate transaction costs
|
|
143
|
+
- π **Chain interaction**: Prepare, simulate, and execute transactions
|
|
144
|
+
- π **Chain identification**: Know which blockchain you're operating on
|
|
145
|
+
|
|
146
|
+
### 2. **Provider** - Your Transfer Protocol
|
|
147
|
+
|
|
148
|
+
A `Provider` implements a specific bridging protocol and defines which chains and tokens it supports.
|
|
149
|
+
|
|
150
|
+
**Available Providers:**
|
|
151
|
+
|
|
152
|
+
- **`CCTPV2BridgingProvider`**(<https://www.npmjs.com/package/@circle-fin/provider-cctp-v2>) - Circle's Cross-Chain Transfer Protocol v2 (CCTPv2)
|
|
153
|
+
|
|
154
|
+
#### What does a Provider do?
|
|
155
|
+
|
|
156
|
+
- π£οΈ **Route validation**: Check if a transfer path is supported
|
|
157
|
+
- π° **Cost estimation**: Calculate gas and protocol fees
|
|
158
|
+
- π **Transfer execution**: Handle the complete cross-chain flow
|
|
159
|
+
- π **Progress tracking**: Monitor transfer status and confirmations
|
|
160
|
+
|
|
161
|
+
> **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.
|
|
162
|
+
|
|
163
|
+
### 3. **Kit** - Your Developer Interface
|
|
164
|
+
|
|
165
|
+
The `BridgeKit` orchestrates Adapters and Providers to create a unified, type-safe API.
|
|
166
|
+
|
|
167
|
+
#### What does the Kit do?
|
|
168
|
+
|
|
169
|
+
- π― **Auto-routing**: Automatically selects the right provider for your transfer
|
|
170
|
+
- β
**Validation**: Comprehensive parameter validation with helpful error messages
|
|
171
|
+
- π§ **Resolution**: Automatically resolves chain definitions and addresses
|
|
172
|
+
- π‘ **Events**: Real-time progress updates and error handling
|
|
173
|
+
|
|
174
|
+
## Quick Setup
|
|
175
|
+
|
|
176
|
+
### Installation
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npm install @circle-fin/bridge-kit @circle-fin/adapter-viem-v2
|
|
180
|
+
# or
|
|
181
|
+
yarn add @circle-fin/bridge-kit @circle-fin/adapter-viem-v2
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### π Easiest Setup with Factory Methods (Recommended)
|
|
185
|
+
|
|
186
|
+
The factory methods make getting started incredibly simple. Plus, you can create just **one adapter** and use it across different chains!
|
|
187
|
+
|
|
188
|
+
#### User-Controlled Setup (Most Common)
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { BridgeKit } from '@circle-fin/bridge-kit'
|
|
192
|
+
import { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
|
|
193
|
+
|
|
194
|
+
// Initialize the kit (CCTPv2 provider included by default)
|
|
195
|
+
const kit = new BridgeKit()
|
|
196
|
+
|
|
197
|
+
// Create ONE adapter that can work across chains!
|
|
198
|
+
// Uses 'user-controlled' by default - addresses resolved automatically
|
|
199
|
+
const adapter = createAdapterFromPrivateKey({
|
|
200
|
+
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// Get cost estimate first
|
|
204
|
+
const estimate = await kit.estimate({
|
|
205
|
+
from: { chain: 'Ethereum' },
|
|
206
|
+
to: { chain: 'Base' },
|
|
207
|
+
amount: '10.50',
|
|
208
|
+
})
|
|
209
|
+
console.log('Estimated fees:', estimate)
|
|
210
|
+
|
|
211
|
+
// Execute a transfer - same adapter, different chains!
|
|
212
|
+
// No addresses needed - they're resolved automatically from your private key
|
|
213
|
+
const result = await kit.bridge({
|
|
214
|
+
from: { adapter, chain: 'Ethereum' }, // Source chain: Ethereum
|
|
215
|
+
to: { adapter, chain: 'Base' }, // Destination chain: Base
|
|
216
|
+
amount: '10.50', // 10.50 USDC
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### Developer-Controlled Setup (Enterprise/Multi-Address)
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { BridgeKit } from '@circle-fin/bridge-kit'
|
|
224
|
+
import { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
|
|
225
|
+
|
|
226
|
+
const kit = new BridgeKit()
|
|
227
|
+
|
|
228
|
+
// Create adapter with explicit address control
|
|
229
|
+
const adapter = createAdapterFromPrivateKey({
|
|
230
|
+
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
|
|
231
|
+
capabilities: {
|
|
232
|
+
addressContext: 'developer-controlled', // Explicit address control
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// Execute transfer with explicit addresses
|
|
237
|
+
// TypeScript will require address fields - compile error if missing!
|
|
238
|
+
const result = await kit.bridge({
|
|
239
|
+
from: {
|
|
240
|
+
adapter,
|
|
241
|
+
chain: 'Ethereum',
|
|
242
|
+
address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', // Required!
|
|
243
|
+
},
|
|
244
|
+
to: {
|
|
245
|
+
adapter,
|
|
246
|
+
chain: 'Base',
|
|
247
|
+
address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', // Can be different
|
|
248
|
+
},
|
|
249
|
+
amount: '10.50',
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### π Production Considerations
|
|
254
|
+
|
|
255
|
+
> **β οΈ 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.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import { BridgeKit } from '@circle-fin/bridge-kit'
|
|
259
|
+
import { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
|
|
260
|
+
import { createPublicClient, http } from 'viem'
|
|
261
|
+
|
|
262
|
+
const kit = new BridgeKit()
|
|
263
|
+
|
|
264
|
+
// Production-ready setup with custom RPC endpoints
|
|
265
|
+
const adapter = createAdapterFromPrivateKey({
|
|
266
|
+
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
|
|
267
|
+
capabilities: {
|
|
268
|
+
addressContext: 'user-controlled', // Explicit for clarity in production
|
|
269
|
+
supportedChains: [Ethereum, Base], // Restrict to your supported chains
|
|
270
|
+
},
|
|
271
|
+
getPublicClient: ({ chain }) =>
|
|
272
|
+
createPublicClient({
|
|
273
|
+
chain,
|
|
274
|
+
transport: http(
|
|
275
|
+
`https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
|
|
276
|
+
{
|
|
277
|
+
retryCount: 3,
|
|
278
|
+
timeout: 10000,
|
|
279
|
+
},
|
|
280
|
+
),
|
|
281
|
+
}),
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
// Same simple usage, but with production-grade infrastructure
|
|
285
|
+
const result = await kit.bridge({
|
|
286
|
+
from: { adapter, chain: 'Ethereum' },
|
|
287
|
+
to: { adapter, chain: 'Base' },
|
|
288
|
+
amount: '10.50',
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### π Browser/Provider Support
|
|
293
|
+
|
|
294
|
+
For browser environments with wallet providers like MetaMask:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { BridgeKit } from '@circle-fin/bridge-kit'
|
|
298
|
+
import { createAdapterFromProvider } from '@circle-fin/adapter-viem-v2'
|
|
299
|
+
|
|
300
|
+
const kit = new BridgeKit()
|
|
301
|
+
|
|
302
|
+
// Create adapters from browser wallet providers
|
|
303
|
+
// Browser wallets are typically user-controlled (one connected address)
|
|
304
|
+
const adapter = await createAdapterFromProvider({
|
|
305
|
+
provider: window.ethereum,
|
|
306
|
+
capabilities: {
|
|
307
|
+
addressContext: 'user-controlled', // Browser wallets = user-controlled
|
|
308
|
+
},
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// Execute a transfer
|
|
312
|
+
const result = await kit.bridge({
|
|
313
|
+
from: { adapter, chain: 'Ethereum' },
|
|
314
|
+
to: { adapter, chain: 'Base' },
|
|
315
|
+
amount: '10.50',
|
|
316
|
+
})
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### π§ Advanced Setup (Custom Configuration)
|
|
320
|
+
|
|
321
|
+
For advanced users who need custom RPC endpoints or client configuration:
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
import { BridgeKit } from '@circle-fin/bridge-kit'
|
|
325
|
+
import { ViemAdapter } from '@circle-fin/adapter-viem-v2'
|
|
326
|
+
import { createPublicClient, createWalletClient, http } from 'viem'
|
|
327
|
+
import { mainnet, base } from 'viem/chains'
|
|
328
|
+
import { privateKeyToAccount } from 'viem/accounts'
|
|
329
|
+
|
|
330
|
+
const kit = new BridgeKit()
|
|
331
|
+
const account = privateKeyToAccount(process.env.PRIVATE_KEY)
|
|
332
|
+
|
|
333
|
+
// Create adapters with custom configuration
|
|
334
|
+
const ethereumAdapter = new ViemAdapter({
|
|
335
|
+
publicClient: createPublicClient({
|
|
336
|
+
chain: mainnet,
|
|
337
|
+
transport: http('https://your-custom-rpc.com'),
|
|
338
|
+
}),
|
|
339
|
+
walletClient: createWalletClient({
|
|
340
|
+
account,
|
|
341
|
+
chain: mainnet,
|
|
342
|
+
transport: http('https://your-custom-rpc.com'),
|
|
343
|
+
}),
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
const baseAdapter = new ViemAdapter({
|
|
347
|
+
publicClient: createPublicClient({ chain: base, transport: http() }),
|
|
348
|
+
walletClient: createWalletClient({ account, chain: base, transport: http() }),
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
// Execute a transfer
|
|
352
|
+
const result = await kit.bridge({
|
|
353
|
+
from: { adapter: ethereumAdapter, chain: 'Ethereum' },
|
|
354
|
+
to: { adapter: baseAdapter, chain: 'Base' },
|
|
355
|
+
amount: '10.50',
|
|
356
|
+
})
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Understanding Bridge Parameters
|
|
360
|
+
|
|
361
|
+
The `BridgeParams` type is designed to be flexible and developer-friendly, with built-in type safety based on your adapter's address context:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
interface BridgeParams {
|
|
365
|
+
from: AdapterContext // Source wallet and chain
|
|
366
|
+
to: BridgeDestination // Destination wallet/address and chain
|
|
367
|
+
amount: string // Amount to transfer (e.g., '10.50')
|
|
368
|
+
token?: 'USDC' // Optional, defaults to 'USDC'
|
|
369
|
+
config?: BridgeConfig // Optional, defaults to FAST transfer
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### β‘ **Type Safety Based on Address Context**
|
|
374
|
+
|
|
375
|
+
The shape of `AdapterContext` changes based on your adapter's `addressContext` setting:
|
|
376
|
+
|
|
377
|
+
#### User-Controlled Adapters
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// User-controlled - address forbidden (TypeScript error if provided)
|
|
381
|
+
const userControlledContext = {
|
|
382
|
+
adapter: userAdapter,
|
|
383
|
+
chain: 'Ethereum',
|
|
384
|
+
// address: '0x...' // β TypeScript error - not allowed!
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### Developer-Controlled Adapters
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// Developer-controlled - address required (TypeScript error if missing)
|
|
392
|
+
const devControlledContext = {
|
|
393
|
+
adapter: devAdapter,
|
|
394
|
+
chain: 'Ethereum',
|
|
395
|
+
address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', // β
Required!
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
This compile-time validation prevents common mistakes and ensures you're using the right pattern for your adapter type.
|
|
400
|
+
|
|
401
|
+
### Configuration Types Explained
|
|
402
|
+
|
|
403
|
+
#### 1. **AdapterContext** - Your Transfer Endpoint
|
|
404
|
+
|
|
405
|
+
Represents where funds come from or go to:
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
type AdapterContext = { adapter: Adapter; chain: ChainIdentifier }
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
**Chain-agnostic adapter creation**:
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
const adapter = createAdapterFromPrivateKey({
|
|
415
|
+
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
|
|
416
|
+
chain: 'Ethereum',
|
|
417
|
+
})
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Chain specification is required**:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
const adapterContext = { adapter, chain: 'Ethereum' }
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
#### 2. **BridgeDestination** - Where Funds Go
|
|
427
|
+
|
|
428
|
+
Can be either a simple `AdapterContext` or include a custom recipient address:
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
type BridgeDestination = AdapterContext | BridgeDestinationWithAddress
|
|
432
|
+
|
|
433
|
+
interface BridgeDestinationWithAddress {
|
|
434
|
+
adapter: Adapter // Adapter for the destination chain
|
|
435
|
+
chain: ChainIdentifier // Chain identifier
|
|
436
|
+
recipientAddress: string // Custom recipient address
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Send to adapter's own address**:
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
const destination = { adapter, chain: 'Base' }
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
**Send to different address**:
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
const destination = {
|
|
450
|
+
adapter,
|
|
451
|
+
chain: 'Base',
|
|
452
|
+
recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Address Resolution - Automatic vs Manual
|
|
457
|
+
|
|
458
|
+
**When you use AdapterContext:**
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
await kit.bridge({
|
|
462
|
+
from: { adapter: ethereumAdapter, chain: 'Ethereum' },
|
|
463
|
+
to: { adapter: baseAdapter, chain: 'Base' },
|
|
464
|
+
amount: '10.50',
|
|
465
|
+
})
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
- The address is automatically derived from `adapter.getAddress()`
|
|
469
|
+
- The chain is explicitly specified for clarity
|
|
470
|
+
- Funds are sent from/to the adapter's own address
|
|
471
|
+
|
|
472
|
+
**When you specify custom recipient address:**
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
await kit.bridge({
|
|
476
|
+
from: { adapter: ethereumAdapter, chain: 'Ethereum' },
|
|
477
|
+
to: {
|
|
478
|
+
adapter: baseAdapter,
|
|
479
|
+
chain: 'Base',
|
|
480
|
+
recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
|
|
481
|
+
},
|
|
482
|
+
amount: '10.50',
|
|
483
|
+
})
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
- Uses the explicit address provided for the recipient
|
|
487
|
+
- Chain must be specified along with adapter
|
|
488
|
+
- Useful for sending to different recipients or custodial services
|
|
489
|
+
|
|
490
|
+
### Chain Specification - Multiple Formats
|
|
491
|
+
|
|
492
|
+
You can specify chains in multiple ways:
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
// 1. Use the exported chain definition (recommended)
|
|
496
|
+
import { Ethereum } from '@circle-fin/bridge-kit/chains'
|
|
497
|
+
{ adapter: ethereumAdapter, chain: Ethereum }
|
|
498
|
+
|
|
499
|
+
// 2. Use the blockchain enum
|
|
500
|
+
import { Blockchain } from '@circle-fin/bridge-kit/chains'
|
|
501
|
+
{ adapter: ethereumAdapter, chain: Blockchain.Ethereum }
|
|
502
|
+
|
|
503
|
+
// 3. Use a string literal
|
|
504
|
+
{ adapter: ethereumAdapter, chain: 'Ethereum' }
|
|
505
|
+
|
|
506
|
+
// Note: Chain specification is required for clarity
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
## Transfer Configuration
|
|
510
|
+
|
|
511
|
+
Customize your transfer behavior with the `config` parameter:
|
|
512
|
+
|
|
513
|
+
```typescript
|
|
514
|
+
interface BridgeConfig {
|
|
515
|
+
transferSpeed: 'FAST' | 'SLOW' // Default: 'FAST'
|
|
516
|
+
/**
|
|
517
|
+
* The maximum bridging fee you're willing to pay (in smallest units for that chain).
|
|
518
|
+
* You should only set this parameter if speed is set to FAST.
|
|
519
|
+
* If this value ends up being less than the protocol fee, the bridge flow will be executed as a SLOW transfer.
|
|
520
|
+
*/
|
|
521
|
+
maxFee?: string // Optional: maximum fee in smallest units
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
- **FAST**: Optimized for speed with potentially higher fees
|
|
526
|
+
- **SLOW**: Optimized for cost with zero transfer fees but longer confirmation times
|
|
527
|
+
|
|
528
|
+
## Examples
|
|
529
|
+
|
|
530
|
+
### Example 1: EVM to EVM Transfer (Ethereum β Base)
|
|
531
|
+
|
|
532
|
+
#### Standard User-Controlled Transfer
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
import { BridgeKit } from '@circle-fin/bridge-kit'
|
|
536
|
+
import { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
|
|
537
|
+
|
|
538
|
+
const kit = new BridgeKit()
|
|
539
|
+
|
|
540
|
+
// Create ONE adapter that can work across chains
|
|
541
|
+
// Uses user-controlled by default - addresses resolved automatically
|
|
542
|
+
const adapter = createAdapterFromPrivateKey({
|
|
543
|
+
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
async function evmToEvmTransfer() {
|
|
547
|
+
try {
|
|
548
|
+
// Get cost estimate first
|
|
549
|
+
// 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
|
|
550
|
+
const estimate = await kit.estimate({
|
|
551
|
+
from: { chain: 'Ethereum' },
|
|
552
|
+
to: { chain: 'Base' },
|
|
553
|
+
amount: '25.0',
|
|
554
|
+
config: { transferSpeed: 'FAST' },
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
console.log('Estimated gas fees:', estimate.gasFees)
|
|
558
|
+
console.log('Protocol fees:', estimate.fees)
|
|
559
|
+
|
|
560
|
+
// Execute the transfer - same adapter, different chains
|
|
561
|
+
// No addresses needed - they're resolved automatically from your private key
|
|
562
|
+
const result = await kit.bridge({
|
|
563
|
+
from: { adapter, chain: 'Ethereum' },
|
|
564
|
+
to: { adapter, chain: 'Base' },
|
|
565
|
+
amount: '25.0',
|
|
566
|
+
config: { transferSpeed: 'FAST' },
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
console.log('Transfer completed!')
|
|
570
|
+
console.log('Steps:', result.steps)
|
|
571
|
+
console.log(
|
|
572
|
+
'Source tx:',
|
|
573
|
+
result.steps.find((s) => s.name === 'depositForBurn')?.txHash,
|
|
574
|
+
)
|
|
575
|
+
console.log(
|
|
576
|
+
'Destination tx:',
|
|
577
|
+
result.steps.find((s) => s.name === 'mint')?.txHash,
|
|
578
|
+
)
|
|
579
|
+
} catch (error) {
|
|
580
|
+
console.error('Transfer failed:', error)
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
#### Enterprise Developer-Controlled Transfer
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
import { BridgeKit } from '@circle-fin/bridge-kit'
|
|
589
|
+
import { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
|
|
590
|
+
|
|
591
|
+
const kit = new BridgeKit()
|
|
592
|
+
|
|
593
|
+
// Create adapter with explicit address control for enterprise use
|
|
594
|
+
const adapter = createAdapterFromPrivateKey({
|
|
595
|
+
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
|
|
596
|
+
capabilities: {
|
|
597
|
+
addressContext: 'developer-controlled', // Enterprise/multi-address mode
|
|
598
|
+
},
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
async function enterpriseEvmToEvmTransfer() {
|
|
602
|
+
try {
|
|
603
|
+
// Execute transfer with explicit address control
|
|
604
|
+
// TypeScript enforces that addresses are provided
|
|
605
|
+
const result = await kit.bridge({
|
|
606
|
+
from: {
|
|
607
|
+
adapter,
|
|
608
|
+
chain: 'Ethereum',
|
|
609
|
+
address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', // Vault A
|
|
610
|
+
},
|
|
611
|
+
to: {
|
|
612
|
+
adapter,
|
|
613
|
+
chain: 'Base',
|
|
614
|
+
address: '0x123d35Cc6634C0532925a3b844Bc454e4438f123', // Vault B (different!)
|
|
615
|
+
},
|
|
616
|
+
amount: '25.0',
|
|
617
|
+
config: { transferSpeed: 'FAST' },
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
console.log('Enterprise transfer completed!')
|
|
621
|
+
console.log('From vault:', result.source.address)
|
|
622
|
+
console.log('To vault:', result.destination.address)
|
|
623
|
+
} catch (error) {
|
|
624
|
+
console.error('Transfer failed:', error)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Example 2: EVM to Non-EVM Transfer (Ethereum β Solana)
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
import { BridgeKit } from '@circle-fin/bridge-kit'
|
|
633
|
+
import { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
|
|
634
|
+
import { SolanaAdapter } from '@circle-fin/adapter-solana'
|
|
635
|
+
import { Connection, Keypair } from '@solana/web3.js'
|
|
636
|
+
import bs58 from 'bs58'
|
|
637
|
+
|
|
638
|
+
const kit = new BridgeKit()
|
|
639
|
+
|
|
640
|
+
// Create EVM adapter (Ethereum) using factory method
|
|
641
|
+
const ethereumAdapter = createAdapterFromPrivateKey({
|
|
642
|
+
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
|
|
643
|
+
chain: 'Ethereum',
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
// Create Solana adapter
|
|
647
|
+
const solanaConnection = new Connection('https://api.mainnet-beta.solana.com')
|
|
648
|
+
const solanaKeypair = Keypair.fromSecretKey(
|
|
649
|
+
bs58.decode(process.env.SOLANA_PRIVATE_KEY),
|
|
650
|
+
)
|
|
651
|
+
const solanaAdapter = new SolanaAdapter({
|
|
652
|
+
connection: solanaConnection,
|
|
653
|
+
signer: solanaKeypair,
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
async function evmToSolanaTransfer() {
|
|
657
|
+
try {
|
|
658
|
+
// Cross-chain bridge from Ethereum to Solana
|
|
659
|
+
const result = await kit.bridge({
|
|
660
|
+
from: { adapter: ethereumAdapter, chain: 'Ethereum' },
|
|
661
|
+
to: { adapter: solanaAdapter, chain: 'Solana' },
|
|
662
|
+
amount: '50.0',
|
|
663
|
+
config: { transferSpeed: 'SLOW' }, // Use slow for lower fees
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
console.log('Cross-chain bridge completed!')
|
|
667
|
+
console.log(
|
|
668
|
+
'Ethereum tx:',
|
|
669
|
+
result.steps.find((s) => s.name === 'depositForBurn')?.txHash,
|
|
670
|
+
)
|
|
671
|
+
console.log(
|
|
672
|
+
'Solana tx:',
|
|
673
|
+
result.steps.find((s) => s.name === 'mint')?.txHash,
|
|
674
|
+
)
|
|
675
|
+
} catch (error) {
|
|
676
|
+
console.error('Transfer failed:', error)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Example 3: Advanced Usage with Custom Addresses
|
|
682
|
+
|
|
683
|
+
**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.**
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
import { BridgeKit } from '@circle-fin/bridge-kit'
|
|
687
|
+
import { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
|
|
688
|
+
|
|
689
|
+
const kit = new BridgeKit()
|
|
690
|
+
|
|
691
|
+
// Create ONE adapter that can work across chains
|
|
692
|
+
const adapter = createAdapterFromPrivateKey({
|
|
693
|
+
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
|
|
694
|
+
chain: 'Ethereum',
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
// Send from your address to a different recipient on another chain
|
|
698
|
+
const result = await kit.bridge({
|
|
699
|
+
from: { adapter, chain: 'Ethereum' }, // Source: your Ethereum address
|
|
700
|
+
to: {
|
|
701
|
+
adapter, // Same adapter, different chain
|
|
702
|
+
chain: 'Base',
|
|
703
|
+
recipientAddress: '0xRecipientAddress', // Send to a different address
|
|
704
|
+
},
|
|
705
|
+
amount: '100.0',
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
// More explicit configuration with custom settings
|
|
709
|
+
const result2 = await kit.bridge({
|
|
710
|
+
from: { adapter, chain: 'Ethereum' },
|
|
711
|
+
to: {
|
|
712
|
+
adapter,
|
|
713
|
+
chain: 'Base',
|
|
714
|
+
recipientAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
|
|
715
|
+
},
|
|
716
|
+
amount: '100.0',
|
|
717
|
+
config: { transferSpeed: 'FAST', maxFee: '1000000' },
|
|
718
|
+
})
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
> **π‘ 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!
|
|
722
|
+
|
|
723
|
+
## Event Handling
|
|
724
|
+
|
|
725
|
+
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', 'attestsion', and 'mint'** are the events you can subscribe to, or you can use **'\*'** to subscribe to all events.
|
|
726
|
+
|
|
727
|
+
Each event can also be subscribed to multiple times with different callbacks.
|
|
728
|
+
|
|
729
|
+
Monitor transfer progress in real-time:
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
import { BridgeKit } from '@circle-fin/bridge-kit'
|
|
733
|
+
|
|
734
|
+
const kit = new BridgeKit()
|
|
735
|
+
|
|
736
|
+
// Listen to all events
|
|
737
|
+
kit.on('*', (event) => {
|
|
738
|
+
console.log(`[${event.method}] ${event.protocol}:`, event.values)
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
// Listen to specific events
|
|
742
|
+
kit.on('approve', (event) => {
|
|
743
|
+
console.log('Approval completed:', event.txHash)
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
kit.on('burn', (event) => {
|
|
747
|
+
console.log('Burn completed:', event.txHash)
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
kit.on('attestation', (event) => {
|
|
751
|
+
console.log('Attestation received:', event.attestation)
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
kit.on('mint', (event) => {
|
|
755
|
+
console.log('Mint completed:', event.txHash)
|
|
756
|
+
})
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
## Supported Chains
|
|
760
|
+
|
|
761
|
+
The Bridge Kit supports chains through Circle's Cross-Chain Transfer Protocol v2 (CCTPv2), enabling **544 total bridge routes** across networks:
|
|
762
|
+
|
|
763
|
+
### Mainnet Chains (17 chains = 272 routes)
|
|
764
|
+
|
|
765
|
+
> **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.
|
|
766
|
+
|
|
767
|
+
**Arbitrum**, **Avalanche**, **Base**, **Codex**, **Ethereum**, **HyperEVM**, **Ink**, **Linea**, **OP Mainnet**, **Plume**, **Polygon PoS**, **Sei**, **Solana**, **Sonic**, **Unichain**, **World Chain**, **XDC**
|
|
768
|
+
|
|
769
|
+
### Testnet Chains (17 chains = 272 routes)
|
|
770
|
+
|
|
771
|
+
**Arbitrum Sepolia**, **Avalanche Fuji**, **Base Sepolia**, **Codex Testnet**, **Ethereum Sepolia**, **HyperEVM Testnet**, **Ink Testnet**, **Linea Sepolia**, **OP Sepolia**, **Plume Testnet**, **Polygon PoS Amoy**, **Sei Testnet**, **Solana Devnet**, **Sonic Testnet**, **Unichain Sepolia**, **World Chain Sepolia**, **XDC Apothem**
|
|
772
|
+
|
|
773
|
+
## Error Handling
|
|
774
|
+
|
|
775
|
+
The Bridge Kit uses the following error handling approach designed for developer control:
|
|
776
|
+
|
|
777
|
+
### Hard Errors (Thrown)
|
|
778
|
+
|
|
779
|
+
The kit only throws exceptions for "hard errors" that prevent execution:
|
|
780
|
+
|
|
781
|
+
- **Validation errors**: Invalid parameters, unsupported routes, malformed data
|
|
782
|
+
- **Configuration errors**: Missing required settings, invalid chain configurations
|
|
783
|
+
- **Authentication errors**: Invalid signatures, insufficient permissions
|
|
784
|
+
|
|
785
|
+
### Soft Errors (Returned)
|
|
786
|
+
|
|
787
|
+
For recoverable issues during transfer execution, the kit returns a result object with partial success:
|
|
788
|
+
|
|
789
|
+
- **Balance insufficient**: Returns successful steps completed before failure
|
|
790
|
+
- **Network errors**: Provides transaction hashes and step details for manual recovery
|
|
791
|
+
- **Timeout issues**: Returns progress made and allows for retry logic
|
|
792
|
+
|
|
793
|
+
This approach gives you full control over recovery scenarios while preventing unexpected crashes.
|
|
794
|
+
|
|
795
|
+
> **Note**: In the future, we will be adding capabilities to continue from a certain point in the bridging lifecycle to accomodate soft error cases.
|
|
796
|
+
|
|
797
|
+
## Troubleshooting
|
|
798
|
+
|
|
799
|
+
### Common Issues and Solutions
|
|
800
|
+
|
|
801
|
+
#### "Route not supported" Error
|
|
802
|
+
|
|
803
|
+
```typescript
|
|
804
|
+
import { BridgeKit } from '@circle-fin/bridge-kit'
|
|
805
|
+
|
|
806
|
+
const kit = new BridgeKit()
|
|
807
|
+
|
|
808
|
+
// Check if your transfer route is supported
|
|
809
|
+
const isSupported = kit.supportsRoute('Ethereum', 'Base', 'USDC')
|
|
810
|
+
if (!isSupported) {
|
|
811
|
+
console.log('This route is not available through CCTPv2')
|
|
812
|
+
}
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
**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
|
|
816
|
+
|
|
817
|
+
#### "Insufficient balance" Error
|
|
818
|
+
|
|
819
|
+
**Check your USDC balance before transfer:**
|
|
820
|
+
|
|
821
|
+
> **π‘ Coming Soon**: The Kit will soon provide built-in balance checking methods. For now, you can check balances manually using the examples below.
|
|
822
|
+
|
|
823
|
+
```typescript
|
|
824
|
+
// On EVM chains (using viem)
|
|
825
|
+
import { createPublicClient, http, getContract } from 'viem'
|
|
826
|
+
import { mainnet } from 'viem/chains'
|
|
827
|
+
import { formatUnits } from 'viem'
|
|
828
|
+
import { Ethereum } from '@circle-fin/bridge-kit/chains'
|
|
829
|
+
|
|
830
|
+
const publicClient = createPublicClient({ chain: mainnet, transport: http() })
|
|
831
|
+
const usdcContract = getContract({
|
|
832
|
+
// Use the chain definitions from the kit to get the correct USDC address
|
|
833
|
+
address: Ethereum.usdcAddress,
|
|
834
|
+
abi: [
|
|
835
|
+
{
|
|
836
|
+
name: 'balanceOf',
|
|
837
|
+
type: 'function',
|
|
838
|
+
inputs: [{ name: 'account', type: 'address' }],
|
|
839
|
+
outputs: [{ name: '', type: 'uint256' }],
|
|
840
|
+
},
|
|
841
|
+
],
|
|
842
|
+
publicClient,
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
const usdcBalance = await usdcContract.read.balanceOf([walletAddress])
|
|
846
|
+
console.log('USDC Balance:', formatUnits(usdcBalance, 6))
|
|
847
|
+
|
|
848
|
+
// On Solana (check ATA balance)
|
|
849
|
+
import { Connection } from '@solana/web3.js'
|
|
850
|
+
|
|
851
|
+
const connection = new Connection('https://api.mainnet-beta.solana.com')
|
|
852
|
+
const ataBalance = await connection.getTokenAccountBalance(ataAddress)
|
|
853
|
+
console.log('USDC Balance:', ataBalance.value.uiAmount)
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
#### Network RPC Issues
|
|
857
|
+
|
|
858
|
+
**Symptoms**: Timeouts, connection errors, or slow responses
|
|
859
|
+
|
|
860
|
+
**Solutions**:
|
|
861
|
+
|
|
862
|
+
- Use reliable RPC endpoints (Alchemy, Infura, QuickNode)
|
|
863
|
+
- Implement retry logic for network calls
|
|
864
|
+
- Consider multiple RPC fallbacks
|
|
865
|
+
|
|
866
|
+
```typescript
|
|
867
|
+
import { createPublicClient, http, fallback } from 'viem'
|
|
868
|
+
import { mainnet } from 'viem/chains'
|
|
869
|
+
|
|
870
|
+
const publicClient = createPublicClient({
|
|
871
|
+
chain: mainnet,
|
|
872
|
+
transport: fallback([
|
|
873
|
+
http('https://eth-mainnet.alchemyapi.io/v2/your-key'),
|
|
874
|
+
http('https://mainnet.infura.io/v3/your-key'),
|
|
875
|
+
http(), // Default public RPC as fallback
|
|
876
|
+
]),
|
|
877
|
+
})
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
#### Transaction Stuck or Failed
|
|
881
|
+
|
|
882
|
+
**For EVM chains**: Check transaction on the block explorer using the `txHash` from the result
|
|
883
|
+
|
|
884
|
+
**For Solana**: Use Solana Explorer or SolScan
|
|
885
|
+
|
|
886
|
+
**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.
|
|
887
|
+
|
|
888
|
+
### Recovery from Soft Errors
|
|
889
|
+
|
|
890
|
+
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.
|
|
891
|
+
|
|
892
|
+
> **π Coming Soon**: We're working on built-in recovery functionality that will allow you to pass the current transfer state directly to `kit.resume(bridgeResult)` for automatic retry logic. For now, you can use the manual recovery patterns below.
|
|
893
|
+
|
|
894
|
+
#### Understanding Transfer State
|
|
895
|
+
|
|
896
|
+
Every CCTPv2 transfer follows these steps:
|
|
897
|
+
|
|
898
|
+
1. **Approval** - Allow the contract to spend USDC
|
|
899
|
+
2. **DepositForBurn** - Burns USDC on source chain, generates attestation
|
|
900
|
+
3. **FetchAttestation** - Wait for Circle to sign the burn proof
|
|
901
|
+
4. **Mint** - Mint USDC on destination chain using the attestation
|
|
902
|
+
|
|
903
|
+
> **β οΈ 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.
|
|
904
|
+
|
|
905
|
+
```typescript
|
|
906
|
+
import { BridgeKit } from '@circle-fin/bridge-kit'
|
|
907
|
+
import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'
|
|
908
|
+
|
|
909
|
+
// Start a transfer that might fail
|
|
910
|
+
const kit = new BridgeKit()
|
|
911
|
+
const result = await kit.bridge({
|
|
912
|
+
from: sourceAdapter,
|
|
913
|
+
to: destAdapter,
|
|
914
|
+
amount: '100.0',
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
// Check which steps completed successfully
|
|
918
|
+
console.log('Transfer state:', result.state)
|
|
919
|
+
console.log('Steps:', result.steps)
|
|
920
|
+
|
|
921
|
+
/*
|
|
922
|
+
Expected output for partial failure:
|
|
923
|
+
result.state: 'error'
|
|
924
|
+
result.steps: [
|
|
925
|
+
{ name: 'approve', state: 'success', txHash: '0x123...' },
|
|
926
|
+
{ name: 'depositForBurn', state: 'success', txHash: '0x456...' },
|
|
927
|
+
{ name: 'fetchAttestation', state: 'error', error: 'Network timeout' },
|
|
928
|
+
// mint step won't be present since fetchAttestation failed
|
|
929
|
+
]
|
|
930
|
+
*/
|
|
931
|
+
|
|
932
|
+
// Helper function to find specific steps
|
|
933
|
+
const getStep = (stepName: string) =>
|
|
934
|
+
result.steps.find((step) => step.name === stepName)
|
|
935
|
+
const approveStep = getStep('approve')
|
|
936
|
+
const burnStep = getStep('depositForBurn')
|
|
937
|
+
const attestationStep = getStep('fetchAttestation')
|
|
938
|
+
const mintStep = getStep('mint')
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
#### Recovery Pattern 1: Failed Attestation Fetch
|
|
942
|
+
|
|
943
|
+
If the attestation fetch fails due to network issues but the burn completed, you can retry just the fetchAttestation step:
|
|
944
|
+
|
|
945
|
+
> **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.
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'
|
|
949
|
+
|
|
950
|
+
// Create provider instance for manual recovery
|
|
951
|
+
const provider = new CCTPV2BridgingProvider()
|
|
952
|
+
|
|
953
|
+
// Use existing contexts or reconstruct wallet contexts from the result and adapters
|
|
954
|
+
const sourceContext = {
|
|
955
|
+
adapter: sourceAdapter,
|
|
956
|
+
address: result.source.address,
|
|
957
|
+
chain: 'Ethereum',
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const destContext = {
|
|
961
|
+
adapter: destAdapter,
|
|
962
|
+
address: result.destination.address,
|
|
963
|
+
chain: 'Base',
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Check if fetchAttestation specifically failed
|
|
967
|
+
const attestationStep = result.steps.find(
|
|
968
|
+
(step) => step.name === 'fetchAttestation',
|
|
969
|
+
)
|
|
970
|
+
const burnStep = result.steps.find((step) => step.name === 'depositForBurn')
|
|
971
|
+
|
|
972
|
+
if (burnStep?.state === 'success' && attestationStep?.state === 'error') {
|
|
973
|
+
console.log('Burn completed but attestation fetch failed. Retrying...')
|
|
974
|
+
|
|
975
|
+
try {
|
|
976
|
+
// Retry fetching the attestation using the burn transaction hash
|
|
977
|
+
const attestationResult = await provider.fetchAttestation(
|
|
978
|
+
sourceContext, // proper wallet context with adapter
|
|
979
|
+
burnStep.txHash,
|
|
980
|
+
)
|
|
981
|
+
console.log('Attestation fetch retry successful')
|
|
982
|
+
|
|
983
|
+
// Continue with minting using the attestation data
|
|
984
|
+
const mintRequest = await provider.mint(
|
|
985
|
+
sourceContext, // proper source context
|
|
986
|
+
destContext, // proper destination context
|
|
987
|
+
attestationResult, // attestation data
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
// Execute the mint transaction
|
|
991
|
+
if (mintRequest.type !== 'noop') {
|
|
992
|
+
const mintTxHash = await mintRequest.execute()
|
|
993
|
+
const mintReceipt = await destAdapter.waitForTransaction(mintTxHash)
|
|
994
|
+
console.log('Recovery completed:', mintTxHash)
|
|
995
|
+
}
|
|
996
|
+
} catch (error) {
|
|
997
|
+
console.error('Attestation retry failed:', error)
|
|
998
|
+
|
|
999
|
+
// Implement exponential backoff for attestation fetching
|
|
1000
|
+
const maxRetries = 5
|
|
1001
|
+
let retryCount = 0
|
|
1002
|
+
const baseDelay = 10000 // Start with 10 seconds
|
|
1003
|
+
|
|
1004
|
+
while (retryCount < maxRetries) {
|
|
1005
|
+
try {
|
|
1006
|
+
const delay = baseDelay * Math.pow(2, retryCount)
|
|
1007
|
+
console.log(`Retry ${retryCount + 1}/${maxRetries} in ${delay}ms...`)
|
|
1008
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
1009
|
+
|
|
1010
|
+
const attestationResult = await provider.fetchAttestation(
|
|
1011
|
+
sourceContext,
|
|
1012
|
+
burnStep.txHash,
|
|
1013
|
+
)
|
|
1014
|
+
console.log('Attestation fetched on retry', retryCount + 1)
|
|
1015
|
+
break
|
|
1016
|
+
} catch (retryError) {
|
|
1017
|
+
retryCount++
|
|
1018
|
+
if (retryCount === maxRetries) {
|
|
1019
|
+
console.error('All attestation retries failed')
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
} else if (!burnStep || burnStep.state !== 'success') {
|
|
1025
|
+
console.error(
|
|
1026
|
+
'Cannot retry attestation: burn step not completed successfully',
|
|
1027
|
+
)
|
|
1028
|
+
} else if (!attestationStep) {
|
|
1029
|
+
console.error('Attestation step not found in transfer result')
|
|
1030
|
+
}
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
#### Recovery Pattern 2: Failed Mint Step
|
|
1034
|
+
|
|
1035
|
+
If minting fails due to gas issues or network problems:
|
|
1036
|
+
|
|
1037
|
+
> **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.
|
|
1038
|
+
|
|
1039
|
+
```typescript
|
|
1040
|
+
import { parseGwei } from 'viem'
|
|
1041
|
+
import { CCTPV2BridgingProvider } from '@circle-fin/provider-cctp-v2'
|
|
1042
|
+
|
|
1043
|
+
const provider = new CCTPV2BridgingProvider()
|
|
1044
|
+
|
|
1045
|
+
// Use existing contexts or reconstruct wallet contexts from the result and adapters
|
|
1046
|
+
const sourceContext = {
|
|
1047
|
+
adapter: sourceAdapter,
|
|
1048
|
+
address: result.source.address,
|
|
1049
|
+
chain: 'Ethereum'
|
|
1050
|
+
|
|
1051
|
+
const destContext = {
|
|
1052
|
+
adapter: destAdapter,
|
|
1053
|
+
address: result.destination.address,
|
|
1054
|
+
chain: 'Base''
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Check if attestation was fetched successfully but mint failed
|
|
1058
|
+
const attestationStep = result.steps.find(
|
|
1059
|
+
(step) => step.name === 'fetchAttestation',
|
|
1060
|
+
)
|
|
1061
|
+
const mintStep = result.steps.find((step) => step.name === 'mint')
|
|
1062
|
+
|
|
1063
|
+
if (attestationStep?.state === 'success' && mintStep?.state === 'error') {
|
|
1064
|
+
try {
|
|
1065
|
+
// Retry the mint with the attestation data
|
|
1066
|
+
const mintRequest = await provider.mint(
|
|
1067
|
+
sourceContext,
|
|
1068
|
+
destContext,
|
|
1069
|
+
attestationStep.data, // The attestation data from the successful step
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
// Execute with custom gas settings for the retry
|
|
1073
|
+
if (mintRequest.type !== 'noop') {
|
|
1074
|
+
// For EVM chains, you can modify gas settings on the adapter
|
|
1075
|
+
if (result.destination.blockchain === 'evm') {
|
|
1076
|
+
// Override gas price for the retry (EVM chains)
|
|
1077
|
+
destAdapter.walletClient.gasPrice = parseGwei('25') // Higher gas price
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const retryTxHash = await mintRequest.execute()
|
|
1081
|
+
const receipt = await destAdapter.waitForTransaction(retryTxHash)
|
|
1082
|
+
console.log('Mint retry successful:', retryTxHash)
|
|
1083
|
+
}
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
console.error('Mint retry failed:', error)
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
#### Recovery Best Practices
|
|
1091
|
+
|
|
1092
|
+
1. **Save Transfer State**: Always persist the transfer result for recovery
|
|
1093
|
+
2. **Check Step Status**: Verify which steps completed before attempting recovery
|
|
1094
|
+
3. **Use Appropriate Timeouts**: Give network operations enough time to complete
|
|
1095
|
+
4. **Implement Exponential Backoff**: For retry logic, use increasing delays
|
|
1096
|
+
5. **Monitor Gas Prices**: Adjust gas settings during network congestion
|
|
1097
|
+
|
|
1098
|
+
```typescript
|
|
1099
|
+
// Example: Robust retry with exponential backoff
|
|
1100
|
+
async function retryWithBackoff<T>(
|
|
1101
|
+
operation: () => Promise<T>,
|
|
1102
|
+
maxRetries = 3,
|
|
1103
|
+
baseDelay = 1000,
|
|
1104
|
+
): Promise<T> {
|
|
1105
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1106
|
+
try {
|
|
1107
|
+
return await operation()
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
if (attempt === maxRetries) throw error
|
|
1110
|
+
|
|
1111
|
+
const delay = baseDelay * Math.pow(2, attempt - 1)
|
|
1112
|
+
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`)
|
|
1113
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
throw new Error('All retry attempts failed')
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Usage for resilient attestation fetching (requires sourceContext from above)
|
|
1120
|
+
const attestation = await retryWithBackoff(
|
|
1121
|
+
() => provider.fetchAttestation(sourceContext, burnTxHash),
|
|
1122
|
+
5, // 5 retries
|
|
1123
|
+
2000, // Start with 2 second delay
|
|
1124
|
+
)
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
### Debugging Tips
|
|
1128
|
+
|
|
1129
|
+
1. **Monitor gas prices**: High network congestion can cause failures
|
|
1130
|
+
2. **Test on testnets first**: Always test your integration on testnets before mainnet
|
|
1131
|
+
3. **Use block explorers**: Always verify transaction status on-chain
|
|
1132
|
+
4. **Save intermediate results**: Persist transfer state for recovery scenarios
|
|
1133
|
+
|
|
1134
|
+
## Best Practices
|
|
1135
|
+
|
|
1136
|
+
1. **Always estimate first**: Use `kit.estimate()` to show costs before transfers
|
|
1137
|
+
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
|
|
1138
|
+
3. **Handle errors gracefully**: Network issues and validation errors are common
|
|
1139
|
+
4. **Monitor events**: Use event listeners for real-time progress updates
|
|
1140
|
+
5. **Validate inputs**: The kit validates automatically, but client-side validation improves UX
|
|
1141
|
+
|
|
1142
|
+
## Next Steps
|
|
1143
|
+
|
|
1144
|
+
- **Explore Examples**: Check out the [examples directory](https://github.com/circlefin/stablecoin-kits-private/tree/main/examples) for more detailed implementations
|
|
1145
|
+
- **Join the community**: Connect with other developers on [discord](https://discord.com/invite/buildoncircle) building on Circle's stablecoin infrastructure
|
|
1146
|
+
|
|
1147
|
+
Ready to start bridging? Let's make cross-chain bridging as easy as a single function call! π
|