Ethereum コントラクト開発 ERC721編

2021/08/18に公開

この記事について

株式会社CauchyEのメンバーが、社内の開発や勉強会等に関する技術的なアウトプットを行い、弊社の開発における取組や、弊社メンバーの学びに対する取組等をお伝えしていく予定です。弊社の取組の内容や、弊社の雰囲気等が伝わりますと幸いです。

記事作成者自己紹介

株式会社CauchyEのエンジニアの松岡靖典です。
ブロックチェーンの技術的な面白さに魅了され、Web開発の世界を歩み始めました。
NPO法人NEM技術普及推進会NEMTUSの理事としてブロックチェーン技術の普及推進活動に従事したり、個人開発等の取り組みも行っています。
JavaScript, TypeScript, Angular, Firebase, NEM, Symbol, Cosmosが好きですが、チャンスがあれば様々な技術に触れていきたいと思っています。

記事内容要約

前々回の記事前回の記事の続編として、NFTの実装で良く使用されるEthereumのERC721のコントラクト開発をローカル環境、テストネット環境で試す内容です。
なお、この記事の作成においては、こちらの記事[1]を参考にさせて頂きました。この場を借りてお礼申し上げます。

この記事での目標

  1. hardhat[2]で構築した環境上で、ERC721のシンプルなコントラクトをOpenZeppelin[3]を用いて実装
  2. ローカル環境、テストネット環境でコントラクトをデプロイ
  3. mint(トークン発行)、transferFrom(トークンの送信)等のコントラクトを呼び出して実行する機能をテスト
  4. hardhatを使わずNode.jsでethers[4]の適切な設定や実装を加えて動作検証する

NFTとは

NFTとはNon-Fungible Tokenの略称で、日本語では非代替性トークンという言葉になります。
扱われる文脈によって様々な意味づけがなされるNFTですが、シンプルに技術的な分類と考えると、FT(=Fungible Token)ではないトークンと捉えるのが自然だと思います。
もう少しかみ砕いて表現すると、例えばETHのようなFT(=Fungible Token=代替可能なトークン)は「Aさんが持っている1ETH」と「Bさんが持っている1ETH」はいずれも同じETHというトークンで代替可能なものですが、
NFTでは、「Aさんが持っている1枚目に発行されたトークン」と「Bさんが持っている2枚目に発行されたトークン」は、同じ仲間のようなトークンであるにも関わらず、それぞれが異なる特性を持ち、結果として唯一無二のものになり、他のトークンで代替することが原理的に不可能な特徴をもったトークンと言えるでしょう。

ERC721とは

次にNFTと同列で語られることの多いERC721ですが、NFTをEthereumブロックチェーン上で実現するために定められたEthereumの規格です。
ERC721以外のNFTも原理的には当然存在し「ERC721でないならNFTではない」というのは必ずしも正しくないと思いますが、ERC721がNFTの事実上のデファクトスタンダードとなりつつある昨今「NFT≒ERC721」のように扱われることも多い印象があります。

技術的には(必須ではなくオプショナルではあるものの)、多くの場合、各トークン毎にブロックチェーン上の情報として、name, symbol, tokenURIというmetadataを持ち、tokenURIは各トークン毎に固有で、tokenURIのURLにアクセスすると、以下の例のようなJSONレスポンスを得られるよう設計・実装されているという特徴があります。

JSONレスポンス一例
{
  "name": "NFT Name",
  "description": "NFT Description.",
  "image": "NFT resource file link"
}

画像や動画等のデータが、tokenURIのURLにアクセスすると得られるJSONの中の"image"というkeyのvalueとして設定される対象ファイルのURLで関連付けされて、アートや何らかの証明等の意味づけを持ったNFTとして発行されることが多いように思います。

NFTの発行プラットフォームや、サービスによっては、それ以外にも様々な機能やパラメータが付加された実装になっていることもあり、NFTとして表現されたゲームキャラクターやアイテム等のゲーム内資産のパラメーターを表現しているような事例もあるようです。

環境構築

前提環境

ERC721に準拠したNFTのコントラクトの実装

前々回の記事前回の記事で、Hello world的なコントラクト開発を通して一連の流れをある程度把握できたので、この記事の目標のERC721に準拠したNFTのコントラクトの実装に進みます。

Hello world程度のシンプルなコントラクトであれば、コントラクトをほぼすべて自分自身で実装しても特に問題はありませんでしたが、少しでも複雑なコントラクトを手動で記述しようとすると、とたんに難易度が爆発的に上がります。

全てを手動で実装するのは面倒なだけでなく、致命的な脆弱性を容易に作りこんでしまうリスクが極めて高いため、OpenZeppelinのようなライブラリを使って実装するのが事実上デファクトスタンダードになっていると思います。

OpenZeppelinの4.x系列のバージョンでは、色々とデフォルトで設定や実装が組み込まれている、ERC721PresetMinterPauserAutoIdというPresetsがあるようなので、それを使用します。

OpenZeppelinのnpmパッケージをインストールします。

npm install -D @openzeppelin/contracts

contracts/contracts/NFT.solファイルを以下の内容で新規作成します。

contracts/contracts/NFT.sol
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol";
contract NFT is ERC721PresetMinterPauserAutoId {
  constructor() ERC721PresetMinterPauserAutoId("NFT Survey Proto", "NFTSP", "https://asia-northeast1-nft-survey.cloudfunctions.net/api/v1/tokens/") {}
}

コントラクトの詳細は、OpenZeppelinのERC721のPresetsのERC721PresetMinterPauserAutoIdままなので、詳細はリンク先の説明を参照頂ければと思いますが、要点としては、ERC721の規格で定められている、name, symbol, baseTokenURIを以下の固定値として設定することで、

  • name: "NFT Survey Proto"
  • symbol: "NFTSP"
  • baseTokenURI: "https://asia-northeast1-nft-survey.cloudfunctions.net/api/v1/tokens/"

このコントラクトをデプロイ後に新たにトークンが発行されるごとに、コントラクトが内部的に保持している各トークン毎のidのような意味合いのtokenIdが0から順に自動的にインクリメントされてトークンが作成されていき、各トークン毎に${baseTokenURI}${tokenId}の値でコントラクトが内部的に保持している各トークン毎のトークン詳細のJSONレスポンスを返すエンドポイントURLのような意味合いのtokenURIが自動的にセットされていきます。

