@circle-fin/adapter-viem-v2
Viem v2 Adapter
Type-safe EVM blockchain adapter powered by Viem v2
Seamlessly interact with 16+ EVM networks using a single, strongly-typed interface
Table of Contents
- Viem v2 Adapter
Overview
The Viem v2 Adapter is a strongly-typed implementation of the Adapter interface for the EVM-compatible blockchains. Built on top of the popular Viem 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, 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 Viem Adapter?
- Zero-config defaults - Built-in reliable RPC endpoints for all supported EVM chains, no setup required
- Full EVM compatibility - Works with any Ethereum-compatible blockchain
- Bring your own setup: Use your existing Viem
PublicClientandWalletClientinstances - Instant connectivity - Connect to Ethereum, Base, Arbitrum, and more without researching RPC providers
- 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 and How Should I Use The Viem Adapter?
I'm a developer using a kit
If you're using one of the kits to do some action, e.g. bridging from chain 'A' to chain 'B', then you only need to instantiate the adapter for your chain and pass it to the kit.
Example
// Private keys can be provided with or without '0x' prefix
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string, // Works with or without '0x'
})
// Both formats are automatically normalized:
const adapter1 = createViemAdapterFromPrivateKey({
privateKey: '0x1234...', // With prefix ✅
})
const adapter2 = createViemAdapterFromPrivateKey({
privateKey: '1234...', // Without prefix ✅ (automatically normalized)
})I'm a developer making a Kit Provider
If you are making a provider for other Kit users to plug in to the kit, e.g. a BridgingProvider, and you'll need to interact with diff chains, then you'll need to use the abstracted Adapter methods to execute on chain.
Installation
npm install @circle-fin/adapter-viem-v2 viem
# or
yarn add @circle-fin/adapter-viem-v2 viemPeer Dependencies
This adapter requires viem as a peer dependency. Install it alongside the adapter:
npm install @circle-fin/adapter-viem-v2 viem
# or
yarn add @circle-fin/adapter-viem-v2 viemSupported Versions: ^2.30.0 (2.30.x through 2.x.x)
Troubleshooting Version Conflicts
If you encounter peer dependency warnings:
- Check your
viemversion:npm ls viem - Ensure viem is between 2.30.0 and 3.0.0 (exclusive)
- Use
npm install viem@^2.30.0to install a compatible version
Quick Start
Easy Setup with Factory Methods (Recommended)
The simplest way to get started is with our factory methods. With reliable default RPC endpoints - no need to research providers or configure endpoints! Plus, you can create just one adapter and use it across different chains!
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { Ethereum, Base, Polygon } from '@circle-fin/bridge-kit/chains'
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string, // Both '0x...' and '...' work
capabilities: {
addressContext: 'user-controlled',
supportedChains: [Ethereum, Base, Polygon],
},
})
// Ready to use with the Bridge Kit!
// Address will be automatically resolved during operationsKey Feature: All chain definitions include reliable default RPC endpoints with automatic failover:
- Ethereum:
https://eth.merkle.io→https://ethereum.publicnode.com - Base:
https://mainnet.base.org→https://base.publicnode.com - Polygon:
https://polygon.publicnode.com→https://polygon.drpc.org - Unichain:
https://rpc.unichain.org→https://mainnet.unichain.org - Arbitrum:
https://arb1.arbitrum.io/rpc(single endpoint) - And all other supported EVM chains!
Automatic Failover: Chains with multiple endpoints automatically switch to backup RPCs if the primary fails!
Production Considerations
While the default RPC endpoints are reliable, you may want to use your own RPC providers for production applications:
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { Ethereum } from '@circle-fin/bridge-kit/chains'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
// Option 1: Use your own PublicClient with custom RPC
const customPublicClient = createPublicClient({
chain: mainnet,
transport: http('https://your-custom-rpc-endpoint.com'),
})
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string,
capabilities: {
addressContext: 'user-controlled',
supportedChains: [Ethereum],
},
getPublicClient: () => customPublicClient,
})Default RPC Benefits:
- Instant setup - Start building without provider research
- Reliable uptime - Cloudflare and official chain endpoints
- No API keys - Public endpoints with generous rate limits
- Development friendly - Perfect for prototyping and testing
When to use custom RPCs:
- High throughput - Your app needs dedicated bandwidth
- Specific features - Archive nodes, debug APIs, etc.
- Analytics - Custom monitoring and logging
- Compliance - Your organization requires specific providers
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { Ethereum, Base, Polygon } from '@circle-fin/bridge-kit/chains'
import { createPublicClient, http } from 'viem'
// Production-ready setup with custom RPC endpoints
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as `0x${string}`,
capabilities: {
addressContext: 'user-controlled',
supportedChains: [Ethereum, Base, Polygon],
},
getPublicClient: ({ chain }) =>
createPublicClient({
chain,
transport: http(
`https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`,
{
retryCount: 3,
timeout: 10000,
},
),
}),
})Browser Support with Wallet Providers
For browser environments with wallet providers like MetaMask:
import { createViemAdapterFromProvider } from '@circle-fin/adapter-viem-v2'
import { Ethereum, Base, Polygon } from '@circle-fin/bridge-kit/chains'
// Create an adapter from a browser wallet
const adapter = await createViemAdapterFromProvider({
provider: window.ethereum,
capabilities: {
addressContext: 'user-controlled',
supportedChains: [Ethereum, Base, Polygon],
},
})Advanced Manual Setup
For advanced patterns like lazy initialization or environment-adaptive configuration:
import { ViemAdapter } from '@circle-fin/adapter-viem-v2'
import { Ethereum, Base, Polygon } from '@circle-fin/bridge-kit/chains'
import { createPublicClient, createWalletClient, http } from 'viem'
import { mainnet } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
// Create clients manually for full control
const account = privateKeyToAccount(process.env.PRIVATE_KEY as string)
// Manual constructor with getter pattern and explicit capabilities
const adapter = new ViemAdapter(
{
getPublicClient: ({ chain }) =>
createPublicClient({
chain: mainnet,
transport: http('https://your-custom-rpc.com'),
}),
getWalletClient: async ({ chain }) => {
// Your custom logic here - sync or async, chain-aware
// The chain parameter allows you to create wallet clients for specific chains
return createWalletClient({
chain,
account,
transport: http('https://your-custom-rpc.com'),
})
},
},
{
addressContext: 'user-controlled', // or 'developer-controlled'
supportedChains: [Ethereum, Base, Polygon], // Specify supported chains
},
)Benefits: Lazy initialization, environment adaptation, custom caching logic.
Next-Generation API (/next)
Preview — The
/nextentrypoint 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,/nextwill become the default import and the current entrypoint will be retired. See Current limitations of/nextbefore adopting it.
Getting Started with /next
Switch to the new architecture by changing a single import path. Everything else stays the same:
import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2/next'
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string,
})
// Pass it to the Bridge Kit exactly as before — no changes needed downstreamBrowser wallets work identically:
import { createViemAdapterFromProvider } from '@circle-fin/adapter-viem-v2/next'
import { Ethereum, Base, Polygon } from '@circle-fin/bridge-kit/chains'
const adapter = createViemAdapterFromProvider({
provider: window.ethereum,
capabilities: {
addressContext: 'user-controlled',
supportedChains: [Ethereum, Base, Polygon],
},
})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 |
| Nonce management | Manual | Automatic via viem's nonceManager |
| 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 Viem 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:
- EIP-5792 batched approve+burn is not yet used by the CCTP
/nextbridge flow.batchExecutesupports EIP-5792wallet_sendCalls, but the CCTP/nextprovider still submits approve and burn as two separate transactions; it does not yet collapse them into a single atomic batch. createViemAdapterFromAccount/createViemAdapterFromWalletClientare deferred. OnlycreateViemAdapterFromPrivateKeyandcreateViemAdapterFromProviderare available on/nexttoday. To use an existing viemAccountorWalletClient, wrap it in an EIP-1193 provider or build the adapter viaassembleAdapterdirectly.- The Circle Wallets adapter has not been migrated to
/next. Only the EVM (viem/ethers) and Solana adapters expose a/nextentrypoint; Circle Wallets remains on the current entrypoint. - Abort semantics stop the local wait only. Passing an aborted (or
abortable)
signalcancelswaitForTransactionand prevents an unsentexecutefrom 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 canonical transaction identifier (string).
// On EVM this is the hex transaction hash; the field is named
// `txId` rather than `hash` so the same shape works for non-EVM
// ecosystems (Solana signatures, etc.).
// - `wait()` resolves once the transaction is confirmed on-chain and
// returns a `Confirmation` with the receipt details.
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 {
createViemAdapterFromPrivateKey,
createRuntime,
} from '@circle-fin/adapter-viem-v2/next'
const adapter = createViemAdapterFromPrivateKey({
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 {
createViemAdapterFromPrivateKey,
createRuntime,
} from '@circle-fin/adapter-viem-v2/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 = createViemAdapterFromPrivateKey({
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 {
createViemAdapterFromPrivateKey,
createRuntime,
} from '@circle-fin/adapter-viem-v2/next'
const adapter = createViemAdapterFromPrivateKey({
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 {
createViemAdapterFromPrivateKey,
createTokenRegistry,
} from '@circle-fin/adapter-viem-v2/next'
const tokens = createTokenRegistry({
tokens: [
{
symbol: 'WETH',
decimals: 18,
locators: {
Ethereum: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
Base: '0x4200000000000000000000000000000000000006',
},
},
],
})
const adapter = createViemAdapterFromPrivateKey({
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 { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2/next'
const adapter = createViemAdapterFromPrivateKey({
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.
Nonce note (same-key, multiple adapters).
createViemAdapterFromPrivateKeygives each adapter instance its own JSON-RPC-backed nonce manager, so distinct adapters (e.g. against different RPC endpoints) no longer share a stale process-wide counter. However, if you construct two or more adapter instances with the same private key and submit transactions concurrently (for example a primary/fallback RPC pair), each manager allocates nonces independently and they can collide. For same-key concurrent submission, route through a single adapter instance or supply a shared nonce source.
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 errorName, the shortMessage, and a reason field. The strings on these fields can include the configured RPC URL and any embedded API key, because viem's HTTP / WebSocket transports format the URL directly into error.message and error.shortMessage (the same is true for ethers v6's makeError()-formatted strings).
The SDK addresses this in two layers:
- Object-graph stripping (always on) — extra properties like
request,info,requestUrl, the originatingXMLHttpRequest, and the configuredtransport.urlare never embedded oncause.trace. This has been the contract since the first round of the redactor work. - Message-content scrubbing (default-on for ~12 known RPC providers; operator-extensible) — the SDK ships with
defaultMessageRedactoractive at module load, scrubbing API-key fragments out oferror.message,shortMessage, andreasonfor the most common public RPC providers without any operator configuration. URL classification happens via the platformURLparser 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:
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'], }), )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-hocconsole.error(err)calls in your application code.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'sredact-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:
- Today — Import from
@circle-fin/adapter-viem-v2/next. The adapter returned is fully compatible with all existing kits and providers — no changes needed on their side. - Next major release — The
/nextarchitecture becomes the default entrypoint (@circle-fin/adapter-viem-v2). The current class-based adapter will be removed.
What do I need to change? If you only use factory functions (createViemAdapterFromPrivateKey, createViemAdapterFromProvider) and pass the adapter to a kit, nothing beyond the import path. The adapter satisfies the same interface that kits expect.
OperationContext Pattern
The Viem v2 Adapter supports the OperationContext pattern for flexible per-operation chain and address specification. This enables advanced use cases like multi-chain operations and enterprise address management.
Address Context Modes
The adapter's behavior is determined by the addressContext capability:
'user-controlled' (Default, Recommended)
- Use case: Browser wallets, private keys, hardware wallets, development
- Address handling: Automatically resolved from connected wallet or key
- Best for: MetaMask, Coinbase Wallet, WalletConnect, private key development
- Key benefit: Simpler API - no need to pass address to every operation
When to use:
- Using a private key (
createViemAdapterFromPrivateKey) - Using a browser wallet (MetaMask, Coinbase Wallet, etc.)
- Single address with automatic resolution
- Building a standard dApp or backend service
// Private key adapter - uses 'user-controlled' by default
const adapter = createViemAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY,
// Defaults to 'user-controlled' - address resolved automatically
})
// Address is resolved automatically from wallet
await adapter.prepare(contractParams, { chain: 'Base' })'developer-controlled' (For Enterprise/Multi-Address Systems)
- Use case: Enterprise custody, multi-address management, institutional wallets
- Address handling: Must be explicitly provided for each operation
- Best for: Fireblocks, Circle Wallets, Coinbase Prime, custom custody solutions
- Key benefit: Flexibility to use different addresses per operation
When to use:
- Managing multiple addresses from a single provider (Fireblocks vaults)
- Requiring different addresses for different operations
- Custody solution with explicit address specification
- Integrating with institutional-grade wallet infrastructure
import { Ethereum, Base } from '@circle-fin/bridge-kit/chains'
// Enterprise custody - use 'developer-controlled'
const adapter = createViemAdapterFromProvider({
provider: fireblocksProvider,
capabilities: {
addressContext: 'developer-controlled',
supportedChains: [Ethereum, Base],
},
})
// Address must be specified per operation - enables multi-address flexibility
await adapter.prepare(contractParams, {
chain: 'Base',
address: '0x123...', // Required: specify which vault/address to use
})Decision Guide:
- Private keys: Use default (
'user-controlled') - Browser wallets: Use default (
'user-controlled') - Fireblocks/Custody: Use
'developer-controlled' - Single address: Use default (
'user-controlled') - Multiple addresses: Use
'developer-controlled'
OperationContext Usage
The second parameter to prepare() is required and specifies the operation context:
// OperationContext is required for all operations
const prepared = await adapter.prepare(
{
address: '0x...',
abi: contractAbi,
functionName: 'transfer',
args: ['0xto', '1000'],
},
{
chain: 'Base', // Chain specified in context
},
)API Reference
Constructor Options
The ViemAdapter constructor requires both configuration options and adapter capabilities:
constructor(options: ViemAdapterOptions, capabilities: AdapterCapabilities)
interface ViemAdapterOptions {
getPublicClient: (params: { chain: Chain }) => PublicClient
getWalletClient: (params: { chain: Chain }) => Promise<WalletClient> | WalletClient
}
interface AdapterCapabilities {
addressContext: 'user-controlled' | 'developer-controlled'
supportedChains: ChainDefinition[]
}getPublicClient
A function that returns a PublicClient for the specified chain.
- Type:
(params: { chain: Chain }) => PublicClient - Purpose: Provides read-only blockchain access for the given chain
- Called: Every time the adapter needs to read blockchain data or switch chains
getWalletClient
A function that returns a WalletClient for signing and sending transactions.
- Type:
(params: { chain: Chain }) => Promise<WalletClient> | WalletClient - Purpose: Provides wallet access for signing transactions and managing accounts
- Called: Only when needed (lazy initialization) and cached automatically per chain
- Supports: Both synchronous and asynchronous initialization
- Chain-aware: Requires chain parameter for explicit multi-chain wallet support
capabilities (Required)
Defines the adapter's capabilities and operational model.
- Type:
AdapterCapabilities - Purpose: Specifies address control model and supported chains
- addressContext:
'user-controlled'- Address managed by wallet (MetaMask, private keys)'developer-controlled'- Address must be specified per operation (enterprise custody)
- supportedChains: Array of blockchain networks this adapter can operate on
Example:
import { ViemAdapter } from '@circle-fin/adapter-viem-v2'
import { Ethereum, Base, Polygon } from '@circle-fin/bridge-kit/chains'
import { createPublicClient, createWalletClient, http } from 'viem'
const adapter = new ViemAdapter(
{
getPublicClient: ({ chain }) =>
createPublicClient({ chain, transport: http() }),
getWalletClient: async ({ chain }) => {
// Your initialization logic - now chain-aware
return createWalletClient({
chain,
transport: http(),
/* ... */
})
},
},
{
addressContext: 'user-controlled', // Address managed by wallet
supportedChains: [Ethereum, Base, Polygon], // Specify supported chains
},
)Methods
calculateTransactionFee(baseComputeUnits, bufferBasisPoints?, chain)- Calculate transaction fees with optional bufferensureChain(targetChain)- Ensures the adapter is connected to the correct chainfetchEIP2612Nonce(tokenAddress, ownerAddress, ctx)- Fetch EIP-2612 nonce for permit signaturesfetchGasPrice(chain)- Fetch current gas price from the networkgetAddress(chain)- Get the connected wallet addressgetPublicClient(chainDef)- Get the cached PublicClient or initialize it for the specified chaingetViemChain(chain)- Get the Viem Chain object for the given chain definitioninitializeWalletClient(chain)- Initialize wallet client with proper caching and error handlingprepare(params, ctx: OperationContext)- Prepare transactions for executionparams: Contract parameters (address, ABI, function, args)ctx: Required operation context specifying the chain and address for this operation
prepareAction(action, params, ctx)- Prepare (but do not execute) an action for the connected blockchainreadContract<T>(params, chain)- Read data from smart contract functionsresetState()- Reset all cached state in the adapter, including Viem-specific cachessignTypedData(typedData, ctx: OperationContext)- Sign EIP-712 typed data with required operation contextswitchToChain(chain)- Switch the adapter to operate on the specified chainvalidateChainSupport(targetChain)- Validate that the target chain is supported by this adapterwaitForTransaction(txHash, config?, chain)- Wait for transaction confirmation
prepare() Method Details
The prepare() method requires an OperationContext for all operations:
// OperationContext is required - chain specified in context
const prepared = await adapter.prepare(
{
address: '0x...',
abi: contractAbi,
functionName: 'transfer',
args: ['0xto', '1000'],
},
{
chain: 'Base', // Chain specified in context
address: '0x...', // Only required for developer-controlled adapters
},
)Token Operations via Actions
For token balance and allowance operations, this adapter uses the standardized action-based system inherited from EvmAdapter:
token.balanceOf- Get balance for any ERC-20 tokentoken.allowance- Get allowance for any ERC-20 tokenusdc.balanceOf- Get USDC balance (usestoken.balanceOfwith USDC address)usdc.allowance- Get USDC allowance (usestoken.allowancewith USDC address)
These actions provide type-safe, validated interfaces and use the adapter's readContract() method internally.
Development
This package is part of the App Kits monorepo.
# Build
nx build @circle-fin/adapter-viem-v2
# Test
nx test @circle-fin/adapter-viem-v2License
This project is licensed under the Apache 2.0 License. Contact support for details.