npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@circle-fin/adapter-ethers-v6

v1.9.1

Published

EVM blockchain adapter powered by Ethers v6

Readme

Ethers v6 Adapter

npm version TypeScript License

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

// Single adapter instance for multi-chain operations
// Note: Private keys can be provided with or without '0x' prefix
const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`, // Both '0x...' and '...' work
})

// Both formats are automatically normalized:
const adapter1 = createEthersAdapterFromPrivateKey({
  privateKey: '0x1234...', // With prefix ✅
})

const adapter2 = createEthersAdapterFromPrivateKey({
  privateKey: '1234...', // Without prefix ✅ (automatically normalized)
})

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
# or
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
# or
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 { createEthersAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6'

// Minimal configuration with lazy initialization
// Note: Private keys work with or without '0x' prefix
const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`, // Both '0x...' and '...' work
  // Defaults applied:
  // - addressContext: 'user-controlled'
  // - supportedChains: all EVM chains (~34 networks)
  // - Lazy initialization: wallet connects to chain on first operation
})

// Chain specified per operation via OperationContext
const prepared = await adapter.prepare(
  {
    address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
    abi: usdcAbi,
    functionName: 'transfer',
    args: ['0xrecipient', '1000000'],
  },
  { chain: 'Ethereum' }, // Chain specified in context
)

const txHash = await prepared.execute()

Production Setup

For production use, provide custom RPC endpoints for better reliability and performance:

import { createEthersAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6'
import { JsonRpcProvider } from 'ethers'
import { Ethereum, Base, Polygon } from '@circle-fin/bridge-kit/chains'

// Production-ready with custom RPC endpoints and lazy initialization
const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
  // Custom RPC provider with explicit chain mapping
  getProvider: ({ chain }) => {
    // Map chain names to RPC endpoints
    // Customize this mapping based on your RPC provider
    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)
  },
  // Optionally restrict to specific chains
  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 { createEthersAdapterFromProvider } from '@circle-fin/adapter-ethers-v6'

// Minimal browser wallet configuration
const adapter = await createEthersAdapterFromProvider({
  provider: window.ethereum,
  // Default capabilities applied automatically
})

// User will be prompted to connect wallet
// Address is automatically resolved from connected wallet
const prepared = await adapter.prepare(
  {
    address: '0xcontract',
    abi: contractAbi,
    functionName: 'approve',
    args: ['0xspender', '1000000'],
  },
  { chain: 'Polygon' },
)

Next-Generation API (/next)

Preview — The /next entrypoint is the upcoming default adapter architecture. It is at parity with the current entrypoint for the supported bridge flows and fully compatible with all existing kits (Bridge Kit, etc.). In the next major release, /next will become the default import and the current entrypoint will be retired. See Current limitations of /next before adopting it.

Getting Started with /next

Switch to the new architecture by changing a single import path. Everything else stays the same:

import { createEthersAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6/next'

const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as string,
})

// Pass it to the Bridge Kit exactly as before — no changes needed downstream

Tip: The /next factories accept privateKey as a plain string — the 0x prefix is optional and normalized automatically. No template literal type assertions needed.

Browser wallets work identically:

import { createEthersAdapterFromProvider } from '@circle-fin/adapter-ethers-v6/next'
import { Ethereum, Base, Polygon } from '@circle-fin/bridge-kit/chains'

const adapter = createEthersAdapterFromProvider({
  provider: window.ethereum,
  capabilities: {
    addressContext: 'user-controlled',
    supportedChains: [Ethereum, Base, Polygon],
  },
})

createEthersAdapterFromProvider also accepts an ethers Provider directly — useful for server-side workflows where you already maintain a JsonRpcProvider and want the adapter to reuse it instead of wrapping the input in a per-chain BrowserProvider:

import { createEthersAdapterFromProvider } from '@circle-fin/adapter-ethers-v6/next'
import { JsonRpcProvider } from 'ethers'

const provider = new JsonRpcProvider(process.env.RPC_URL)

const adapter = createEthersAdapterFromProvider({ provider })

The factory detects the input shape at runtime: an EIP-1193 provider ({ request }) is wrapped per chain, while an ethers Provider ({ getNetwork }) is used directly and returns its own getSigner() result.

Why /next?