具体的には、今回のコントラクトでは、以下のようにトークンが発行されていくことになるわけです。(これらすべてのトークンにおいて、name = "NFT Survey Proto", symbol = "NFTSP"となります。)

  1. 初回トークン発行時には、tokenId = 0, tokenURI = https://asia-northeast1-nft-survey.cloudfunctions.net/api/v1/tokens/0 のトークンが発行され、
  2. 次のトークン発行時には、tokenId = 1, tokenURI = https://asia-northeast1-nft-survey.cloudfunctions.net/api/v1/tokens/1 のトークンが発行され、
  3. ...以降も同様にtokenIdとtokenURIの末尾の数字が順に1ずつ増えていくのが繰り返されていく

実際にtokenURIのエンドポイントを叩いて得られるJSONレスポンスは、以下のような形で実装してみました。

JSONレスポンス実装例
{
  "description": "This is test NFT Description for test only.",
  "external_url": "https://nft-survey.web.app/tokens/0",
  "image": "https://next-web-technology.net/assets/common/logo-image/logo-transparent.png",
  "name": "Test NFT Name"
}

このJSONの中に、NFTの肝となる画像等のファイルのリンクが記述されているという形になります。

OpenZeppelinのERC721のPresetsのERC721PresetMinterPauserAutoIdでは、様々なコントラクト内関数がInterfaceとして定義されており、一例として以下のような関数が使用可能です。

  • ブロックチェーン上の状態を変化させるもの ... トランザクションの発行が必要なもの
    • mint(toAddress) ... トークンを発行し、トークンの所有者をtoAddressのアドレスのアカウントに設定
    • transferFrom(fromAddress, toAddress, tokenId) ... tokenIdのトークンをfromAddressの現所有者からtoAddressのアドレスのアカウントに所有者を変更
    • burn(tokenId) ... tokenIDのトークンを消す
  • ブロックチェーン上の状態を参照するもの ... トランザクションの発行は不要なもの
    • name() ... トークンのnameを取得
    • symbol() ... トークンのsymbolを取得
    • totalSupply() ... トークンの発行総数を取得。burnするとこの数字も減る。したがって、totalSupply - 1より大きな数のtokenIdが発行されている状態もあり得ることに注意が必要そう。
    • ownerOf(tokenId) ... tokenIdのトークンの所有者のアドレスを取得
    • tokenURI(tokenId) ... tokenURIを取得。
    • balanceOf(address) ... addressのアドレスのアカウントのトークン保有数を取得

他にも、トークンの流通を強制的に止めたり、流通を再び許可したり、保有しているトークンを管理者的アカウントが代わりに送信することを許可する設定に最初にしておいて、管理者が代わりに送信するといったことも可能なようですが、いったんそれらは保留にして、上記にリストアップした機能のテストを実装してみます。

contracts/test/nft-test.jsファイルを以下のような内容で作成します。

contracts/test/nft-test.js
const { expect } = require("chai");

describe("NFT", async function () {
  it("should be able to mint, transferFrom, burn. And it should return appropriate name, symbol, totalSupply, tokenURI, ownerOf, balanceOf", async function () {
    const [signer, badSigner] = await ethers.getSigners();
    const NFT = await ethers.getContractFactory("NFT");
    const nft = await NFT.deploy();
    await nft.deployed();
    console.log(`nft deploy tx hash: ${nft.deployTransaction.hash}`);
    console.log(`greeter contract address: ${nft.address}`);

    // before initial minting
    expect(await nft.name()).to.equal("NFT Survey Proto");
    expect(await nft.symbol()).to.equal("NFTSP");
    expect(await nft.totalSupply()).to.equal(0);

    // mint tokenId = 0
    const mint0Tx = await nft.connect(signer).mint(signer.address);
    await mint0Tx.wait();
    console.log(`mint 0 tx hash: ${mint0Tx.hash}`);

    // Assertion for token(tokenId = 0)
    expect(await nft.totalSupply()).to.equal(1);
    expect(await nft.tokenURI(0)).to.equal("https://asia-northeast1-nft-survey.cloudfunctions.net/api/v1/tokens/0")
    expect(await nft.ownerOf(0)).to.equal(signer.address);
    expect(await nft.balanceOf(signer.address)).to.equal(1);

    // mint tokenId = 1
    const mint1Tx = await nft.connect(signer).mint(signer.address);
    await mint1Tx.wait();
    console.log(`mint 1 tx hash: ${mint1Tx.hash}`);

    // Assertion for token(tokenId = 1) and contract state
    expect(await nft.totalSupply()).to.equal(2);
    expect(await nft.tokenURI(1)).to.equal("https://asia-northeast1-nft-survey.cloudfunctions.net/api/v1/tokens/1")
    expect(await nft.ownerOf(1)).to.equal(signer.address);
    expect(await nft.balanceOf(signer.address)).to.equal(2);

    // transfer token(tokenId = 1) from signer.address to badSigner.address
    const transfer1FromSignerToAddressTx = await nft.connect(signer).transferFrom(signer.address, badSigner.address, 1);
    await transfer1FromSignerToAddressTx.wait();
    console.log(`transfer1FromSignerToAddressTx tx hash: ${transfer1FromSignerToAddressTx.hash}`);

    // Assertion for transferred token(tokenId = 1)
    expect(await nft.totalSupply()).to.equal(2);
    expect((await nft.ownerOf(1))).to.equal(badSigner.address);
    expect(await nft.balanceOf(signer.address)).to.equal(1);
    expect(await nft.balanceOf(badSigner.address)).to.equal(1);

    // burn token(tokenId = 0)
    const burn0Tx = await nft.burn(0);
    await burn0Tx.wait();
    console.log(`burn0 tx hash: ${burn0Tx.hash}`);

    // Assertion for burned token(tokenId = 0)
    expect(await nft.totalSupply()).to.equal(1);
    expect(nft.ownerOf(0)).to.revertedWith("ERC721: owner query for nonexistent token");
    expect(nft.tokenURI(0)).to.revertedWith("ERC721Metadata: URI query for nonexistent token");
    expect(await nft.balanceOf(signer.address)).to.equal(0);

    // mint token(tokenId = 2)
    const mint2Tx = await nft.mint(badSigner.address);
    await mint2Tx.wait();
    console.log(`mint 2 tx hash: ${mint2Tx.hash}`);

    // Assertion for re-minted token(tokenId = 0)
    expect(await nft.totalSupply()).to.equal(2);
    expect(await nft.ownerOf(2)).to.equal(badSigner.address);
    expect(await nft.tokenURI(2)).to.equal("https://asia-northeast1-nft-survey.cloudfunctions.net/api/v1/tokens/2");
    expect(await nft.balanceOf(badSigner.address)).to.equal(2);

    // transfer token(tokenId = 2) from badSigner.address to signer.address
    const transfer2FromBadSignerToSignerAddressTx = await nft.connect(badSigner).transferFrom(badSigner.address, signer.address, 2);
    await transfer2FromBadSignerToSignerAddressTx.wait();
    console.log(`transfer2FromBadSignerToSignerAddress tx hash: ${transfer2FromBadSignerToSignerAddressTx.hash}`);

    // Assertion for transferred token(tokenId = 2)
    expect(await nft.totalSupply()).to.equal(2);
    expect(await nft.ownerOf(2)).to.equal(signer.address);
    expect(await nft.balanceOf(signer.address)).to.equal(1);
    expect(await nft.balanceOf(badSigner.address)).to.equal(1);

    // Assertion fail to mint with badSigner who has not minter role
    expect(nft.connect(badSigner).mint(signer.address)).to.revertedWith("ERC721PresetMinterPauserAutoId: must have minter role to mint");
  });
});

