forge initforge install smartcontractkit/chainlink-brownie-contractsforge install smartcontractkit/ccip@b06a3c2eecb9892ec6f76a015624413fffa1a122forge install OpenZeppelin/openzeppelin-contractsforge install smartcontractkit/chainlink-local# 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/',
]ETHEREUM_SEPOLIA_RPC_URL=""
ARBITRUM_SEPOLIA_RPC_URL=""# 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// 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...
}// 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);
}
}struct NetworkDetails {
uint64 chainSelector;
address routerAddress;
address linkAddress;
address wrappedNativeAddress;
address ccipBnMAddress;
address ccipLnMAddress;
}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"
); 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);
}// 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);
}
}