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.
npm install @chainlink/ccip-sdk viem
Two Adapters
| Adapter | Input | Output | Purpose |
|---|---|---|---|
fromViemClient(publicClient) | viem PublicClient | EVMChain | Create a chain instance for queries |
viemWallet(walletClient) | viem WalletClient | ethers-compatible Signer | Sign transactions (sendMessage, execute) |
Connecting
fromViemClient wraps a viem PublicClient as an EVMChain. Works with all viem transports — http(), webSocket(), custom(), fallback().
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:
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).
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:
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:
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:
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
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>
}
Related
- Sending Messages — Send workflow and message types
- Manual Execution — Execute stuck messages
- Browser Setup — Browser bundling and polyfills