跳至主要内容
本页面使用机器翻译自英语,可能包含错误或不清楚的语言。如需最准确的信息,请参阅英文原文。由于更新频繁,部分内容可能与英文原文有出入。请加入我们在 Crowdin 上的努力,帮助我们改进本页面的翻译。 (Crowdin translation page, Contributing guide)

利用链式链接 CCIP 在 Kaia 上构建跨链 NFT:实用指南

介绍​

NFT 已成为区块链技术最广为人知的用例之一,能够创建独一无二、可验证的数字资产。然而,传统的 NFT 实现方式被束缚在单个区块链上。这种限制降低了灵活性,使资产无法在社区、流动性和效用可能不同的生态系统中自由流动。

跨链 NFT 允许 NFT 在区块链之间无缝移动,同时保留其唯一性和出处,从而解决了这一难题。借助 Chainlink 的跨链互操作性协议 (CCIP),开发人员可以使用标准化的安全消息框架在链之间建立可靠的桥梁。

在本指南中,您将使用 "先烧后铸 "模式构建和部署跨链 NFT。 NFT 将在源链上刻录,并以相同的 tokenId 和元数据在目标链上重新铸模,确保任何时候都只存在一个有效副本。

要求​

开始之前,请确保您已做好以下设置:

  • Node.js 和 npm
  • 硬礼帽
    • 安装:npm install --save-dev hardhat
    • 初始化项目: npx hardhat --init
  • MetaMask 钱包
    • 创建或设置开发钱包。
    • 将 Kaia Kairos 测试网络和以太坊 Sepolia 网络添加到 MetaMask 中。
  • 从水龙头测试代币
    • KAIA: 在部署合约或发送交易时支付 Kaia 上的燃气费。
    • LINK (testnet):使用 LINK 支付时涵盖 CCIP 费用。
    • Sepolia ETH: 支付 Sepolia 上的燃气费,如果选择,还可支付本地 ETH 中的 CCIP 费用。
  • Filebase 账户
    • 需要上传和检索 NFT 元数据(IPFS 存储)。

跨链 NFT 如何工作?​

NFT 是记录在单一区块链上的唯一数字代币。其核心行为,包括铸币、转让和所有权,由与该链绑定的智能合约定义。正因为如此,如果没有额外的机制,NFT 无法自然地在区块链之间移动。为了实现互操作性,开发人员会在多个链上部署配套合约,并通过跨链消息传递将它们连接起来。这就是跨链 NFT:存在于不同区块链上的等价代币,但在任何时候都只有一个副本处于活动状态。

跨链 NFT 通常有三种实现方式:

  • 焚烧和铸币:在源链上烧制 NFT,然后在目的链上铸造等价物。

  • 锁定和铸币:在源链上锁定 NFT,并在目标链上铸造副本。返回时需要烧毁复制品才能解锁原件。

  • 锁定和解锁:在多个链上部署相同的集合。所有者锁定一条链上的 NFT,就能解锁另一条链上的对应 NFT,确保一次只能使用一个副本。

在本指南中,我们将在跨链 NFT 中使用燃烧和薄荷模型。 NFT 将从一条链上移除,然后在另一条链上重新创建,整个过程由 Chainlink CCIP 提供动力。

入门​

在本指南中,您将使用 Chainlink CCIP 在 Kaia Kairos Testnet 和 Ethereum Sepolia 之间铸币和传输跨链 NFT。

到最后,您将能够

  • 初始化为 Kairos Testnet 和 Ethereum Sepolia 配置的 Hardhat 项目
  • 将 Chainlink CCIP 合同和接口添加为依赖项
  • 实施跨链 NFT 合约,为跨链转账提供烧钱机制
  • 将合同部署到两个网络,并跨链发送 NFT。

创建硬礼帽项目​

在本教程中,我们将使用 Hardhat 3 来部署合同并与之交互。 Hardhat 3 提供了一些新功能,例如对加密密钥存储的本地支持、在 Solidity 中编写测试的能力以及改进的项目工具。

请按照以下步骤设置项目:

  1. 验证 Node.js 和 npm 的安装

    运行以下命令验证 Node.js 和 npm 是否已安装:


node -v
npm -v

  1. 初始化新项目目录

    创建一个新文件夹,导航进入,然后初始化一个 Node.js 项目:


