HelloERC721

HelloERC721 is an ERC721 token implementation showcasing cross-chain NFT functionality without the need for traditional bridges, making the NFT cross-chain native. Utilizing the Via Labs package it is trivial to add native cross-chain functionality to any NFT contract. Below is a bare-bones ERC721 contract to showcase the ease of use.

Prerequisites

Node.js and npm

Linux (Ubuntu/Debian)
# Install using APT
sudo apt update
sudo apt install nodejs npm
Windows
# Download and install from:
https://nodejs.org/en/download/
macOS
# Install using Homebrew
brew install node

Git

Linux (Ubuntu/Debian)
# Install using APT
sudo apt install git
Windows
# Download and install from:
https://gitforwindows.org/
macOS
# Install using Homebrew (if not pre-installed)
brew install git

Testnet Tokens

Installation

Open a terminal to run the following commands. You can use any terminal of your choice, including the built in terminal in (vscode) (Terminal -> New Terminal)

  1. Clone the Repository:
    git clone https://github.com/VIALabs-io/hello-erc721.git
    
  2. Change into Project Directory:
    cd hello-erc721
    
  3. Install Dependencies:
    npm install
    
  4. Set Up Environment Variables:
    Create a new .env file to set your EVM private key for contract deployment or copy and edit the existing .env.example to .env
    PRIVATE_KEY=0000000000000000000000000000
    

Deployment

Deploy the HelloERC721 contract to your desired networks. This must be done for each network you wish to operate on. You can see a list of our networks in the Package documentation.

  1. Ethereum Sepolia Testnet Deployment:
npx hardhat --network ethereum-sepolia deploy
  1. Polygon Testnet Deployment:
npx hardhat --network polygon-amoy deploy

Configuration

Edit the networks-testnet.json file and include all of the networks the contract is deployed on.

[
    "ethereum-sepolia",
    "polygon-amoy"
]

Once all contracts are deployed across the desired networks and listed in networks-testnet.json, configure them using the provided script. Remember, if a new network is added later, all contracts must be reconfigured.

  1. Ethereum Sepolia Testnet Configuration:
npx hardhat --network ethereum-sepolia configure
  1. Polygon Testnet Configuration:
npx hardhat --network polygon-amoy configure

Usage

Minting an NFT

To mint an NFT on a chain:

npx hardhat --network polygon-amoy mint-nft

You will get the next available NFT. NFTs start at chain-id0000 so the first NFT minted on Polygon Testnet will be 800020000 and the next 800020001 etc.. You can look up the chain ids in the Package documentation. You can also look up the transaction on the testnet explorer to see the NFT details using your wallet address.

Viewing NFT Details

To view the details of an NFT including its Metadata and Owner:

npx hardhat --network polygon-amoy get-nft ---nftid 800020000

Bridging NFTs to Another Chain

To send NFTs to another chain it is required to set the --dest parameter to the destination chain id. The example below uses the id for the Ethereum Sepolia Testnet. Chain IDs can be looked up in the Package documentation.

npx hardhat --network polygon-amoy bridge-nft --dest 11155111 --nftid 800020000

Certainly! Here's a detailed breakdown of the HelloERC721 contract including implementation details:

Contract Breakdown of HelloERC721

HelloERC721 is an ERC721 token contract designed for cross-chain operations, leveraging Via Labs's message-passing capabilities.

Contract Overview

pragma solidity =0.8.17;

import "@vialabs-io/contracts/message/MessageClient.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "base64-sol/base64.sol";

contract HelloERC721 is ERC721, MessageClient {
    uint public nextNftId;

    constructor() ERC721("Hello ERC721!", "H721") {
        nextNftId = block.chainid * 10**4;
    }

    function mint() external {
        _mint(msg.sender, nextNftId);
        nextNftId++;
    }

    function bridge(uint _destChainId, address _recipient, uint _nftId) external onlyActiveChain(_destChainId) {
        require(ownerOf(_nftId) == msg.sender, "HelloERC721: caller is not the owner of the nft");

        // burn nft
        _burn(_nftId);

        // send cross chain message
        _sendMessage(_destChainId, abi.encode(_recipient, _nftId));
    }

    function messageProcess(uint, uint _sourceChainId, address _sender, address, uint, bytes calldata _data) external override  onlySelf(_sender, _sourceChainId)  {
        // decode message
        (address _recipient, uint _nftId) = abi.decode(_data, (address, uint));

        // mint tokens
        _mint(_recipient, _nftId);
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        return string(abi.encodePacked('data:application/json;base64,', 
            Base64.encode(bytes(abi.encodePacked(
                '{"name":"Via Labs Hello ERC721 #', tokenId, '", "description":"Hello ERC721 cross chain NFT example. https://github.com/VIALabs-io/hello-erc721", "image":"https://i.postimg.cc/FKkpPByb/cl-logo.png"}')
            )))
        );
    }
}

Constructor

constructor() ERC721("Hello ERC721!", "H721") {
    nextNftId = block.chainid * 10**4;
}

Initializes the token with a unique name ("Hello ERC721!") and symbol ("H721"). It sets the nextNftId based on the chain ID, ensuring unique NFT IDs across different chains.

Mint Function

function mint() external {
    _mint(msg.sender, nextNftId);
    nextNftId++;
}

Allows users to mint new NFTs. Each minted NFT gets a unique ID, which is then incremented for the next minting operation.

Bridge Function

function bridge(uint _destChainId, address _recipient, uint _nftId) external onlyActiveChain(_destChainId) {
    require(ownerOf(_nftId) == msg.sender, "HelloERC721: caller is not the owner of the nft");
    _burn(_nftId);
    _sendMessage(_destChainId, abi.encode(_recipient, _nftId));
}

Enables users to bridge their NFTs to a different chain. It verifies the ownership, burns the NFT on the current chain, and sends a message to mint it on the destination chain.

Message Processing

function messageProcess(uint, uint _sourceChainId, address _sender, address, uint, bytes calldata _data) external override onlySelf(_sender, _sourceChainId)  {
    (address _recipient, uint _nftId) = abi.decode(_data, (address, uint));
    _mint(_recipient, _nftId);
}

Processes incoming messages from other chains. It decodes the message to get the recipient's address and the NFT ID, and then mints the NFT on the current chain.

Token URI

function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
    require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
    return string(abi.encodePacked('data:application/json;base64,', 
        Base64.encode(bytes(abi.encodePacked(
            '{"name":"Via Labs Hello ERC721 #', tokenId, '", "description":"Hello ERC721 cross chain NFT example. https://github.com/VIALabs-io/hello-erc721", "image":"https://i.postimg.cc/FKkpPByb/cl-logo.png"}')
        )))
    );
}

Generates a token URI for each NFT. The URI includes a base64-encoded JSON object with the NFT's name, description, and image URL.