# Viem v2 Adapter

<div align="center">

[![npm version](https://badge.fury.io/js/@circle-fin%2Fadapter-viem-v2.svg)](https://badge.fury.io/js/@circle-fin%2Fadapter-viem-v2)
[![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Discord](https://img.shields.io/discord/473781666251538452?label=Discord&logo=discord)](https://discord.com/invite/buildoncircle)

**Type-safe EVM blockchain adapter powered by Viem v2**

_Seamlessly interact with 16+ EVM networks using a single, strongly-typed interface_

</div>

## Table of Contents

- [Viem v2 Adapter](#viem-v2-adapter)
  - [Table of Contents](#table-of-contents)
  - [Overview](#overview)
    - [Why Viem Adapter?](#why-viem-adapter)
    - [When and How Should I Use The Viem Adapter?](#when-and-how-should-i-use-the-viem-adapter)
      - [I'm a developer using a kit](#im-a-developer-using-a-kit)
        - [Example](#example)
      - [I'm a developer making a Kit Provider](#im-a-developer-making-a-kit-provider)
  - [Installation](#installation)
  - [Peer Dependencies](#peer-dependencies)
    - [Troubleshooting Version Conflicts](#troubleshooting-version-conflicts)
  - [Quick Start](#quick-start)
    - [🚀 Easy Setup with Factory Methods (Recommended)](#-easy-setup-with-factory-methods-recommended)
    - [🏭 Production Considerations](#-production-considerations)
    - [🌐 Browser Support with Wallet Providers](#-browser-support-with-wallet-providers)
    - [🔧 Advanced Manual Setup](#-advanced-manual-setup)
  - [Next-Generation API (`/next`)](#next-generation-api-next)
    - [Getting Started with `/next`](#getting-started-with-next)
    - [Why `/next`?](#why-next)
    - [Current limitations of `/next`](#current-limitations-of-next)
    - [Prepared Transactions: Estimate, Simulate, Execute](#prepared-transactions-estimate-simulate-execute)
    - [Logging](#logging)
    - [Metrics](#metrics)
    - [Token Registry](#token-registry)
    - [Transaction Tuning](#transaction-tuning)
    - [Migration Path](#migration-path)
  - [OperationContext Pattern](#operationcontext-pattern)
    - [Address Context Modes](#address-context-modes)
      - [`'user-controlled'` (Default, Recommended)](#user-controlled-default-recommended)
      - [`'developer-controlled'` (For Enterprise/Multi-Address Systems)](#developer-controlled-for-enterprisemulti-address-systems)
    - [OperationContext Usage](#operationcontext-usage)
  - [API Reference](#api-reference)
    - [Constructor Options](#constructor-options)
      - [`getPublicClient`](#getpublicclient)
      - [`getWalletClient`](#getwalletclient)
      - [`capabilities` (Required)](#capabilities-required)
    - [Methods](#methods)
      - [`prepare()` Method Details](#prepare-method-details)
    - [Token Operations via Actions](#token-operations-via-actions)
  - [Development](#development)
  - [License](#license)

## 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](https://viem.sh/) library, it provides type-safe blockchain interactions through a unified interface that's designed to work seamlessly with the [Bridge Kit](https://www.npmjs.com/package/@circle-fin/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

```typescript
// 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

```bash
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:

```bash
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

### 🚀 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!

```typescript
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.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:

```typescript
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

```typescript
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:

```typescript
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:

```typescript
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`](#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:

```typescript
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:

```typescript
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](#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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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.

```typescript
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:

```typescript
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`:

   ```typescript
   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:

#### `'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

```typescript
// 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

```typescript
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:

```typescript
// 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:

```typescript
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**:

```typescript
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:

```typescript
// 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.

```bash
# 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](https://help.circle.com/s/submit-ticket) for details.

---

<div align="center">

**Ready to integrate?**

[Join Discord](https://discord.com/invite/buildoncircle) •
[Visit our Help-Desk](https://help.circle.com/s/submit-ticket)

_Built with ❤️ by Circle_

</div>
