@circle-fin/adapter-viem-v2
v1.1.1
Published
EVM blockchain adapter powered by Viem v2
Downloads
3,450
Keywords
Readme
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 Stablecoin 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 = createAdapterFromPrivateKey({
privateKey: process.env.PRIVATE_KEY as string, // Works with or without '0x'
})
// Both formats are automatically normalized:
const adapter1 = createAdapterFromPrivateKey({
privateKey: '0x1234...', // With prefix ✅
})
const adapter2 = createAdapterFromPrivateKey({
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 { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
const adapter = createAdapterFromPrivateKey({
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.io→https://ethereum.publicnode.com🔄 - Base:
https://mainnet.base.org→https://base.publicnode.com🔄 - Polygon:
https://polygon-rpc.com→https://polygon.publicnode.com🔄 - 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 { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
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 = createAdapterFromPrivateKey({
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 { createAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
import { createPublicClient, http } from 'viem'
// Production-ready setup with custom RPC endpoints
const adapter = createAdapterFromPrivateKey({
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.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}`,
{
retryCount: 3,
timeout: 10000,
},
),
}),
})🌐 Browser Support with Wallet Providers
For browser environments with wallet providers like MetaMask:
import { createAdapterFromProvider } from '@circle-fin/adapter-viem-v2'
// Create an adapter from a browser wallet
const adapter = await createAdapterFromProvider({
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 { 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.
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 (
createAdapterFromPrivateKey) - 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 = createAdapterFromPrivateKey({
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
// Enterprise custody - use 'developer-controlled'
const adapter = createAdapterFromProvider({
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 { 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 Stablecoin 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.
Ready to integrate?
Join Discord • Visit our Help-Desk
Built with ❤️ by Circle
