Skip to main content
Version: 1.14.0

Viem Integration

The CCIP SDK is built on ethers.js internally, but provides adapters for viem so you can use existing viem clients without importing ethers directly. Import the adapters from @chainlink/ccip-sdk/viem.

Bash
npm install @chainlink/ccip-sdk viem

Two Adapters​

AdapterInputOutputPurpose
fromViemClient(publicClient)viem PublicClientEVMChainCreate a chain instance for queries
viemWallet(walletClient)viem WalletClientethers-compatible SignerSign transactions (sendMessage, execute)

Connecting​

fromViemClient wraps a viem PublicClient as an EVMChain. Works with all viem transports — http(), webSocket(), custom(), fallback().

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)

// Use all SDK read methods
const messages = await chain.getMessagesInTx('0x1234...')
const tokenInfo = await chain.getTokenInfo('0xTokenAddress...')

Throws CCIPViemAdapterError if the PublicClient doesn't have a chain property defined.

Signing Transactions​

For write operations (sendMessage, execute), wrap your WalletClient with viemWallet:

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

const walletClient = createWalletClient({
chain: sepolia,
transport: http('https://rpc.sepolia.org'),
account: privateKeyToAccount('0x...'),
})

const chain = await fromViemClient(publicClient)

const request = await chain.sendMessage({
router,
destChainSelector: networkInfo('avalanche-testnet-fuji').chainSelector,
message,
wallet: viemWallet(walletClient), // Wraps viem WalletClient for signing
})

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

viemWallet works anywhere the SDK expects a wallet parameter — sendMessage, execute, etc. See Sending Messages and Manual Execution for the full workflows.

Throws CCIPViemAdapterError if the WalletClient doesn't have both chain and account defined.

Unsigned Transactions​

As an alternative to viemWallet, you can generate unsigned transactions with the SDK and sign them directly with viem. This separates transaction construction (SDK + reliable RPCs) from signing (viem wallet).

TypeScript
import { fromViemClient } from '@chainlink/ccip-sdk/viem'
import { networkInfo } from '@chainlink/ccip-sdk'

const chain = await fromViemClient(publicClient)

// Generate unsigned send transaction
const unsignedTx = await chain.generateUnsignedSendMessage({
router,
destChainSelector: networkInfo('avalanche-testnet-fuji').chainSelector,
message,
sender: walletClient.account.address,
})

// Sign and send each transaction with viem (approvals + ccipSend)
for (const tx of unsignedTx.transactions) {
const hash = await walletClient.sendTransaction(tx)
await publicClient.waitForTransactionReceipt({ hash })
}

The same pattern works for manual execution with generateUnsignedExecute:

TypeScript
const unsignedTx = await dest.generateUnsignedExecute({
messageId: '0x1234...abcd',
payer: walletClient.account.address,
})

const hash = await walletClient.sendTransaction(unsignedTx.transactions[0])

See Sending Messages — Unsigned Transactions and Manual Execution — Unsigned Transactions for details.

Browser Wallets​

The adapters work with injected providers (MetaMask, WalletConnect, Coinbase Wallet) via viem's custom transport:

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

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)

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

Wagmi Integration​

When using wagmi, getPublicClient() returns a chain-specific typed client that doesn't directly match the SDK's expected type. Create a type bridge:

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

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

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

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

React Component 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 SendButton() {
const publicClient = usePublicClient()
const { data: walletClient } = useWalletClient()

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

const chain = await fromViemClient(toGenericPublicClient(publicClient))

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

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

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