Skip to main content
Version: 1.1.1

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>
}