Web3dAppDevCamp

Posted on Feb 11, 2022Read on Mirror.xyz

Build SVG NFT dApp by Scaffold-Eth | Web3.0 dApp Dev 0x07

国内用户如需交流,可加微信:197626581.

Authors: qiwihui, msfew

History Articles List:

loogies-svg-nft is a simple NFT minting and displaying project provided by scaffold-eth. In this tutorial, we will walk you through a step-by-step analysis and implementation of this project.

Since the loogies-svg-nft branch and the master branch of the project have some differences in the component library and home page, the master branch code needs to be merged with the loogies-svg-nft branch to resolve the conflicts and get a brand new set of code based on the new component library. You can see the loogies-svg-nft branch of the project at https://github.com/qiwihui/scaffold-eth.git. The following part of this article will be based on these codes for deployment and analysis.

0x01 Running and testing locally

First let's run the project to see what we are going to analyze and implement.

Running locally

First we run the project locally.

  1. clone the project and switch to the loogies-svg-nft branch:

    git clone https://github.com/qiwihui/scaffold-eth.git loogies-svg-nft
    cd loogies-svg-nft
    git checkout loogies-svg-nft
    
  2. install dependencies

    yarn install
    
  3. start the front end server

    yarn start
    
  4. run the local test network in a second terminal window

    yarn chain
    
  5. deploy contract in a third terminal window

    yarn deploy
    
  6. go to http://localhost:3000 in browser to see the app

Testing locally

  1. First add the local network to the MetaMask wallet and switch to the local network;
    • Network name: Localhost 8545
    • Add RPC URL: http://localhost:8545
    • Chain ID: 31337
    • Currency Symbol: ETH
  2. Create a new local wallet address;
  3. Copy the wallet address and send some test ETH to this address in the bottom left corner of the page;
  4. Click on connect in the top right corner of the page to connect to the wallet;
  5. Click on Mint button to mint;
  6. When the transaction succeed, you can see the newly minted NFT.

Now we begin our analysis of the project contract.

0x02 Loogies Contract Analysis

NFT and ERC721

NFT (Non-Fungible Token), refers to non-homogenized tokens, corresponding to the ERC-721 standard on Ethereum. Generally in smart contracts, the definition of NFT contains tokenId and tokenURI, tokenId is unique ID for each NFT, and tokenURI for the metadata of the NFT is saved, which can be image URL, description, attributes, etc. If an NFT wants to display and sell in the NFT marketplace, the tokenURI content needs to correspond to the standards of the NFT marketplace. For example, in the NFT marketplace OpenSea's metadata standards, it is indicate attributes that need to be set for NFT display.

NFT metadata and presentation's connection in OpenSea

NFT metadata and presentation's connection in OpenSea

Contract overview

loogies-svg-nft project's contract code is at ' packages/hardhat/contracts/, and contains three files.

packages/hardhat/contracts/
├── HexStrings.sol
├── ToColor.sol
└── YourCollectible.sol
  • HexString.sol: generate address string;
  • ToColor.sol: generate color value string;
  • YourCollectible.solLoogies NFT's contract file with functions of minting and metadata generating.

Contract architecture and methods:

contract YourCollectible is ERC721, Ownable {

	// Constructor function
  constructor() public ERC721("Loogies", "LOOG") {
  }
  // Mint NFT
  function mintItem()
      public
      returns (uint256)
  {
    ...
  }
	// Get tokenURI from tokenId
  function tokenURI(uint256 id) public view override returns (string memory) {
    ...
  }
	// Generate SVG from tokenId
  function generateSVGofTokenById(uint256 id) internal view returns (string memory) {
    ...
  }

	// Render tokenId's svg code for graphics display
  function renderTokenById(uint256 id) public view returns (string memory) {
    ...
  }

}

Constructor function

constructor() public ERC721("Loogies", "LOOG") {
    // RELEASE THE LOOGIES!
  }

Token name: Loogies

Token symbol: LOOG

The contract is inherited from OpenZeppelin's ERC721.sol, which is the basic contract code provided by OpenZeppelin and can be easily used by developers.

Library functions

