轻松上手 Chainlink CCIP:Sepolia → BSC Testnet 跨链消息全流程
CCIP 简介(超简)
- CCIP(Cross-Chain Interoperability Protocol):Chainlink 推出的跨链消息与资产传输协议。
- 本示例:在 Ethereum Sepolia 上发送一条文本消息,经 CCIP 路由传到 BSC Testnet 并被目标链合约接收与解码。
架构与流程
- 源链:Sepolia;合约:
Sender
- 目标链:BSC Testnet;合约:
Receiver
- 流程:
Sender.sendMessage(...)
→ CCIP 路由跨链 →Receiver._ccipReceive(...)
解码与记录
快速上手步骤(概览)
- 在 BSC Testnet 部署
Receiver
(构造写死 Router 地址) - 在 Sepolia 部署
Sender
(构造写死 Router 与 LINK 地址) - 向
Sender
合约转入足量 LINK → 调sendMessage(chainSelector, receiver, "hello")
- 在 BSC Testnet 监听
MessageReceived
或读getLastReceivedMessageDetails()
源码:Receiver(BSC Testnet 接收)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import {Client} from "@chainlink/contracts-ccip@1.6.0/contracts/libraries/Client.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip@1.6.0/contracts/applications/CCIPReceiver.sol";
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
/**
bsc测试网接收消息
1. 部署合约
**/
/// @title - A simple contract for receiving string data across chains.
contract Receiver is CCIPReceiver {
// Event emitted when a message is received from another chain.
event MessageReceived(
bytes32 indexed messageId, // The unique ID of the message.
uint64 indexed sourceChainSelector, // The chain selector of the source chain.
address sender, // The address of the sender from the source chain.
string text // The text that was received.
);
bytes32 private s_lastReceivedMessageId; // Store the last received messageId.
string private s_lastReceivedText; // Store the last received text.
/// @notice Constructor initializes the contract with the router address.
/// 写死bsc testnet的router:0xE1053aE1857476f36A3C62580FF9b016E8EE8F6f
constructor() CCIPReceiver(0xE1053aE1857476f36A3C62580FF9b016E8EE8F6f) {}
/// handle a received message
function _ccipReceive(
Client.Any2EVMMessage memory any2EvmMessage
) internal override {
s_lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageId
s_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); // abi-decoding of the sent text
emit MessageReceived(
any2EvmMessage.messageId,
any2EvmMessage.sourceChainSelector, // fetch the source chain identifier (aka selector)
abi.decode(any2EvmMessage.sender, (address)), // abi-decoding of the sender address,
abi.decode(any2EvmMessage.data, (string))
);
}
/// @notice Fetches the details of the last received message.
/// @return messageId The ID of the last received message.
/// @return text The last received text.
function getLastReceivedMessageDetails()
external
view
returns (bytes32 messageId, string memory text)
{
return (s_lastReceivedMessageId, s_lastReceivedText);
}
}
源码:Sender(Sepolia 发送)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import {IRouterClient} from "@chainlink/contracts-ccip@1.6.0/contracts/interfaces/IRouterClient.sol";
import {OwnerIsCreator} from "@chainlink/contracts@1.4.0/src/v0.8/shared/access/OwnerIsCreator.sol";
import {Client} from "@chainlink/contracts-ccip@1.6.0/contracts/libraries/Client.sol";
import {LinkTokenInterface} from "@chainlink/contracts@1.4.0/src/v0.8/shared/interfaces/LinkTokenInterface.sol";
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
/**
从sepolia发送消息给bsc测试网
1. 部署合约
2. 给合约转账70link
**/
/// @title - A simple contract for sending string data across chains.
contract Sender is OwnerIsCreator {
// Custom errors to provide more descriptive revert messages.
error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough balance.
// Event emitted when a message is sent to another chain.
event MessageSent(
bytes32 indexed messageId, // The unique ID of the CCIP message.
uint64 indexed destinationChainSelector, // The chain selector of the destination chain.
address receiver, // The address of the receiver on the destination chain.
string text, // The text being sent.
address feeToken, // the token address used to pay CCIP fees.
uint256 fees // The fees paid for sending the CCIP message.
);
IRouterClient private s_router;
LinkTokenInterface private s_linkToken;
/// @notice Constructor initializes the contract with the router address.
constructor() {
// 写死sepolia的router和link地址
address _router =0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59;
address _link = 0x779877A7B0D9E8603169DdbD7836e478b4624789;
s_router = IRouterClient(_router);
s_linkToken = LinkTokenInterface(_link);
}
/// @notice Sends data to receiver on the destination chain.
/// @dev Assumes your contract has sufficient LINK.
/// @param destinationChainSelector The identifier (aka selector) for the destination blockchain.
/// @param receiver The address of the recipient on the destination blockchain.
/// @param text The string text to be sent.
/// @return messageId The ID of the message that was sent.
function sendMessage(
uint64 destinationChainSelector,
address receiver,
string calldata text
) external onlyOwner returns (bytes32 messageId) {
// Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
receiver: abi.encode(receiver), // ABI-encoded receiver address
data: abi.encode(text), // ABI-encoded string
tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array indicating no tokens are being sent
extraArgs: Client._argsToBytes(
// Additional arguments, setting gas limit and allowing out-of-order execution.
// Best Practice: For simplicity, the values are hardcoded. It is advisable to use a more dynamic approach
// where you set the extra arguments off-chain. This allows adaptation depending on the lanes, messages,
// and ensures compatibility with future CCIP upgrades. Read more about it here: https://docs.chain.link/ccip/concepts/best-practices/evm#using-extraargs
Client.GenericExtraArgsV2({
gasLimit: 200_000, // Gas limit for the callback on the destination chain
allowOutOfOrderExecution: true // Allows the message to be executed out of order relative to other messages from the same sender
})
),
// Set the feeToken address, indicating LINK will be used for fees
feeToken: address(s_linkToken)
});
// Get the fee required to send the message
uint256 fees = s_router.getFee(
destinationChainSelector,
evm2AnyMessage
);
if (fees > s_linkToken.balanceOf(address(this)))
revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees);
// approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
s_linkToken.approve(address(s_router), fees);
// Send the message through the router and store the returned message ID
messageId = s_router.ccipSend(destinationChainSelector, evm2AnyMessage);
// Emit an event with message details
emit MessageSent(
messageId,
destinationChainSelector,
receiver,
text,
address(s_linkToken),
fees
);
// Return the message ID
return messageId;
}
}
实战提示(简版)
- 确认两链的 CCIP Router 地址与 Chain Selector 为官方最新值。
feeToken
使用 LINK 时,需要先将 LINK 转入Sender
合约,并approve
费用。gasLimit
需与接收逻辑复杂度匹配;开启allowOutOfOrderExecution
时注意幂等与去重(用messageId
)。
如果需要,我可以补充一份一键部署与调用的脚本(Hardhat/Foundry),并附上常用测试网的 chain selector
与 Router 地址清单,帮助你从部署到演示全流程跑通。