arrow-left

All pages
gitbookPowered by GitBook
1 of 1

Loading...

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

代码时间 🎉

hashtag
CCIP 配置参数

参数
Arbitrum Sepolia
Ethereum Sepolia

Chain Selector

hashtag
开始使用

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

让我们创建一个新项目。

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

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

进入该文件夹

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

然后选择"Create a TypeScript project"。

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

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

或者你可以克隆:

hashtag
安装@chainlink/contracts-ccip NPM包

要使用Chainlink CCIP,您需要与 NPM 包中的一些Chainlink CCIP特定合约进行交互。

安装此包,请按照您用于本x的开发环境进行操作。

我们还需要一个标准的 NPM包用于本模块,所以在这里我们也可以通过运行以下命令来安装它:

最后,对于本次练习,我们还需要安装 NPM包。为此,请运行以下命令:

运行:

然后在您的 foundry.toml 或 remappings.txt 文件中设置 remappings 为

我们还需要一个标准的 NPM包用于本模块,所以趁现在我们通过运行以下命令来安装它:

并设置remappings为:

hashtag
水龙头

您可以使用 LINK 代币或给定区块链上的原生代币/wrapped原生代币来支付 CCIP 费用。在本次练习中,我们需要至少 1 个 LINK 或 Arbitrum Sepolia 测试网代币。要获取它,请前往

hashtag
开发xNFT智能合约

在contracts文件夹中创建新文件XNFT.sol

运行以下指令来编译:

在src文件夹中创建新文件XNFT.sol

点击“Create new file”按钮来创建新的Solidity文件XNFT.sol

hashtag
准备部署

按照步骤添加必要的环境变量,以便部署这些合约并发送您的第一个 CCIP 消息。

该合约需要至少 0.8.20 的 Solidity 版本。因为在 0.8.20 版本的 Solc 中,默认的 EVM 版本为“上海”。在上海升级中,一个新的操作码 PUSH0 被添加到以太坊虚拟机中。

然而,除了以太坊外的大多数区块链尚未支持 PUSH0 操作码。

这意味着 PUSH0 操作码现在是合约字节码的一部分,如果您正在使用的链不支持它,它将会报出“无效操作码”错误。

为了了解更多信息,我们强烈建议您查看此 StackOverflow 的回复:

我们将使用 包来提高安全性。它通过创建一个新的 .env.enc 文件来加密敏感数据,而不是将其以明文形式存储在 .env 文件中。虽然不建议将此文件上传到网络,但如果意外发生,您的机密信息仍然会被加密。 通过运行以下命令来安装该包:

设置一个密码用于加密和解密环境变量文件。您可以稍后通过输入相同的命令来更改密码。

现在设置以下环境变量:PRIVATE_KEY、源区块链的 RPC URL、目标区块链的 RPC URL。在此示例中,我们将使用 Arbitrum Sepolia 和 Ethereum Sepolia。

要设置以上变量,请输入以下命令并按照终端中的说明进行操作:

完成以上操作后,.env.enc 文件将自动生成。如果您想验证您的输入,可以随时运行以下命令:

最后,扩展 hardhat.config 文件以支持这两个网络:

hashtag
步骤1)在Ethereum Sepolia上部署XNFT.sol

准备 Ethereum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。

进入scripts文件夹并创建一个名为 deployXNFT.ts的新文件。

运行部署脚本:

准备 Ethereum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。

hashtag
步骤 2) 在Arbitrum Sepolia上部署XNFT.sol

准备 Arbitrum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。

进入scripts文件夹并创建一个名为 deployXNFTArbitrum.ts的新文件。

运行部署脚本:

准备 Arbitrum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。

选项 1)

通过运行以下命令部署 XNFT.sol

hashtag
步骤 3) 在Ethereum Sepolia网络中, 调用enableChain方法

需要准备:

  • 您之前部署到 Ethereum Sepolia 的 XNFT.sol 智能合约地址;

  • 您之前部署到 Arbitrum Sepolia 的 XNFT.sol 智能合约地址;

  • chainSelector 参数:3478487238524512106,这是 Arbitrum Sepolia 网络的 CCIP Chain Selector;

hashtag
步骤 4) 在Arbitrum Sepolia网络中, 调用enableChain方法

需要准备:

  • 您之前部署到 Arbitrum Sepolia 的 XNFT.sol 智能合约地址;

  • 您之前部署到 Ethereum Sepolia 的 XNFT.sol 智能合约地址,作为 xNftAddress 参数;

  • chainSelector 参数:16015286601757825753,这是 Ethereum Sepolia 网络的 CCIP Chain Selector;