mkdir ccip-nft-kaia-hardhat-example
cd ccip-nft-kaia-hardhat-example
npm init -y

  1. 创建 Hardhat 项目

    运行:


npx hardhat --init

出现提示时,选择包含 Node.js 测试运行器和ether 的示例项目。在当前目录下对其进行初始化,并安装所有需要的依赖项。

安装所需合同​

安装 Chainlink CCIP 合同:


npm i @chainlink/contracts-ccip --save-dev

安装标准链锁合同:


npm i @chainlink/contracts --save-dev

安装 OpenZeppelin 合约(提供 ERC-721 和其他基本实现):


npm i @openzeppelin/contracts --save-dev

配置 NFT 元数据​

在撰写合同之前,让我们先确定一下我们要铸造的 NFT 的规格。每个 NFT 都需要描述其名称、说明和图像的元数据,这些数据存储在一个 JSON 文件中,并托管在 IPFS 上。

在本指南中,我们将使用 Filebase 来存储图像和元数据。如果您想创建自己的 NFT,请通过 Filebase 将图像和元数据 JSON 文件上传到 IPFS。上传后,单击 "文件 "选项卡中的文件名并复制 IPFS URL。它看起来类似于这样:


https://disastrous-turquoise-parakeet.myfilebase.com/ipfs/QmY1LZF8JHo2r3h4X5VzLLXtJujqnBFGTyo2aqR9joXnt8

下面是您可以使用的元数据文件示例:


{
"name": "Kairos NFT",
"description": "gkaia frens! gazuaaaaa!!!",
"image": "https://disastrous-turquoise-parakeet.myfilebase.com/ipfs/QmRvQc4wZCp6NF7dFL4ywiWTG7FSH3KKGUAkXGgsdYfcKi"
}

编写智能合约​

在本节中,您将使用由 Chainlink CCIP 支持的 "烧钱-铸币 "模式来实现在区块链之间进行 NFT 传输的合约。

在项目的合约目录下新建一个名为 "CrosschainNFT.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/contracts/libraries/Client.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/contracts/interfaces/IAny2EVMMessageReceiver.sol";
import {OwnerIsCreator} from "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol";
import {LinkTokenInterface} from "@chainlink/contracts/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.
*/
// Source chain is Ethereum Sepolia
// Destination chain is Kairos Testnet
contract CrosschainNFT is ERC721, ERC721URIStorage, ERC721Burnable, IAny2EVMMessageReceiver, ReentrancyGuard, OwnerIsCreator {
using SafeERC20 for IERC20;
enum PayFeesIn {
Native,
LINK
}
error InvalidRouter(address router);
error OnlyOnEthereumSepolia();
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 crosschainNFTDetails {
address crosschainNFTAddress;
bytes ccipExtraArgsBytes;
}
uint256 constant ETHEREUM_SEPOLIA_CHAIN_ID = 11155111;
string tokenNFTURI = "https://disastrous-turquoise-parakeet.myfilebase.com/ipfs/QmY1LZF8JHo2r3h4X5VzLLXtJujqnBFGTyo2aqR9joXnt8";
IRouterClient internal immutable i_ccipRouter;
LinkTokenInterface internal immutable i_linkToken;
uint64 private immutable i_currentChainSelector;
uint256 private _nextTokenId;
mapping(uint64 destChainSelector => crosschainNFTDetails crosschainNFTPerChain) 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 onlyOnEthereumSepolia() {
if (block.chainid != ETHEREUM_SEPOLIA_CHAIN_ID) {
revert OnlyOnEthereumSepolia();
}
_;
}
modifier onlyEnabledChain(uint64 _chainSelector) {
if (s_chains[_chainSelector].crosschainNFTAddress == address(0)) {
revert ChainNotEnabled(_chainSelector);
}
_;
}
modifier onlyEnabledSender(uint64 _chainSelector, address _sender) {
if (s_chains[_chainSelector].crosschainNFTAddress != _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 onlyOnEthereumSepolia {
uint256 tokenId = _nextTokenId++;
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, tokenNFTURI);
}
function enableChain(uint64 chainSelector, address crosschainNFTAddress, bytes memory ccipExtraArgs)
external
onlyOwner
onlyOtherChains(chainSelector)
{
s_chains[chainSelector] = crosschainNFTDetails({crosschainNFTAddress: crosschainNFTAddress, ccipExtraArgsBytes: ccipExtraArgs});
emit ChainEnabled(chainSelector, crosschainNFTAddress, 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].crosschainNFTAddress),
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);
}
}

