使用 Chainlink CCIP 发送跨链“Hello World”

本文介绍了如何使用 Chainlink CCIP 将数据从一个链发送到另一个链。具体来说,展示了如何使用 Foundry 设置项目,编写一个 Solidity 函数,该函数可以将简单的字符串“hello world”从源链发送到目标链,并解释了关键的 CCIP 概念和函数,例如 ccipSendEVM2AnyMessage 结构体。

在上一篇文章中,我尝试解释 CCIP 的架构,我在这里对 "尝试" 这个词很宽容。我知道那个解释不是最好的。但是从这篇文章开始,我们就进入了我的区域:编写和解释代码。所以做好准备,我们将编写一些代码,我将引导你完成它的每个部分。

你可能知道,CCIP 让我们可以在一条链向另一条链发送数据或代币。我们使用 CCIP 构建的第一步是了解如何发送数据。更具体地说,我们将把一个简单的字符串 "hello world" 从源链发送到目标链。

项目设置

让我们设置 Foundry,这样我们就可以编写发送此数据的函数。在 VS Code 或任何编辑器中打开你的项目文件夹,并在终端中运行以下命令:

## 创建一个新的 Foundry 项目
forge init

## 安装依赖
forge install smartcontractkit/chainlink-evm@contracts-v1.4.0
forge install smartcontractkit/chainlink-ccip@contracts-ccip-v1.6.0

这将创建一个新的 Foundry 项目并安装我们将用于 CCIP 的 Chainlink 包。接下来,我们需要更新重映射。创建一个 remappings.txt 文件并添加以下行:

@chainlink/contracts/=lib/chainlink-evm/contracts/
@chainlink/contracts-ccip/contracts/=lib/chainlink-ccip/chains/evm/contracts/

在完成设置之前,请确保删除 srctestscript 文件夹中的所有 .sol 文件。然后,在 src 文件夹中,创建一个名为 CCIPMessenger.sol 的新文件。这就是我们的合约所在的地方。现在设置已经完成,让我们来谈谈到本文末尾我们的合约会是什么样子。

我们将编写一个将字符串发送到另一条链上的合约的函数。当我们发送消息时,我们将发出一个事件,其中包含消息 ID(当我们调用 ccipSend 时由 CCIP 返回)、用户的地址和目标链选择器(这些选择器是 CCIP 支持的链的唯一 ID)。

sendMessage 如何工作?

要使用 CCIP 发送消息,我们使用 Router 合约上的 ccipSend 函数。这是函数定义:

function ccipSend(
   uint64 destinationChainSelector,
   Client.EVM2AnyMessage calldata message
) external payable returns (bytes32);

当我们调用它时,我们需要两件事:目标链选择器和一个 Client.EVM2AnyMessage 结构体。这是该结构体的样子:

struct EVM2AnyMessage {
    bytes receiver; // abi.encode(receiver address) for destination EVM chains.
    bytes data; // Data payload.
    EVMTokenAmount[] tokenAmounts; // Token transfers.
    address feeToken; // Address of fee token. address(0) = native token.
    bytes extraArgs; // Use _argsToBytes(EVMExtraArgsV2) to populate.
}

这些字段中的大多数要么很明显,要么用注释解释得很好,除了 extraArgs。它用于设置 gas 限制或启用乱序执行等操作,但我们会在需要时再讨论它。现在先记住它。

所以,回到我们的 sendMessage 函数。它将接受三个输入:目标链选择器、目标合约地址和字符串消息。我们将所有这些打包到 EVM2AnyMessage 结构体中,并将其传递给 ccipSend

我们还没有讨论费用。由于 Chainlink CCIP 是一项服务,它会收取费用,可以使用原生代币或支持的 ERC20 代币支付。对于此示例,我们将使用原生代币支付。这意味着我们的 sendMessage 函数必须是 payable,并且我们将检查用户是否发送了足够的 ETH 来支付费用。

现在我们知道它是如何工作的了,让我们开始编码。

CCIPMessenger 合约

我们的合约将写在 CCIPMessenger.sol 文件中。我们将从 Chainlink CCIP 包中导入两件事:IRouterClient,它允许我们与 Router 合约交互,以及 Client,它让我们能够访问构建 CCIP 消息所需的 EVM2AnyMessage 结构体。

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity 0.8.24;

import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";

contract CCIPMessenger {}

我们还希望在发送消息时发出一个事件。让我们定义 MessageSent 事件:

contract CCIPMessenger {
    event MessageSent(
        bytes32 indexed messageId,
        address indexed sender,
        uint64 indexed destinationChainSelector
    );
}

现在我们可以开始构建 sendMessage 函数。它将是 payable,并且将接受我们讨论过的三个参数:

function sendMessage(
    uint64 destinationChainSelector,
    address receiver,
    string calldata message
) public payable {
    // create router client instance

    // build the message

    // 获取费用

    // 发送消息

    // 发出事件
}

让我们填写每个步骤。

使用 Router 合约的地址创建 Router 客户端实例:

// 创建 Router 客户端实例
IRouterClient routerClient =
    IRouterClient(0x881e3A65B4d4a04dD529061dd0071cf975F58bCD);

接下来,使用 EVM2AnyMessage 结构体构建消息。接收者和消息是用户输入。对于其余部分:

  • tokenAmounts:空数组(未发送任何代币)

  • feeToken:零地址(表示原生代币)

  • extraArgs:空字节

// 构建消息
Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
    receiver: abi.encode(receiver),
    data: abi.encode(message),
    tokenAmounts: new Client.EVMTokenAmount[](0),
    feeToken: address(0),
    extraArgs: bytes("")
});

使用 getFee 估算费用,并确保用户已发送足够的 ETH:

uint256 fees = routerClient.getFee(destinationChainSelector, evm2AnyMessage);
require(msg.value >= fees, "Insufficient fee");

最后,调用 ccipSend 发送消息并发出事件:

// 发送消息
bytes32 messageId = routerClient.ccipSend{value: msg.value}(
    destinationChainSelector, evm2AnyMessage
);

// 发出事件
emit MessageSent(messageId, msg.sender, destinationChainSelector);

所以完整的函数如下所示:

function sendMessage(
    uint64 destinationChainSelector,
    address receiver,
    string calldata message
) public payable {
    // 创建 Router 客户端实例
    IRouterClient routerClient =
        IRouterClient(0x881e3A65B4d4a04dD529061dd0071cf975F58bCD);

    // 构建消息
    Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
        receiver: abi.encode(receiver),
        data: abi.encode(message),
        tokenAmounts: new Client.EVMTokenAmount[](0),
        feeToken: address(0),
        extraArgs: bytes("")
    });

    // 获取费用
    uint256 fees =
        routerClient.getFee(destinationChainSelector, evm2AnyMessage);
    require(msg.value >= fees, "Insufficient fee");

    // 发送消息
    bytes32 messageId = routerClient.ccipSend{value: msg.value}(
        destinationChainSelector, evm2AnyMessage
    );

    // 发出事件
    emit MessageSent(messageId, msg.sender, destinationChainSelector);
}

完整的合约

完成 sendMessage 函数后,我们已经涵盖了我们在本文中要构建的所有内容。此时你的合约应如下所示:

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity 0.8.24;

import {IRouterClient} from
    "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";

contract CCIPMessenger {
    event MessageSent(
        bytes32 indexed messageId,
        address indexed sender,
        uint64 indexed destinationChainSelector
    );

    function sendMessage(
        uint64 destinationChainSelector,
        address receiver,
        string calldata message
    ) public payable {
        // 创建 Router 客户端实例
        IRouterClient routerClient =
            IRouterClient(0x881e3A65B4d4a04dD529061dd0071cf975F58bCD);

        // 构建消息
        Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
            receiver: abi.encode(receiver),
            data: abi.encode(message),
            tokenAmounts: new Client.EVMTokenAmount[](0),
            feeToken: address(0),
            extraArgs: bytes("")
        });

        // 获取费用
        uint256 fees =
            routerClient.getFee(destinationChainSelector, evm2AnyMessage);
        require(msg.value >= fees, "Insufficient fee");

        // 发送消息
        bytes32 messageId = routerClient.ccipSend{value: msg.value}(
            destinationChainSelector, evm2AnyMessage
        );

        // 发出事件
        emit MessageSent(messageId, msg.sender, destinationChainSelector);
    }
}

接下来是什么?

现在我们可以发送消息了,下一个合乎逻辑的步骤是弄清楚如何接收消息,这正是我们将在下一部分中解决的问题。

当我们添加接收消息的功能时,我们还将稍微重构 sendMessage 函数。目前,我们使用的 Router 地址是为特定链(Base Mainnet)硬编码的。这需要更改,以便我们的合约可以部署在不同的链上,并且仍然可以使用 CCIP。

希望你喜欢这一部分。如果你正在关注,我会在下一部分中见到你。如果你渴望更深入地了解 CCIP,Chainlink CCIP 的官方文档是一个很好的探索场所。

我是谁?

嘿,我是 Nikhil,一名软件开发人员,喜欢撰写关于 Solidity、EVM 以及任何引起我注意的新技术的文章。你可以在此 Medium 个人资料上查看更多我的作品,并在此处找到我的其余作品和社交资料:https://linktr.ee/nikbhintade

  • 原文链接: blog.blockmagnates.com/s...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
blockmagnates
blockmagnates
The New Crypto Publication on The Block