hashtag
步骤 5) 在Arbitrum Sepolia网络中, 为XNFT.sol充值3 LINK

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

hashtag
步骤 6) 在Arbitrum Sepolia网络中, 铸造新的xNFT

在scripts文件夹中创建TypeScript文件mint.ts:

运行以下指令调用mint方法:

执行:

在“Deployed Contracts”部分,您应该能找到之前部署到 Arbitrum Sepolia 的 XNFT.sol合约。找到 mint 函数,然后点击橙色的“Transact”按钮。

hashtag
步骤 7) 在Arbitrum Sepolia网络中, 跨链转移xNFT

需要准备:

  • from 参数:您的 EOA 地址;

  • to 参数:您希望跨链转移您的 NFT 的另一个链上的 EOA 地址,可以是您的 EOA 地址;

您现在可以在 CCIP Explorer 页面监控此次NFT的跨链转移。

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

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

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

进入该文件夹

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

进入网站 https://remix.ethereum.org/arrow-up-right 并点击“Create new Workspace”按钮。选择“Blank”模板并将工作区命名为“CCIP Bootcamp day2”。

最后对于本次练习,我们还需要安装 @openzeppelin/contractsarrow-up-right NPM包。为此,请运行以下命令:

并在 foundry.toml或 remappings.txt 文件中将 remappings 设置为 @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/。

创建一个新的 Solidity 文件,并粘贴上以下内容。这是一个空合约,只导入了 @chainlink/contracts-ccip 包中的一个合约。

编译它。如果成功编译并生成了 .deps/npm/@chainlink/contracts-ccip、.deps/npm/@chainlink/contracts 和 .deps/npm/@openzeppelin/contracts 文件夹,意味着我们已将所有必要的包导入到了 Remix IDE 工作区。

创建一个新文件.env。在其中填写您的钱包的 PRIVATE_KEY 以及至少两个区块链的 RPC URL。在此示例中,我们将使用 Arbitrum Sepolia 和 Ethereum Sepolia。

完成后,为了加载 .env 文件中的变量,请运行以下命令:

最后,扩展 foundry.toml文件以支持这两个网络:

点击进入到“Solidity compiler”选项卡

  • 展开“Advanced Configurations”下拉菜单

  • 展开“EVM VERSION”下拉菜单并选择“paris”而不是“default”

点击进入“Deploy & run transactions”选项卡,并从“Environment”下拉菜单中选择“Injected Provider - Metamask”选项。

Connect your wallet to Remix IDE

如果您使用的是 Metamask 钱包,其中已经预装了Ethereum Sepolia 网络。确保您添加了 Arbitrum Sepolia 网络。

访问 并搜索“arbitrum sepolia”。一旦看到 Chain ID 为 421614 的网络,点击“Add to Metamask”按钮。

hashtag
选项 1)

通过运行以下命令部署 XNFT.sol

hashtag
选项 2)

在 scripts 文件夹下创建文件 XNFT.s.sol

请注意,此示例中的XNFT 的部署是直接硬编码 Ethereum Sepolia 的相关配置,但您可以重构以下部署脚本以支持其他网络。这里可参考 CCIP Starter Kit (Foundry version)arrow-up-right 。

运行以下指令来部署XNFT.sol:

准备 Ethereum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。CCIP 配置参数

打开 Metamask 钱包,切换到 Ethereum Sepolia 网络。

打开 XNFT.sol 文件。

打开“Solidity Compiler”选项卡,然后点击“Compile XNFT.sol”按钮。

打开“Deploy & run Transaction”选项卡,然后在“Environment”下拉菜单中选择“Injected Provider - Metamask”选项。确保`chainId`切换为11155111(如果没有切换,你需要在浏览器中刷新Remix IDE页面)

在“Contract”下拉菜单中,确保被选中的是“XNFT - XNFT.sol”。

找到橘黄色的按钮“Deploy”。在 ccipRouterAddress 中填入0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59,在 linkTokenAddress 中填入 0x779877A7B0D9E8603169DdbD7836e478b4624789,在 currentChainSelector 中填入 16015286601757825753。

点击橘黄色的“Deploy/Transact”按钮。

Metamask 通知会弹出来,对交易进行签名。

hashtag
选项 2)

在 scripts 文件夹下创建文件 XNFTArbitrum.s.sol