npx hardhat test test/nft-test.jsコマンドでコントラクトのテストを実行します。

$ npx hardhat test test/nft-test.js

NFT
nft deploy tx hash: 0xfea1bca25d5256c62b8c24c53df310b241e992c30ffdd3d642b23b5eca20064b
greeter contract address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
mint 0 tx hash: 0xc9f4a490423984d5188c6921afd50668b5c394b460b4b597986019df7b3f536a
mint 1 tx hash: 0x47ecfdddd4784646f4f0ad53778ac3991e36d1fff0d598360468036441a3b587
transfer1FromSignerToAddressTx tx hash: 0x37aa01d2955764aaa7166479fe03568c605a91c24087aa51e6005d80948cdcab
burn0 tx hash: 0xe01e511303c4576a4e6cfff681fa54e33abafe4f77582e0977dc57ffb98fe112
mint 2 tx hash: 0x5f853e216f58e89af79b9f520bec7152dd5b16a0501ad754e549a9db2d750ae0
transfer2FromBadSignerToSignerAddress tx hash: 0x60825385eaef4dde9ddf7857bfcb01766b31ed4945f09e5a4a71092124d38c88
    ✓ should be able to mint, transferFrom, burn. And it should return name, symbol, totalSupply, tokenURI, ownerOf, balanceOf (684ms)

1 passing (690ms)

コントラクトのテストpassしました!🎉

同じテストコードで、ローカル開発用ネットワークを起動(npx hardhat node)した上で、明示的にローカル開発用ネットワークを指定して同様にテスト(npx hardhat test test/nft-test.js --network localhost)を行います。

$ npx hardhat test test/nft-test.js --network localhost

NFT
nft deploy tx hash: 0xb7e5a54f25feeae2571134a5aede8f52374a821acb89c1f1616521d1d0fb562e
greeter contract address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
mint 0 tx hash: 0x18f83480b49468aacb02f0564e3e45cd11829cbf3b7dd4de9c7dfa8c830379a6
mint 1 tx hash: 0x1aaf07d36fa679b82d8f510b3aa7eb352bcda441b8a8732d39deeeccabf674c1
transfer1FromSignerToAddressTx tx hash: 0xf878455e816915652529e748d5135cf129bf77dd03473b59844d4aebd09b23bf
burn0 tx hash: 0x8289aa943300dbccc86720c681a469fa9b88602a8fecdaca65d82706ece42159
mint 2 tx hash: 0x8526405e39a9d7fc8619703fdf6b4f4ecf36603c9b18490d0edfa7301e904dfc
transfer2FromBadSignerToSignerAddress tx hash: 0xfbed632ff768362978412945ef0d1f7e909a09b3241f4c6ba0a5778738bcda96
    ✓ should be able to mint, transferFrom, burn. And it should return name, symbol, totalSupply, tokenURI, ownerOf, balanceOf (946ms)

1 passing (948ms)

明示的にローカル開発用ネットワークを指定して行ったテストも同様にpassしました!🎉

ローカル開発用ネットワークのノードのログはかなり長くなったので折り畳み表示しておきました。興味ある方はご覧になってみてみると雰囲気が感じ取れて面白いかもしれません。

ローカル開発用ネットワークのノードのログ
$ npx hardhat node

Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========
Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Account #1: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

Account #2: 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc (10000 ETH)
Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a

Account #3: 0x90f79bf6eb2c4f870365e785982e1f101e93b906 (10000 ETH)
Private Key: 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6

Account #4: 0x15d34aaf54267db7d7c367839aaf71a00a2c6a65 (10000 ETH)
Private Key: 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a

Account #5: 0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc (10000 ETH)
Private Key: 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba

Account #6: 0x976ea74026e726554db657fa54763abd0c3a0aa9 (10000 ETH)
Private Key: 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e

Account #7: 0x14dc79964da2c08b23698b3d3cc7ca32193d9955 (10000 ETH)
Private Key: 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356

Account #8: 0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f (10000 ETH)
Private Key: 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97

Account #9: 0xa0ee7a142d267c1f36714e4a8f75612f20a79720 (10000 ETH)
Private Key: 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6

Account #10: 0xbcd4042de499d14e55001ccbb24a551f3b954096 (10000 ETH)
Private Key: 0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897

Account #11: 0x71be63f3384f5fb98995898a86b02fb2426c5788 (10000 ETH)
Private Key: 0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82

Account #12: 0xfabb0ac9d68b0b445fb7357272ff202c5651694a (10000 ETH)
Private Key: 0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1

Account #13: 0x1cbd3b2770909d4e10f157cabc84c7264073c9ec (10000 ETH)
Private Key: 0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd

Account #14: 0xdf3e18d64bc6a983f673ab319ccae4f1a57c7097 (10000 ETH)
Private Key: 0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa

Account #15: 0xcd3b766ccdd6ae721141f452c550ca635964ce71 (10000 ETH)
Private Key: 0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61

Account #16: 0x2546bcd3c84621e976d8185a91a922ae77ecec30 (10000 ETH)
Private Key: 0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0

Account #17: 0xbda5747bfd65f08deb54cb465eb87d40e51b197e (10000 ETH)
Private Key: 0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd

Account #18: 0xdd2fd4581271e230360230f9337d5c0430bf44c0 (10000 ETH)
Private Key: 0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0

Account #19: 0x8626f6940e2eb28930efb4cef49b2d1f2c9c1199 (10000 ETH)
Private Key: 0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e