Contract applies different library functions for uint256, uint160 and bytes3 to extend features:

// let uint256 have toHexString
using Strings for uint256;
// let uint160 have configurable toHexString
using HexStrings for uint160;
// let bytes3 have easy color representation
using ToColor for bytes3;
// counter feature
using Counters for Counters.Counter;

Mint time limit

The following code is for Mint time limit:

uint256 mintDeadline = block.timestamp + 24 hours;

function mintItem()
      public
      returns (uint256)
  {
      require( block.timestamp < mintDeadline, "DONE MINTING");
...

Contracts is mintable within 24 hours of deployment, and beyond that time an exception is raised. This mechanism is similar to pre-sales. Since this contract is relatively simple, no whitelist mechanism is used, and generally in practice, pre-sales and whitelists are used to control the NFTs issuing.

Mint NFT

Minting NFT is to set two variables in contract:

  • tokenId with owner
  • tokenId with tokenURI

Let's look at the minting function mintItem:

// Store each Loogies' attribute
mapping (uint256 => bytes3) public color;
mapping (uint256 => uint256) public chubbiness;

...

function mintItem()
      public
      returns (uint256)
  {
      require( block.timestamp < mintDeadline, "DONE MINTING");
			// Self increment for _tokenIds to make sure _tokenIds is non-fungiable
      _tokenIds.increment();

      uint256 id = _tokenIds.current();
			// Binder minter and tokenId
      _mint(msg.sender, id);
			// Randomly generate tokenId's attributes
      bytes32 predictableRandom = keccak256(abi.encodePacked( blockhash(block.number-1), msg.sender, address(this), id ));
      color[id] = bytes2(predictableRandom[0]) | ( bytes2(predictableRandom[1]) >> 8 ) | ( bytes3(predictableRandom[2]) >> 16 );
      chubbiness[id] = 35+((55*uint256(uint8(predictableRandom[3])))/255);

      return id;
  }

Also:

  • tokenId self increments when minting to make sure each tokenId is only one;
  • _mint function binds tokenId with owner;
  • Each tokenId's attribute is generated randomly:
    • By pre-block's hash (blockhash(block.number-1)), msg.sender, contract address (address(this)) and tokenId's generated hash predictableRandom;
    • Calculate NFT color: doing bit operations to predictableRandom's first three bits to get the color, color is represented by bytes3. bytes2(predictableRandom[0]) is the lowest blue value, ( bytes2(predictableRandom[1]) >> 8 ) is the green value, ( bytes3(predictableRandom[2]) >> 16 ) is the red value;
    • Calculate NFT chubbiness: 35+((55*uint256(uint8(predictableRandom[3])))/255);uint8(predictableRandom[3]) is between 0~255, so the minimum is 35, and the maximum is 35 + 55 = 90.

For example: When color is 0x4cc4c1, chubbiness is 88, the NFT picture is:

loogies-1

tokenURI function

Function tokenURI takes tokenId parameter, returns encoded metadata string:

function tokenURI(uint256 id) public view override returns (string memory) {
			// check id if exist
      require(_exists(id), "not exist");
      string memory name = string(abi.encodePacked('Loogie #',id.toString()));
      string memory description = string(abi.encodePacked('This Loogie is the color #',color[id].toColor(),' with a chubbiness of ',uint2str(chubbiness[id]),'!!!'));
      // generate svg base64 for image
			string memory image = Base64.encode(bytes(generateSVGofTokenById(id)));

      return
          string(
              abi.encodePacked(
                'data:application/json;base64,',
								// encode metadata with base64
                Base64.encode(
                    bytes(
                          abi.encodePacked(
                              '{"name":"',
                              name,
                              '", "description":"',
                              description,
                              '", "external_url":"https://burnyboys.com/token/',
                              id.toString(),
                              '", "attributes": [{"trait_type": "color", "value": "#',
                              color[id].toColor(),
                              '"},{"trait_type": "chubbiness", "value": ',
                              uint2str(chubbiness[id]),
                              '}], "owner":"',
                              (uint160(ownerOf(id))).toHexString(20),
                              '", "image": "',
                              'data:image/svg+xml;base64,',
                              image,
                              '"}'
                          )
                        )
                    )
              )
          );
  }
// Generated SVG string
function generateSVGofTokenById(uint256 id) internal view returns (string memory) {
...
}

// Render image
// Visibility is `public` to enable it being called by other contracts for composition.
function renderTokenById(uint256 id) public view returns (string memory) {
...
}

In this, generateSVGofTokenById function returns tokenId's SVG string on its color and chubbiness, renderTokenById renders the token.

We see that the NFT needs to include:

  • name
  • description
  • external_url
  • attributes
    • color
    • chubbiness
    • owner
    • image

We can get to know more about SVG by an example.When tokenId is 1, the tokenURI is:

data:application/json;base64,eyJuYW1lIjoiTG9vZ2llICMxIiwiZGVzY3JpcHRpb24iOiJUaGlzIExvb2dpZSBpcyB0aGUgY29sb3IgIzRjYzRjMSB3aXRoIGEgY2h1YmJpbmVzcyBvZiA4OCEhISIsImV4dGVybmFsX3VybCI6Imh0dHBzOi8vYnVybnlib3lzLmNvbS90b2tlbi8xIiwiYXR0cmlidXRlcyI6W3sidHJhaXRfdHlwZSI6ImNvbG9yIiwidmFsdWUiOiIjNGNjNGMxIn0seyJ0cmFpdF90eXBlIjoiY2h1YmJpbmVzcyIsInZhbHVlIjo4OH1dLCJvd25lciI6IjB4MTY5ODQxYWEzMDI0Y2ZhNTcwMDI0ZWI3ZGQ2YmY1Zjc3NDA5MjA4OCIsImltYWdlIjoiZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCM2FXUjBhRDBpTkRBd0lpQm9aV2xuYUhROUlqUXdNQ0lnZUcxc2JuTTlJbWgwZEhBNkx5OTNkM2N1ZHpNdWIzSm5Mekl3TURBdmMzWm5JajQ4WnlCcFpEMGlaWGxsTVNJK1BHVnNiR2x3YzJVZ2MzUnliMnRsTFhkcFpIUm9QU0l6SWlCeWVUMGlNamt1TlNJZ2NuZzlJakk1TGpVaUlHbGtQU0p6ZG1kZk1TSWdZM2s5SWpFMU5DNDFJaUJqZUQwaU1UZ3hMalVpSUhOMGNtOXJaVDBpSXpBd01DSWdabWxzYkQwaUkyWm1aaUl2UGp4bGJHeHBjSE5sSUhKNVBTSXpMalVpSUhKNFBTSXlMalVpSUdsa1BTSnpkbWRmTXlJZ1kzazlJakUxTkM0MUlpQmplRDBpTVRjekxqVWlJSE4wY205clpTMTNhV1IwYUQwaU15SWdjM1J5YjJ0bFBTSWpNREF3SWlCbWFXeHNQU0lqTURBd01EQXdJaTgrUEM5blBqeG5JR2xrUFNKb1pXRmtJajQ4Wld4c2FYQnpaU0JtYVd4c1BTSWpOR05qTkdNeElpQnpkSEp2YTJVdGQybGtkR2c5SWpNaUlHTjRQU0l5TURRdU5TSWdZM2s5SWpJeE1TNDRNREEyTlNJZ2FXUTlJbk4yWjE4MUlpQnllRDBpT0RnaUlISjVQU0kxTVM0NE1EQTJOU0lnYzNSeWIydGxQU0lqTURBd0lpOCtQQzluUGp4bklHbGtQU0psZVdVeUlqNDhaV3hzYVhCelpTQnpkSEp2YTJVdGQybGtkR2c5SWpNaUlISjVQU0l5T1M0MUlpQnllRDBpTWprdU5TSWdhV1E5SW5OMloxOHlJaUJqZVQwaU1UWTRMalVpSUdONFBTSXlNRGt1TlNJZ2MzUnliMnRsUFNJak1EQXdJaUJtYVd4c1BTSWpabVptSWk4K1BHVnNiR2x3YzJVZ2NuazlJak11TlNJZ2NuZzlJak1pSUdsa1BTSnpkbWRmTkNJZ1kzazlJakUyT1M0MUlpQmplRDBpTWpBNElpQnpkSEp2YTJVdGQybGtkR2c5SWpNaUlHWnBiR3c5SWlNd01EQXdNREFpSUhOMGNtOXJaVDBpSXpBd01DSXZQand2Wno0OEwzTjJaejQ9In0=

Decode data:application/json;base64, with base64. Then the string can generate the following json (simplified version):

{
  "name": "Loogie #1",
  "description": "This Loogie is the color #4cc4c1 with a chubbiness of 88!!!",
  "external_url": "https://burnyboys.com/token/1",
  "attributes": [
    {
      "trait_type": "color",
      "value": "#4cc4c1"
    },
    {
      "trait_type": "chubbiness",
      "value": 88
    }
  ],
  "owner": "0x169841aa3024cfa570024eb7dd6bf5f774092088",
  "image": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBpZD0iZXllMSI+PGVsbGlwc2Ugc3Ryb2tlLXdpZHRoPSIzIiByeT0iMjkuNSIgcng9IjI5LjUiIGlkPSJzdmdfMSIgY3k9IjE1NC41IiBjeD0iMTgxLjUiIHN0cm9rZT0iIzAwMCIgZmlsbD0iI2ZmZiIvPjxlbGxpcHNlIHJ5PSIzLjUiIHJ4PSIyLjUiIGlkPSJzdmdfMyIgY3k9IjE1NC41IiBjeD0iMTczLjUiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlPSIjMDAwIiBmaWxsPSIjMDAwMDAwIi8+PC9nPjxnIGlkPSJoZWFkIj48ZWxsaXBzZSBmaWxsPSIjNGNjNGMxIiBzdHJva2Utd2lkdGg9IjMiIGN4PSIyMDQuNSIgY3k9IjIxMS44MDA2NSIgaWQ9InN2Z181IiByeD0iODgiIHJ5PSI1MS44MDA2NSIgc3Ryb2tlPSIjMDAwIi8+PC9nPjxnIGlkPSJleWUyIj48ZWxsaXBzZSBzdHJva2Utd2lkdGg9IjMiIHJ5PSIyOS41IiByeD0iMjkuNSIgaWQ9InN2Z18yIiBjeT0iMTY4LjUiIGN4PSIyMDkuNSIgc3Ryb2tlPSIjMDAwIiBmaWxsPSIjZmZmIi8+PGVsbGlwc2Ugcnk9IjMuNSIgcng9IjMiIGlkPSJzdmdfNCIgY3k9IjE2OS41IiBjeD0iMjA4IiBzdHJva2Utd2lkdGg9IjMiIGZpbGw9IiMwMDAwMDAiIHN0cm9rZT0iIzAwMCIvPjwvZz48L3N2Zz4="
}

We decode image value and format it to get image SVG:

<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
   <g id="eye1">
      <ellipse stroke-width="3" ry="29.5" rx="29.5" id="svg_1" cy="154.5" cx="181.5" stroke="#000" fill="#fff" />
      <ellipse ry="3.5" rx="2.5" id="svg_3" cy="154.5" cx="173.5" stroke-width="3" stroke="#000" fill="#000000" />
   </g>
   <g id="head">
      <ellipse fill="#4cc4c1" stroke-width="3" cx="204.5" cy="211.80065" id="svg_5" rx="88" ry="51.80065" stroke="#000" />
   </g>
   <g id="eye2">
      <ellipse stroke-width="3" ry="29.5" rx="29.5" id="svg_2" cy="168.5" cx="209.5" stroke="#000" fill="#fff" />
      <ellipse ry="3.5" rx="3" id="svg_4" cy="169.5" cx="208" stroke-width="3" fill="#000000" stroke="#000" />
   </g>
</svg>

SVG is a language defined in XML to describe two-dimensional vector and vector/raster graphics. It is widely used as it can be displayed in arbitrarily large sizes and without sacrificing image quality, it can be described using code, and it is easy to edit.

As can be seen from the above code combined with the following image, this SVG contains the following.

  • An XML declaration in the first line, indicating the version and encoding type, followed by the width and height of the SVG;
  • eye1: eye circle and black eyes drawn by two ellipses;
  • head: ellipse filled with #4cc4c1 color as the body;
  • eye2: identical to eye1, in a different position;

eye1, head and eye2 are superimposed in sequence to get the final shape.

loogies-1

Helper function

  1. uint2str converts uint to string, like convert 123 to '123'
function uint2str(uint _i) internal pure returns (string memory _uintAsString) {
      if (_i == 0) {
          return "0";
      }
      uint j = _i;
			// uint length
      uint len;
      while (j != 0) {
          len++;
          j /= 10;
      }
      bytes memory bstr = new bytes(len);
      uint k = len;
      while (_i != 0) {
          k = k-1;
			    // _i single digits
          uint8 temp = (48 + uint8(_i - _i / 10 * 10));
          bytes1 b1 = bytes1(temp);
          bstr[k] = b1;
          _i /= 10;
      }
      return string(bstr);
  }
  1. ToColor.sol library: convert byte3 type to color strings. Eg. 0x4cc4c1 as input and '4cc4c1' as output.
library ToColor {
    bytes16 internal constant ALPHABET = '0123456789abcdef';

    function toColor(bytes3 value) internal pure returns (string memory) {
      bytes memory buffer = new bytes(6);
      for (uint256 i = 0; i < 3; i++) {
          buffer[i*2+1] = ALPHABET[uint8(value[i]) & 0xf];
          buffer[i*2] = ALPHABET[uint8(value[i]>>4) & 0xf];
      }
      return string(buffer);
    }
}
  1. HexStrings.sol library: Mainly extract uint based on length index, just like extract 20 indices of address: (*uint160*(ownerOf(id))).toHexString(20), this expression generates the coresponding tokenId owner address.
library HexStrings {
    bytes16 internal constant ALPHABET = '0123456789abcdef';

    function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
        bytes memory buffer = new bytes(2 * length + 2);
        buffer[0] = '0';
        buffer[1] = 'x';
        for (uint256 i = 2 * length + 1; i > 1; --i) {
            buffer[i] = ALPHABET[value & 0xf];
            value >>= 4;
        }
        return string(buffer);
    }
}