请注意,此示例中的XNFT 的部署是直接硬编码 Arbitrum Sepolia 的相关配置,但您可以重构以下部署脚本以支持其他网络。这里可参考 CCIP Starter Kit (Foundry version)arrow-up-right 。

运行以下指令来部署XNFT.sol:

准备 Arbitrum Sepolia 的 Chain Selector 和 CCIP Router & LINK 代币合约地址。您可以在页面开头滚动查看以获取这些地址。CCIP 配置参数

打开您的 Metamask 钱包并切换到 Arbitrum Sepolia 网络。

打开 XNFT.sol 文件。

点击进入“Solidity Compiler”选项卡并点击“Compile XNFT.sol”按钮。

点击进入“Deploy & run transactions”选项卡,并从“Environment”下拉菜单中选择“Injected Provider - Metamask”选项。确保 chainId 切换到 421614(如果没有,您可能需要刷新浏览器中的 Remix IDE 页面)。

在“Contract”下拉菜单中,确保选择了“XNFT - XNFT.sol”。

找到橙色的“Deploy”按钮。提供以下信息:

  • ccipRouterAddress:0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165

  • linkTokenAddress:0xb1D4538B4571d411F07960EF2838Ce337FE1E80E

  • currentChainSelector:3478487238524512106

点击橙色的“Deploy”/“Transact”按钮。

