Skip to content
Information Circle

The Spanning Demo App is live! Check it out here: https://demo.spanning.network/

We have also published the open-source demo code and related JavaScript utilities.

Your First Spanning ERC721 - JadeNFT

This tutorial will walk you through all of the setup necessary to deploy your second Spanning application; a SpanningERC721 token JadeNFT. JadeNFT is capable of being minted, owned, and transferred to users across the Spanning Network and only requires a single contract deployment.

This is the second of a series of tutorials that will teach you the basics of writing Spanning applications, from smart contracts to frontend deployments. By the end, you will have your own version of this demo application.

Recommended Developer Environment

We recommend using VS Code with the Remix plugin. For a more detailed description of how to set this up, please refer to this tutorial.

The source code and API for the Spanning contracts and base classes can be installed via:

npm install @spanning/contracts

Smart Contract Code

Contract Definition and Imports

Let's start by defining a new contract file called jadeNFT.sol:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@spanning/contracts/token/ERC721/SpanningERC721.sol";
// Note: the following imports are included via SpanningERC721.sol
// but are included here to be explicit
import "@spanning/contracts/SpanningUtils.sol";
import "@spanning/contracts/Spanning.sol";
// We will use this utility later in the tutorial
import "@openzeppelin/contracts/utils/Strings.sol";
/**
 * The JadeNFT contract, utilizing the Spanning Protocol for multichain functionality.
 */
contract JadeNFT is SpanningERC721 {
    // Allows use of `Strings` functionality on uint256 types
    using Strings for uint256;

    // Defines the maximum number of NFTs that can be minted in the collection
    uint256 public constant MAX_ID_PLUS_ONE = 128;
    // Defines the initial index to mint first of the NFT collection
    uint256 public currentIndex = 0;
    // The base URI for the NFT images on IPFS
    // Note: If you want to use your own images you must replace this base URI
    string public baseURI =
        "ipfs://QmPNyfSWaHkAL2gZCnv3cvoJLADE3cJL9TYSrVY5RK8fSZ/";
}

This defines our new contract as a SpanningERC721 and gives us access to a host of multichain functionality and utilities, but it won't compile yet as we need to define a constructor.

Defining a Spanning Constructor

Just as in the previous tutorial, we must define a constructor that defines some ERC721 initialization parameters and which Spanning Delegate to use as our network endpoint.

    /**
     * @dev Creates the contract, initializing various base contracts
     *
     * @param delegate_ - Chain-local address of our Spanning Delegate
     */
    constructor(address delegate_)
        SpanningERC721("Jade Spanning Non-fungible Token", "JADENFT", delegate_)
    {}

Add Mint Functions

Like all ERC721s, SpanningERC721s have a default _mint function that can only be called internally. For an external user to access it we must create a new function:

    /**
     * @dev Mint the next jadeNFT to `recipientAddress`
     *
     * @param recipientAddress - Spanning Address that jadeNFT is minted to
     */
    function mint(bytes32 recipientAddress) external {
        uint256 _currentIndex = currentIndex;
        require(_currentIndex < MAX_ID_PLUS_ONE);

        _mint(recipientAddress, _currentIndex);
        unchecked {
            _currentIndex++;
        }
        currentIndex = _currentIndex;
    }

This mint function takes in a Spanning Address directly, but it can be overloaded to take in a local address as well. If you are curious about adding this as a convenience function or for more backward compatibility please see the previous tutorial.

Getting the Token URI

The token-specific URI is what allows people to get the IPFS link to the actual NFT image. We can implement a function that takes in a specific tokenId value and returns the correct IPFS link:

    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        require(tokenId < MAX_ID_PLUS_ONE, "invalid id");
        return
            string(
                // Note: we take the modulus of the token ID by 128
                // as the JadeNFT project only has 128 unique images
                abi.encodePacked(baseURI, (tokenId % 128).toString(), ".json")
            );
    }

This returns an IPFS path that can be used by web apps to get the NFT's attributes which include a path to the NFT's image.

Information Circle

To resolve a tokenURI manually in your browser you must replace ipfs:// with https://ipfs.io/ipfs/.

In the next tutorial that walks through the frontend UI of the Spanning Demo application, we will walk through how to display this NFT and pull this data programmatically from the blockchain.

Convenience functions

Let's add some additional functions that can be used to get some information about the project in general:

    function currentSupply() public view returns (uint256) {
        return currentIndex;
    }

    function totalSupply() public pure returns (uint256) {
        return MAX_ID_PLUS_ONE;
    }

    function burn(uint256 tokenId) public {
        bytes32 owner = SpanningERC721.ownerOfSpanning(tokenId);
        require(
            owner == spanningMsgSender(),
            "ERC721: burn from incorrect owner"
        );
        _burn(tokenId);
    }

Note the use of the spanningMsgSender() utility in the burn function. This is the multichain capable equivalent of msg.sender and can be used to enforce owner-only permissions, like burning an NFT you own.

JadeNFT Full Code

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@spanning/contracts/token/ERC721/SpanningERC721.sol";
// Note: the following imports are included via SpanningERC721.sol
// but are included here to be explicit
import "@spanning/contracts/SpanningUtils.sol";
import "@spanning/contracts/Spanning.sol";
// We will use this utility later in the tutorial
import "@openzeppelin/contracts/utils/Strings.sol";
/**
 * The JadeNFT contract, utilizing the Spanning Protocol for multichain functionality.
 */