That's it for the contract code analysis.

Below we will provide a brief analysis of the front-end logic, and then we will implement the NFT minting and display functionality step by step. Before submitting front-end code, follow the steps below to add the functionality.

git checkout a98156f6a03a0bc8fc98c8c77cef6fbf59f03b31

0x03 Front end logic analysis

The project front-end files are in packages/react-app, and the locations of the files covered in the following articles will be found in this file.

check App.jsx:

https://github.com/qiwihui/scaffold-eth/blob/loogies-svg-nft/packages/react-app/src/App.jsx

First we take a look at src/App.jsx. It is the main component for the project. We can view it in our editor.

Appjsx.png

It has sections and functions of:

  • Header: show title
  • _NetworkDisplay: show network
  • _Menu, Switch: show and switch menu
  • _ThemeSwitch: switch light and dark theme
  • _Account: show account info
  • And two Rows show gas and supported faucet in the lower left corner

Then we take a look at _NetworkDisplay and Account's logic design, and functions in Menu, and Switch.

NetworkDisplay

Component is at: src/components/NetworkDisplay.jsx.

With those two functions:

  1. Show current network;
  2. If the current network is not supported, then show error. The network id for local network should be 31337.
function NetworkDisplay({
  NETWORKCHECK,
  localChainId,
  selectedChainId,
  targetNetwork,
  USE_NETWORK_SELECTOR,
  logoutOfWeb3Modal,
}) {
  let networkDisplay = "";
  if (NETWORKCHECK && localChainId && selectedChainId && localChainId !== selectedChainId) {
    const networkSelected = NETWORK(selectedChainId);
    const networkLocal = NETWORK(localChainId);
    if (selectedChainId === 1337 && localChainId === 31337) {
			// show error network id
			...
    } else {
			// show network is not correct
			...
    }
  } else {
    networkDisplay = USE_NETWORK_SELECTOR ? null : (
      // show network name
      <div style={{ zIndex: -1, position: "absolute", right: 154, top: 28, padding: 16, color: targetNetwork.color }}>
        {targetNetwork.name}
      </div>
    );
  }

  console.log({ networkDisplay });

  return networkDisplay;
}

