练习 2: 创建你的首个跨链NFT
代码时间 🎉
CCIP 配置参数
Chain Selector
Copy
3478487238524512106
Copy
16015286601757825753
CCIP 路由合约地址
0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165
0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59
LINK Token i之
0xb1D4538B4571d411F07960EF2838Ce337FE1E80E
0x779877A7B0D9E8603169DdbD7836e478b4624789
开始使用
您可以使用任何区块链开发框架来使用Chainlink CCIP。对于本次训练营,我们准备了Hardhat、Foundry 和 Remix IDE 的步骤说明。
让我们创建一个新项目。
或者你可以克隆:
安装@chainlink/contracts-ccip NPM包
要使用Chainlink CCIP,您需要与 @chainlink/contracts-ccip NPM 包中的一些Chainlink CCIP特定合约进行交互。
安装此包,请按照您用于本x的开发环境进行操作。
npm i @chainlink/contracts-ccip --save-dev
我们还需要一个标准的 @chainlink/contracts NPM包用于本模块,所以在这里我们也可以通过运行以下命令来安装它:
npm i @chainlink/contracts --save-dev
最后,对于本次练习,我们还需要安装 @openzeppelin/contracts NPM包。为此,请运行以下命令:
npm i @openzeppelin/contracts --save-dev
水龙头
您可以使用 LINK 代币或给定区块链上的原生代币/wrapped原生代币来支付 CCIP 费用。在本次练习中,我们需要至少 1 个 LINK 或 Arbitrum Sepolia 测试网代币。要获取它,请前往https://faucets.chain.link/arbitrum-sepolia
Get Arbitrum Sepolia Testnet LINK Tokens | Chainlink Faucets