web3_clientVersion (2)
eth_accounts
eth_chainId
eth_accounts
eth_blockNumber
eth_chainId (2)
eth_estimateGas
eth_feeHistory
eth_sendTransaction
Contract deployment: NFT
Contract address:    0x5fbdb2315678afecb367f032d93f642f64180aa3
Transaction:         0xb7e5a54f25feeae2571134a5aede8f52374a821acb89c1f1616521d1d0fb562e
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
Value:               0 ETH
Gas used:            4529406 of 4529406
Block #1:            0xd91c3b98c723532cb495230e95ba5c5c6cc67d5d2cc15b9370bb5c3dc3e3f8ca

eth_chainId
eth_getTransactionByHash
eth_chainId
eth_getTransactionReceipt
eth_chainId
eth_call
Contract call:       NFT#name
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#symbol
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#totalSupply
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_getTransactionReceipt
eth_chainId
eth_estimateGas
eth_feeHistory
eth_sendTransaction
Contract call:       NFT#mint
Transaction:         0x18f83480b49468aacb02f0564e3e45cd11829cbf3b7dd4de9c7dfa8c830379a6
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3
Value:               0 ETH
Gas used:            127944 of 127944
Block #2:            0xb3cd4e662f481156f09e604e7e97151d236de0c3901eaa3e0daab889f2371927

eth_chainId
eth_getTransactionByHash
eth_chainId
eth_getTransactionReceipt
eth_chainId
eth_call
Contract call:       NFT#totalSupply
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#tokenURI
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#ownerOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#balanceOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_getTransactionReceipt
eth_chainId
eth_estimateGas
eth_feeHistory
eth_sendTransaction
Contract call:       NFT#mint
Transaction:         0x1aaf07d36fa679b82d8f510b3aa7eb352bcda441b8a8732d39deeeccabf674c1
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3
Value:               0 ETH
Gas used:            156244 of 156244
Block #3:            0x9defea4a338ddcdcab54202cc9efdec131a3417af7f32f1db5c968b5f8b2086c

eth_chainId
eth_getTransactionByHash
eth_chainId
eth_getTransactionReceipt
eth_chainId
eth_call
Contract call:       NFT#totalSupply
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#tokenURI
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#ownerOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#balanceOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_getTransactionReceipt
eth_chainId
eth_estimateGas
eth_feeHistory
eth_sendTransaction
Contract call:       NFT#transferFrom
Transaction:         0xf878455e816915652529e748d5135cf129bf77dd03473b59844d4aebd09b23bf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3
Value:               0 ETH
Gas used:            90029 of 99875
Block #4:            0xa2068a02e121b2e72ee3dccc3adf28809d143fc7bdf6aea238bf5870689fbd33

eth_chainId
eth_getTransactionByHash
eth_chainId
eth_getTransactionReceipt
eth_chainId
eth_call
Contract call:       NFT#totalSupply
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#ownerOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#balanceOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#balanceOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_estimateGas
eth_feeHistory
eth_sendTransaction
Contract call:       NFT#burn
Transaction:         0x8289aa943300dbccc86720c681a469fa9b88602a8fecdaca65d82706ece42159
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3
Value:               0 ETH
Gas used:            70312 of 87890
Block #5:            0x152a04dcfa56801f05b6f12794f2a7b3183b6a716d74d0b97861e2b37193f06f

eth_chainId
eth_getTransactionByHash
eth_chainId
eth_getTransactionReceipt
eth_chainId
eth_call
Contract call:       NFT#totalSupply
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#ownerOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

Error: VM Exception while processing transaction: reverted with reason string 'ERC721: owner query for nonexistent token'
    at NFT.ownerOf (@openzeppelin/contracts/token/ERC721/ERC721.sol:71)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
    at HardhatNode.runCall (/home/yasunori_matsuoka/github.com/next-web-technology/nft-erc721-sample/contracts/node_modules/hardhat/src/internal/hardhat-network/provider/node.ts:510:20)
    at EthModule._callAction (/home/yasunori_matsuoka/github.com/next-web-technology/nft-erc721-sample/contracts/node_modules/hardhat/src/internal/hardhat-network/provider/modules/eth.ts:353:9)
    at HardhatNetworkProvider._sendWithLogging (/home/yasunori_matsuoka/github.com/next-web-technology/nft-erc721-sample/contracts/node_modules/hardhat/src/internal/hardhat-network/provider/provider.ts:127:22)
    at HardhatNetworkProvider.request (/home/yasunori_matsuoka/github.com/next-web-technology/nft-erc721-sample/contracts/node_modules/hardhat/src/internal/hardhat-network/provider/provider.ts:104:18)
    at JsonRpcHandler._handleRequest (/home/yasunori_matsuoka/github.com/next-web-technology/nft-erc721-sample/contracts/node_modules/hardhat/src/internal/hardhat-network/jsonrpc/handler.ts:188:20)
    at JsonRpcHandler._handleSingleRequest (/home/yasunori_matsuoka/github.com/next-web-technology/nft-erc721-sample/contracts/node_modules/hardhat/src/internal/hardhat-network/jsonrpc/handler.ts:167:17)

eth_call
Contract call:       NFT#tokenURI
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

Error: VM Exception while processing transaction: reverted with reason string 'ERC721Metadata: URI query for nonexistent token'
    at NFT.tokenURI (@openzeppelin/contracts/token/ERC721/ERC721.sol:93)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
    at HardhatNode.runCall (/home/yasunori_matsuoka/github.com/next-web-technology/nft-erc721-sample/contracts/node_modules/hardhat/src/internal/hardhat-network/provider/node.ts:510:20)
    at EthModule._callAction (/home/yasunori_matsuoka/github.com/next-web-technology/nft-erc721-sample/contracts/node_modules/hardhat/src/internal/hardhat-network/provider/modules/eth.ts:353:9)
    at HardhatNetworkProvider._sendWithLogging (/home/yasunori_matsuoka/github.com/next-web-technology/nft-erc721-sample/contracts/node_modules/hardhat/src/internal/hardhat-network/provider/provider.ts:127:22)
    at HardhatNetworkProvider.request (/home/yasunori_matsuoka/github.com/next-web-technology/nft-erc721-sample/contracts/node_modules/hardhat/src/internal/hardhat-network/provider/provider.ts:104:18)
    at JsonRpcHandler._handleRequest (/home/yasunori_matsuoka/github.com/next-web-technology/nft-erc721-sample/contracts/node_modules/hardhat/src/internal/hardhat-network/jsonrpc/handler.ts:188:20)
    at JsonRpcHandler._handleSingleRequest (/home/yasunori_matsuoka/github.com/next-web-technology/nft-erc721-sample/contracts/node_modules/hardhat/src/internal/hardhat-network/jsonrpc/handler.ts:167:17)

