国内用户如需交流,可加微信: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.
First let's run the project to see what we are going to analyze and implement.
First we run the project locally.
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
install dependencies
yarn install
start the front end server
yarn start
run the local test network in a second terminal window
yarn chain
deploy contract in a third terminal window
yarn deploy
go to http://localhost:3000
in browser to see the app
Localhost 8545
http://localhost:8545
31337
ETH
connect
in the top right corner of the page to connect to the wallet;Now we begin our analysis of the project contract.
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
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.sol
: Loogies
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() 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.
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;
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.
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
;tokenId
's attribute is generated randomly:
blockhash(block.number-1)
), msg.sender
, contract address (address(this)
) and tokenId
's generated hash predictableRandom
;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;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:
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:
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.
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.
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);
}
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);
}
}
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
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.
It has sections and functions of:
Header
: show titleNetworkDisplay
: show networkMenu
, Switch
: show and switch menuThemeSwitch
: switch light and dark themeAccount
: show account infoRow
s show gas and supported faucet in the lower left cornerThen 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:
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:
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);
};
Menu
, Switch
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.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.
We will implement the following three main components of functionality.
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:
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.
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:
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.
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:
At this point, we are done with a simple NFT minting and display DApp.
Through this project, we learnt and understood the following knowledge: