练习 2: 创建你的首个跨链NFT

代码时间 🎉

CCIP 配置参数

参数
Arbitrum Sepolia
Ethereum Sepolia

Chain Selector

Copy

3478487238524512106

Copy

16015286601757825753

CCIP 路由合约地址

0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165

0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59

LINK Token i之

0xb1D4538B4571d411F07960EF2838Ce337FE1E80E

0x779877A7B0D9E8603169DdbD7836e478b4624789

开始使用

您可以使用任何区块链开发框架来使用Chainlink CCIP。对于本次训练营,我们准备了Hardhat、Foundry 和 Remix IDE 的步骤说明。

让我们创建一个新项目。

确保你已安装 Node.jsNPM。使用下列命令进行检查:

node -v
npm -v

创建一个新的文件夹并命名为ccip-masterclass-3

mkdir ccip-masterclass-3

进入该文件夹

cd ccip-masterclass-3

运行下面命令来创建一个新的Hardhat工程:

npx hardhat init

然后选择"Create a TypeScript project"。

或者你可以克隆:

要使用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

Chainlink水龙头

开发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 的回复:

在不同的开发环境下设置solc EVM版本

我们将使用 @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

为支付 CCIP 费用,向XNFT.sol充值一定数量的 LINK。3 个 LINK 对于此演示应该绰绰有余。当然,为了实现完整的功能,您还应在其他区块链上为XNFT.sol智能合约充值,以便在所有区块链之间执行跨链转账。

为您的xNFT充值LINK

步骤 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的跨链转移。

CCIP Explorer

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

Import NFT

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

Fill in NFT details

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

Last updated