| | Current entrypoint | /next entrypoint | | ---------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | Bundle size | Entire adapter is included | Per-primitive imports unlock tree-shaking; the default factory still pulls the compat shim — see bundle-size note | | Prepare result | estimate() and execute() | estimate(), simulate(), and execute(overrides?) | | Fee model | { gas, gasPrice, fee } | { fee, units, unitPrice } — consistent across all ecosystems | | Observability | Not available | Built-in logging, metrics, and event instrumentation | | Retry & resilience | Not available | Configurable retry with exponential backoff for transient failures | | Token resolution | Hard-coded addresses | Pluggable token registry — resolve any token by symbol across chains | | Transaction tuning | Fixed gas defaults | Configurable fee buffer, priority-fee floor, and confirmation timeout |

Bundle-size note

The /next entrypoint's clean architecture is designed to be tree-shakable on a per-primitive basis: importing createPrepare without createBatchExecute should only ship the prepare path. In the current preview release the default createAdapter factory still pulls in the legacy compat shim (withLegacyCompat@core/adapter-compat's action registry) so that consumers can pass the resulting adapter to existing kits and providers without changes. That shim is what makes the default /next bundle measure ~11% larger than the legacy entrypoint in our internal benchmarks.

If bundle size is critical to you, compose primitives directly via assembleAdapter from @core/adapter-base and skip withLegacyCompat. A future minor release will split the compat shim behind a separate entrypoint so the default factory ships the new API only.

Current limitations of /next

The /next Ethers adapter is at parity with the current entrypoint for the supported bridge flows, but a few capabilities are still in progress. They are tracked in the project issue tracker for follow-up releases:

  • No atomic batched approve+burn. EIP-5792 wallet_sendCalls batching is only available on the Viem /next adapter; the Ethers adapter (and the CCTP /next bridge flow when driven by it) submits approve and burn as two separate transactions.
  • The Circle Wallets adapter has not been migrated to /next. Only the EVM (viem/ethers) and Solana adapters expose a /next entrypoint; Circle Wallets remains on the current entrypoint.
  • Abort semantics stop the local wait only. Passing an aborted (or abortable) signal cancels waitForTransaction and prevents an unsent execute from broadcasting, but once a transaction has been broadcast, aborting does not cancel it on-chain — the transaction continues and should be reconciled via its hash.

Prepared Transactions: Estimate, Simulate, Execute

When you prepare a transaction through the /next adapter, you get back a richer object with three lifecycle methods:

const prepared = await adapter.prepare(
  {
    address: '0xContractAddress',
    abi: contractAbi,
    functionName: 'transfer',
    args: ['0xRecipient', 1_000_000n],
  },
  { chain: 'Base' },
)

// 1. Estimate gas and fees before committing
const { fee, units, unitPrice } = await prepared.estimate()

// 2. Simulate the transaction to catch reverts without spending gas
const simulation = await prepared.simulate()

// 3. Execute when ready (with optional gas overrides)
const result = await prepared.execute()

// `result` is a `{ txId, wait }` envelope:
// - `txId` is the transaction hash on EVM (the field is named `txId`
//   rather than `hash` so the same shape works across ecosystems).
// - `wait()` resolves once the transaction is confirmed on-chain.
console.log('submitted tx:', result.txId)
const confirmation = await result.wait()
console.log('confirmed in block:', confirmation.blockNumber)

The simulate() step is new in /next — it dry-runs the transaction against the current chain state and surfaces revert reasons before you spend gas.

Logging

Pass a runtime option to get structured logs for every adapter operation, including RPC calls, retries, and errors. The quickest way to see what's happening is to use the built-in createRuntime helper:

import {
  createEthersAdapterFromPrivateKey,
  createRuntime,
} from '@circle-fin/adapter-ethers-v6/next'

const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as string,
  runtime: createRuntime(), // Logs to stdout via pino at 'info' level
})

To integrate with your own logger, pass any object that implements debug, info, warn, error, and child to createRuntime. The logger interface uses (message, fields) argument order — libraries like pino use (fields, message), so a thin wrapper is needed:

import {
  createEthersAdapterFromPrivateKey,
  createRuntime,
} from '@circle-fin/adapter-ethers-v6/next'
import pino from 'pino'