Account

Component is at src/components/Account.jsx.

With those three functions:

  1. show current address
  2. show address balance
  3. show Connect or Logout

When user clicks Connect, the front end calls loadWeb3Modal. This function is needed to connect with wallets such as MetaMask and listen to the wallet's _ chainChanged, accountsChanged and disconnect events, i.e. to modify the display status when we switch networks, select connected accounts and cancel connections in the wallet.

const loadWeb3Modal = useCallback(async () => {
  // connect wallet
  const provider = await web3Modal.connect();
  setInjectedProvider(new ethers.providers.Web3Provider(provider));
  // listen to network switch
  provider.on('chainChanged', (chainId) => {
    console.log(`chain changed to ${chainId}! updating providers`);
    setInjectedProvider(new ethers.providers.Web3Provider(provider));
  });
  // listen to changed accounts
  provider.on('accountsChanged', () => {
    console.log(`account changed!`);
    setInjectedProvider(new ethers.providers.Web3Provider(provider));
  });
  // Subscribe to session disconnection
  provider.on('disconnect', (code, reason) => {
    console.log(code, reason);
    logoutOfWeb3Modal();
  });
  // eslint-disable-next-line
}, [setInjectedProvider]);

Similarly, in the case of a connected wallet, the user clicks Logout to call the logoutOfWeb3Modal function.

