Skip to main content
Version: 0.96.0

Viem Integration

The CCIP SDK provides adapters for viem, allowing you to use existing viem clients with the SDK without managing separate ethers.js providers.

Installation

The viem adapters are included in the main SDK package:

Bash
npm install @chainlink/ccip-sdk viem

Basic Usage

Import from @chainlink/ccip-sdk/viem:

TypeScript
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
import { fromViemClient } from '@chainlink/ccip-sdk/viem'

// Create a viem public client
const publicClient = createPublicClient({
chain: mainnet,
transport: http('https://eth.llamarpc.com'),
})

// Convert to CCIP EVMChain
const chain = await fromViemClient(publicClient)

// Use all SDK features
const messages = await chain.getMessagesInTx('0x1234...')
console.log('Found', messages.length, 'CCIP messages')

Supported Transports

The viem adapters work with ALL viem transport types:

TypeScript
import { createPublicClient, http } from 'viem'
import { sepolia } from 'viem/chains'
import { fromViemClient } from '@chainlink/ccip-sdk/viem'

const publicClient = createPublicClient({
chain: sepolia,
transport: http('https://rpc.sepolia.org'),
})

const chain = await fromViemClient(publicClient)

Signing Transactions

For write operations like sendMessage, wrap your viem WalletClient with viemWallet:

TypeScript
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { sepolia } from 'viem/chains'
import { fromViemClient, viemWallet } from '@chainlink/ccip-sdk/viem'
import { encodeExtraArgs, networkInfo } from '@chainlink/ccip-sdk'

// Create clients
const transport = http('https://rpc.sepolia.org')
const account = privateKeyToAccount('0x...')

const publicClient = createPublicClient({
chain: sepolia,
transport,
})

const walletClient = createWalletClient({
chain: sepolia,
transport,
account,
})

// Create CCIP chain
const chain = await fromViemClient(publicClient)

// Send message with viem wallet
const router = chain.network.router
const destSelector = networkInfo('avalanche-testnet-fuji').chainSelector

const message = {
receiver: '0xReceiverAddress...',
data: '0x',
tokenAmounts: [],
feeToken: '0x0000000000000000000000000000000000000000',
extraArgs: encodeExtraArgs({
gasLimit: 200000n,
allowOutOfOrderExecution: true,
}),
}

const fee = await chain.getFee({
router,
destChainSelector: destSelector,
message,
})

const request = await chain.sendMessage({
router,
destChainSelector: destSelector,
message,
wallet: viemWallet(walletClient),
})

console.log('Message ID:', request.message.messageId)

Browser Wallet Integration

The viem adapter works seamlessly with browser wallets:

TypeScript
import { createPublicClient, createWalletClient, custom } from 'viem'
import { mainnet } from 'viem/chains'
import { fromViemClient, viemWallet } from '@chainlink/ccip-sdk/viem'

// Connect to MetaMask
const [address] = await window.ethereum.request({
method: 'eth_requestAccounts',
})

const publicClient = createPublicClient({
chain: mainnet,
transport: custom(window.ethereum),
})

const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum),
account: address,
})

const chain = await fromViemClient(publicClient)

// Send message - user will be prompted to sign
const request = await chain.sendMessage({
router,
destChainSelector: destSelector,
message,
wallet: viemWallet(walletClient),
})

Manual Execution with Viem

Execute stuck messages using viem wallets:

TypeScript
import { fromViemClient, viemWallet } from '@chainlink/ccip-sdk/viem'
import { calculateManualExecProof, discoverOffRamp } from '@chainlink/ccip-sdk'

const source = await fromViemClient(sourcePublicClient)
const dest = await fromViemClient(destPublicClient)

// Get the stuck message
const requests = await source.getMessagesInTx('0x1234...')
const request = requests[0]

// Find OffRamp and get commit
const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp)
const commit = await dest.getCommitReport({ commitStore: offRamp, request })

// Calculate merkle proof
const messagesInBatch = await source.getMessagesInBatch(request, commit.report)

const proof = calculateManualExecProof(
messagesInBatch,
request.lane,
request.message.messageId,
commit.report.merkleRoot
)

// Execute with viem wallet
const execution = await dest.executeReport({
offRamp,
execReport: {
...proof,
message: request.message,
offchainTokenData: [],
},
wallet: viemWallet(walletClient),
})

console.log('Manual execution tx:', execution.log.transactionHash)

API Reference

fromViemClient

Creates an EVMChain from a viem PublicClient.

TypeScript
function fromViemClient(
client: PublicClient<Transport, Chain>,
ctx?: ChainContext
): Promise<EVMChain>

Parameters:

ParameterTypeDescription
clientPublicClient<Transport, Chain>viem PublicClient with chain defined
ctxChainContextOptional context (logger, etc.)

Returns: Promise<EVMChain>

Throws: CCIPViemAdapterError if client doesn't have a chain defined.

viemWallet

Converts a viem WalletClient to an ethers-compatible Signer.

TypeScript
function viemWallet(
client: WalletClient<Transport, Chain, Account>
): AbstractSigner

Parameters:

ParameterTypeDescription
clientWalletClient<Transport, Chain, Account>viem WalletClient with account and chain

Returns: AbstractSigner compatible with SDK wallet parameters.

Throws: CCIPViemAdapterError if client doesn't have account or chain defined.

Using with Wagmi

When using the SDK with wagmi, you may encounter type mismatches because wagmi's getPublicClient() returns a chain-specific typed client.

Type Bridge Function

Create a type bridge to convert wagmi's client to the SDK's expected type:

TypeScript
import { getPublicClient } from '@wagmi/core'
import { fromViemClient } from '@chainlink/ccip-sdk/viem'
import type { PublicClient, Transport, Chain } from 'viem'

// Type bridge for wagmi compatibility
function toGenericPublicClient(
client: ReturnType<typeof getPublicClient>
): PublicClient<Transport, Chain> {
return client as PublicClient<Transport, Chain>
}

// Usage
const wagmiClient = getPublicClient(wagmiConfig, { chainId: 11155111 })
const publicClient = toGenericPublicClient(wagmiClient)
const chain = await fromViemClient(publicClient)

This cast is safe when both packages use the same viem version.

Complete Wagmi Example

TypeScript
import { usePublicClient, useWalletClient } from 'wagmi'
import { fromViemClient, viemWallet } from '@chainlink/ccip-sdk/viem'
import { networkInfo } from '@chainlink/ccip-sdk'
import type { PublicClient, Transport, Chain } from 'viem'

function toGenericPublicClient(
client: ReturnType<typeof usePublicClient>
): PublicClient<Transport, Chain> {
return client as PublicClient<Transport, Chain>
}

function MyComponent() {
const publicClient = usePublicClient()
const { data: walletClient } = useWalletClient()

async function sendMessage() {
if (!publicClient || !walletClient) return

const chain = await fromViemClient(toGenericPublicClient(publicClient))

const request = await chain.sendMessage({
router: chain.network.router,
destChainSelector: networkInfo('avalanche-testnet-fuji').chainSelector,
message: {
receiver: '0xReceiverAddress...',
data: '0x',
},
wallet: viemWallet(walletClient),
})

console.log('Message ID:', request.message.messageId)
}

return <button onClick={sendMessage}>Send Message</button>
}

Requirements

  • viem PublicClient must have a chain property defined
  • viem WalletClient must have both chain and account properties defined
  • All viem transport types are supported (http, webSocket, custom, fallback)