eth_call
Contract call:       NFT#balanceOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_estimateGas
eth_feeHistory
eth_sendTransaction
Contract call:       NFT#mint
Transaction:         0x8526405e39a9d7fc8619703fdf6b4f4ecf36603c9b18490d0edfa7301e904dfc
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3
Value:               0 ETH
Gas used:            156244 of 156244
Block #6:            0x791ac2bbfa13a4741d6bfe7972a57283500c09793fb7f010d60f5758dc94f6cf

eth_chainId
eth_getTransactionByHash
eth_chainId
eth_getTransactionReceipt
eth_chainId
eth_call
Contract call:       NFT#totalSupply
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#ownerOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#tokenURI
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#balanceOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_getTransactionReceipt
eth_chainId
eth_estimateGas
eth_feeHistory
eth_sendTransaction
Contract call:       NFT#transferFrom
Transaction:         0xfbed632ff768362978412945ef0d1f7e909a09b3241f4c6ba0a5778738bcda96
From:                0x70997970c51812dc3a010c7d01b50e0d17dc79c8
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3
Value:               0 ETH
Gas used:            90029 of 99875
Block #7:            0xd72195c202b9e289c66c68fc3d61b8a9166c8b7064fe9069531f240b4b5f251b

eth_chainId
eth_getTransactionByHash
eth_chainId
eth_getTransactionReceipt
eth_chainId
eth_call
Contract call:       NFT#totalSupply
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#ownerOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#balanceOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

eth_chainId
eth_call
Contract call:       NFT#balanceOf
From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To:                  0x5fbdb2315678afecb367f032d93f642f64180aa3

ローカルでのコントラクトのテストがある程度できたと思うので、いよいよテストネットへのコントラクトのデプロイやテストを行います。

既に環境変数PRIVATE_KEYのアカウントでGoerliテストネットで利用可能なテスト用のETHの残高のあるアカウントが準備できていますが、もう一個別のアカウントからのtransferFromもテストに含まれているので、もう一個別のアカウントを作成し、テスト用のETHの残高を少し送っておき、以下のような環境変数にそのアカウントの秘密鍵を設定し、contracts/hardhat.config.jsファイルに以下のように追記します。

  • SECOND_PRIVATE_KEY
contracts/hardhat.config.js
require("@nomiclabs/hardhat-etherscan");
require("@nomiclabs/hardhat-waffle");

// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();
  
  for (const account of accounts) {
    console.log(account.address);
  }
});

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