代码演练​

CrosschainNFT 是一种 ERC-721 合约,它集成了 Chainlink CCIP,可在区块链之间传输 NFT。它会在源链上刻录 NFT,并用相同的 tokenId 和 tokenURI 在目标链上重新刻录。该合约通过 enableChain 维护经批准的目标链注册表,依靠 Chainlink 路由器(IRouterClient)进行跨链消息传递,并支持以本地天然气代币或 LINK 支付费用。

主要功能

  • 启用链

允许合约所有者注册目标区块链。它会在 s_chains 映射中存储对应的 NFT 合约地址和 CCIP 参数,将链作为有效的传输目标列入白名单。设置完成后,会发出 ChainEnabled 事件。

  • 交叉链转移从

执行 NFT 跨链转移。它首先检查目标链是否已启用,然后检索 NFT 元数据(tokenURI)并在源链上刻录令牌。然后,它将建立一个包含转账详情的 CCIP 报文,计算所需费用,并以 LINK 或本地天然气支付。一旦信息通过路由器发送,就会发出 CrossChainSent 事件来记录传输过程。

现在,"CrosschainNFT.sol "的核心流程已经清楚,让我们进入下一步。

编译智能合约​

要编译智能合约,请运行


npx hardhat build

部署智能合约​

在本节中,我们将配置必要的变量,然后在 Ethereum Sepolia(源链)和 Kairos Testnet(目的链)上部署 "CrosschainNFT.sol "合约。

使用加密密钥库​

Hardhat 3 的优势之一是能将私钥和 RPC URL 等敏感值存储在加密的密钥库中,而不是纯文本文件中。在本指南中,我们将为 Sepolia 和 Kairos 的 PRIVATE_KEY 和 RPC URL 加密。

添加私人密钥


npx hardhat keystore set PRIVATE_KEY

第一次运行该命令时,Hardhat 会提示你为密钥存储创建一个密码。每次添加或更新值时都需要这个密码。

为每个网络添加 RPC URL


npx hardhat keystore set KAIROS_RPC_URL
npx hardhat keystore set SEPOLIA_RPC_URL

最后,编辑 hardhat.config.ts 文件,加载这些加密值并配置两个网络。