function wrapPino(p: pino.Logger) {
  return {
    debug: (msg: string, fields?: Record<string, unknown>) =>
      p.debug(fields, msg),
    info: (msg: string, fields?: Record<string, unknown>) =>
      p.info(fields, msg),
    warn: (msg: string, fields?: Record<string, unknown>) =>
      p.warn(fields, msg),
    error: (msg: string, fields?: Record<string, unknown>) =>
      p.error(fields, msg),
    child: (tags: Record<string, unknown>) => wrapPino(p.child(tags)),
  }
}

const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as string,
  runtime: createRuntime({ logger: wrapPino(pino({ level: 'debug' })) }),
})

Metrics

The runtime option also accepts a metrics implementation for counters, histograms, and timers. Plug into Prometheus, StatsD, Datadog, OpenTelemetry, or any other backend:

import {
  createEthersAdapterFromPrivateKey,
  createRuntime,
} from '@circle-fin/adapter-ethers-v6/next'

const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as string,
  runtime: createRuntime({
    metrics: {
      counter: (name) => ({
        inc: (labels, value) => {
          /* emit counter */
        },
      }),
      histogram: (name) => ({
        observe: (labels, value) => {
          /* emit histogram */
        },
      }),
      timer: (name) => ({
        start: (labels) => {
          const t0 = Date.now()
          return () => {
            /* observe Date.now() - t0 */
          }
        },
      }),
      child: (labels) => {
        /* return scoped Metrics with base labels merged */
      },
    },
  }),
})

When no metrics is provided, metric calls are silently no-op'd — zero overhead.

Token Registry

By default the adapter knows about USDC (and other built-in tokens) on every supported chain. The tokens option lets you register additional tokens so they can be referenced by symbol instead of raw addresses.

import {
  createEthersAdapterFromPrivateKey,
  createTokenRegistry,
} from '@circle-fin/adapter-ethers-v6/next'

const tokens = createTokenRegistry({
  tokens: [
    {
      symbol: 'WETH',
      decimals: 18,
      locators: {
        Ethereum: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
        Base: '0x4200000000000000000000000000000000000006',
      },
    },
  ],
})

const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as string,
  tokens,
})

The registry resolves the correct contract address and decimals for each chain automatically, so downstream code can refer to 'WETH' rather than hard-coding addresses per network.

Transaction Tuning

The config option lets you tune gas pricing, retry behavior, and confirmation timeouts:

import { createEthersAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6/next'

const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as string,
  config: {
    transaction: {
      feePriceBufferBps: 2000n, // 20% buffer on gas pricing (default)
      minPriorityFeeWei: 1_500_000_000n, // 1.5 gwei priority-fee floor
      confirmationTimeoutMs: 120_000, // 2 minute confirmation timeout
    },
    retry: {
      maxAttempts: 3,
      baseDelayMs: 1000,
    },
  },
})

| Setting | What it controls | Default | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | feePriceBufferBps | Buffer on gas pricing to protect against price fluctuations between estimation and inclusion (basis points; 10 000 = 100%) | 2000n (20%) | | minPriorityFeeWei | Floor for maxPriorityFeePerGas — prevents near-zero tips from being silently dropped by validators | 1_500_000_000n (1.5 gwei) | | confirmationTimeoutMs | How long waitForTransaction waits before timing out | Client default | | retry.maxAttempts | Maximum number of retry attempts (not including the initial attempt) | 3 | | retry.baseDelayMs | Base delay between retries in milliseconds (doubles on each retry via exponential backoff) | 200 |

Set retry: false to disable automatic retries entirely.

Error Telemetry & Secret Redaction

KitError.cause.trace is the SDK's diagnostic surface for telemetry pipelines. It carries useful post-mortem context — the redacted rawError shape, the provider code, and a shortMessage field. The strings on these fields can include the configured RPC URL and any embedded API key, because ethers v6's makeError() formats info keys directly into the message string itself, e.g.:

missing response (requestUrl="https://mainnet.infura.io/v3/MY_KEY", code=SERVER_ERROR, ...)