const logoutOfWeb3Modal = async () => {
  // clear cached network provider and disconnect
  await web3Modal.clearCachedProvider();
  if (
    injectedProvider &&
    injectedProvider.provider &&
    typeof injectedProvider.provider.disconnect == 'function'
  ) {
    await injectedProvider.provider.disconnect();
  }
  setTimeout(() => {
    window.location.reload();
  }, 1);
};

MenuSwitch

These two correspond to the display menu and correspond to the switch menu functions, these menus include:

  • App Home: the project wants us to put the functions we need to implement in this menu, such as the minting and display functions of NFT that we will implement.
  • Debug Contracts: debug their own written contract functions, will be based on the contract's ABI file to show the state variables and functions that can be called.
  • Hints: hints for coding.
  • ExampleUI: sample UI, which can be used for programming purposes.
  • Mainnet DAI: the state of the contracts and available functions of the DAI network, with the same functionality as Debug Contracts.
  • Subgraph: Listening and querying for events in contracts using The Graph protocol.

Debug output

App.jsx also has debugging output that prints the current page status, allowing you to view the current status variables in real time during development.

//
// 🧫 DEBUG 👨🏻‍🔬
//
useEffect(() => {
  if (
    DEBUG &&
    mainnetProvider &&
    address &&
    selectedChainId &&
    yourLocalBalance &&
    yourMainnetBalance &&
    readContracts &&
    writeContracts &&
    mainnetContracts
  ) {
    console.log(
      '_____________________________________ 🏗 scaffold-eth _____________________________________',
    );
    console.log('🌎 mainnetProvider', mainnetProvider);
    console.log('🏠 localChainId', localChainId);
    console.log('👩‍💼 selected address:', address);
    console.log('🕵🏻‍♂️ selectedChainId:', selectedChainId);
    console.log(
      '💵 yourLocalBalance',
      yourLocalBalance ? ethers.utils.formatEther(yourLocalBalance) : '...',
    );
    console.log(
      '💵 yourMainnetBalance',
      yourMainnetBalance ? ethers.utils.formatEther(yourMainnetBalance) : '...',
    );
    console.log('📝 readContracts', readContracts);
    console.log('🌍 DAI contract on mainnet:', mainnetContracts);
    console.log('💵 yourMainnetDAIBalance', myMainnetDAIBalance);
    console.log('🔐 writeContracts', writeContracts);
  }
}, [
  mainnetProvider,
  address,
  selectedChainId,
  yourLocalBalance,
  yourMainnetBalance,
  readContracts,
  writeContracts,
  mainnetContracts,
  localChainId,
  myMainnetDAIBalance,
]);

