Hello NFT

Post Directory

Intro

緊接著上次的 Hello Smart ContractAlchemy Doc 接下來的範例為 ERC 721

也就是今年紅起來的 NFT(Non-Fungible Token),貓咪、明星卡、藝術品、甚至是石頭 Ether Rock(下圖),都能成為 NFT

etherrock0

一樣為該文件的學習筆記,詳細範例可以參考: https://github.com/VagrantPi/hello-nft

而流程基本上一篇差不多,不過這次多使用到一個 contracts library - OpenZepplin

A library for secure smart contract development. Build on a solid foundation of community-vetted code.

因此我們可以更安全、簡單的建立合約

先安裝一下

npm install @openzeppelin/contracts@3.1.0-solc-0.7


🎨 新建合約

Source: 🎨 How to Create an NFT

接下來看到合約的部份

//Contract based on https://docs.openzeppelin.com/contracts/3.x/erc721
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.3;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";


contract MyNFT is ERC721, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    // 第一個參數為 NFT 名稱
    // 第二個參數為 NFT's metadata
    constructor() public ERC721("MyNFT", "NFT") {}

    // 第一個參數為接收者 address
    // 第二個參數為該 NFT metadata,名稱、圖片...等描述,下面會提到
    function mintNFT(address recipient, string memory tokenURI)
        public onlyOwner
        returns (uint256)
    {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(recipient, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}

所以該合約簡單來說就是建立一個叫做 MyNFT,Symbol 為 NFT 的 ERC721,可以透過 mintNFT 方法來鑄造(mint) NFT

deploy script

async function main() {
   // Grab the contract factory 
   const MyNFT = await ethers.getContractFactory("MyNFT");

   // Start deployment, returning a promise that resolves to a contract object
   const myNFT = await MyNFT.deploy(); // Instance of the contract 
   console.log("Contract deployed to address:", myNFT.address);
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

流程跟上一篇一樣,compile 後

npx hardhat compile

然後 deploy

npx hardhat run scripts/deploy.js --network ropsten
Contract deployed to address: 0xa1270bb0BFacb686cCbc3D89072bBD4d7743cA87

https://ropsten.etherscan.io/address/0xa1270bb0bfacb686ccbc3d89072bbd4d7743ca87


🪄鑄造 NFT

Source: 🪄How to Mint an NFT

呼叫合約的 mintNFT 來鑄造 NFT,呼叫方法可以參考上一篇文章,而這邊還需要傳入一個 tokenURI 參數,其內容為 NFT Metadata,按造 “ERC721 Metadata JSON Schema

{
    "title": "Asset Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "Identifies the asset to which this NFT represents"
        },
        "description": {
            "type": "string",
            "description": "Describes the asset to which this NFT represents"
        },
        "image": {
            "type": "string",
            "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
        }
    }
}

至少須包含 name, description, image,我們會使用透過 Pinata 這個 IPFS(P2P 網路的去中心化檔案系統)先上傳圖片,然後在將該格式的 json 上傳至 IPFS

上傳圖片至 IPFS

然後複製該圖片的 CID,該圖片的位置為 https://gateway.pinata.cloud/ipfs/<CID>

https://gateway.pinata.cloud/ipfs/QmYqfs79uSDJzdMebdA7z5BhMzggm4HzZHMGbxkhRZMEBW

存成 NFT Metadata 後再次上傳

{
  "attributes" : [ {
    "trait_type" : "Breed",
    "value" : "MeMe"
  }, {
    "trait_type" : "rank",
    "value" : "no. 1"
  } ],
  "description" : "the popular meme in 2021/08",
  "image" : "https://gateway.pinata.cloud/ipfs/QmYqfs79uSDJzdMebdA7z5BhMzggm4HzZHMGbxkhRZMEBW",
  "name" : "POPCAT data"
}

https://gateway.pinata.cloud/ipfs/QmRYHRzgEt6jSGQutyVr3MqNFptsEaL4NMZUPRUQqZHPNf

呼叫合約

// ...
const nftContract = new web3.eth.Contract(contract.abi, contractAddress);

async function mintNFT(tokenURI) {
  const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, 'latest'); 

  // the transaction
  const tx = {
    'from': PUBLIC_KEY,
    'to': contractAddress,
    'nonce': nonce,
    'gas': 500000,
    'maxPriorityFeePerGas': 1999999987,
    'data': nftContract.methods.mintNFT(PUBLIC_KEY, tokenURI).encodeABI()
  };
  
  const signPromise = web3.eth.accounts.signTransaction(tx, PRIVATE_KEY);
  // ...
  });
}

mintNFT('https://gateway.pinata.cloud/ipfs/QmRYHRzgEt6jSGQutyVr3MqNFptsEaL4NMZUPRUQqZHPNf')

Mint NFT

node mint-nft.js 
The hash of your transaction is:  0x6bac10518d8eb89e4c6ff535411b83316a6d52a6f529f9e2b2e75d60aab9b91f 
Check Alchemy's Mempool to view the status of your transaction!

https://ropsten.etherscan.io/tx/0x6bac10518d8eb89e4c6ff535411b83316a6d52a6f529f9e2b2e75d60aab9b91f

此時看合約的頁面就可以看到

切到該錢包頁面也可以看到持有的 ERC721 新增了一個 MyNFT

使用手機版 Metamask 來查看 NFT

在收藏品那邊按下添加收藏品,然後將合約地址跟發行編號貼上就行