The same pattern surfaces in viem's HttpRequestError.shortMessage. The SDK addresses this in two layers:

  1. Object-graph stripping (always on) — the request, info, originating XMLHttpRequest, and transport.url properties are never embedded on cause.trace. This has been the contract since the first round of the redactor work.
  2. Message-content scrubbing (default-on for ~12 known RPC providers; operator-extensible) — the SDK ships with defaultMessageRedactor active at module load, scrubbing API-key fragments out of the message strings ethers/viem format internally for the most common public RPC providers without any operator configuration. URL classification happens via the platform URL parser plus a host-suffix list (DEFAULT_RPC_HOST_SUFFIXES), so adding a new provider is a one-line change with no regex audit needed.

What the defaults catch out of the box

| Provider | Host suffix(es) | | --------------------- | ------------------------------------------------------------ | | Alchemy | .g.alchemy.com | | Infura | .infura.io | | QuickNode | .quiknode.pro | | Ankr | .ankr.com | | Chainstack | .p2pify.com | | Tenderly | .gateway.tenderly.co | | BlockPI | .blockpi.network | | GetBlock | .getblock.io | | Moralis | .moralis-nodes.com | | Helius (Solana) | .helius-rpc.com, .helius.xyz | | Triton (Solana) | .rpcpool.com | | Generic query strings | ?api-key=…, ?apikey=…, ?token=…, ?secret=…, ?key=… | | Generic auth headers | Bearer <token> |

Any URL whose host.endsWith(suffix) matches the table is replaced wholesale with [REDACTED]. The classification is done by the same URL parser the platform uses for fetch(), so it cannot accidentally match calldata, contract addresses, transaction hashes, or revert reasons (none of which parse as URLs).

Defense in depth, not a guarantee

Three things still need attention from you, even with defaults on:

  1. Custom or self-hosted RPC URLs aren't covered by defaults. Extend the host-suffix list with composeRedactor:

    import {
      composeRedactor,
      setMessageRedactor,
      DEFAULT_RPC_HOST_SUFFIXES,
    } from '@core/errors'
    
    setMessageRedactor(
      composeRedactor({
        hostSuffixes: [...DEFAULT_RPC_HOST_SUFFIXES, '.rpc.my-corp.example'],
      }),
    )
  2. Error messages are still untrusted output. Don't write them to durable storage (S3, log files, ticket systems, support transcripts) without your own log-shipping-layer scrub. The SDK protects telemetry pipelines that JSON.stringify cause.trace; it does not protect ad-hoc console.error(err) calls in your application code.

  3. Defaults can false-negative. New providers, URL format changes, or odd error shapes from a custom transport may slip through. Treat every error message as potentially containing a secret you don't recognize, and pin scrubbing at your log-shipping layer too (Sentry's denyUrls / beforeSend, Datadog's redact-paths, your own forwarder).

Other escape hatches

| Goal | Call | | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | Restore defaults after a custom policy | resetMessageRedactor() | | Read the active policy (e.g. /healthz) | getMessageRedactor() | | Disable redaction entirely (local dev / raw-provider tests) | setMessageRedactor(undefined) | | Length-preserving redaction (function form) | setMessageRedactor((msg) => msg.replace(/[A-Fa-f0-9]{32,64}/g, (m) => '*'.repeat(m.length))) |

The setter replaces the active policy — use composeRedactor({ hostSuffixes, patterns }) (or call defaultMessageRedactor(msg) from inside a function-form redactor) if you want to extend rather than replace. See setMessageRedactor's JSDoc for the contract.

Why on by default

The dominant pattern across modern SDKs (AWS SDK v3 middleware, Stripe's request logger, Sentry's Default Integrations, OpenTelemetry's URL/query-param scrubbers) is to ship secret scrubbing on by default for high-confidence patterns and let operators extend or disable. The opposite posture — "errors may contain secrets, scrub them yourself" — has historically been a frequent cause of credentials leaking into log aggregators across the JS ecosystem (notably with axios, which dumps the full request URL into error.config by default). The SDK adopts the conservative-defaults posture deliberately.

Migration Path

The /next entrypoint is designed for a zero-friction migration:

  1. Today — Import from @circle-fin/adapter-ethers-v6/next. The adapter returned is fully compatible with all existing kits and providers — no changes needed on their side.
  2. Next major release — The /next architecture becomes the default entrypoint (@circle-fin/adapter-ethers-v6). The current class-based adapter will be removed.

What do I need to change? If you only use factory functions (createEthersAdapterFromPrivateKey, createEthersAdapterFromProvider) and pass the adapter to a kit, nothing beyond the import path. The adapter satisfies the same interface that kits expect.

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 { createEthersAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6'

// Create adapter without specifying a chain - true lazy initialization
const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})