Metamask 通知将弹出。签署交易。

  • ccipExtraArgs 参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。

  • 如果您想自己计算这个值,可以重用以下辅助智能合约:

    在scripts文件夹中创建TypeScript文件enableChain.ts:

    运行以下指令来调用enableChain方法:

    需要准备:

    • 您之前部署到 Ethereum Sepolia 的 XNFT.sol 智能合约地址;

    • 您之前部署到 Arbitrum Sepolia 的 XNFT.sol 智能合约地址;

    • chainSelector 参数:3478487238524512106,这是 Arbitrum Sepolia 网络的 CCIP Chain Selector;

    • ccipExtraArgs 参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。

    如果您想自己计算这个值,可以重用以下辅助智能合约。在scripts文件夹内创建文件EncodeExtraArgs.s.sol并在其中粘贴以下代码:

    运行:

    在“Deployed Contracts”部分,您应该能找到之前部署到 Ethereum Sepolia 的 XNFT.sol 合约。找到 enableChain 函数并提供以下参数:

    • chainSelector 参数:3478487238524512106,这是 Arbitrum Sepolia 网络的 CCIP Chain Selector;

    • 您之前部署到 Arbitrum Sepolia 的 XNFT.sol 智能合约地址,作为 xNftAddress 参数;

    • ccipExtraArgs 参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。

    点击橙色的“Transact”按钮。 如果您想自己计算这个值,可以重用以下辅助智能合约。创建 EncodeExtraArgs.sol 文件并粘贴以下代码:

  • ccipExtraArgs 参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。

  • 如果您想自己计算这个值,可以重用以下辅助智能合约:

    在scripts文件夹中创建TypeScript文件enableChainArbitrum.ts:

    运行以下指令来调用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。

    如果您想自己计算这个值,可以重用以下辅助智能合约。在scripts文件夹内创建文件EncodeExtraArgs.s.sol并在其中粘贴以下代码:

    运行:

    在“Deployed Contracts”部分,您应该能找到之前部署到 Arbitrum Sepolia 的 XNFT.sol 合约。找到 enableChain 函数并提供以下参数:

    • chainSelector 参数:16015286601757825753,这是 Ethereum Sepolia 网络的 CCIP Chain Selector;

    • 您之前部署到 Ethereum Sepolia 的 XNFT.sol 智能合约地址,作为 xNftAddress 参数;

    • ccipExtraArgs 参数:0x97a657c90000000000000000000000000000000000000000000000000000000000030d40,这是 CCIP extraArgs 的bytes版本,其中gasLimit为默认值 200_000。

    点击橙色的“Transact”按钮。 如果您想自己计算这个值,可以重用以下辅助智能合约。创建 EncodeExtraArgs.sol 文件并粘贴以下代码:

    tokenId 参数:您要跨链转移的 xNFT 的 ID;
  • destinationChainSelector 参数:16015286601757825753,这是 Ethereum Sepolia 区块链的 CCIP Chain Selector;

  • payFeesIn 参数:1,表示我们用 LINK 支付 CCIP 费用。

  • 在 scripts 文件夹下创建一个 TypeScript 文件crossChainTransferFrom.ts

    运行以下指令调用crossChainTransferFrom方法:

    需要准备:

    • from 参数:您的 EOA 地址;

    • to 参数:您希望跨链转移您的 NFT 的另一个链上的 EOA 地址,可以是您的 EOA 地址;

    • tokenId 参数:您要跨链转移的 xNFT 的 ID;

    • destinationChainSelector 参数:16015286601757825753,这是 Ethereum Sepolia 区块链的 CCIP Chain Selector;

    • payFeesIn 参数:1,表示我们用 LINK 支付 CCIP 费用。

    运行:

    在“Deployed Contracts”部分,您应该能找到之前部署到 Arbitrum Sepolia 的 XNFT.sol 合约。找到 crossChainTransferFrom 函数并提供以下参数:

    • from 参数:您的 EOA 地址;

    • to 参数:您希望跨链转移您的 NFT 的另一个链上的 EOA 地址,可以是您的 EOA 地址;

    • tokenId 参数:您要跨链转移的 xNFT 的 ID;

    • destinationChainSelector 参数:16015286601757825753,这是 Ethereum Sepolia 区块链的 CCIP Chain Selector;

    • payFeesIn 参数:1,表示我们用 LINK 支付 CCIP 费用。

    点击橙色的“Transact”按钮。

    Copy

    Copy

    CCIP 路由合约地址

    0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165

    0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59

    LINK Token i之

    0xb1D4538B4571d411F07960EF2838Ce337FE1E80E

    0x779877A7B0D9E8603169DdbD7836e478b4624789

    Node.jsarrow-up-right
    NPMarrow-up-right
    Foundryarrow-up-right
    CCIP Starter Kit (Hardhat version)arrow-up-right
    CCIP Starter Kit (Foundry version)arrow-up-right
    @chainlink/contracts-cciparrow-up-right
    @chainlink/contractsarrow-up-right
    @openzeppelin/contractsarrow-up-right
    @chainlink/contractsarrow-up-right
    https://faucets.chain.link/arbitrum-sepoliaarrow-up-right
    Get Arbitrum Sepolia Testnet LINK Tokens | Chainlink Faucetsarrow-up-right
    @chainlink/env-encarrow-up-right
    Chainlink水龙头
    为您的xNFT充值LINK
    CCIP Explorer
    Import NFT
    Fill in NFT details
    CCIP 配置参数
    CCIP 配置参数
    CCIP 配置参数
    CCIP 配置参数
    node -v
    npm -v
    mkdir ccip-masterclass-3
    cd ccip-masterclass-3
    npx hardhat init
    forge --version
    mkdir ccip-masterclass-3
    cd ccip-masterclass-3
    forge init
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.20;
    
    import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
    import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
    import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
    
    contract Empty {}
    3478487238524512106
    16015286601757825753
    npm i @chainlink/contracts-ccip --save-dev
    npm i @chainlink/contracts --save-dev
    npm i @openzeppelin/contracts --save-dev
    forge install smartcontractkit/ccip@ccip-develop
    # foundry.toml
    remappings = [
        '@chainlink/contracts-ccip/=lib/ccip/contracts/'
    ]
    forge install smartcontractkit/chainlink
    # foundry.toml
    remappings = [
        '@chainlink/contracts/=lib/chainlink/contracts/'
    ]
    // 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
    // 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);
        }
    }
    // 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);
        }
    }
    npm i @chainlink/env-enc --save-dev
    npx env-enc set-pw
    PRIVATE_KEY=""
    ARBITRUM_SEPOLIA_RPC_URL=""
    ETHEREUM_SEPOLIA_RPC_URL=""
    npx env-enc set
    npx env-enc view
    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;
    // 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
    // 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
    // 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;
    });
    npx hardhat run ./scripts/mint.ts --network arbitrumSepolia
    cast send <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY "mint()"
    forge install OpenZeppelin/openzeppelin-contracts
    # foundry.toml
    remappings = [
        '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/'
    ]
    PRIVATE_KEY=""
    ARBITRUM_SEPOLIA_RPC_URL=""
    ETHEREUM_SEPOLIA_RPC_URL=""
    source .env
    [profile.default]
    src = 'src'
    out = 'out'
    remappings = [
        '@chainlink/contracts/=lib/chainlink/contracts',
        '@chainlink/contracts-ccip/=lib/chainlink/contracts-ccip',
        '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/'
    ]
    solc = '0.8.20'
    evm_version = 'paris'
    
    [rpc_endpoints]
    arbitrumSepolia = "${ARBITRUM_SEPOLIA_RPC_URL}"
    ethereumSepolia = "${ETHEREUM_SEPOLIA_RPC_URL}"
    
    # See more config options https://github.com/foundry-rs/foundry/tree/master/config
    forge create --rpc-url ethereumSepolia --private-key=$PRIVATE_KEY src/XNFT.sol:XNFT --constructor-args 0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59 0x779877A7B0D9E8603169DdbD7836e478b4624789 16015286601757825753
    // script/XNFT.s.sol
    
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.19;
    
    import "forge-std/Script.sol";
    import {XNFT} from "../src/XNFT.sol";
    
    contract DeployXNFT is Script {
        function run() public {
            uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
            vm.startBroadcast(deployerPrivateKey);
            
            address ccipRouterAddressEthereumSepolia = 0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59;
            address linkTokenAddressEthereumSepolia = 0x779877A7B0D9E8603169DdbD7836e478b4624789;
            uint64 chainSelectorEthereumSepolia = 16015286601757825753;
    
            XNFT xNft = new XNFT(
                ccipRouterAddressEthereumSepolia,
                linkTokenAddressEthereumSepolia,
                chainSelectorEthereumSepolia
            );
    
            console.log(
                "XNFT deployed to ",
                address(xNft)
            );
    
            vm.stopBroadcast();
        }
    }
    forge script ./script/XNFT.s.sol:XNFT -vvv --broadcast --rpc-url ethereumSepolia
    forge create --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY src/XNFT.sol:XNFT --constructor-args 0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E 3478487238524512106
    // script/XNFTArbitrum.s.sol
    
    // SPDX-License-Identifier: MIT
    pragma solidity 0.8.19;
    
    import "forge-std/Script.sol";
    import {XNFT} from "../src/XNFT.sol";
    
    contract DeployXNFTArbitrum is Script {
        function run() public {
            uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
            vm.startBroadcast(deployerPrivateKey);
            
            address ccipRouterAddressArbitrumSepolia = 0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165;
            address linkTokenAddressArbitrumSepolia = 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E;
            uint64 chainSelectorArbitrumSepolia = 3478487238524512106;
    
            XNFT xNft = new XNFT(
                ccipRouterAddressArbitrumSepolia,
                linkTokenAddressArbitrumSepolia,
                chainSelectorArbitrumSepolia
            );
    
            console.log(
                "XNFT deployed to ",
                address(xNft)
            );
    
            vm.stopBroadcast();
        }
    }
    forge script ./script/XNFTArbitrum.s.sol:XNFT -vvv --broadcast --rpc-url arbitrumSepolia
    // 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/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;
    });
    npx hardhat run ./scripts/enableChain.ts --network ethereumSepolia
    // 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/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;
    });
    npx hardhat run ./scripts/enableChainArbitrum.ts --network arbitrumSepolia
    // 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;
    });
    npx hardhat run ./scripts/crossChainTransfer.ts --network arbitrumSepolia
    Chainlist.orgarrow-up-right
    向Metamask添加Arbitrum Sepolia网络
    https://www.npmjs.com/package/@chainlink/contracts-ccipwww.npmjs.comchevron-right
    // 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);
      }
    }
    cast send <XNFT_ADDRESS_ON_ETHEREUM_SEPOLIA> --rpc-url ethereumSepolia --private-key=$PRIVATE_KEY "enableChain(uint64,address,bytes)" 3478487238524512106 <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> 0x97a657c90000000000000000000000000000000000000000000000000000000000030d40
    // 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);
      }
    }
    // 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);
      }
    }
    cast send <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY "enableChain(uint64,address,bytes)" 16015286601757825753 <XNFT_ADDRESS_ON_ETHEREUM_SEPOLIA> 0x97a657c90000000000000000000000000000000000000000000000000000000000030d40
    // 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);
      }
    }
    cast send <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY "crossChainTransferFrom(address,address,uint256,uint64,uint8)" <YOUR_EOA_ADDRESS> <RECEIVER_ADDRESS> 0 16015286601757825753 1
    Logo
    Remix: Returned error: {"jsonrpc":"2.0","error":"invalid opcode: PUSH0", "id":2405507186007008}Stack Overflowchevron-right
    在不同的开发环境下设置solc EVM版本
    Logo