import type { HardhatUserConfig } from "hardhat/config";
import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers";
import { configVariable } from "hardhat/config";
const config: HardhatUserConfig = {
plugins: [hardhatToolboxMochaEthersPlugin],
solidity: {
profiles: {
default: {
version: "0.8.28",
},
production: {
version: "0.8.28",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
},
},
networks: {
hardhatMainnet: {
type: "edr-simulated",
chainType: "l1",
},
hardhatOp: {
type: "edr-simulated",
chainType: "op",
},
kairosTestnet: {
type: "http",
chainType: "l1",
url: configVariable("KAIROS_RPC_URL"),
accounts: [configVariable("PRIVATE_KEY")],
},
ethereumSepolia: {
type: "http",
chainType: "l1",
url: configVariable("SEPOLIA_RPC_URL"),
accounts: [configVariable("PRIVATE_KEY")],
},
},
};
export default config;

下一步是分别在 Ethereum Sepolia 和 Kairos Testnet 上部署 CrosschainNFT 智能合约。

在以太坊 Sepolia 上部署 CrosschainNFT.sol​

部署前,从 Chainlink CCIP Directory 中获取以太坊 Sepolia 的以下值:

  • 链条选择器
  • CCIP 路由器地址
  • LINK 令牌地址

您的部署脚本将需要这些值。接下来,导航到项目中的ignition/modules文件夹,创建一个名为 "deployEthereumSepolia.ts "的新文件,并粘贴以下代码:


// This setup uses Hardhat Ignition to manage smart contract deployments.
// Learn more about it at https://hardhat.org/ignition
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
const ETHEREUM_SEPOLIA_ROUTER_ADDRESS = `0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59`;
const ETHEREUM_SEPOLIA_LINK_TOKEN_ADDRESS = `0x779877A7B0D9E8603169DdbD7836e478b4624789`;
const ETHEREUM_SEPOLIA_CHAIN_SELECTOR = `16015286601757825753`;
const CrosschainNFTSepoliaModule = buildModule("CrosschainNFTSepoliaModule", (m) => {
const crosschainNFTSepolia = m.contract("CrosschainNFT", [ETHEREUM_SEPOLIA_ROUTER_ADDRESS, ETHEREUM_SEPOLIA_LINK_TOKEN_ADDRESS, ETHEREUM_SEPOLIA_CHAIN_SELECTOR], {
});
return { crosschainNFTSepolia };
});
export default CrosschainNFTSepoliaModule;

运行部署脚本:


npx hardhat ignition deploy ignition/modules/deployEthereumSepolia.ts --network ethereumSepolia

将 CrosschainNFT.sol 部署到 Kairos 测试网​

部署前,从 Chainlink CCIP Directory 中获取下列 Kairos Testnet 的值:

  • 链条选择器
  • CCIP 路由器地址
  • LINK 令牌地址

您的部署脚本将需要这些值。然后,导航到项目中的 ignition/modules 文件夹,创建一个名为 deployKairosTestnet.ts 的新文件,并粘贴以下代码:


// This setup uses Hardhat Ignition to manage smart contract deployments.
// Learn more about it at https://hardhat.org/ignition
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
const KAIROS_TESTNET_ROUTER_ADDRESS = `0x41477416677843fCE577748D2e762B6638492755`;
const KAIROS_TESTNET_LINK_TOKEN_ADDRESS = `0xAF3243f975afe2269Da8Ffa835CA3A8F8B6A5A36`;
const KAIROS_TESTNET_CHAIN_SELECTOR = `2624132734533621656`;
const CrosschainNFTKairosModule = buildModule("CrosschainNFTKairosModule", (m) => {
const crosschainNFTKairos = m.contract("CrosschainNFT", [KAIROS_TESTNET_ROUTER_ADDRESS, KAIROS_TESTNET_LINK_TOKEN_ADDRESS, KAIROS_TESTNET_CHAIN_SELECTOR], {
});
return { crosschainNFTKairos };
});
export default CrosschainNFTKairosModule;

运行部署脚本:


npx hardhat ignition deploy ignition/modules/deployKairosTestnet.ts --network kairosTestnet

与智能合约互动​

在本节中,我们将分别通过执行 enableChain、mint 和 crosschainTransfer 函数与已部署的 CrosschainNFT 智能合约进行交互。

步骤 1:在以太坊 Sepolia 上,调用 enableChain​

在调用 enableChain 之前,请准备好以下值:

  • Sepolia合约地址:部署在以太坊Sepolia上的CrosschainNFT.sol合约的地址。
  • Kairos 合约地址:部署在 Kairos Testnet 上的 CrosschainNFT.sol 合约的地址。
  • 链选择器:2624132734533621656(Kairos Testnet 的 CCIP 链选择器)。
  • CCIP extraArgs:0x97a657c900000000000000000000000000000000000000000007A120 (这是 extraArgs 的默认编码值,gasLimit 设置为 500,000)。

接下来,在脚本文件夹中新建一个 TypeScript 文件,命名为 "enableChainSepolia.ts",并粘贴以下代码:


// scripts/enableChainSepolia.ts
import { network } from "hardhat";
async function main() {
const connection = await network.connect({
network: "ethereumSepolia"
});
const { ethers } = connection;
const [signer] = await ethers.getSigners();
console.log(`Using account: ${signer.address}`);
// Get the contract factory by name
const CrosschainNFT = await ethers.getContractFactory("CrosschainNFT", signer);
// Contract addresses and parameters
const crosschainNFTAddressEthereumSepolia = `0xb1fe42BBd7842703820C7480c22409b872319B22`;
const crosschainNFTAddressKairosTestnet = `0x8c464Bb9Bf364F68b898ed0708b8f5F66EF6Cfb1`;
const chainSelectorKairosTestnet = `2624132734533621656`;
const ccipExtraArgs = `0x97a657c9000000000000000000000000000000000000000000000000000000000007A120`;
// Attach to the deployed contract
const crosschainNFTSepolia = CrosschainNFT.attach(crosschainNFTAddressEthereumSepolia);
console.log(`Enabling chain for Kairos Testnet...`);
const tx = await crosschainNFTSepolia.enableChain(
chainSelectorKairosTestnet,
crosschainNFTAddressKairosTestnet,
ccipExtraArgs
);
console.log(`Transaction hash: ${tx.hash}`);
console.log(`Waiting for confirmation...`);
const receipt = await tx.wait();
console.log(`Transaction confirmed in block: ${receipt?.blockNumber}`);
console.log(`Chain enabled successfully!`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

运行以下命令调用该函数:


npx hardhat run scripts/enableChainSepolia.ts --network ethereumSepolia

步骤 2:在 Kairos Testnet 上,调用 enableChain​

在调用 enableChain 之前,请准备好以下值:

  • Kairos合约地址:部署在Kairos测试网上的CrosschainNFT.sol合约的地址
  • Sepolia合约地址:部署在以太坊Sepolia上的CrosschainNFT.sol合约的地址
  • 链选择器:16015286601757825753 (Kairos Testnet 的 CCIP 链选择器)
  • CCIP extraArgs:0x97a657c900000000000000000000000000000000000000000007A120 (这是 extraArgs 的默认编码值,气体限值设置为 500,000)

接下来,在脚本文件夹中新建一个 TypeScript 文件,命名为 "enableChainKairos.ts",并粘贴以下代码:


// scripts/enableChainKairos.ts
import { network } from "hardhat";
async function main() {
const connection = await network.connect({
network: "kairosTestnet"
});
const { ethers } = connection;
const [signer] = await ethers.getSigners();
console.log(`Using account: ${signer.address}`);
// Get the contract factory by name
const CrosschainNFT = await ethers.getContractFactory("CrosschainNFT", signer);
// Contract addresses and parameters
const crosschainNFTAddressKairosTestnet = `0x8c464Bb9Bf364F68b898ed0708b8f5F66EF6Cfb1`;
const crosschainNFTAddressEthereumSepolia = `0xb1fe42BBd7842703820C7480c22409b872319B22`;
const chainSelectorEthereumSepolia = `16015286601757825753`;
const ccipExtraArgs = `0x97a657c9000000000000000000000000000000000000000000000000000000000007A120`;
// Attach to the deployed contract on Kairos
const crosschainNFTKairos = CrosschainNFT.attach(crosschainNFTAddressKairosTestnet);
console.log(`Enabling chain for Ethereum Sepolia...`);
const tx = await crosschainNFTKairos.enableChain(
chainSelectorEthereumSepolia,
crosschainNFTAddressEthereumSepolia,
ccipExtraArgs
);
console.log(`Transaction hash: ${tx.hash}`);
console.log(`Waiting for confirmation...`);
const receipt = await tx.wait();
console.log(`Transaction confirmed in block: ${receipt?.blockNumber}`);
console.log(`Chain enabled successfully!`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

运行以下命令调用该函数:


npx hardhat run scripts/enableChainKairos.ts --network KairosTestnet

为支付 CCIP 费用,请使用 LINK 为部署在以太坊 Sepolia(crosschainNFTAddressEthereumSepolia)上的 CrosschainNFT 合约提供资金。您可以从提供的 水龙头 中获取测试 LINK。在本指南中,发送 3 个 LINK 即可。

步骤 4:在以太坊 Sepolia 上铸币一个新的 CrosschainNFT​

接下来,在部署到以太坊 Sepolia 的 CrosschainNFT 合约上创建一个新的 NFT。

在脚本文件夹中创建一个新的 TypeScript 文件,命名为 "mint.ts",并粘贴以下代码:


// scripts/mint.ts
import { network } from "hardhat";
async function main() {
// Connect to the network
const connection = await network.connect({
network: "ethereumSepolia"
});
if (connection.networkName !== "ethereumSepolia") {
console.error(`Must be called from Ethereum Sepolia`);
process.exitCode = 1;
return;
}
const { ethers } = connection;
const [signer] = await ethers.getSigners();
console.log(`Using account: ${signer.address}`);
// Get the contract factory
const CrosschainNFT = await ethers.getContractFactory("CrosschainNFT", signer);
const crosschainNFTAddressEthereumSepolia = `0xb1fe42BBd7842703820C7480c22409b872319B22`
// Attach to the deployed contract
const crosschainNFT = CrosschainNFT.attach(crosschainNFTAddressEthereumSepolia);
console.log(`Minting NFT...`);
const tx = await crosschainNFT.mint();
console.log(`Transaction hash: ${tx.hash}`);
console.log(`Waiting for confirmation...`);
const receipt = await tx.wait();
console.log(`Transaction confirmed in block: ${receipt?.blockNumber}`);
console.log(`NFT minted successfully!`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

该脚本将处理铸币过程,并为跨链传输 NFT 做好准备。

运行以下命令调用该函数:


npx hardhat run scripts/mint.ts --network ethereumSepolia

步骤 5:跨链转移 NFT​

在以太坊 Sepolia 上,您将调用 crossChainTransferFrom 函数将您的 NFT 发送到 Kairos Testnet。

准备以下值:

  • 来自:您在以太坊 Sepolia 上的 EOA 地址
  • 收件人*:收件人在 Kairos Testnet 上的 EOA 地址(也可以是您自己的地址)
  • tokenId:要转移的 NFT 的 ID
  • 目标链选择器:2624132734533621656(Kairos Testnet 的 CCIP 链选择器)
  • payFeesIn:1(表示将在 LINK 中支付 CCIP 费用)

运行传输脚本

在脚本文件夹中新建一个 TypeScript 文件,命名为 "crossChainTransferNFT.ts",并粘贴以下代码:


// scripts/crossChainTransferNFT.ts
import { network } from "hardhat";
async function main() {
// Connect to the network
const connection = await network.connect({
network: "ethereumSepolia"
});
// Check if we're on the correct network
if (connection.networkName !== "ethereumSepolia") {
console.error(`Must be called from Ethereum Sepolia`);
process.exitCode = 1;
return;
}
const { ethers } = connection;
const [signer] = await ethers.getSigners();
console.log(`Using account: ${signer.address}`);
// Get the contract factory
const CrosschainNFT = await ethers.getContractFactory("CrosschainNFT", signer);
const crosschainNFTAddressEthereumSepolia = `0xb1fe42BBd7842703820C7480c22409b872319B22`;
// Transfer parameters
const from = `0x7b467A6962bE0ac80784F131049A25CDE27d62Fb`;
const to = `0x7b467A6962bE0ac80784F131049A25CDE27d62Fb`;
const tokenId = 0; // Put NFT token id here
const destinationChainSelector = "2624132734533621656"; // Kairos Testnet
const payFeesIn = 1; // 0 - Native, 1 - LINK
// Attach to the deployed contract
const crosschainNFT = CrosschainNFT.attach(crosschainNFTAddressEthereumSepolia);
const tx = await crosschainNFT.crossChainTransferFrom(
from,
to,
tokenId,
destinationChainSelector,
payFeesIn
);
console.log(`Transaction hash: ${tx.hash}`);
console.log(`Waiting for confirmation...`);
const receipt = await tx.wait();
console.log(`Transaction confirmed in block: ${receipt?.blockNumber}`);
console.log(`Cross-chain transfer initiated successfully!`);
console.log(`Note: The NFT will arrive on Kairos Testnet after CCIP processes the message.`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

然后用


npx hardhat run scripts/crossChainTransferNFT.ts --network ethereumSepolia

验证转移

您可以在 CCIP Explorer 上监控跨链转账,在 Kaiascan 上确认交易。

NFT 到达 Kairos Testnet 后,将其添加到您的 MetaMask 钱包中:

  1. 在 MetaMask 中打开 NFT 选项卡。
  2. 单击导入 NFT。
  3. 输入 Kairos Testnet 上的 CrosschainNFT 合约地址和您收到的 tokenId(例如 0)。

现在,您的 NFT 将出现在 MetaMask 钱包中。

结论​

在本教程中,您将学习如何使用 Chainlink CCIP 在 Kaia Kairos Testnet 和 Ethereum Sepolia 之间使用烧毁和铸币模型传输 NFT。

要深入了解 CCIP 并探索其他用例,请访问官方Chainlink CCIP Documentation。

让这个页面变得更好