# Exercise 2: Build Your First Cross-Chain NFT

Coding Time 🎉

### CCIP Config Details <a href="#ccip-config-details" id="ccip-config-details"></a>

<table><thead><tr><th></th><th>Arbitrum Sepolia</th><th>Ethereum Sepolia</th></tr></thead><tbody><tr><td>Chain Selector</td><td><pre><code>3478487238524512106
</code></pre></td><td><pre><code>16015286601757825753
</code></pre></td></tr><tr><td>CCIP Router Address</td><td>0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165</td><td>0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59</td></tr><tr><td>LINK Token Address</td><td>0xb1D4538B4571d411F07960EF2838Ce337FE1E80E</td><td>0x779877A7B0D9E8603169DdbD7836e478b4624789</td></tr></tbody></table>

### Getting started <a href="#getting-started" id="getting-started"></a>

You can use Chainlink CCIP with any blockchain development framework. For this Masterclass, we prepared the steps for Hardhat, Foundry, and Remix IDE.

Let's create a new project

{% tabs %}
{% tab title="Hardhat" %}
Make sure you have [Node.js](https://nodejs.org/en/download) and [NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed. To check, run the following command:

```sh
node -v
```

```sh
npm -v
```

Create a new folder and name it `ccip-masterclass-3`

```sh
mkdir ccip-masterclass-3
```

Navigate to it

```sh
cd ccip-masterclass-3
```

Create a hew Hardhat project by running:

```shell
npx hardhat init
```

And then select "Create a TypeScript project".
{% endtab %}

{% tab title="Foundry" %}
Make sure you have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed. To check, run the following command:

```sh
forge --version
```

Create a new folder and name it `ccip-masterclass-3`

```sh
mkdir ccip-masterclass-3
```

Navigate to it

```sh
cd ccip-masterclass-3
```

Create a hew Foundry project by running:

```sh
forge init
```
{% endtab %}

{% tab title="Remix" %}
Navigate to [https://remix.ethereum.org/](https://remix.ethereum.org/) and click the "Create new Workspace" button. Select "Blank" template and name the workspace as "CCIP Masterclass 3".
{% endtab %}
{% endtabs %}

Alternatively, you can clone:

* [CCIP Starter Kit (Hardhat version)](https://github.com/smartcontractkit/ccip-starter-kit-hardhat)
* [CCIP Starter Kit (Foundry version)](https://github.com/smartcontractkit/ccip-starter-kit-foundry)

### The @chainlink/contracts-ccip NPM package <a href="#the-chainlink-contracts-ccip-npm-package" id="the-chainlink-contracts-ccip-npm-package"></a>

To use Chainlink CCIP, you need to interact with Chainlink CCIP-specific contracts from the [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip) NPM package.

{% embed url="https://www.npmjs.com/package/@chainlink/contracts-ccip" %}

To install it, follow steps specific to the development environment you will use for this Masterclass.

{% tabs %}
{% tab title="Hardhat" %}
```bash
npm i @chainlink/contracts-ccip --save-dev
```

We will need a standard [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) NPM package for this Module as well, so let's install it too while we are here by running the following command:

```bash
npm i @chainlink/contracts --save-dev
```

Finally, for this exercise we will need to install and the [@openzeppelin/contracts](https://www.npmjs.com/package/@openzeppelin/contracts) NPM package, as well. To do so, run:

```bash
npm i @openzeppelin/contracts --save-dev
```
{% endtab %}

{% tab title="Foundry" %}
Since Foundry is designed to run with Solidity, NPM packages, though usable, ,can be replaced with directly installying Solidity contract packages from Github source repositories. We will install Chainlink CCIP contracts and then the contracts for other Chainlink Services.



First the CCIP Contracts.

```sh
forge install smartcontractkit/ccip@ccip-develop
```

And after that set remappings in your `foundry.toml` or `remappings.txt` files to

```toml
# foundry.toml
remappings = [
    '@chainlink/contracts-ccip/=lib/ccip/contracts/'
]
```

Next we install the other Chainlink Services contracts by running the following command:

```bash
forge install smartcontractkit/chainlink
```

And set remmapings to

```toml
# foundry.toml
remappings = [
    '@chainlink/contracts/=lib/chainlink/contracts/'
]
```

Finally, for this exercise we will need to install and the [@openzeppelin/contracts](https://www.npmjs.com/package/@openzeppelin/contracts) NPM package, as well. To do so, run:

```bash
forge install OpenZeppelin/openzeppelin-contracts
```

And set remappings to `@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/` in `foundry.toml` or `remappings.txt` files.

```toml
# foundry.toml
remappings = [
    '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/'
]
```
{% endtab %}

{% tab title="Remix" %}
Create a new Solidity file, and paste the following content. It is an empty contract that just imports one of the contracts from the `@chainlink/contracts-ccip` package.

```solidity
// 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 {}
```

Compile it. If compiled successfully and new `.deps/npm/@chainlink/contracts-ccip` , `.deps/npm/@chainlink/contracts` and `.deps/npm/@openzeppelin/contracts` folders are generated, that means we imported all of the necessary packages into the Remix IDE Workspace.
{% endtab %}
{% endtabs %}

## Faucet

To pay for CCIP Fees you can use either LINK token or native/wrapped native asset on a given blockchain. For this exercise we will need at least 1 LINK or Arbitrum Sepolia testnet. To get it, navigate to the [https://faucets.chain.link/arbitrum-sepolia](https://faucets.chain.link/arbitrum-sepolia)

{% embed url="https://faucets.chain.link/arbitrum-sepolia" %}

<figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2FG2JwHOG71TO4Y5igXBaS%2FFaucets.png?alt=media&#x26;token=f0ec4848-091f-4b5a-abbb-f5f6cd8ebbe9" alt=""><figcaption><p>Chainlink Faucets</p></figcaption></figure>

## Develop xNFT smart contract

{% tabs %}
{% tab title="Hardhat" %}
Create a new file inside the `contracts` folder and name it `XNFT.sol`

{% code lineNumbers="true" %}
```solidity
// 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/security/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);
    }
}
```
{% endcode %}

Compile your contract by running:

```bash
npx hardhat compile
```
{% endtab %}

{% tab title="Foundry" %}
Create a new file inside the `src` folder and name it `XNFT.sol`

{% code lineNumbers="true" %}
```solidity
// 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/security/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);
    }
}
```
{% endcode %}
{% endtab %}

{% tab title="Remix" %}
Create a new Solidity file by clicking on the "Create new file" button and name it `XNFT.sol`

{% code lineNumbers="true" %}
```solidity
// 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/security/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);
    }
}
```
{% endcode %}
{% endtab %}
{% endtabs %}

## Prepare for deployment

Follow the steps to add the necessary environment variables for deploying these contracts and sending your first CCIP Message.

This contract expects at least 0.8.20 Solidity version. It is very important to understand that with Solc version 0.8.20, the default EVM version is set to "Shanghai".  A new opcode, PUSH0, was added to the Ethereum Virtual Machine in the Shanghai upgrade.&#x20;

However, besides Ethereum, the majority of blockchains haven't included PUSH0 opcode.

That means the PUSH0 opcode can now be part of the contract's bytecode and if the chain you are working on does not support it, it will error with the "Invalid opcode" error.

To understand more, we highly encourage you to check this StackOverflow answer:

{% embed url="https://stackoverflow.com/a/76332341" %}
Setting solc EVM version in different environments
{% endembed %}

{% tabs %}
{% tab title="Hardhat" %}
We are going to use the [`@chainlink/env-enc`](https://www.npmjs.com/package/@chainlink/env-enc) package for extra security. It encrypts sensitive data instead of storing them as plain text in the `.env` file by creating a new `.env.enc` file. Although it's not recommended to push this file online, if that accidentally happens, your secrets will still be encrypted.

Install the package by running the following command:

```bash
npm i @chainlink/env-enc --save-dev
```

Set a password for encrypting and decrypting the environment variable file. You can change it later by typing the same command.

```bash
npx env-enc set-pw
```

Now set the following environment variables: `PRIVATE_KEY`, Source Blockchain RPC URL, Destination Blockchain RPC URL. For this example, we are going to use Arbitrum Sepolia and Ethereum Sepolia.

```bash
PRIVATE_KEY=""
ARBITRUM_SEPOLIA_RPC_URL=""
ETHEREUM_SEPOLIA_RPC_URL=""
```

To set these variables, type the following command and follow the instructions in the terminal:

```bash
npx env-enc set
```

After you are done, the `.env.enc` file will be automatically generated. If you want to validate your inputs, you can always run the next command:

```bash
npx env-enc view
```

Finally, expand the `hardhat.config` to support these two networks:

```typescript
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;
```
{% endtab %}

{% tab title="Foundry" %}
Create a new file and name it `.env`. Fill in your wallet's PRIVATE\_KEY and RPC URLs for at least two blockchains. For this example, we are going to use Arbitrum Sepolia and Ethereum Sepolia.

```bash
PRIVATE_KEY=""
ARBITRUM_SEPOLIA_RPC_URL=""
ETHEREUM_SEPOLIA_RPC_URL=""
```

Once that is done, to load the variables in the `.env` file, run the following command:

```bash
source .env
```

Finally, expand the `foundry.toml` to support these two networks:

```toml
[profile.default]
src = 'src'
out = 'out'
remappings = [
    '@chainlink/contracts/=lib/chainlink/contracts',
    '@chainlink/contracts-ccip/=lib/ccip/contracts',
    '@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
```
{% endtab %}

{% tab title="Remix" %}


Navigate to the "Solidity compiler" tab

* Toggle the "Advanced Configurations" dropdown
* Toggle the "EVM VERSION" dropdown menu and select "paris" instead of "default"

Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu.

<div data-full-width="true"><figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2FwCGQc330j8pHmZHG2IMZ%2FXNFT.col%20Remix.png?alt=media&#x26;token=bbb4a975-ecae-4ff7-8345-8197d83ce729" alt="" width="563"><figcaption><p>Connect your wallet to Remix IDE</p></figcaption></figure></div>

If you are using Metamask wallet, the Ethereum Sepolia network should already came preinstalled. Make sure you added the Arbitrum Sepolia network.

Go to [Chainlist.org](https://chainlist.org/?testnets=true\&search=arbitrum+sepolia) and search for "arbitrum sepolia". Once you see the network with Chain ID 421614, click the "Add to Metamask" button.

<figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2FrXCqhesE1TjmWQI4aPWg%2FXNFT.sol%20Chainlist.png?alt=media&#x26;token=95d44d1d-5418-408a-85f3-2fcc4a2dd6ff" alt="" width="563"><figcaption><p>Add Arbitrum Sepolia network to Metamask</p></figcaption></figure>
{% endtab %}
{% endtabs %}

## Step 1) Deploy XNFT.sol to Ethereum Sepolia



{% tabs %}
{% tab title="Hardhat" %}
Prepare Chain Selector and CCIP Router & LINK token addresses on Ethereum Sepolia. You can get them if you scroll to the beginning of this page, at [#ccip-config-details](#ccip-config-details "mention")&#x20;

Navigate to the `scripts` folder and create new file named `deployXNFT.ts`

<pre class="language-typescript"><code class="lang-typescript">// deployXNFT.ts
<strong>
</strong>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;
});
</code></pre>

Run the deployment script:

```bash
npx hardhat run ./scripts/deployXNFT.ts --network ethereumSepolia
```
{% endtab %}

{% tab title="Foundry" %}
Prepare Chain Selector and CCIP Router & LINK token addresses on Ethereum Sepolia. You can get them if you scroll to begging of this page, at [#ccip-config-details](#ccip-config-details "mention")&#x20;

**Option 1)**

Deploy `XNFT.sol` smart contract by running:

```sh
forge create --rpc-url ethereumSepolia --private-key=$PRIVATE_KEY src/XNFT.sol:XNFT --constructor-args 0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59 0x779877A7B0D9E8603169DdbD7836e478b4624789 16015286601757825753
```

#### Option 2)

Create a new smart contract under the `script` folder and name it `XNFT.s.sol`

Note that deployment of the `XNFT` smart contract is hard coded to Ethereum Sepolia for this example, but feel free to refactor the following deployment script to support other networks. You can check [CCIP Starter Kit (Foundry version)](https://github.com/smartcontractkit/ccip-starter-kit-foundry) for reference.

```solidity
// 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();
    }
}
```

Deploy `XNFT.sol` smart contract by running:

```sh
forge script ./script/XNFT.s.sol:XNFT -vvv --broadcast --rpc-url ethereumSepolia
```
{% endtab %}

{% tab title="Remix" %}
Prepare Chain Selector and CCIP Router & LINK token addresses on Ethereum Sepolia. You can get them if you scroll to begging of this page, at [#ccip-config-details](#ccip-config-details "mention")&#x20;

Open your Metamask wallet and switch to the Ethereum Sepolia network.

Open the XNFT.sol file.

Navigate to the "Solidity Compiler" tab and click the "Compile XNFT.sol" button.

Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu. Make sure that `chainId` is switched to 11155111 (if not, you may need to refresh the Remix IDE page in your browser).

Under the "Contract" dropdown menu, make sure that the "XNFT - XNFT.sol" is selected.

Locate the orange "Deploy" button. Provide `0x0bf3de8c5d3e8a2b34d2beeb17abfcebaf363a59` as the `ccipRouterAddress` ,  `0x779877A7B0D9E8603169DdbD7836e478b4624789` as the `linkTokenAddress` and `16015286601757825753` as the `currentChainSelector`.

Click the orange "Deploy"/"Transact" button.

Metamask notification will pop up. Sign the transaction.
{% endtab %}
{% endtabs %}

## Step 2) Deploy XNFT.sol to Arbitrum Sepolia <a href="#step-2-deploy-xnft.sol-to-arbitrum-sepolia" id="step-2-deploy-xnft.sol-to-arbitrum-sepolia"></a>

{% tabs %}
{% tab title="Hardhat" %}
Prepare Chain Selector and CCIP Router & LINK token addresses on Arbitrum Sepolia. You can get them if you scroll to beginning of this page, at [#ccip-config-details](#ccip-config-details "mention")&#x20;

Navigate to the `scripts` folder and create new file named `deployXNFTArbitrum.ts`

<pre class="language-typescript"><code class="lang-typescript">// deployXNFTArbitrum.ts
<strong>
</strong>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;
});
</code></pre>

Run the deployment script:

```bash
npx hardhat run ./scripts/deployXNFTArbitrum.ts --network arbitrumSepolia
```
{% endtab %}

{% tab title="Foundry" %}
Prepare Chain Selector and CCIP Router & LINK token addresses on Arbitrum Sepolia. You can get them if you scroll to begging of this page, at [#ccip-config-details](#ccip-config-details "mention")&#x20;

**Option 1)**

Deploy `XNFT.sol` smart contract by running:

```sh
forge create --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY src/XNFT.sol:XNFT --constructor-args 0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E 3478487238524512106
```

#### Option 2)

Create a new smart contract under the `script` folder and name it `XNFTArbitrum.s.sol`

Note that deployment of the `XNFT` smart contract is hard coded to Arbitrum Sepolia for this example, but feel free to refactor the following deployment script to support other networks. You can check [CCIP Starter Kit (Foundry version)](https://github.com/smartcontractkit/ccip-starter-kit-foundry) for reference.

```solidity
// 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();
    }
}
```

Deploy `XNFT.sol` smart contract by running:

```sh
forge script ./script/XNFTArbitrum.s.sol:XNFT -vvv --broadcast --rpc-url arbitrumSepolia
```
{% endtab %}

{% tab title="Remix" %}
Prepare Chain Selector and CCIP Router & LINK token addresses on Arbitrum Sepolia. You can get them if you scroll to begging of this page, at [#ccip-config-details](#ccip-config-details "mention")&#x20;

Open your Metamask wallet and switch to the Arbitrum Sepolia network.

Open the XNFT.sol file.

Navigate to the "Solidity Compiler" tab and click the "Compile XNFT.sol" button.

Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu. Make sure that `chainId` is switched to 421614 (if not, you may need to refresh the Remix IDE page in your browser).

Under the "Contract" dropdown menu, make sure that the "XNFT - XNFT.sol" is selected.

Locate the orange "Deploy" button. Provide `0x2a9c5afb0d0e4bab2bcdae109ec4b0c4be15a165` as the `ccipRouterAddress` ,  `0xb1D4538B4571d411F07960EF2838Ce337FE1E80E` as the `linkTokenAddress` and `3478487238524512106` as the `currentChainSelector`.

Click the orange "Deploy"/"Transact" button.

Metamask notification will pop up. Sign the transaction.
{% endtab %}
{% endtabs %}

## Step 3) On Ethereum Sepolia, call enableChain function <a href="#step-3-on-ethereum-sepolia-call-enablechain-function" id="step-3-on-ethereum-sepolia-call-enablechain-function"></a>

{% tabs %}
{% tab title="Hardhat" %}
Prepare:

* The address of the address of the `XNFT.sol` smart contract you previously deployed to Ethereum Sepolia;
* The address of the address of the `XNFT.sol` smart contract you previously deployed to Arbitrum Sepolia;
* 3478487238524512106, which is the CCIP Chain Selector for the Arbitrum Sepolia network, as the `chainSelector` parameter;
* `0x97a657c90000000000000000000000000000000000000000000000000000000000030d40`, which is the bytes version of CCIP extraArgs' default value with 200\_000 gas set for gasLimit, as `ccipExtraArgs` parameter.

If you would like to calculate this value by yourself, you can reuse the following helper smart contract:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";

contract EncodeExtraArgs {
  // Below is a simplistic example (same params for all messages) of using storage to allow for new options without
  // upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
  // and will always be backwards compatible i.e. upgrades are opt-in.
  // Offchain we can compute the V1 extraArgs:
  //    Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
  //    bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
  // Then later compute V2 extraArgs, for example if a refund feature was added:
  //    Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
  //    bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
  // and update storage with the new args.
  // If different options are required for different messages, for example different gas limits,
  // one can simply key based on (chainSelector, messageType) instead of only chainSelector.

  function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
      Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
      extraArgsBytes = Client._argsToBytes(extraArgs);
  }
}
```

Create a new TypeScript file under the `scripts` folder and name it `enableChain.ts`

```typescript
// 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;
});
```

Call the function by running the following command:

```sh
npx hardhat run ./scripts/enableChain.ts --network ethereumSepolia
```
{% endtab %}

{% tab title="Foundry" %}
Prepare:

* The address of the address of the `XNFT.sol` smart contract you previously deployed to Ethereum Sepolia;
* The address of the address of the `XNFT.sol` smart contract you previously deployed to Arbitrum Sepolia;
* 3478487238524512106, which is the CCIP Chain Selector for the Arbitrum Sepolia network, as the `chainSelector` parameter;
* `0x97a657c90000000000000000000000000000000000000000000000000000000000030d40`, which is the bytes version of CCIP extraArgs' default value with 200\_000 gas set for gasLimit, as `ccipExtraArgs` parameter.

If you would like to calculate this value by yourself, you can reuse the following helper smart contract. Inside the scripts folder, create `EncodeExtraArgs.s.sol` and paste the following code:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";

contract EncodeExtraArgs {
  // Below is a simplistic example (same params for all messages) of using storage to allow for new options without
  // upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
  // and will always be backwards compatible i.e. upgrades are opt-in.
  // Offchain we can compute the V1 extraArgs:
  //    Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
  //    bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
  // Then later compute V2 extraArgs, for example if a refund feature was added:
  //    Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
  //    bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
  // and update storage with the new args.
  // If different options are required for different messages, for example different gas limits,
  // one can simply key based on (chainSelector, messageType) instead of only chainSelector.

  function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
      Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
      extraArgsBytes = Client._argsToBytes(extraArgs);
  }
}
```

Run:

```sh
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
```
{% endtab %}

{% tab title="Remix" %}
Under the "Deployed Contracts" section, you should find the `XNFT.sol` contract you previously deployed to Ethereum Sepolia. Find the `enableChain` function and provide:

* 3478487238524512106, which is the CCIP Chain Selector for the Arbitrum Sepolia network, as the `chainSelector` parameter;
* The address of the address of the `XNFT.sol` smart contract you previously deployed to Arbitrum Sepolia, as `xNftAddress` parameter;
* `0x97a657c90000000000000000000000000000000000000000000000000000000000030d40`, which is the bytes version of CCIP extraArgs' default value with 200\_000 gas set for gasLimit, as `ccipExtraArgs` parameter.

Hit the "Transact" orange button.

If you would like to calculate this value by yourself, you can reuse the following helper smart contract. Create `EncodeExtraArgs.sol` file and paste the following code:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";

contract EncodeExtraArgs {
  // Below is a simplistic example (same params for all messages) of using storage to allow for new options without
  // upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
  // and will always be backwards compatible i.e. upgrades are opt-in.
  // Offchain we can compute the V1 extraArgs:
  //    Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
  //    bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
  // Then later compute V2 extraArgs, for example if a refund feature was added:
  //    Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
  //    bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
  // and update storage with the new args.
  // If different options are required for different messages, for example different gas limits,
  // one can simply key based on (chainSelector, messageType) instead of only chainSelector.

  function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
      Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
      extraArgsBytes = Client._argsToBytes(extraArgs);
  }
}
```
{% endtab %}
{% endtabs %}

## Step 4) On Arbitrum Sepolia, call enableChain function <a href="#step-4-on-arbitrum-sepolia-call-enablechain-function" id="step-4-on-arbitrum-sepolia-call-enablechain-function"></a>

{% tabs %}
{% tab title="Hardhat" %}
Prepare:

* The address of  the `XNFT.sol` smart contract you previously deployed to Arbitrum Sepolia;
* The address of the address of the `XNFT.sol` smart contract you previously deployed to Ethereum Sepolia, as `xNftAddress` parameter;
* `16015286601757825753`, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the `chainSelector` parameter;
* `0x97a657c90000000000000000000000000000000000000000000000000000000000030d40`, which is the bytes version of CCIP extraArgs' default value with 200\_000 gas set for gasLimit, as `ccipExtraArgs` parameter.

If you would like to calculate this value by yourself, you can reuse the following helper smart contract:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";

contract EncodeExtraArgs {
  // Below is a simplistic example (same params for all messages) of using storage to allow for new options without
  // upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
  // and will always be backwards compatible i.e. upgrades are opt-in.
  // Offchain we can compute the V1 extraArgs:
  //    Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
  //    bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
  // Then later compute V2 extraArgs, for example if a refund feature was added:
  //    Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
  //    bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
  // and update storage with the new args.
  // If different options are required for different messages, for example different gas limits,
  // one can simply key based on (chainSelector, messageType) instead of only chainSelector.

  function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
      Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
      extraArgsBytes = Client._argsToBytes(extraArgs);
  }
}
```

Create a new TypeScript file under the `scripts` folder and name it `enableChainArbitrum.ts`

```typescript
// 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;
});
```

Call the function by running the following command:

```sh
npx hardhat run ./scripts/enableChainArbitrum.ts --network arbitrumSepolia
```
{% endtab %}

{% tab title="Foundry" %}
Prepare:

* The address of the address of the `XNFT.sol` smart contract you previously deployed to Arbitrum Sepolia;
* The address of the address of the `XNFT.sol` smart contract you previously deployed to Ethereum Sepolia, as `xNftAddress` parameter;
* `16015286601757825753`, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the `chainSelector` parameter;
* `0x97a657c90000000000000000000000000000000000000000000000000000000000030d40`, which is the bytes version of CCIP extraArgs' default value with 200\_000 gas set for gasLimit, as `ccipExtraArgs` parameter.

If you would like to calculate this value by yourself, you can reuse the following helper smart contract. Inside the scripts folder, create `EncodeExtraArgs.s.sol` and paste the following code:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";

contract EncodeExtraArgs {
  // Below is a simplistic example (same params for all messages) of using storage to allow for new options without
  // upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
  // and will always be backwards compatible i.e. upgrades are opt-in.
  // Offchain we can compute the V1 extraArgs:
  //    Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
  //    bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
  // Then later compute V2 extraArgs, for example if a refund feature was added:
  //    Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
  //    bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
  // and update storage with the new args.
  // If different options are required for different messages, for example different gas limits,
  // one can simply key based on (chainSelector, messageType) instead of only chainSelector.

  function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
      Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
      extraArgsBytes = Client._argsToBytes(extraArgs);
  }
}
```

Run:

```sh
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
```
{% endtab %}

{% tab title="Remix" %}
Under the "Deployed Contracts" section, you should find the `XNFT.sol` contract you previously deployed to Arbitrum Sepolia. Find the `enableChain` function and provide:

* `16015286601757825753`, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the `chainSelector` parameter;
* The address of the address of the `XNFT.sol` smart contract you previously deployed to Ethereum Sepolia, as `xNftAddress` parameter;
* `0x97a657c90000000000000000000000000000000000000000000000000000000000030d40`, which is the bytes version of CCIP extraArgs' default value with 200\_000 gas set for gasLimit, as `ccipExtraArgs` parameter.

Hit the "Transact" orange button.

If you would like to calculate this value by yourself, you can reuse the following helper smart contract. Create `EncodeExtraArgs.sol` file and paste the following code:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";

contract EncodeExtraArgs {
  // Below is a simplistic example (same params for all messages) of using storage to allow for new options without
  // upgrading the dapp. Note that extra args are chain family specific (e.g. gasLimit is EVM specific etc.).
  // and will always be backwards compatible i.e. upgrades are opt-in.
  // Offchain we can compute the V1 extraArgs:
  //    Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 300_000});
  //    bytes memory encodedV1ExtraArgs = Client._argsToBytes(extraArgs);
  // Then later compute V2 extraArgs, for example if a refund feature was added:
  //    Client.EVMExtraArgsV2 memory extraArgs = Client.EVMExtraArgsV2({gasLimit: 300_000, destRefundAddress: 0x1234});
  //    bytes memory encodedV2ExtraArgs = Client._argsToBytes(extraArgs);
  // and update storage with the new args.
  // If different options are required for different messages, for example different gas limits,
  // one can simply key based on (chainSelector, messageType) instead of only chainSelector.

  function encode(uint256 gasLimit) external pure returns(bytes memory extraArgsBytes) {
      Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: gasLimit});
      extraArgsBytes = Client._argsToBytes(extraArgs);
  }
}
```
{% endtab %}
{% endtabs %}

## Step 5) On Arbitrum Sepolia, fund XNFT.sol with 3 LINK <a href="#step-5-on-arbitrum-sepolia-fund-xnft.sol-with-3-link" id="step-5-on-arbitrum-sepolia-fund-xnft.sol-with-3-link"></a>

To cover for CCIP fees, fund `XNFT.sol` with some amount of LINK, 3 should be more than enough for this demo. Obviously, for the sake of full functionality, you should fund `XNFT.sol` smart contract on other blockchains as well, so you can perform cross-chain transfers between all of them.

<figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2FTmyy3dQ02PoBKCHAOiNN%2F111.png?alt=media&#x26;token=5b8e0323-ed7d-4a1a-a581-34896dc77938" alt="" width="353"><figcaption><p>Fund your xNFT with LINK</p></figcaption></figure>

## Step 6) On Arbitrum Sepolia, mint new xNFT <a href="#step-6-on-arbitrum-sepolia-mint-new-xnft" id="step-6-on-arbitrum-sepolia-mint-new-xnft"></a>

{% tabs %}
{% tab title="Hardhat" %}
Create a new TypeScript file under the `scripts` folder and name it `mint.ts`

```typescript
// 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;
});
```

Call the function by running the following command:

```sh
npx hardhat run ./scripts/mint.ts --network arbitrumSepolia
```
{% endtab %}

{% tab title="Foundry" %}
Run:

```sh
cast send <XNFT_ADDRESS_ON_ARBITRUM_SEPOLIA> --rpc-url arbitrumSepolia --private-key=$PRIVATE_KEY "mint()"
```
{% endtab %}

{% tab title="Remix" %}
Under the "Deployed Contracts" section, you should find the `XNFT.sol` contract you previously deployed to Arbitrum Sepolia. Find the `mint` function and hit the "Transact" orange button.
{% endtab %}
{% endtabs %}

## Step 7) On Arbitrum Sepolia, crossTransferFrom xNFT <a href="#step-7-on-arbitrum-sepolia-crosstransferfrom-xnft" id="step-7-on-arbitrum-sepolia-crosstransferfrom-xnft"></a>

{% tabs %}
{% tab title="Hardhat" %}
Prepare:

* Your EOA address, as the `from` parameter;
* The address of an EOA on other chain where you want to cross-transfer your NFT, can be your EOA address, as `to` parameter;
* The ID of a xNFT you want to cross-transfer, as `tokenId` parameter;
* `16015286601757825753` which is the CCIP Chain Selector of Ethereum Sepolia blockchain, as the `destinationChainSelector` parameter;
* `1` which stands that we are paying for CCIP fees in LINK, as the `payFeesIn` parameter.

Create a new TypeScript file under the `scripts` folder and name it `crossChainTransferFrom.ts`

```typescript
// 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;
});
```

Call the function by running the following command:

```sh
npx hardhat run ./scripts/crossChainTransfer.ts --network arbitrumSepolia
```
{% endtab %}

{% tab title="Foundry" %}
Prepare:

* Your EOA address, as the `from` parameter;
* The address of an EOA on other chain where you want to cross-transfer your NFT, can be your EOA address, as `to` parameter;
* The ID of a xNFT you want to cross-transfer, as `tokenId` parameter;
* `16015286601757825753` which is the CCIP Chain Selector of Ethereum Sepolia blockchain, as the `destinationChainSelector` parameter;
* `1` which stands that we are paying for CCIP fees in LINK, as the `payFeesIn` parameter.

Run:

```sh
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
```
{% endtab %}

{% tab title="Remix" %}
Under the "Deployed Contracts" section, you should find the `XNFT.sol` contract you previously deployed to Arbitrum Sepolia. Find the `crossChainTransferFrom` function and provide the following parameters:

* Your EOA address, as the `from` parameter;
* The address of an EOA on other chain where you want to cross-transfer your NFT, can be your EOA address, as `to` parameter;
* The ID of a xNFT you want to cross-transfer, as `tokenId` parameter;
* `16015286601757825753` which is the CCIP Chain Selector of Ethereum Sepolia blockchain, as the `destinationChainSelector` parameter;
* `1` which stands that we are paying for CCIP fees in LINK, as the `payFeesIn` parameter.

Hit the "Transact" orange button.
{% endtab %}
{% endtabs %}

You can now monitor this cross-chain transfer on CCIP Explorer page.

<figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2FR67dfrSaK2dHLhkrD6Ku%2F222.avif?alt=media&#x26;token=328320a2-4c4f-4bf8-b2a8-663a3a575eff" alt=""><figcaption><p>CCIP Explorer</p></figcaption></figure>

Once cross-chain NFT arrives to Ethereum Sepolia, you can manually display it inside your Metamask wallet. Navigate to the "NFT" tab and hit the "Import NFT" button.

<figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2FZDRxwhSrReqtirmg6HKH%2F333.png?alt=media&#x26;token=2f76b767-6274-4dda-be85-b78291ec4976" alt="" width="355"><figcaption><p>Import NFT</p></figcaption></figure>

Then, fill in `XNFT.sol` smart contract address on Ethereum Sepolia and token ID you received (0).

<figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2F8gxRKZk5UAxedgyBHgYG%2F444.png?alt=media&#x26;token=2e1baa4b-11f8-41a1-86f0-6d4ca6771624" alt="" width="355"><figcaption><p>Fill in NFT details</p></figcaption></figure>

Finally, your NFT will appear inside Metamask wallet.

<figure><img src="https://3336775262-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVxFPzCGStHLGRVz3QBdQ%2Fuploads%2FxslW37Wa52OvhMk7ya0m%2F555.png?alt=media&#x26;token=98aeb3eb-fde3-4218-9f1e-09da3186b4fc" alt="" width="355"><figcaption><p>See your newly minted NFT</p></figcaption></figure>