After viewing the basic functions of the home page, we start implementing the two functions of NFT minting and displaying the NFT list.

0x04 NFT functions

We will implement the following three main components of functionality.

  • Minting NFTs;
  • Displaying the NFT list;
  • Displaying a list of NFT contract interfaces.

Minting NFT

First we find the component corresponding to App Home, as you can see from the code below, which uses the Home component, located at src/views/Home.jsx.

    ...
    <Switch>
        <Route exact path="/">
          {/* pass in any web3 props to this Home component. For example, yourLocalBalance */}
          <Home yourLocalBalance={yourLocalBalance} readContracts={readContracts} />
        </Route>
    ....

Delete contents in Home.jsx and add the following Mint button:

import React, { useState } from 'react';
import { Button, Card, List } from 'antd';

function Home({ isSigner, loadWeb3Modal, tx, writeContracts }) {
  return (
    <div>
      {/* Mint button */}
      <div
        style={{
          maxWidth: 820,
          margin: 'auto',
          marginTop: 32,
          paddingBottom: 32,
        }}
      >
        {isSigner ? (
          <Button
            type={'primary'}
            onClick={() => {
              tx(writeContracts.YourCollectible.mintItem());
            }}
          >
            MINT
          </Button>
        ) : (
          <Button type={'primary'} onClick={loadWeb3Modal}>
            CONNECT WALLET
          </Button>
        )}
      </div>
    </div>
  );
}

