npm.io
1.12.1 • Published 4d ago

@circle-fin/adapter-viem-v2

Licence
Version
1.12.1
Deps
9
Size
4.5 MB
Vulns
0
Weekly
3.3K

Viem v2 Adapter

npm version TypeScript License Discord

Type-safe EVM blockchain adapter powered by Viem v2

Seamlessly interact with 16+ EVM networks using a single, strongly-typed interface

Table of Contents

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 PublicClient and WalletClient instances
  • 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 viem

Peer 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 viem

Supported Versions: ^2.30.0 (2.30.x through 2.x.x)

Troubleshooting Version Conflicts

If you encounter peer dependency warnings:

  • Check your viem version: npm ls viem
  • Ensure viem is between 2.30.0 and 3.0.0 (exclusive)
  • Use npm install viem@^2.30.0 to install a compatible version

Quick Start

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 operations

Key Feature: All chain definitions include reliable default RPC endpoints with automatic failover:

  • Ethereum: https://eth.merkle.iohttps://ethereum.publicnode.com
  • Base: https://mainnet.base.orghttps://base.publicnode.com
  • Polygon: https://polygon.publicnode.comhttps://polygon.drpc.org
  • Unichain: https://rpc.unichain.orghttps://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 /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 { 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 downstream

Browser 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 /next bridge flow. batchExecute supports EIP-5792 wallet_sendCalls, but the CCTP /next provider still submits approve and burn as two separate transactions; it does not yet collapse them into a single atomic batch.
  • createViemAdapterFromAccount / createViemAdapterFromWalletClient are deferred. Only createViemAdapterFromPrivateKey and createViemAdapterFromProvider are available on /next today. To use an existing viem Account or WalletClient, wrap it in an EIP-1193 provider or build the adapter via assembleAdapter directly.
  • 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 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). createViemAdapterFromPrivateKey gives 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:

  1. Object-graph stripping (always on) — extra properties like request, info, requestUrl, the originating XMLHttpRequest, and the configured transport.url 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 error.message, shortMessage, and reason 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-viem-v2/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-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:

  • 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 buffer
  • ensureChain(targetChain) - Ensures the adapter is connected to the correct chain
  • fetchEIP2612Nonce(tokenAddress, ownerAddress, ctx) - Fetch EIP-2612 nonce for permit signatures
  • fetchGasPrice(chain) - Fetch current gas price from the network
  • getAddress(chain) - Get the connected wallet address
  • getPublicClient(chainDef) - Get the cached PublicClient or initialize it for the specified chain
  • getViemChain(chain) - Get the Viem Chain object for the given chain definition
  • initializeWalletClient(chain) - Initialize wallet client with proper caching and error handling
  • prepare(params, ctx: OperationContext) - Prepare transactions for execution
    • params: 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 blockchain
  • readContract<T>(params, chain) - Read data from smart contract functions
  • resetState() - Reset all cached state in the adapter, including Viem-specific caches
  • signTypedData(typedData, ctx: OperationContext) - Sign EIP-712 typed data with required operation context
  • switchToChain(chain) - Switch the adapter to operate on the specified chain
  • validateChainSupport(targetChain) - Validate that the target chain is supported by this adapter
  • waitForTransaction(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 token
  • token.allowance - Get allowance for any ERC-20 token
  • usdc.balanceOf - Get USDC balance (uses token.balanceOf with USDC address)
  • usdc.allowance - Get USDC allowance (uses token.allowance with 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-v2

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

Keywords