contract JadeNFT is SpanningERC721 {
    // Allows use of `Strings` functionality on uint256 types
    using Strings for uint256;

    // Defines the maximum number of NFTs that can be minted in the collection
    uint256 public constant MAX_ID_PLUS_ONE = 128;
    // Defines the initial index to mint first of the NFT collection
    uint256 public currentIndex = 0;
    // The base URI for the NFT images on IPFS
    // Note: If you want to use your own images you must replace this base URI
    string public baseURI =
        "ipfs://QmPNyfSWaHkAL2gZCnv3cvoJLADE3cJL9TYSrVY5RK8fSZ/";

    /**
     * @dev Creates the contract, initializing various base contracts
     *
     * @param delegate_ - Chain-local address of our Spanning Delegate
     */
    constructor(address delegate_)
        SpanningERC721("Jade Spanning Non-fungible Token", "JADENFT", delegate_)
    {}

    /**
     * @dev Mint the next jadeNFT to `recipientAddress`
     *
     * @param recipientAddress - Spanning Address that jadeNFT is minted to
     */
    function mint(bytes32 recipientAddress) external {
        uint256 _currentIndex = currentIndex;
        require(_currentIndex < MAX_ID_PLUS_ONE);

        _mint(recipientAddress, _currentIndex);
        unchecked {
            _currentIndex++;
        }
        currentIndex = _currentIndex;
    }

    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        require(tokenId < MAX_ID_PLUS_ONE, "invalid id");
        return
            string(
                // Note: we take the modulus of the token ID by 128
                // as the JadeNFT project only has 128 unique images
                abi.encodePacked(baseURI, (tokenId % 128).toString(), ".json")
            );
    }

    function currentSupply() public view returns (uint256) {
        return currentIndex;
    }

    function totalSupply() public pure returns (uint256) {
        return MAX_ID_PLUS_ONE;
    }

    function burn(uint256 tokenId) public {
        bytes32 owner = SpanningERC721.ownerOfSpanning(tokenId);
        require(
            owner == spanningMsgSender(),
            "ERC721: burn from incorrect owner"
        );
        _burn(tokenId);
    }
}

Deploying Your Spanning Token

Your SpanningERC721 token is ready for deployment! The last consideration is where you want to deploy.

Check the latest Spanning Network deployment to see the most up-to-date Spanning Delegate addresses and deployable chains:

Mainnets

NetworkDomain IDSpanning Delegate AddressSpanning Address Library
Ethereum0x00000001Coming soon0x95e171b7ED0A147B51638d97aed92f15ffDE5f0D
Avalanche0x0000A86AComing soon0xF7139eA302b6735f57Ee9c563b547b295b25CedC
Arbitrum One0x0000A4B1Coming soon0x7Cdb610Ebd09B9f813EE2F5764d12898BC0286dC
Binance Smart Chain0x00000038Coming soon0xF7139eA302b6735f57Ee9c563b547b295b25CedC
Polygon0x00000089Coming soon0x1Cd3Ce05Ace44c3F4c560B0fcE0F39764030c299

Testnets

NetworkDomain IDSpanning Delegate Address
Mumbai0x000138810x2e613A930149A86D2fc42Dfbc1C5A63619FB3Bcb
Fuji0x0000A8690x9E6058C2bC70d11C7E2E4fF7128616C290374827
Goerli0x000000050x33f69d8e62e38C28bd7f0cbc61af6Eb8a0b3EF77

Deployable chains are networks on the Spanning Network that transactions can be written to. To get full multichain functionality, you must deploy your token on a deployable chain. Non-deployable networks can still host SpanningERC721s and be owned and transferred to multichain users, but they will be frozen for remote multichain owners until deployable status is added.

Here let's deploy JadeNFT to the Avalanche Fuji testnet. This means we must pass the constructor the Avalanche Fuji Delegate address upon construction.

Connecting VS Code Remix

If you are using VS Code and the Remix plugin, you can launch Remix by clicking the start remixd client menu option in the Ethereum Remix side menu:

Remix Launch

Follow the instructions in your VS Code terminal to go to https://remix.ethereum.org/ and connect to localhost in the File Explorer:

Remix Connect

Deploying a Contract on Remix

Open the jadeNFT.sol file you created, compile and deploy your contract via Injected Web3 while connected to the Fuji Network.

When deploying the contract, make sure to use the correct Spanning Delegate address we got previously. Also, make sure that the contract being deployed is jadeNFT.sol. Remix may default to deploying only the IERC721 contract.

Remix Deploy

You may also need Fuji testnet gas to deploy your application, which you can get here.

Your NFT is now live! In the next tutorials, we will walk through building a user interface for your tokens and NFTs that can connect to multiple networks to support multichain users.

Testing

You can test your token by using using the mint and tokenURI function calls right in Remix, and verify the results by looking at the Snowtrace testnet scanner and plugging in your newly deployed contract address: https://testnet.snowtrace.io/token/<YOUR_CONTRACT_ADDDRESS_HERE>

To test cross-chain functionality easily you can visit delegate.spanning.network which will allow you to submit transactions from any supported network to any smart contract on a deployable network. It can also be used for testing on the same chain if you don't want to go through Remix at all! Find more information about the Delegate App here.