export default Home;

Change Switch's component into:

      ...
      <Switch>
        <Route exact path="/">
          {/* pass in any web3 props to this Home component. For example, yourLocalBalance */}
          <Home
            isSigner={userSigner}
            loadWeb3Modal={loadWeb3Modal}
            tx={tx}
            writeContracts={writeContracts}
          />
       ...

It should look like this:

mint-button

After clicking Mint, we can see that the transaction was successfully issued, at this point, although we successfully minted the NFT, we still need to add the list to show our NFT.

Show NFT list

Add a list to display containing the NFT can be transferred to other addresses.

import React, { useState } from 'react';
import { Button, Card, List } from 'antd';
import { useContractReader } from 'eth-hooks';
import { Address, AddressInput } from '../components';

function Home({
  isSigner,
  loadWeb3Modal,
  yourCollectibles,
  address,
  blockExplorer,
  mainnetProvider,
  tx,
  readContracts,
  writeContracts,
}) {
  const [transferToAddresses, setTransferToAddresses] = useState({});

  return (
    <div>
      {/* Mint button */}
      ...
      {/* List */}
      <div style={{ width: 820, margin: 'auto', paddingBottom: 256 }}>
        <List
          bordered
          dataSource={yourCollectibles}
          renderItem={(item) => {
            const id = item.id.toNumber();
            console.log('IMAGE', item.image);
            return (
              <List.Item key={id + '_' + item.uri + '_' + item.owner}>
                <Card
                  title={
                    <div>
                      <span style={{ fontSize: 18, marginRight: 8 }}>
                        {item.name}
                      </span>
                    </div>
                  }
                >
                  <a
                    href={
                      'https://opensea.io/assets/' +
                      (readContracts &&
                        readContracts.YourCollectible &&
                        readContracts.YourCollectible.address) +
                      '/' +
                      item.id
                    }
                    target='_blank'
                  >
                    <img src={item.image} />
                  </a>
                  <div>{item.description}</div>
                </Card>
                {/* NFT transfering */}
                <div>
                  owner:{' '}
                  <Address
                    address={item.owner}
                    ensProvider={mainnetProvider}
                    blockExplorer={blockExplorer}
                    fontSize={16}
                  />
                  <AddressInput
                    ensProvider={mainnetProvider}
                    placeholder='transfer to address'
                    value={transferToAddresses[id]}
                    onChange={(newValue) => {
                      const update = {};
                      update[id] = newValue;
                      setTransferToAddresses({
                        ...transferToAddresses,
                        ...update,
                      });
                    }}
                  />
                  <Button
                    onClick={() => {
                      console.log('writeContracts', writeContracts);
                      tx(
                        writeContracts.YourCollectible.transferFrom(
                          address,
                          transferToAddresses[id],
                          id,
                        ),
                      );
                    }}
                  >
                    Transfer
                  </Button>
                </div>
              </List.Item>
            );
          }}
        />
      </div>
      {/* Display info */}
      <div
        style={{
          maxWidth: 820,
          margin: 'auto',
          marginTop: 32,
          paddingBottom: 256,
        }}
      >
        🛠 built with{' '}
        <a
          href='https://github.com/austintgriffith/scaffold-eth'
          target='_blank'
        >
          🏗 scaffold-eth
        </a>
        🍴 <a
          href='https://github.com/austintgriffith/scaffold-eth'
          target='_blank'
        >
          Fork this repo
        </a> and build a cool SVG NFT!
      </div>
    </div>
  );
}