// Chain specified explicitly in every operation
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:

// Create adapter once for use across multiple chains
const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})

// Transfer USDC on Ethereum
const ethPrepared = await adapter.prepare(
  {
    address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
    abi: usdcAbi,
    functionName: 'transfer',
    args: ['0xrecipient', '1000000'],
  },
  { chain: 'Ethereum' },
)

// Transfer USDC on Base using the same adapter
const basePrepared = await adapter.prepare(
  {
    address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base
    abi: usdcAbi,
    functionName: 'transfer',
    args: ['0xrecipient', '1000000'],
  },
  { chain: 'Base' },
)

// Execute both transfers
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
// User-controlled adapter (default for factory functions)
const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
  // addressContext: 'user-controlled' is the default
})

// Address automatically resolved from private key/wallet
const prepared = await adapter.prepare(
  {
    address: '0xcontract',
    abi: contractAbi,
    functionName: 'approve',
    args: ['0xspender', '1000000'],
  },
  { chain: 'Polygon' }, // No address needed in context for user-controlled
)

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 '@circle-fin/bridge-kit/chains'

// Developer-controlled adapter (manual constructor)
const adapter = new EthersAdapter(
  {
    getProvider: ({ chain }) => new JsonRpcProvider('https://...'),
    signer: wallet,
  },
  {
    addressContext: 'developer-controlled', // ← Explicit address required
    supportedChains: [Ethereum, Base],
  },
)

// Address must be provided in context for developer-controlled adapters
const prepared = await adapter.prepare(
  {
    address: '0xcontract',
    abi: contractAbi,
    functionName: 'approve',
    args: ['0xspender', '1000000'],
  },
  {
    chain: 'Ethereum',
    address: '0x1234...', // Required for developer-controlled
  },
)

Usage Examples

Contract Interactions

Transfer USDC across different chains with the same adapter:

import { createEthersAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6'

// Create adapter with lazy initialization
const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})

const usdcAbi = ['function transfer(address to, uint256 amount) returns (bool)']

// Transfer on Ethereum - chain specified in operation
const ethPrepared = await adapter.prepare(
  {
    address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
    abi: usdcAbi,
    functionName: 'transfer',
    args: ['0xrecipient', '1000000'], // 1 USDC (6 decimals)
  },
  { chain: 'Ethereum' },
)

// Estimate and execute
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 { createEthersAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6'

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

// Sign ERC-2612 permit (gasless USDC approval)
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' }, // Chain must be specified
)

console.log('Permit signature:', signature)
// Use signature for gasless approval

Cross-Chain Bridging

Bridge USDC using the Bridge Kit with OperationContext:

import { createEthersAdapterFromPrivateKey } from '@circle-fin/adapter-ethers-v6'
import { BridgeKit } from '@circle-fin/bridge-kit'

// Create adapter for multi-chain operations
const adapter = createEthersAdapterFromPrivateKey({
  privateKey: process.env.PRIVATE_KEY as `0x${string}`,
})

const kit = new BridgeKit()

// Bridge from Ethereum to Base using the same adapter
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

createEthersAdapterFromPrivateKey(params)

Creates an adapter from a private key for server-side use.

Parameters:

  • privateKey - 32-byte hex string (with or without 0x prefix — normalized automatically)
  • 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 = createEthersAdapterFromPrivateKey({
  privateKey: '0x...',
})

createEthersAdapterFromProvider(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 createEthersAdapterFromProvider({
  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' }, // Required
)

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:

// Get USDC balance
const balance = await adapter.actions.usdc.balanceOf({
  address: '0xwallet',
  chain: 'Ethereum',
})

// Get token allowance
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 App Kits monorepo.

# Build
nx build @circle-fin/adapter-ethers-v6

# Test
nx test @circle-fin/adapter-ethers-v6

License

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


Ready to integrate?

Join DiscordVisit our Help-Desk

Built with ❤️ by Circle