开发xNFT智能合约
在contracts
文件夹中创建新文件XNFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/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.
*/
contract XNFT is
ERC721,
ERC721URIStorage,
ERC721Burnable,
IAny2EVMMessageReceiver,
ReentrancyGuard,
OwnerIsCreator
{
using SafeERC20 for IERC20;
enum PayFeesIn {
Native,
LINK
}
error InvalidRouter(address router);
error OnlyOnArbitrumSepolia();
error NotEnoughBalanceForFees(
uint256 currentBalance,
uint256 calculatedFees
);
error NothingToWithdraw();
error FailedToWithdrawEth(address owner, address target, uint256 value);
error ChainNotEnabled(uint64 chainSelector);
error SenderNotEnabled(address sender);
error OperationNotAllowedOnCurrentChain(uint64 chainSelector);
struct XNftDetails {
address xNftAddress;
bytes ccipExtraArgsBytes;
}
uint256 constant ARBITRUM_SEPOLIA_CHAIN_ID = 421614;
string[] characters = [
"https://ipfs.io/ipfs/QmTgqnhFBMkfT9s8PHKcdXBn1f5bG3Q5hmBaR4U6hoTvb1?filename=Chainlink_Elf.png",
"https://ipfs.io/ipfs/QmZGQA92ri1jfzSu61JRaNQXYg1bLuM7p8YT83DzFA2KLH?filename=Chainlink_Knight.png",
"https://ipfs.io/ipfs/QmW1toapYs7M29rzLXTENn3pbvwe8ioikX1PwzACzjfdHP?filename=Chainlink_Orc.png",
"https://ipfs.io/ipfs/QmPMwQtFpEdKrUjpQJfoTeZS1aVSeuJT6Mof7uV29AcUpF?filename=Chainlink_Witch.png"
];
IRouterClient internal immutable i_ccipRouter;
LinkTokenInterface internal immutable i_linkToken;
uint64 private immutable i_currentChainSelector;
uint256 private _nextTokenId;
mapping(uint64 destChainSelector => XNftDetails xNftDetailsPerChain)
public s_chains;
event ChainEnabled(
uint64 chainSelector,
address xNftAddress,
bytes ccipExtraArgs
);
event ChainDisabled(uint64 chainSelector);
event CrossChainSent(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
event CrossChainReceived(
address from,
address to,
uint256 tokenId,
uint64 sourceChainSelector,
uint64 destinationChainSelector
);
modifier onlyRouter() {
if (msg.sender != address(i_ccipRouter))
revert InvalidRouter(msg.sender);
_;
}
modifier onlyOnArbitrumSepolia() {
if (block.chainid != ARBITRUM_SEPOLIA_CHAIN_ID)
revert OnlyOnArbitrumSepolia();
_;
}
modifier onlyEnabledChain(uint64 _chainSelector) {
if (s_chains[_chainSelector].xNftAddress == address(0))
revert ChainNotEnabled(_chainSelector);
_;
}
modifier onlyEnabledSender(uint64 _chainSelector, address _sender) {
if (s_chains[_chainSelector].xNftAddress != _sender)
revert SenderNotEnabled(_sender);
_;
}
modifier onlyOtherChains(uint64 _chainSelector) {
if (_chainSelector == i_currentChainSelector)
revert OperationNotAllowedOnCurrentChain(_chainSelector);
_;
}
constructor(
address ccipRouterAddress,
address linkTokenAddress,
uint64 currentChainSelector
) ERC721("Cross Chain NFT", "XNFT") {
if (ccipRouterAddress == address(0)) revert InvalidRouter(address(0));
i_ccipRouter = IRouterClient(ccipRouterAddress);
i_linkToken = LinkTokenInterface(linkTokenAddress);
i_currentChainSelector = currentChainSelector;
}
function mint() external onlyOnArbitrumSepolia {
uint256 tokenId = _nextTokenId++;
string memory uri = characters[tokenId % characters.length];
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
}
function enableChain(
uint64 chainSelector,
address xNftAddress,
bytes memory ccipExtraArgs
) external onlyOwner onlyOtherChains(chainSelector) {
s_chains[chainSelector] = XNftDetails({
xNftAddress: xNftAddress,
ccipExtraArgsBytes: ccipExtraArgs
});
emit ChainEnabled(chainSelector, xNftAddress, ccipExtraArgs);
}
function disableChain(
uint64 chainSelector
) external onlyOwner onlyOtherChains(chainSelector) {
delete s_chains[chainSelector];
emit ChainDisabled(chainSelector);
}
function crossChainTransferFrom(
address from,
address to,
uint256 tokenId,
uint64 destinationChainSelector,
PayFeesIn payFeesIn
)
external
nonReentrant
onlyEnabledChain(destinationChainSelector)
returns (bytes32 messageId)
{
string memory tokenUri = tokenURI(tokenId);
_burn(tokenId);
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(
s_chains[destinationChainSelector].xNftAddress
),
data: abi.encode(from, to, tokenId, tokenUri),
tokenAmounts: new Client.EVMTokenAmount[](0),
extraArgs: s_chains[destinationChainSelector].ccipExtraArgsBytes,
feeToken: payFeesIn == PayFeesIn.LINK
? address(i_linkToken)
: address(0)
});
// Get the fee required to send the CCIP message
uint256 fees = i_ccipRouter.getFee(destinationChainSelector, message);
if (payFeesIn == PayFeesIn.LINK) {
if (fees > i_linkToken.balanceOf(address(this)))
revert NotEnoughBalanceForFees(
i_linkToken.balanceOf(address(this)),
fees
);
// Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
i_linkToken.approve(address(i_ccipRouter), fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend(
destinationChainSelector,
message
);
} else {
if (fees > address(this).balance)
revert NotEnoughBalanceForFees(address(this).balance, fees);
// Send the message through the router and store the returned message ID
messageId = i_ccipRouter.ccipSend{value: fees}(
destinationChainSelector,
message
);
}
emit CrossChainSent(
from,
to,
tokenId,
i_currentChainSelector,
destinationChainSelector
);
}
/// @inheritdoc IAny2EVMMessageReceiver
function ccipReceive(
Client.Any2EVMMessage calldata message
)
external
virtual
override
onlyRouter
nonReentrant
onlyEnabledChain(message.sourceChainSelector)
onlyEnabledSender(
message.sourceChainSelector,
abi.decode(message.sender, (address))
)
{
uint64 sourceChainSelector = message.sourceChainSelector;
(
address from,
address to,
uint256 tokenId,
string memory tokenUri
) = abi.decode(message.data, (address, address, uint256, string));
_safeMint(to, tokenId);
_setTokenURI(tokenId, tokenUri);
emit CrossChainReceived(
from,
to,
tokenId,
sourceChainSelector,
i_currentChainSelector
);
}
function withdraw(address _beneficiary) public onlyOwner {
uint256 amount = address(this).balance;
if (amount == 0) revert NothingToWithdraw();
(bool sent, ) = _beneficiary.call{value: amount}("");
if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);
}
function withdrawToken(
address _beneficiary,
address _token
) public onlyOwner {
uint256 amount = IERC20(_token).balanceOf(address(this));
if (amount == 0) revert NothingToWithdraw();
IERC20(_token).safeTransfer(_beneficiary, amount);
}
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}
function getCCIPRouter() public view returns (address) {
return address(i_ccipRouter);
}
function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, ERC721URIStorage) returns (bool) {
return
interfaceId == type(IAny2EVMMessageReceiver).interfaceId ||
super.supportsInterface(interfaceId);
}
}
运行以下指令来编译:
npx hardhat compile
准备部署
按照步骤添加必要的环境变量,以便部署这些合约并发送您的第一个 CCIP 消息。
该合约需要至少 0.8.20 的 Solidity 版本。因为在 0.8.20 版本的 Solc 中,默认的 EVM 版本为“上海”。在上海升级中,一个新的操作码 PUSH0 被添加到以太坊虚拟机中。
然而,除了以太坊外的大多数区块链尚未支持 PUSH0 操作码。
这意味着 PUSH0 操作码现在是合约字节码的一部分,如果您正在使用的链不支持它,它将会报出“无效操作码”错误。
为了了解更多信息,我们强烈建议您查看此 StackOverflow 的回复:
我们将使用 @chainlink/env-enc
包来提高安全性。它通过创建一个新的 .env.enc
文件来加密敏感数据,而不是将其以明文形式存储在 .env 文件中。虽然不建议将此文件上传到网络,但如果意外发生,您的机密信息仍然会被加密。 通过运行以下命令来安装该包:
npm i @chainlink/env-enc --save-dev
设置一个密码用于加密和解密环境变量文件。您可以稍后通过输入相同的命令来更改密码。
npx env-enc set-pw
现在设置以下环境变量:PRIVATE_KEY、源区块链的 RPC URL、目标区块链的 RPC URL。在此示例中,我们将使用 Arbitrum Sepolia 和 Ethereum Sepolia。
PRIVATE_KEY=""
ARBITRUM_SEPOLIA_RPC_URL=""
ETHEREUM_SEPOLIA_RPC_URL=""
要设置以上变量,请输入以下命令并按照终端中的说明进行操作:
npx env-enc set
完成以上操作后,.env.enc 文件将自动生成。如果您想验证您的输入,可以随时运行以下命令:
npx env-enc view
最后,扩展 hardhat.config
文件以支持这两个网络:
import * as dotenvenc from '@chainlink/env-enc'
dotenvenc.config();
import { HardhatUserConfig } from 'hardhat/config';
import '@nomicfoundation/hardhat-toolbox';
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const ARBITRUM_SEPOLIA_RPC_URL = process.env.ARBITRUM_SEPOLIA_RPC_URL;
const ETHEREUM_SEPOLIA_RPC_URL = process.env.ETHEREUM_SEPOLIA_RPC_URL;
const config: HardhatUserConfig = {
solidity: {
compilers: [
{
version: '0.8.20',
settings: {
evmVersion: 'paris'
}
}
]
},
networks: {
hardhat: {
chainId: 31337
},
arbitrumSepolia: {
url: ARBITRUM_SEPOLIA_RPC_URL !== undefined ? ARBITRUM_SEPOLIA_RPC_URL : '',
accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
chainId: 421614
},
ethereumSepolia: {
url: ETHEREUM_SEPOLIA_RPC_URL !== undefined ? ETHEREUM_SEPOLIA_RPC_URL : '',
accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
chainId: 11155111
},
}
};
export default config;
步骤1)在Ethereum Sepolia上部署XNFT.sol
准备 Ethereum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。CCIP 配置参数
进入scripts
文件夹并创建一个名为 deployXNFT.ts
的新文件。
// deployXNFT.ts
import { ethers, network } from "hardhat";
async function main() {
const ccipRouterAddressEthereumSepolia = `0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59`;
const linkTokenAddressEthereumSepolia = `0x779877A7B0D9E8603169DdbD7836e478b4624789`;
const chainIdEthereumSepolia = `16015286601757825753`;
const xNft = await ethers.deployContract("XNFT", [
ccipRouterAddressEthereumSepolia,
linkTokenAddressEthereumSepolia,
chainIdEthereumSepolia
]);
await xNft.waitForDeployment();
console.log(`XNFT deployed on ${network.name} with address ${xNft.target}`);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
运行部署脚本:
npx hardhat run ./scripts/deployXNFT.ts --network ethereumSepolia
步骤 2) 在Arbitrum Sepolia上部署XNFT.sol
准备 Arbitrum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。CCIP 配置参数
进入scripts
文件夹并创建一个名为 deployXNFTArbitrum.ts
的新文件。
// deployXNFTArbitrum.ts
import { ethers, network } from "hardhat";
async function main() {
const ccipRouterAddressArbitrumSepolia = `0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165`;
const linkTokenAddressArbitrumSepolia = `0xb1D4538B4571d411F07960EF2838Ce337FE1E80E`;
const chainIdArbitrumSepolia = `3478487238524512106`;
const xNft = await ethers.deployContract("XNFT", [
ccipRouterAddressArbitrumSepolia,
linkTokenAddressArbitrumSepolia,
chainIdArbitrumSepolia
]);
await xNft.waitForDeployment();
console.log(`XNFT deployed on ${network.name} with address ${xNft.target}`);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
运行部署脚本:
npx hardhat run ./scripts/deployXNFTArbitrum.ts --network arbitrumSepolia
步骤 3) 在Ethereum Sepolia网络中, 调用enableChain方法
需要准备:
您之前部署到 Ethereum Sepolia 的 XNFT.sol 智能合约地址;
您之前部署到 Arbitrum Sepolia 的 XNFT.sol 智能合约地址;
chainSelector
参数:3478487238524512106,这是 Arbitrum Sepolia 网络的 CCIP Chain Selector;ccipExtraArgs
参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。
如果您想自己计算这个值,可以重用以下辅助智能合约:
// 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);
}
}
在scripts
文件夹中创建TypeScript文件enableChain.ts
:
// scripts/enableChain.ts
import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { XNFT, XNFT__factory } from "../typechain-types";
async function main() {
if (network.name !== `ethereumSepolia`) {
console.error(`Must be called from Ethereum Sepolia`);
return 1;
}
const privateKey = process.env.PRIVATE_KEY!;
const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;
const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
const wallet = new Wallet(privateKey);
const signer = wallet.connect(provider);
const xNftAddressEthereumSepolia = `PUT XNFT ADDRESS ON ETHEREUM SEPOLIA HERE`;
const xNftAddressArbitrumSepolia = `PUT XNFT ADDRESS ON ARBITRUM SEPOLIA HERE`;
const chainSelectorArbitrumSepolia = `3478487238524512106`;
const ccipExtraArgs = `0x97a657c90000000000000000000000000000000000000000000000000000000000030d40`;
const xNft: XNFT = XNFT__factory.connect(xNftAddressEthereumSepolia, signer);
const tx = await xNft.enableChain(
chainSelectorArbitrumSepolia,
xNftAddressArbitrumSepolia,
ccipExtraArgs
);
console.log(`Transaction hash: ${tx.hash}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
运行以下指令来调用enableChain方法:
npx hardhat run ./scripts/enableChain.ts --network ethereumSepolia
步骤 4) 在Arbitrum Sepolia网络中, 调用enableChain方法
需要准备:
您之前部署到 Arbitrum Sepolia 的 XNFT.sol 智能合约地址;
您之前部署到 Ethereum Sepolia 的 XNFT.sol 智能合约地址,作为 xNftAddress 参数;
chainSelector
参数:16015286601757825753,这是 Ethereum Sepolia 网络的 CCIP Chain Selector;ccipExtraArgs
参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。
如果您想自己计算这个值,可以重用以下辅助智能合约:
// 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);
}
}
在scripts
文件夹中创建TypeScript文件enableChainArbitrum.ts
:
// scripts/enableChainArbitrum.ts
import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { XNFT, XNFT__factory } from "../typechain-types";
async function main() {
if (network.name !== `arbitrumSepolia`) {
console.error(`Must be called from Arbitrum Sepolia`);
return 1;
}
const privateKey = process.env.PRIVATE_KEY!;
const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;
const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
const wallet = new Wallet(privateKey);
const signer = wallet.connect(provider);
const xNftAddressArbitrumSepolia = `PUT XNFT ADDRESS ON ARBITRUM SEPOLIA HERE`;
const xNftAddressEthereumSepolia = `PUT XNFT ADDRESS ON ETHEREUM SEPOLIA HERE`;
const chainSelectorEthereumSepolia = `16015286601757825753`;
const ccipExtraArgs = `0x97a657c90000000000000000000000000000000000000000000000000000000000030d40`;
const xNft: XNFT = XNFT__factory.connect(xNftAddressArbitrumSepolia, signer);
const tx = await xNft.enableChain(
chainSelectorEthereumSepolia,
xNftAddressEthereumSepolia,
ccipExtraArgs
);
console.log(`Transaction hash: ${tx.hash}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
运行以下指令来调用enableChain方法:
npx hardhat run ./scripts/enableChainArbitrum.ts --network arbitrumSepolia
步骤 5) 在Arbitrum Sepolia网络中, 为XNFT.sol充值3 LINK
为支付 CCIP 费用,向XNFT.sol
充值一定数量的 LINK。3 个 LINK 对于此演示应该绰绰有余。当然,为了实现完整的功能,您还应在其他区块链上为XNFT.sol
智能合约充值,以便在所有区块链之间执行跨链转账。

步骤 6) 在Arbitrum Sepolia网络中, 铸造新的xNFT
在scripts文件夹中创建
TypeScript文件mint.ts:
// scripts/mint.ts
import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { XNFT, XNFT__factory } from "../typechain-types";
async function main() {
if (network.name !== `arbitrumSepolia`) {
console.error(`Must be called from Arbitrum Sepolia`);
return 1;
}
const privateKey = process.env.PRIVATE_KEY!;
const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;
const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
const wallet = new Wallet(privateKey);
const signer = wallet.connect(provider);
const xNftAddressArbitrumSepolia = `PUT XNFT ADDRESS ON ARBITRUM SEPOLIA HERE`;
const xNft: XNFT = XNFT__factory.connect(xNftAddressArbitrumSepolia, signer);
const tx = await xNft.mint();
console.log(`Transaction hash: ${tx.hash}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
运行以下指令调用mint方法:
npx hardhat run ./scripts/mint.ts --network arbitrumSepolia
步骤 7) 在Arbitrum Sepolia网络中, 跨链转移xNFT
需要准备:
from
参数:您的 EOA 地址;to
参数:您希望跨链转移您的 NFT 的另一个链上的 EOA 地址,可以是您的 EOA 地址;tokenId
参数:您要跨链转移的 xNFT 的 ID;destinationChainSelector
参数:16015286601757825753,这是 Ethereum Sepolia 区块链的 CCIP Chain Selector;payFeesIn
参数:1,表示我们用 LINK 支付 CCIP 费用。
在 scripts
文件夹下创建一个 TypeScript 文件crossChainTransferFrom.ts
// scripts/crossChainTransfer.ts
import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { XNFT, XNFT__factory } from "../typechain-types";
async function main() {
if (network.name !== `arbitrumSepolia`) {
console.error(`Must be called from Arbitrum Sepolia`);
return 1;
}
const privateKey = process.env.PRIVATE_KEY!;
const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;
const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
const wallet = new Wallet(privateKey);
const signer = wallet.connect(provider);
const xNftAddressArbitrumSepolia = `PUT XNFT ADDRESS ON ARBITRUM SEPOLIA HERE`;
const from = `PUT YOUR EOA ADDRESS HERE`;
const to = `PUT RECEIVER's ADDRESS HERE`;
const tokenId = 0; // put NFT token id here
const destinationChainSelector = `16015286601757825753`;
const payFeesIn = 1; // 0 - Native, 1 - LINK
const xNft: XNFT = XNFT__factory.connect(xNftAddressArbitrumSepolia, signer);
const tx = await xNft.crossChainTransferFrom(
from,
to,
tokenId,
destinationChainSelector,
payFeesIn
);
console.log(`Transaction hash: ${tx.hash}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
运行以下指令调用crossChainTransferFrom方法:
npx hardhat run ./scripts/crossChainTransfer.ts --network arbitrumSepolia
您现在可以在 CCIP Explorer 页面监控此次NFT的跨链转移。

一旦跨链 NFT 到达 Ethereum Sepolia,您可以在 Metamask 钱包中手动显示它。点击进入“NFT”选项卡并点击“Import NFT”按钮。

然后填写 Ethereum Sepolia 上的 XNFT.sol
智能合约地址和您收到的代币 ID(0)。

最后,您的 NFT 将显示在 Metamask 钱包中。

Last updated