export default Home;

Change the component into:

      ...
      <Switch>
        <Route exact path="/">
          {/* pass in any web3 props to this Home component. For example, yourLocalBalance */}
          <Home
            isSigner={userSigner}
            loadWeb3Modal={loadWeb3Modal}
            yourCollectibles={yourCollectibles}
            address={address}
            blockExplorer={blockExplorer}
            mainnetProvider={mainnetProvider}
            tx={tx}
            writeContracts={writeContracts}
            readContracts={readContracts}
          />
       ...

It should look like this:

nft-display-list

But we found that when we mint again, the list doesn't update, it's still the same, so we need to add an event listener to App.jsx that will refresh the list once we mint NFT:

// Track current NFT numbers
const balance = useContractReader(
  readContracts,
  'YourCollectible',
  'balanceOf',
  [address],
);
console.log('🤗 balance:', balance);

const yourBalance = balance && balance.toNumber && balance.toNumber();
const [yourCollectibles, setYourCollectibles] = useState();
//
// 🧠 This useEffect hook updates yourCollectibles when balance is changing.
//
useEffect(() => {
  const updateYourCollectibles = async () => {
    const collectibleUpdate = [];
    for (let tokenIndex = 0; tokenIndex < balance; tokenIndex++) {
      try {
        console.log('GEtting token index', tokenIndex);
        const tokenId = await readContracts.YourCollectible.tokenOfOwnerByIndex(
          address,
          tokenIndex,
        );
        console.log('tokenId', tokenId);
        const tokenURI = await readContracts.YourCollectible.tokenURI(tokenId);
        const jsonManifestString = atob(tokenURI.substring(29));
        console.log('jsonManifestString', jsonManifestString);

        try {
          const jsonManifest = JSON.parse(jsonManifestString);
          console.log('jsonManifest', jsonManifest);
          collectibleUpdate.push({
            id: tokenId,
            uri: tokenURI,
            owner: address,
            ...jsonManifest,
          });
        } catch (e) {
          console.log(e);
        }
      } catch (e) {
        console.log(e);
      }
    }
    setYourCollectibles(collectibleUpdate.reverse());
  };
  updateYourCollectibles();
}, [address, yourBalance]);

At this point, when we mint again, the list is automatically updated to show the latest NFT.

Show NFT contract ABI list

This function is relatively simple and requires only the following changes to the corresponding debug section:

      <Route exact path="/debug">
          {/*
                🎛 this scaffolding is full of commonly used components
                this <Contract/> component will automatically parse your ABI
                and give you a form to interact with it locally
            */}

          <Contract
            name="YourCollectible"
            price={price}
            signer={userSigner}
            provider={localProvider}
            address={address}
            blockExplorer={blockExplorer}
            contractConfig={contractConfig}
          />

更新之后,可以在 Debug Contracts 菜单下看到合约的可以调用的函数。

After the update, you can see the callable functions of the contract under the Debug Contracts menu:

contract-funcs

At this point, we are done with a simple NFT minting and display DApp.

0x05 Conclusion

Through this project, we learnt and understood the following knowledge:

  1. the basic of NFT contracts and how to upload NFT in marketplaces such as OpenSea.
  2. how the DApp front-end connects to wallets such as MetaMask.
  3. how to call contract functions on the front end.

msfew

NFT