您可能已经注意到,由于存在一些额外的摩擦点——例如设置钱包、获取测试网代币、等待跨链交易完成等,直接在测试网络上使用 CCIP 构建并不理想。这是因为测试网络用于测试,而本地环境(如 Foundry、Hardhat 或 Remix IDE)用于构建和单元测试。
介绍 Chainlink Local
为了解决这个问题,我们创建了 Chainlink Local - the Chainlink CCIP Local Simulator 。在本章中,您将学习如何在本地模拟跨链交易,并以比在测试网络上工作快 2000 倍的速度使用 Chainlink CCIP 构建项目!
Chainlink Local 是一个可安装的依赖项,例如 OpenZeppelin。它提供了一个工具(Chainlink Local 模拟器),开发人员可以将其导入到他们的 Foundry、Hardhat 或 Remix IDE 项目中。这个工具在本地运行 Chainlink CCIP ,这意味着开发人员可以在本地环境中快速探索、设计原型和迭代 CCIP dApps,只有在准备好在实际环境中测试时再将项目移动至测试网。
最重要的是,用 Chainlink Local 测试的智能合约可以在不做任何修改的情况下部署到测试网络(假设通过构造函数传入了网络特定的合约地址,如 Router 合约和 LINK 代币地址)。
要查看更详细的文档和更多示例,请访问 Chainlink Local Documentation 和 Chainlink Local YouTube Playlist 。
本地模式 vs 分叉模式
模拟器支持两种模式:
本地模式:使用在本地运行的开发区块链节点上的模拟合约,运行在 localhost
上。
分叉模式:使用已部署的 Chainlink CCIP 合约,通过多个分叉网络 进行工作。
在这个示例中,我们将使用分叉模式 。
在作业中,您必须使用本地模式 。
本地模式
在本地模拟模式下工作时,模拟器会将一组智能合约预部署到一个空白的 Hardhat/Anvil 网络的EVM 状态,并通过调用 configuration()
函数公开它们的详细信息。尽管应该存在两个 Router 合约(sourceRouter
和 destinationRouter
),开发人员通过这两个不同的 Router 路由跨链消息,但在本地模式中它们都是在本地区块链节点上运行的同一个合约。
分叉模式
在分叉模式下,您需要创建多个本地运行的区块链网络(您需要一个归档节点,该节点具有固定块中的历史网络状态且您已经从中进行本地分叉 - 请参见此处 )并与官方 Chainlink 文档 中提供的合约地址进行交互。
代码时间: 为我们的XNFT.sol合约编写单元测试
完整的例子参见:https://github.com/smartcontractkit/ccip-cross-chain-nft
创建一个新的Foundry工程
运行以下命令创建一个新的Foundry工程:
如果命令执行失败,请确保您已安装了Foundry 。
安装必要的依赖项
接下来,我们需要安装以下依赖项。如果您不想在每次安装后提交更改,请在运行以下每个命令后附加--no-commit
标志。
@chainlink/contracts
Copy forge install smartcontractkit/chainlink-brownie-contracts
@chainlink/contracts-ccip
Copy forge install smartcontractkit/ccip@b06a3c2eecb9892ec6f76a015624413fffa1a122
@openzeppelin/contracts
Copy forge install OpenZeppelin/openzeppelin-contracts
@chainlink/local
Copy forge install smartcontractkit/chainlink-local
然后在您的 foundry.toml
或 remappings.txt
文件中设置以下 remappings:
Copy # foundry.toml
[profile.default]
src = "src"
out = "out"
test = "test"
libs = [ "lib" ]
solc = '0.8.24'
remappings = [
'@chainlink/contracts-ccip=lib/ccip/contracts' ,
'@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/' ,
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/' ,
'@chainlink/local/=lib/chainlink-local/' ,
]
删除 src/Counter.sol
、test/Counter.t.sol
和 script/Counter.s.sol
文件。在 src
文件夹中创建一个新的 XNFT.sol
文件,并粘贴上一页/练习中的 XNFT 智能合约内容。运行 forge build
来编译该合约。如果您正确安装了依赖项会编译成功。
配置Ethereum Sepolia和Arbitrum Sepolia的RPC URLs
通过复制 .env.example
文件创建一个新文件并将其命名为 .env
。在其中加入 Ethereum Sepolia 和 Arbitrum Sepolia 的 RPC URL。这里可以使用本地归档节点或提供归档数据的服务,例如 Infura 或 Alchemy 。
Copy ETHEREUM_SEPOLIA_RPC_URL=""
ARBITRUM_SEPOLIA_RPC_URL=""
然后在您的 foundry.toml
文件中添加 rpc_endpoints
部分。其最终版如下所示:
Copy # foundry.toml
[profile.default]
src = "src"
out = "out"
test = "test"
libs = [ "lib" ]
solc = '0.8.24'
remappings = [
'@chainlink/contracts-ccip=lib/ccip/contracts' ,
'@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts/' ,
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/' ,
'@chainlink/local/=lib/chainlink-local/' ,
]
[rpc_endpoints]
ethereumSepolia = "${ETHEREUM_SEPOLIA_RPC_URL}"
arbitrumSepolia = "${ARBITRUM_SEPOLIA_RPC_URL}"
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
使用Chainlink Local来编写您第一个测试文件
在上一个练习中,我们在两个不同的测试网络上执行了 7 个步骤:从部署、铸造、转移 XNFT 到另一个账户。使用 Chainlink Local,您可以编写一个测试文件在本地环境中执行相同的操作。测试中使用与测试网络上完全相同的 CCIP 合约,并且速度更快。
创建一个新的 test/XNFT.t.sol
文件。粘贴以下内容:
Copy // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import { Test , console } from "forge-std/Test.sol" ;
import { CCIPLocalSimulatorFork , Register } from "@chainlink/local/src/ccip/CCIPLocalSimulatorFork.sol" ;
import { XNFT } from "../src/XNFT.sol" ;
import { EncodeExtraArgs } from "./utils/EncodeExtraArgs.sol" ;
contract XNFTTest is Test {
CCIPLocalSimulatorFork public ccipLocalSimulatorFork;
uint256 ethSepoliaFork;
uint256 arbSepoliaFork;
Register.NetworkDetails ethSepoliaNetworkDetails;
Register.NetworkDetails arbSepoliaNetworkDetails;
address alice;
address bob;
XNFT public ethSepoliaXNFT;
XNFT public arbSepoliaXNFT;
EncodeExtraArgs public encodeExtraArgs;
function setUp () public {
alice = makeAddr ( "alice" );
bob = makeAddr ( "bob" );
string memory ETHEREUM_SEPOLIA_RPC_URL = vm. envString ( "ETHEREUM_SEPOLIA_RPC_URL" );
string memory ARBITRUM_SEPOLIA_RPC_URL = vm. envString ( "ARBITRUM_SEPOLIA_RPC_URL" );
ethSepoliaFork = vm. createSelectFork (ETHEREUM_SEPOLIA_RPC_URL);
arbSepoliaFork = vm. createFork (ARBITRUM_SEPOLIA_RPC_URL);
ccipLocalSimulatorFork = new CCIPLocalSimulatorFork ();
vm. makePersistent ( address (ccipLocalSimulatorFork));
}
// YOUR TEST GOES HERE...
}
如果您尝试编译此合约,可能会遇到错误,因为缺少上一个练习中的 EncodeExtraArgs.sol
辅助智能合约。通过创建一个新文件 test/utils/EncodeExtraArgs.sol
并粘贴以下代码来修复此问题。
Copy // SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { Client } from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol" ;
contract EncodeExtraArgs {
// 以下是一个使用存储的简单示例(所有消息使用相同的参数),该示例允许在不升级dapp的情况下添加新选项。
// 请注意,额外参数是由链种类决定的(比如,gasLimit是EVM特有的等),并且始终向后兼容,即升级是可选择的。
// 我们可以在链下计算V1 extraArgs:
// Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
// bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
// 如果V2增加了一个退款功能,可按照以下方式计算V2 extraArgs并用新的extraArgs更新存储:
// Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
// bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
// 如果不同的消息需要不同的选项,如:gasLimit不同,可以简单地基于(chainSelector, messageType)而不是只基于chainSelector进行存储。
function encode ( uint256 gasLimit) external pure returns ( bytes memory extraArgsBytes) {
Client.EVMExtraArgsV1 memory extraArgs = Client. EVMExtraArgsV1 ({gasLimit : gasLimit});
extraArgsBytes = Client. _argsToBytes (extraArgs);
}
}
让我们分析一下目前的代码。我们从 @chainlink/local
包中导入了 CCIPLocalSimulatorFork
,这意味着我们使用的是分叉模式 。我们创建了一个新的 CCIPLocalSimulatorFork 实例,并在所有不同的分叉之间使其保持持久性,使它能正常工作。
在作业中,您需要从 @chainlink/local
包中导入 CCIPLocalSimulator
。
我们还创建了两个分叉网络(即最新区块的区块链网络副本——最好固定这个区块号,而不是总是使用最新的区块号)Ethereum Sepolia 和 Arbitrum Sepolia 测试网络。现在我们可以使用 Foundry 在多个分叉网络之间切换,但我们选择 Ethereum Sepolia 作为开始的源区块链。
Foundry 使用分叉 ID 管理同一测试中不同的分叉网络,通常是 1、2、3 等这样的数字。分叉 ID 与 block.chainid
并不相同——后者是可以从Solidity 中获取的区块链网络的实际 Chain ID,例如,Ethereum Sepolia 的 Chain ID 是 11155111。
Foundry 中的 CCIPLocalSimulatorFork
提供了Register
辅助合约,其中包含 NetworkDetails
结构,如下所示:
Copy struct NetworkDetails {
uint64 chainSelector;
address routerAddress;
address linkAddress;
address wrappedNativeAddress;
address ccipBnMAddress;
address ccipLnMAddress;
}
在该结构体中已预先设置了一些测试网络的详细信息,并且其中特意不包含主网络。因此最好的做法是始终验证这些信息——否则模拟将无法工作:
Copy ethSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); // we are currently on Ethereum Sepolia Fork
assertEq (
ethSepoliaNetworkDetails.chainSelector ,
16015286601757825753 ,
"Sanity check: Ethereum Sepolia chain selector should be 16015286601757825753"
);
如果没有预置的网络详细信息(例如主网络),您必须使用 setNetworkDetails 函数手动添加这些信息。
我们将在 setup()
函数中直接执行前一个练习的步骤 1)和 2),如下所示:
Copy function setUp () public {
alice = makeAddr ( "alice" );
bob = makeAddr ( "bob" );
string memory ETHEREUM_SEPOLIA_RPC_URL = vm. envString ( "ETHEREUM_SEPOLIA_RPC_URL" );
string memory ARBITRUM_SEPOLIA_RPC_URL = vm. envString ( "ARBITRUM_SEPOLIA_RPC_URL" );
ethSepoliaFork = vm. createSelectFork (ETHEREUM_SEPOLIA_RPC_URL);
arbSepoliaFork = vm. createFork (ARBITRUM_SEPOLIA_RPC_URL);
ccipLocalSimulatorFork = new CCIPLocalSimulatorFork ();
vm. makePersistent ( address (ccipLocalSimulatorFork));
// 步骤 1) 在Ethereum Sepolia网络中部署XNFT.sol
assertEq (vm. activeFork () , ethSepoliaFork);
ethSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); // 目前我们处于Ethereum Sepolia的分叉网络中
assertEq (
ethSepoliaNetworkDetails.chainSelector ,
16015286601757825753 ,
"Sanity check: Ethereum Sepolia chain selector should be 16015286601757825753"
);
ethSepoliaXNFT = new XNFT (
ethSepoliaNetworkDetails.routerAddress ,
ethSepoliaNetworkDetails.linkAddress ,
ethSepoliaNetworkDetails.chainSelector
);
// 步骤 2) 在Arbitrum Sepolia网络中部署XNFT.sol
vm. selectFork (arbSepoliaFork);
assertEq (vm. activeFork () , arbSepoliaFork);
arbSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); // 目前我们处于Arbitrum Sepolia的分叉网络中
assertEq (
arbSepoliaNetworkDetails.chainSelector ,
3478487238524512106 ,
"Sanity check: Arbitrum Sepolia chain selector should be 421614"
);
arbSepoliaXNFT = new XNFT (
arbSepoliaNetworkDetails.routerAddress ,
arbSepoliaNetworkDetails.linkAddress ,
arbSepoliaNetworkDetails.chainSelector
);
}
在一个新的test
函数中直接执行前一个练习的其余步骤,如下所示:
Copy function testShouldMintNftOnArbitrumSepoliaAndTransferItToEthereumSepolia () public {
// 步骤 3) 在Ethereum Sepolia网络中, 调用enableChain方法
vm. selectFork (ethSepoliaFork);
assertEq (vm. activeFork () , ethSepoliaFork);
encodeExtraArgs = new EncodeExtraArgs ();
uint256 gasLimit = 200_000 ;
bytes memory extraArgs = encodeExtraArgs. encode (gasLimit);
assertEq(extraArgs, hex"97a657c90000000000000000000000000000000000000000000000000000000000030d40"); // 该值来源于 https://cll-devrel.gitbook.io/ccip-masterclass-3/ccip-masterclass/exercise-xnft#step-3-on-ethereum-sepolia-call-enablechain-function
ethSepoliaXNFT. enableChain (arbSepoliaNetworkDetails.chainSelector , address (arbSepoliaXNFT) , extraArgs);
// 步骤 4) 在Arbitrum Sepolia网络中, 调用enableChain方法
vm. selectFork (arbSepoliaFork);
assertEq (vm. activeFork () , arbSepoliaFork);
arbSepoliaXNFT. enableChain (ethSepoliaNetworkDetails.chainSelector , address (ethSepoliaXNFT) , extraArgs);
// 步骤 5) 在Arbitrum Sepolia网络中, 向XNFT.sol充值3 LINK
assertEq (vm. activeFork () , arbSepoliaFork);
ccipLocalSimulatorFork. requestLinkFromFaucet ( address (arbSepoliaXNFT) , 3 ether );
// 步骤 6) 在Arbitrum Sepolia网络中, 铸造新的xNFT
assertEq (vm. activeFork () , arbSepoliaFork);
vm. startPrank (alice);
arbSepoliaXNFT. mint ();
uint256 tokenId = 0 ;
assertEq (arbSepoliaXNFT. balanceOf (alice) , 1 );
assertEq (arbSepoliaXNFT. ownerOf (tokenId) , alice);
// 步骤 7) 在Arbitrum Sepolia网络中, 跨链转移xNFT
arbSepoliaXNFT. crossChainTransferFrom (
address (alice) , address (bob) , tokenId , ethSepoliaNetworkDetails.chainSelector , XNFT.PayFeesIn.LINK
);
vm. stopPrank ();
assertEq (arbSepoliaXNFT. balanceOf (alice) , 0 );
// 在Ethereum Sepolia中验证xNFT已成功跨链转移
ccipLocalSimulatorFork. switchChainAndRouteMessage (ethSepoliaFork); // 这行代码将更换CHAINLINK CCIP DONs, 不要遗漏
assertEq (vm. activeFork () , ethSepoliaFork);
assertEq (ethSepoliaXNFT. balanceOf (bob) , 1 );
assertEq (ethSepoliaXNFT. ownerOf (tokenId) , bob);
}
最终的完整代码:
Copy // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import { Test , console } from "forge-std/Test.sol" ;
import { CCIPLocalSimulatorFork , Register } from "@chainlink/local/src/ccip/CCIPLocalSimulatorFork.sol" ;
import { XNFT } from "../src/XNFT.sol" ;
import { EncodeExtraArgs } from "./utils/EncodeExtraArgs.sol" ;
contract XNFTTest is Test {
CCIPLocalSimulatorFork public ccipLocalSimulatorFork;
uint256 ethSepoliaFork;
uint256 arbSepoliaFork;
Register.NetworkDetails ethSepoliaNetworkDetails;
Register.NetworkDetails arbSepoliaNetworkDetails;
address alice;
address bob;
XNFT public ethSepoliaXNFT;
XNFT public arbSepoliaXNFT;
EncodeExtraArgs public encodeExtraArgs;
function setUp () public {
alice = makeAddr ( "alice" );
bob = makeAddr ( "bob" );
string memory ETHEREUM_SEPOLIA_RPC_URL = vm. envString ( "ETHEREUM_SEPOLIA_RPC_URL" );
string memory ARBITRUM_SEPOLIA_RPC_URL = vm. envString ( "ARBITRUM_SEPOLIA_RPC_URL" );
ethSepoliaFork = vm. createSelectFork (ETHEREUM_SEPOLIA_RPC_URL);
arbSepoliaFork = vm. createFork (ARBITRUM_SEPOLIA_RPC_URL);
ccipLocalSimulatorFork = new CCIPLocalSimulatorFork ();
vm. makePersistent ( address (ccipLocalSimulatorFork));
// 步骤 1) 在Ethereum Sepolia网络中部署XNFT.sol
assertEq (vm. activeFork () , ethSepoliaFork);
ethSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); // 目前我们处于Ethereum Sepolia的分叉网络中
assertEq (
ethSepoliaNetworkDetails.chainSelector ,
16015286601757825753 ,
"Sanity check: Ethereum Sepolia chain selector should be 16015286601757825753"
);
ethSepoliaXNFT = new XNFT (
ethSepoliaNetworkDetails.routerAddress ,
ethSepoliaNetworkDetails.linkAddress ,
ethSepoliaNetworkDetails.chainSelector
);
// 步骤 2) 在Arbitrum Sepolia网络中部署XNFT.sol
vm. selectFork (arbSepoliaFork);
assertEq (vm. activeFork () , arbSepoliaFork);
arbSepoliaNetworkDetails = ccipLocalSimulatorFork.getNetworkDetails(block.chainid); // 目前我们处于Arbitrum Sepolia的分叉网络中
assertEq (
arbSepoliaNetworkDetails.chainSelector ,
3478487238524512106 ,
"Sanity check: Arbitrum Sepolia chain selector should be 421614"
);
arbSepoliaXNFT = new XNFT (
arbSepoliaNetworkDetails.routerAddress ,
arbSepoliaNetworkDetails.linkAddress ,
arbSepoliaNetworkDetails.chainSelector
);
}
function testShouldMintNftOnArbitrumSepoliaAndTransferItToEthereumSepolia () public {
// 步骤 3) 在Ethereum Sepolia网络中, 调用enableChain方法
vm. selectFork (ethSepoliaFork);
assertEq (vm. activeFork () , ethSepoliaFork);
encodeExtraArgs = new EncodeExtraArgs ();
uint256 gasLimit = 200_000 ;
bytes memory extraArgs = encodeExtraArgs. encode (gasLimit);
assertEq(extraArgs, hex"97a657c90000000000000000000000000000000000000000000000000000000000030d40"); // 该值来源于 https://cll-devrel.gitbook.io/ccip-masterclass-3/ccip-masterclass/exercise-xnft#step-3-on-ethereum-sepolia-call-enablechain-function
ethSepoliaXNFT. enableChain (arbSepoliaNetworkDetails.chainSelector , address (arbSepoliaXNFT) , extraArgs);
// 步骤 4) 在Arbitrum Sepolia网络中, 调用enableChain方法
vm. selectFork (arbSepoliaFork);
assertEq (vm. activeFork () , arbSepoliaFork);
arbSepoliaXNFT. enableChain (ethSepoliaNetworkDetails.chainSelector , address (ethSepoliaXNFT) , extraArgs);
// 步骤 5) 在Arbitrum Sepolia网络中, 向XNFT.sol充值3 LINK
assertEq (vm. activeFork () , arbSepoliaFork);
ccipLocalSimulatorFork. requestLinkFromFaucet ( address (arbSepoliaXNFT) , 3 ether );
// 步骤 6) 在Arbitrum Sepolia网络中, 增发新的xNFT
assertEq (vm. activeFork () , arbSepoliaFork);
vm. startPrank (alice);
arbSepoliaXNFT. mint ();
uint256 tokenId = 0 ;
assertEq (arbSepoliaXNFT. balanceOf (alice) , 1 );
assertEq (arbSepoliaXNFT. ownerOf (tokenId) , alice);
// 步骤 7) 在Arbitrum Sepolia网络中, 跨链转移xNFT
arbSepoliaXNFT. crossChainTransferFrom (
address (alice) , address (bob) , tokenId , ethSepoliaNetworkDetails.chainSelector , XNFT.PayFeesIn.LINK
);
vm. stopPrank ();
assertEq (arbSepoliaXNFT. balanceOf (alice) , 0 );
// 在Ethereum Sepolia中验证xNFT已成功跨链转移
ccipLocalSimulatorFork. switchChainAndRouteMessage (ethSepoliaFork); // 这行代码将更换CHAINLINK CCIP DONs, 不要遗漏
assertEq (vm. activeFork () , ethSepoliaFork);
assertEq (ethSepoliaXNFT. balanceOf (bob) , 1 );
assertEq (ethSepoliaXNFT. ownerOf (tokenId) , bob);
}
}
Last updated 3 months ago