/**
 * @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
  networks: {
    goerli: {
      url: `https://goerli.infura.io/v3/${process.env.INFURA_PROJECT_ID}`,
      accounts: [process.env.PRIVATE_KEY, process.env.SECOND_PRIVATE_KEY] //この行追記
    }
  },
  solidity: "0.8.4",
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_TOKEN
  },
};

また、テストコードcontracts/test/nft-test.jsでローカル開発用ネットワークと異なりトランザクションの承認に時間がかかることを踏まえ、テストのタイムアウトを明示的に10分に設定し、各トランザクションの確認を1confirmedではなく、2confirmedで行うようにし、トランザクションを発行する箇所に、トランザクションの状態を確認できるよう、Etherscanのリンクをログ出力しておきます。結果としてテストコードに以下の差分のような変更を加えることになります。

contracts/test/nft-test.js
 const { expect } = require("chai");
 
 describe("NFT", async function () {
+  this.timeout(600 * 1000);
   it("should be able to mint, transferFrom, burn. And it should return appropriate name, symbol, totalSupply, tokenURI, ownerOf, balanceOf", async function () {
     const [signer, badSigner] = await ethers.getSigners();
     const NFT = await ethers.getContractFactory("NFT");
     const nft = await NFT.deploy();
     await nft.deployed();
-    console.log(`nft deploy tx hash: ${nft.deployTransaction.hash}`);
+    console.log(`nft deploy tx: https://goerli.etherscan.io/tx/${nft.deployTransaction.hash}`);
     console.log(`greeter contract address: ${nft.address}`);
 
     // before initial minting
     expect(await nft.name()).to.equal("NFT Survey Proto");
     expect(await nft.symbol()).to.equal("NFTSP");
     expect(await nft.totalSupply()).to.equal(0);
 
     // mint tokenId = 0
     const mint0Tx = await nft.connect(signer).mint(signer.address);
-    await mint0Tx.wait();
-    console.log(`mint 0 tx hash: ${mint0Tx.hash}`);
+    await mint0Tx.wait([confirms = 2]);
+    console.log(`mint 0 tx: https://goerli.etherscan.io/tx/${mint0Tx.hash}`);
 
     // Assertion for token(tokenId = 0)
     expect(await nft.totalSupply()).to.equal(1);
     expect(await nft.tokenURI(0)).to.equal("https://asia-northeast1-nft-survey.cloudfunctions.net/api/v1/tokens/0")
     expect(await nft.ownerOf(0)).to.equal(signer.address);
     expect(await nft.balanceOf(signer.address)).to.equal(1);
 
     // mint tokenId = 1
     const mint1Tx = await nft.connect(signer).mint(signer.address);
-    await mint1Tx.wait();
-    console.log(`mint 1 tx hash: ${mint1Tx.hash}`);
+    await mint1Tx.wait([confirms = 2]);
+    console.log(`mint 1 tx: https://goerli.etherscan.io/tx/${mint1Tx.hash}`);
 
     // Assertion for token(tokenId = 1) and contract state
     expect(await nft.totalSupply()).to.equal(2);
     expect(await nft.tokenURI(1)).to.equal("https://asia-northeast1-nft-survey.cloudfunctions.net/api/v1/tokens/1")
     expect(await nft.ownerOf(1)).to.equal(signer.address);
     expect(await nft.balanceOf(signer.address)).to.equal(2);
 
     // transfer token(tokenId = 1) from signer.address to badSigner.address
     const transfer1FromSignerToAddressTx = await nft.connect(signer).transferFrom(signer.address, badSigner.address, 1);
-    await transfer1FromSignerToAddressTx.wait();
-    console.log(`transfer1FromSignerToAddressTx tx hash: ${transfer1FromSignerToAddressTx.hash}`);
+    await transfer1FromSignerToAddressTx.wait([confirms = 2]);
+    console.log(`transfer1FromSignerToAddress tx: https://goerli.etherscan.io/tx/${transfer1FromSignerToAddressTx.hash}`);
 
     // Assertion for transferred token(tokenId = 1)
     expect(await nft.totalSupply()).to.equal(2);
     expect((await nft.ownerOf(1))).to.equal(badSigner.address);
     expect(await nft.balanceOf(signer.address)).to.equal(1);
     expect(await nft.balanceOf(badSigner.address)).to.equal(1);
 
     // burn token(tokenId = 0)
     const burn0Tx = await nft.burn(0);
-    await burn0Tx.wait();
-    console.log(`burn0 tx hash: ${burn0Tx.hash}`);
+    await burn0Tx.wait([confirms = 2]);
+    console.log(`burn0 tx: https://goerli.etherscan.io/tx/${burn0Tx.hash}`);
 
     // Assertion for burned token(tokenId = 0)
     expect(await nft.totalSupply()).to.equal(1);
     expect(nft.ownerOf(0)).to.revertedWith("ERC721: owner query for nonexistent token");
     expect(nft.tokenURI(0)).to.revertedWith("ERC721Metadata: URI query for nonexistent token");
     expect(await nft.balanceOf(signer.address)).to.equal(0);
 
     // mint token(tokenId = 2)
     const mint2Tx = await nft.mint(badSigner.address);
-    await mint2Tx.wait();
-    console.log(`mint 2 tx hash: ${mint2Tx.hash}`);
+    await mint2Tx.wait([confirms = 2]);
+    console.log(`mint 2 tx: https://goerli.etherscan.io/tx/${mint2Tx.hash}`);
 
     // Assertion for re-minted token(tokenId = 0)
     expect(await nft.totalSupply()).to.equal(2);
     expect(await nft.ownerOf(2)).to.equal(badSigner.address);
     expect(await nft.tokenURI(2)).to.equal("https://asia-northeast1-nft-survey.cloudfunctions.net/api/v1/tokens/2");
     expect(await nft.balanceOf(badSigner.address)).to.equal(2);
 
     // transfer token(tokenId = 2) from badSigner.address to signer.address
     const transfer2FromBadSignerToSignerAddressTx = await nft.connect(badSigner).transferFrom(badSigner.address, signer.address, 2);
-    await transfer2FromBadSignerToSignerAddressTx.wait();
-    console.log(`transfer2FromBadSignerToSignerAddress tx hash: ${transfer2FromBadSignerToSignerAddressTx.hash}`);
+    await transfer2FromBadSignerToSignerAddressTx.wait([confirms = 2]);
+    console.log(`transfer2FromBadSignerToSignerAddress tx: https://goerli.etherscan.io/tx/${transfer2FromBadSignerToSignerAddressTx.hash}`);
 
     // Assertion for transferred token(tokenId = 2)
     expect(await nft.totalSupply()).to.equal(2);
     expect(await nft.ownerOf(2)).to.equal(signer.address);
     expect(await nft.balanceOf(signer.address)).to.equal(1);
     expect(await nft.balanceOf(badSigner.address)).to.equal(1);
 
     // Assertion fail to mint with badSigner who has not minter role
     expect(nft.connect(badSigner).mint(signer.address)).to.revertedWith("ERC721PresetMinterPauserAutoId: must have minter role to mint");
   });
 });

npx hardhat test test/nft-test.js --network goerliコマンドでこのテストコードを実行し、

$ npx hardhat test test/nft-test.js --network goerli

NFT
nft deploy tx: https://goerli.etherscan.io/tx/0xd79f2733e6bd276fa77b9f1ac189165dc4152eabc283bac525a3bd46d67104bc
greeter contract address: 0x506A7AfdeE85D3432f6caE1767cac8a5C228f5E2
mint 0 tx: https://goerli.etherscan.io/tx/0x6d2db9bbbdda147f29dc95d3efe5c076654049be554ec5fa283788b1d68a2e78
mint 1 tx: https://goerli.etherscan.io/tx/0x311d098eec8983a5b9490f782c7abfa581beca2e606f01a52d9a70ec2e5a7cf1
transfer1FromSignerToAddress tx: https://goerli.etherscan.io/tx/0xfeeb8938ede851699b895d42685354fc69fa8530c0897b601c5e792d220ddac7
burn0 tx: https://goerli.etherscan.io/tx/0xbf4d4e3a4f80dcd506351dba7c4e1da60e910593f3622f73fcb2f1da76afdc84
mint 2 tx: https://goerli.etherscan.io/tx/0x4ecab1f4df0653bb591d70c16d3d6c87fcaf84e31e2b9fdd2f000a5cac9263ae
transfer2FromBadSignerToSignerAddress tx: https://goerli.etherscan.io/tx/0x39607b8c59df837498250a0db53b2eb1b8fc0d095277128d29487baf6c62d93f
    ✓ should be able to mint, transferFrom, burn. And it should return name, symbol, totalSupply, tokenURI, ownerOf, balanceOf (234037ms)

1 passing (4m)

テストpassすることが確認できました!🎉

最後に、hardhatに依存せずに、Node.jsで同様の実装の動作検証を行います。テストコードの中で実行しているような細かい状態の確認は省略して、一通りの関数が実行されるよう、同様のトランザクションを発行して、いくつか状態を確認するスクリプトをcontracts/scripts/nft-script-without-hardhat.jsファイルに作成します。

contracts/scripts/nft-script-without-hardhat.js
const { ethers } = require("ethers");
const contractJsonData = require("../artifacts/contracts/NFT.sol/NFT.json");

async function main () {
  const provider = ethers.getDefaultProvider(
    "goerli",
    {
      infura: process.env.INFURA_PROJECT_ID,
      alchemy: process.env.ALCHEMY_API_TOKEN,
      etherscan: process.env.ETHERSCAN_API_TOKEN,
    }
  );
  const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
  const NFT = new ethers.ContractFactory(contractJsonData.abi, contractJsonData.bytecode, wallet);
  const nft = await NFT.deploy();
  await nft.deployTransaction.wait([confirms = 2]);
  console.log(`nft deploy tx: https://goerli.etherscan.io/tx/${nft.deployTransaction.hash}`);
  console.log(`greeter contract address: ${nft.address}`);
  
  const contractAddress = nft.address;
  const deployedNftContract = new ethers.Contract(contractAddress, contractJsonData.abi, provider);
  
  // before initial minting
  console.log("name() = NFT Survey Proto?", await deployedNftContract.name());
  console.log("symbol() = NFTSP?", await deployedNftContract.symbol());
  console.log("totalSupply() = 0?", await deployedNftContract.totalSupply());
  
  // mint tokenId = 0
  const mint0Tx = await deployedNftContract.connect(wallet).mint(await wallet.getAddress());
  await mint0Tx.wait([confirms = 2]);
  console.log(`mint 0 tx: https://goerli.etherscan.io/tx/${mint0Tx.hash}`);
  
  // Assertion for token(tokenId = 0)
  console.log("totalSupply() = 1?", await deployedNftContract.totalSupply());
  console.log("tokenURI(0) = https://asia-northeast1-nft-survey.cloudfunctions.net/api/v1/tokens/0 ?", await deployedNftContract.tokenURI(0));
  console.log("ownerOf(0) = ↓", await deployedNftContract.ownerOf(0));
  console.log("wallet.getAddress() = ↑", await wallet.getAddress());
  console.log("balanceOf(await wallet.getAddress()) = 1?", await deployedNftContract.balanceOf(await wallet.getAddress()));
  
  // mint tokenId = 1
  const mint1Tx = await deployedNftContract.connect(wallet).mint(await wallet.getAddress());
  await mint1Tx.wait([confirms = 2]);
  console.log(`mint 1 tx: https://goerli.etherscan.io/tx/${mint1Tx.hash}`);
  
  // transfer token(tokenId = 1) from await wallet.getAddress() to await secondWallet.getAddress()
  const secondWallet = new ethers.Wallet(process.env.SECOND_PRIVATE_KEY, provider);
  const transfer1FromWalletToSecondWalletTx = await deployedNftContract.connect(wallet).transferFrom(await wallet.getAddress(), await secondWallet.getAddress(), 1);
  await transfer1FromWalletToSecondWalletTx.wait([confirms = 2]);
  console.log(`transfer1FromWalletToSecondWalletTx tx: https://goerli.etherscan.io/tx/${transfer1FromWalletToSecondWalletTx.hash}`);
  
  // burn token(tokenId = 0)
  const burn0Tx = await deployedNftContract.connect(wallet).burn(0);
  await burn0Tx.wait([confirms = 2]);
  console.log(`burn0 tx: https://goerli.etherscan.io/tx/${burn0Tx.hash}`);
  
  // mint token(tokenId = 2)
  const mint2Tx = await deployedNftContract.connect(wallet).mint(await secondWallet.getAddress());
  await mint2Tx.wait([confirms = 2]);
  console.log(`mint 2 tx: https://goerli.etherscan.io/tx/${mint2Tx.hash}`);
  
  // transfer token(tokenId = 2) from await secondWallet.getAddress() to await wallet.getAddress()
  const transfer2FromSecondWalletToWalletTx = await deployedNftContract.connect(secondWallet).transferFrom(await secondWallet.getAddress(), await wallet.getAddress(), 2);
  await transfer2FromSecondWalletToWalletTx.wait([confirms = 2]);
  console.log(`transfer2FromSecondWalletToWalletTx tx: https://goerli.etherscan.io/tx/${transfer2FromSecondWalletToWalletTx.hash}`);
}

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

node scripts/nft-script-without-hardhat.jsコマンドを実行すると、

$ node scripts/nft-script-without-hardhat.js 

nft deploy tx: https://goerli.etherscan.io/tx/0x4416b479b06b4711c027af9b6461352e24c35fc9804c75daabffb300393bd6d7
greeter contract address: 0x043f991806a2363016e4341e1C25a35d6C29Fe40
name() = NFT Survey Proto? NFT Survey Proto
symbol() = NFTSP? NFTSP
totalSupply() = 0? BigNumber { _hex: '0x00', _isBigNumber: true }
mint 0 tx: https://goerli.etherscan.io/tx/0x748321d464f6ba4bd3a2f4b6e61eff02f57ad507c36b9dbd997863f55bc1e4f0
totalSupply() = 1? BigNumber { _hex: '0x01', _isBigNumber: true }
tokenURI(0) = https://asia-northeast1-nft-survey.cloudfunctions.net/api/v1/tokens/0 ? https://asia-northeast1-nft-survey.cloudfunctions.net/api/v1/tokens/0
ownerOf(0) = ↓ 0xE6F12305713cea9cf70ef4B351E2aC377425f7Da
wallet.getAddress() = ↑ 0xE6F12305713cea9cf70ef4B351E2aC377425f7Da
balanceOf(await wallet.getAddress()) = 1? BigNumber { _hex: '0x01', _isBigNumber: true }
mint 1 tx: https://goerli.etherscan.io/tx/0xd4712c9d79addeae8d80d79247fa4974272134fa54d4ca402cc14ecb188caa31
transfer1FromWalletToSecondWalletTx tx: https://goerli.etherscan.io/tx/0xb2286f96ab0ec0768d7d531bc28091a694039cda802d1f4b2b78cdc8aefb0654
burn0 tx: https://goerli.etherscan.io/tx/0x2df56cd958927523557153f3082b357fecc6f430afa92023c54dd4c6a169cb9b
mint 2 tx: https://goerli.etherscan.io/tx/0x369c2f732c735a98426840393c61c4fae291c11509775f8e4f07282ea14c9d35
transfer2FromSecondWalletToWalletTx tx: https://goerli.etherscan.io/tx/0xd15dc3de1230486995a3ad46947409e27be926f9a971051648264ef03fb85164

このような結果が表示され、意図通り、Goerliテストネット上でコントラクトのデプロイやコントラクトの呼び出しが実行できていることが確認できました!🎉

最終的にデプロイしたコントラクト関連のトランザクションやコントラクトの情報は以下の通りとなりました。

最後に、以下コマンドでverify and publishしておきます。

$ npx hardhat verify --contract contracts/NFT.sol:NFT --network goerli 0x043f991806a2363016e4341e1C25a35d6C29Fe40

Nothing to compile
Compiling 1 file with 0.8.4
Successfully submitted source code for contract
contracts/NFT.sol:NFT at 0x043f991806a2363016e4341e1C25a35d6C29Fe40
for verification on Etherscan. Waiting for verification result...

Successfully verified contract NFT on Etherscan.
https://goerli.etherscan.io/address/0x043f991806a2363016e4341e1C25a35d6C29Fe40#code

コントラクトのverify and publishも無事完了し、以下リンクにてコントラクトの詳細な情報を誰もが確認でき、(需要があるかどうかはさておき、)このコントラクトを(定められた可能な権限の範囲で)誰でも容易に呼び出して実行することができるようになりました!🎉

https://goerli.etherscan.io/address/0x043f991806a2363016e4341e1c25a35d6c29fe40#code

まとめ

シンプルなERC721のコントラクト開発の流れ

  • 開発の流れは、前々回記事前回記事のHello world的なコントラクトと同様で、どんなコントラクト開発でも全体的な流れは同じ
  • コントラクトを自前で実装するのは手間がかかるだけでなく、致命的な脆弱性を容易に作りこんでしまうリスクがとても高くなってしまうため、可能ならばOpenZeppelinのような雛型となるコントラクトをできるだけそのまま使用するのが望ましい
  • OpenZeppelinの4.x系列のバージョンでは、色々とデフォルトで設定や実装が組み込まれている、ERC721PresetMinterPauserAutoIdというPresetsがあるため、ERC721の開発で特別な要件が無いなら、これを使うとシンプルにコントラクトを記述できる

所感

前々回記事前回記事、本記事のように、Ethereumの開発に触れてきた中で、(これまで自分が触れてきたNEM, Symbol, Cosmos等のブロックチェーンとの違いにも着目して、)感じたことを以下にまとめてみます。

EthereumではAPIは脇役、コントラクトが主役という印象

Ethereumでも当然APIを叩いて様々な処理が行われるわけですが、APIの種類はそれほど多くなく、すべてethers等のようなSDKがラップしてくれて、開発者はコントラクトの開発と、アプリからコントラクトを呼び出して利用することに注力するのが自然という印象を感じました。NEM, Symbol等はAPIの種類が豊富で、自身がブロックチェーン上で実行したい処理によってAPIを使い分けて開発するというスタイルに自然となるので、そのスタイルとの違いを新鮮に感じました。

開発環境やツール等のエコシステムの充実

開発のいずれのステップにおいても、開発環境やツール群が整備されていて、かなり感動しました。多くの開発者がEthereum関連プロジェクトに自然と貢献し、それによって優れた開発環境やツールが増え、さらに多くの開発者がEthereum関連プロジェクトに集ってくるという非常に強い良い循環が機能しているんだろうなあ...と感じました。

Ethereumに対するイメージ

これまでは漠然と、Ethereum≒全世界分散型AWS のようなものと捉えていたのですが、(おそらくそいういう側面もなくはないと思うものの、)全世界分散型のプログラム実行空間があり、そこに誰でも自由に自分なりのClassを定義することができ、それらClassを自由にnewして、Class内の状態変数を参照したり、Class内の関数を呼び出して実行したりすることができるものなんだなあ...という印象を感じました。

規格に準拠した、ライブラリの実装ほぼそのままのようなシンプルなコントラクトであれば、開発環境やツール等のエコシステムが充実しており、ある程度容易に利用することができるという印象も受け、これまでは漠然と感じていた「非常に難しいもの」というイメージが自分の中で少し緩和された印象があります。

デフォルトの雛型的な実装から一歩足を踏み出すと

しかし、デフォルトの雛型的な実装から一歩足を踏み出して、オリジナルのコードを書き始めると、難しさが一気に体感10~100倍くらいになる印象を感じました。オリジナルなコードを書くには、かなりの勇気と追加の知識や経験と時間が必要そうという印象を感じました。

ERC721の規格は自由度が高い

ERC721以外の規格にも同じことを感じたのですが、NFT的な特徴を意図して作られたERC721の規格は、かなり緩やかで、同じ規格で作成されたERC721の間でも、開発環境のバージョン差異や、サポートする機能等によって、かなり幅が生まれそうに感じ、様々なプラットフォームにて発行されているERC721を統一的に扱うといったことを行うためには、ERC721の規格に対する正確な理解と、各プラットフォームでどのような振れ幅のある実装がされているかを正確に把握した上で、注意深くアプリ側から利用できるよう実装する必要があると感じました。

Decentralizedについて

Ethereumのコントラクトは、ブロックチェーン上にDecentralizedにデプロイされるのですが、デプロイされたコントラクトの中身としては、管理者的な特権アカウントのみが実行可能な管理者的な操作があったりして、コントラクトの中身次第ではDecentralizedな度合があまり感じられないものも当然ながら生まれるという当たり前のことに改めて気づかされました。

ブロックチェーンに刻まれたデータの活用について

Ethereumではブロックチェーンに刻まれたデータの活用は、コントラクトを介してすべて実行されることになり、コントラクトのインターフェースに定められていないデータの利用はできず、コントラクトの実装の段階では無限の自由度があるものの、いったんコントラクトを実装するとかなり強い制約が生まれてしまうと思います。

NEMやSymbolはその点に関しては、ブロックチェーンに刻まれたデータの活用をAPIを介して行いやすく、必要に応じて柔軟にブロックチェーンにデータを刻んでいき、必要な際にブロックチェーンからデータを取り出して利用するといったことが実行しやすいように感じました。

複数のブロックチェーンを触って思うこと

複数のブロックチェーンを触ってみて思うこととして、多くの場合、「どちらが優れているか」みたいな議論は不毛なものになってしまいがちです。(本当にこれはダメじゃないですか...みたいなものもあったり、結局自分自身も好き嫌いもあったりで、なかなかバランスは難しいのですが...)

自分自身、技術的な特徴やユースケースに応じた向き不向きを正確に把握して、適切な技術をその都度選択できるような開発者でありたいですし、適材適所で使われる様々なブロックチェーンがインターオペラビリティを実現できるCosmosのようなブロックチェーンを介して滑らかにつながり、その結果、様々な社会的課題を解決していけるような、そんな未来を思い描きながら、株式会社CauchyEのブロックチェーン部門(LCNEM)で様々な開発や取り組みを進めています。

もし株式会社CauchyEの取組にご興味ある方いらっしゃいましたら、どうぞお気軽にお声かけください。皆様とともによりよいサービスやより良い社会を実現していけるよう、今後ともよろしくお願いいたします。

【エンジニア採用中】
ブロックチェーンやデータサイエンスに興味のあるエンジニアを積極的に採用中です!
以下のページから応募お待ちしております。
https://cauchye.com/company/recruit

脚注
  1. https://qiita.com/mogiken/items/54bc24669068ecadf957 ... この記事を作成するのに、こちらの資料がとても参考になりました。この場を借りてお礼申し上げます。 ↩︎

  2. https://hardhat.org/ ↩︎

  3. https://openzeppelin.com/contracts/ ↩︎

  4. https://docs.ethers.io/v5/ ↩︎

Discussion