這邊的 MyNFT #1 當初在 mint 時直接貼上圖片網址。而不是後來上傳的 NFT Metadata JSON,所以顯示上就無法正確讀取

且如果填錯資訊也無法顯示


🪄使用 Ethers.js 來鑄造 NFT

🪄How to Mint an NFT with Ethers.js

這邊使用 Ethers.js 代替 Web3 的方式再做一次,並且使用到 hardhat-waffle 套件來測試合約,然後改用 TypeScript 來開發

安裝 TypeScript & hardhat-waffle

TypeScript

npm install --save-dev ts-node typescript
npm install --save-dev chai @types/node @types/mocha @types/chai

TypeScript Support

其他還有設定檔要副檔名,新增設定等,這邊請看連結,就不展開說明了

hardhat-waffle

為 HardHat 套件,用於智能合約測試

npm install --save-dev @nomiclabs/hardhat-waffle 'ethereum-waffle@^3.0.0' @nomiclabs/hardhat-ethers 'ethers@^5.0.0'

hardhat-waffle

分別將用到的用到的功能封裝起來

mkdir lib

建立 Hardhat tasks

可以寫 Hardhat 的 task 來擴增 Hardhat 指令來完成一些自動化的操作

例如在該篇範例後實作的 deploymint

AVAILABLE TASKS:

  deploy-contract    Deploy NFT contract
  mint-nft      Mint an NFT

並且可以通會 help 指令來查看說明

$ npx hardhat help deploy-contract

Hardhat version 2.6.1

Usage: hardhat [GLOBAL OPTIONS] deploy-contract

deploy-contract: Deploy NFT contract

For global options help run: hardhat help

在根目錄新增 tasks

import { task, types } from "hardhat/config";
import { Contract } from "ethers";
import { TransactionResponse } from "@ethersproject/abstract-provider";
import { env } from "../lib/env";
import { getContract } from "../lib/contract";
import { getWallet } from "../lib/wallet";

task("deploy-contract", "Deploy NFT contract").setAction(async (_, hre) => {
  return hre.ethers
    .getContractFactory("MyNFT", getWallet())
    .then((contractFactory) => contractFactory.deploy())
    .then((result) => {
      process.stdout.write(`Contract address: ${result.address}`);
    });
});

task("mint-nft", "Mint an NFT")
  .addParam("tokenUri", "Your ERC721 Token URI", undefined, types.string)
  .setAction(async (params, hre) => {
    return getContract("MyNFT", hre)
      .then((contract: Contract) => {
        return contract.mintNFT(env("ETH_PUBLIC_KEY"), params.tokenUri, {
          gasLimit: 500_000,
        });
      })
      .then((tr: TransactionResponse) => {
        process.stdout.write(`TX hash: ${tr.hash}`);
      });
  });
$ npx hardhat deploy-contract 

Contract address: 0x6dA6416fC6E68Dcb2CF8d148f02FdA1A614c4327

https://ropsten.etherscan.io/address/0x6dA6416fC6E68Dcb2CF8d148f02FdA1A614c4327

$ npx hardhat mint-nft --token-uri https://gateway.pinata.cloud/ipfs/QmRYHRzgEt6jSGQutyVr3MqNFptsEaL4NMZUPRUQqZHPNf

TX hash: 0x38f64758b99b498372551d75fa09781408d40bd6f4daf842557580b95a929c34

https://ropsten.etherscan.io/tx/0x38f64758b99b498372551d75fa09781408d40bd6f4daf842557580b95a929c34

Testing

這裡多安裝了一些工具來幫忙測試

npm install --save-dev sinon-chai sinon chai

參考範例:單元測試:Mocha、Chai 和 Sinon

mkdir test
$ npx hardhat test


  MyNFT
    mintNft
      ✓ emits the Transfer event (236ms)
      ✓ returns the new item ID
      ✓ increments the item ID (132ms)
      ✓ cannot mint to address zero (75ms)
    balanceOf
      ✓ gets the count of NFTs for this address (78ms)

  tasks
    deploy-contract
    mint-nft


  7 passing (4s)

結語

通過 OpenZepplin 可以很簡單就實作一個 ERC Spec 的合約,這邊也題到了合約測試的部份

https://github.com/VagrantPi/hello-nft使用 Ethers.js 來鑄造 NFT 內容有些調整

比如 task test 中 mint-nft 後的 TX hash,這邊不確定是如何預先知道 TX hash 的,我跑測試出來的 Hash 會跟 How to Mint an NFT with Ethers.js - Step 4: Create tests 不同

還有在 tasks/nft.ts 中的 mint-nft

task("mint-nft", "Mint an NFT")
  .addParam("tokenUri", "Your ERC721 Token URI", undefined, types.string)
+  .setAction(async (params, hre) => {
-  .setAction(async (tokenUri, hre) => {
    return getContract("MyNFT", hre)
      .then((contract: Contract) => {
+        return contract.mintNFT(env("ETH_PUBLIC_KEY"), params.tokenUri, {
-        return contract.mintNFT(env("ETH_PUBLIC_KEY"), tokenUri, {
          gasLimit: 500_000,
        });
      })
      .then((tr: TransactionResponse) => {
        process.stdout.write(`TX hash: ${tr.hash}`);
      });
  });

不然會造成 tokenURI 是空的

Tweet

發表評論前可以看一下 Pixiv 網站日排行榜,壓壓驚

source: https://github.com/mokeyjay/Pixiv-daily-ranking-widget