@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/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! πŸŒ‰