Ethers v6 Adapter

Type-safe EVM blockchain adapter powered by Ethers v6
Seamlessly interact with 16+ EVM networks using a single, strongly-typed interface
Table of Contents
Overview
The Ethers v6 Adapter is a strongly-typed implementation of the Adapter interface for EVM-compatible blockchains. Built on top of the popular Ethers v6 library, it provides type-safe blockchain interactions through a unified interface that's designed to work seamlessly with the Bridge Kit for cross-chain USDC transfers between Solana and EVM networks, as well as any future kits for additional stablecoin operations. It can be used by any Kit built using the Stablecoin Kits architecture and/or any providers plugged into those kits.
Why Ethers v6 Adapter?
- π§ Bring your own setup: Use your existing Ethers
JsonRpcProvider and Wallet instances
- β‘ EVM-compatible: Works with Ethereum, Base, Arbitrum, and all EVM chains
- π Type-safe: Built with TypeScript strict mode for complete type safety
- π― Simple API: Clean abstraction over complex blockchain operations
- π Transaction lifecycle - Complete prepare/estimate/execute workflow
- π Cross-chain ready - Seamlessly bridge USDC between EVM chains and Solana
When to Use This Adapter
For Kit Users
If you're using the Bridge Kit or other Stablecoin Kits for cross-chain operations, you only need to instantiate one adapter and pass it to the kit. The same adapter works across all supported chains.
const adapter = createAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})
const adapter1 = createAdapterFromPrivateKey({
privateKey: '0x1234...',
})
const adapter2 = createAdapterFromPrivateKey({
privateKey: '1234...',
})
For Kit Provider Developers
If you're building a provider (e.g., a custom BridgingProvider implementation), you'll use the adapter's abstracted methods to interact with different chains. The OperationContext pattern makes multi-chain operations seamless.
Installation
npm install @circle-fin/adapter-ethers-v6 ethers
yarn add @circle-fin/adapter-ethers-v6 ethers
Peer Dependencies
This adapter requires ethers (v6) as a peer dependency. Install it alongside the adapter:
npm install @circle-fin/adapter-ethers-v6 ethers
yarn add @circle-fin/adapter-ethers-v6 ethers
Supported Versions: ^6.11.0 (6.11.x through 6.x.x, excluding 7.x.x)
Troubleshooting Version Conflicts
If you encounter peer dependency warnings:
- Check your
ethers version: npm ls ethers
- Ensure ethers v6 is between 6.11.0 and 7.0.0 (exclusive)
- Use
npm install ethers@^6.11.0 to install a compatible version
- Note: This adapter is not compatible with ethers v5 or v7
Quick Start
Zero-Config Setup (Recommended)
The simplest way to get started with lazy initialization. Default configuration handles adapter setup automatically.
import { createAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6'
const adapter = createAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})
const prepared = await adapter.prepare(
{
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: usdcAbi,
functionName: 'transfer',
args: ['0xrecipient', '1000000'],
},
{ chain: 'Ethereum' },
)
const txHash = await prepared.execute()
Production Setup
For production use, provide custom RPC endpoints for better reliability and performance:
import { createAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6'
import { JsonRpcProvider } from 'ethers'
import { Ethereum, Base, Polygon } from '@core/chains'
const adapter = createAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
getProvider: ({ chain }) => {
const rpcEndpoints: Record<string, string> = {
Ethereum: `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
Base: `https://base-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
Polygon: `https://polygon-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
}
const endpoint = rpcEndpoints[chain.name]
if (!endpoint) {
throw new Error(`RPC endpoint not configured for chain: ${chain.name}`)
}
return new JsonRpcProvider(endpoint, chain.chainId)
},
capabilities: {
supportedChains: [Ethereum, Base, Polygon],
},
})
β οΈ Production Note: Default factory methods use public RPC endpoints which may have rate limits. For production, use dedicated providers like Alchemy, Infura, or QuickNode.
Browser Wallet Setup
For browser environments with MetaMask or WalletConnect:
import { createAdapterFromProvider } from '@circle-fin/adapter-ethers-v6'
const adapter = await createAdapterFromProvider({
provider: window.ethereum,
})
const prepared = await adapter.prepare(
{
address: '0xcontract',
abi: contractAbi,
functionName: 'approve',
args: ['0xspender', '1000000'],
},
{ chain: 'Polygon' },
)
OperationContext Pattern
Why OperationContext?
The OperationContext pattern is the modern approach for multi-chain operations. Instead of locking an adapter to a single chain, you specify the chain per operation. This enables powerful patterns like using a single adapter for cross-chain bridging.
Benefits:
- β
One adapter, many chains - No need to create separate adapters for each network
- β
Explicit is better - Chain is always clear in your code
- β
Type-safe - Full TypeScript support with compile-time checks
- β
Eliminates ambiguity - No confusion about which chain is being used
Basic Usage
Every operation accepts an OperationContext parameter that specifies the chain:
import { createAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6'
const adapter = createAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})
const prepared = await adapter.prepare(
{
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: usdcAbi,
functionName: 'transfer',
args: ['0xrecipient', '1000000'],
},
{ chain: 'Ethereum' },
)
const gas = await prepared.estimate()
const txHash = await prepared.execute()
Multi-Chain Operations
Use a single adapter instance for operations across multiple chains:
const adapter = createAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})
const ethPrepared = await adapter.prepare(
{
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: usdcAbi,
functionName: 'transfer',
args: ['0xrecipient', '1000000'],
},
{ chain: 'Ethereum' },
)
const basePrepared = await adapter.prepare(
{
address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
abi: usdcAbi,
functionName: 'transfer',
args: ['0xrecipient', '1000000'],
},
{ chain: 'Base' },
)
await ethPrepared.execute()
await basePrepared.execute()
Address Context Guide
The adapter supports two address control patterns. Choose the one that fits your use case.
User-Controlled (Recommended)
Best for: Private key wallets, browser wallets (MetaMask), hardware wallets
How it works: Address is automatically resolved from the connected signer/wallet. You don't need to specify it in the OperationContext.
When to use:
- β
Building a dApp where users connect their wallets
- β
Using a private key for backend automation
- β
Single wallet signing all transactions
- β
Server-side scripts with one identity
const adapter = createAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})
const prepared = await adapter.prepare(
{
address: '0xcontract',
abi: contractAbi,
functionName: 'approve',
args: ['0xspender', '1000000'],
},
{ chain: 'Polygon' },
)
Developer-Controlled (Advanced)
Best for: Custody solutions, multi-entity systems, enterprise applications
How it works: Address must be explicitly provided in the OperationContext for each operation.
When to use:
- β
Building a custody solution managing multiple client wallets
- β
Enterprise system where different users have different signing keys
- β
Multi-sig or delegated signing infrastructure
- β
Systems where address varies per transaction
import { EthersAdapter } from '@circle-fin/adapter-ethers-v6'
import { Ethereum, Base } from '@core/chains'
const adapter = new EthersAdapter(
{
getProvider: ({ chain }) => new JsonRpcProvider('https://...'),
signer: wallet,
},
{
addressContext: 'developer-controlled',
supportedChains: [Ethereum, Base],
},
)
const prepared = await adapter.prepare(
{
address: '0xcontract',
abi: contractAbi,
functionName: 'approve',
args: ['0xspender', '1000000'],
},
{
chain: 'Ethereum',
address: '0x1234...',
},
)
Usage Examples
Contract Interactions
Transfer USDC across different chains with the same adapter:
import { createAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6'
import { parseAbi } from 'ethers'
const adapter = createAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})
const usdcAbi = parseAbi([
'function transfer(address to, uint256 amount) returns (bool)',
])
const ethPrepared = await adapter.prepare(
{
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: usdcAbi,
functionName: 'transfer',
args: ['0xrecipient', '1000000'],
},
{ chain: 'Ethereum' },
)
const gas = await ethPrepared.estimate()
console.log('Estimated gas:', gas.gas)
const txHash = await ethPrepared.execute()
console.log('Transaction hash:', txHash)
EIP-712 Signatures
Sign permit approvals for gasless token approvals:
import { createAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6'
const adapter = createAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})
const signature = await adapter.signTypedData(
{
domain: {
name: 'USD Coin',
version: '2',
chainId: 1,
verifyingContract: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
},
types: {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
},
primaryType: 'Permit',
message: {
owner: '0xowner',
spender: '0xspender',
value: '1000000',
nonce: '0',
deadline: '1735689600',
},
},
{ chain: 'Ethereum' },
)
console.log('Permit signature:', signature)
Cross-Chain Bridging
Bridge USDC using the Bridge Kit with OperationContext:
import { createAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6'
import { BridgeKit } from '@circle-fin/bridge-kit'
const adapter = createAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})
const kit = new BridgeKit()
const result = await kit.bridge({
from: { adapter, chain: 'Ethereum' },
to: { adapter, chain: 'Base' },
amount: '100.50',
token: 'USDC',
})
console.log('Bridge transaction:', result.transactionHash)
API Reference
Factory Functions
createAdapterFromPrivateKey(params)
Creates an adapter from a private key for server-side use.
Parameters:
privateKey - 32-byte hex string with 0x prefix
getProvider? - Optional custom provider function
capabilities? - Optional partial capabilities (defaults: user-controlled + all EVM chains)
Returns: EthersAdapter instance with lazy initialization
Note: No chain required at creation time. The adapter connects to chains lazily on first operation.
const adapter = createAdapterFromPrivateKey({
privateKey: '0x...',
})
createAdapterFromProvider(params)
Creates an adapter from a browser wallet provider (MetaMask, WalletConnect, etc.).
Parameters:
provider - EIP-1193 compatible provider
getProvider? - Optional custom provider function
capabilities? - Optional partial capabilities (defaults: user-controlled + all EVM chains)
Returns: Promise<EthersAdapter> instance
const adapter = await createAdapterFromProvider({
provider: window.ethereum,
})
Core Methods
prepare(params, ctx)
Prepares a contract function call for estimation and execution.
Parameters:
params - Contract call parameters (address, abi, functionName, args)
ctx - Required OperationContext with chain specification
Returns: Promise<PreparedChainRequest> with estimate() and execute() methods
const prepared = await adapter.prepare(
{
address: '0xcontract',
abi: contractAbi,
functionName: 'transfer',
args: ['0xto', '1000000'],
},
{ chain: 'Ethereum' },
)
signTypedData(typedData, ctx)
Signs EIP-712 typed data for permits, meta-transactions, etc.
Parameters:
typedData - EIP-712 structured data
ctx - Required OperationContext with chain specification
Returns: Promise<string> - Signature as hex string
const signature = await adapter.signTypedData(permitData, {
chain: 'Ethereum',
})
waitForTransaction(txHash, config?)
Waits for transaction confirmation.
Parameters:
txHash - Transaction hash to wait for
config? - Optional wait configuration (confirmations, timeout)
Returns: Promise<TransactionReceipt>
const receipt = await adapter.waitForTransaction('0x...')
getAddress(chain)
Gets the connected wallet address. Chain parameter is provided automatically by OperationContext resolution.
Returns: Promise<string> - Wallet address
Token Operations
Built-in token operations using the action system:
const balance = await adapter.actions.usdc.balanceOf({
address: '0xwallet',
chain: 'Ethereum',
})
const allowance = await adapter.actions.token.allowance({
tokenAddress: '0xtoken',
owner: '0xowner',
spender: '0xspender',
chain: 'Base',
})
Supported Chains & Routes
The Ethers v6 adapter supports 34 EVM-compatible chains across mainnet and testnet environments through Circle's CCTP v2 protocol:
Mainnet Chains (17 chains)
Arbitrum, Avalanche, Base, Celo, Codex, Ethereum, HyperEVM, Ink, Linea, OP Mainnet, Plume, Polygon PoS, Sonic, Unichain, World Chain, XDC, ZKSync Era
Testnet Chains (17 chains)
Arbitrum Sepolia, Avalanche Fuji, Base Sepolia, Celo Alfajores, Codex Testnet, Ethereum Sepolia, HyperEVM Testnet, Ink Testnet, Linea Sepolia, OP Sepolia, Plume Testnet, Polygon PoS Amoy, Sonic Testnet, Unichain Sepolia, World Chain Sepolia, XDC Apothem, ZKSync Era Sepolia
Development
This package is part of the Stablecoin Kits monorepo.
nx build @circle-fin/adapter-ethers-v6
nx test @circle-fin/adapter-ethers-v6
License
This project is licensed under the Apache 2.0 License. Contact support for details.