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.
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
Network | Domain ID | Spanning Delegate Address | Spanning Address Library |
---|---|---|---|
Ethereum | 0x00000001 | Coming soon | 0x95e171b7ED0A147B51638d97aed92f15ffDE5f0D |
Avalanche | 0x0000A86A | Coming soon | 0xF7139eA302b6735f57Ee9c563b547b295b25CedC |
Arbitrum One | 0x0000A4B1 | Coming soon | 0x7Cdb610Ebd09B9f813EE2F5764d12898BC0286dC |
Binance Smart Chain | 0x00000038 | Coming soon | 0xF7139eA302b6735f57Ee9c563b547b295b25CedC |
Polygon | 0x00000089 | Coming soon | 0x1Cd3Ce05Ace44c3F4c560B0fcE0F39764030c299 |
Testnets
Network | Domain ID | Spanning Delegate Address |
---|---|---|
Mumbai | 0x00013881 | 0x2e613A930149A86D2fc42Dfbc1C5A63619FB3Bcb |
Fuji | 0x0000A869 | 0x9E6058C2bC70d11C7E2E4fF7128616C290374827 |
Goerli | 0x00000005 | 0x33f69d8e62e38C28bd7f0cbc61af6Eb8a0b3EF77 |
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:

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

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.

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.