ブロックチェーンテクノロジーで今一番興味があるのはNFTです。
EthereumのERC721という規格のコントラクトでの実装が多いということで、試してみました。
以下参考にさせていただきました。
「Ethereum コントラクト開発 ERC721編」
https://zenn.dev/cauchye/articles/ethereum-contract-erc721
まずはHardHatという便利なフレームワークの動作確認とOpenZeppelinというライブラリのインストールをします。
以下生成されるファイルですが、コマンド実行にあたり内容を理解しておいた方がいい思われる3ファイルですので引用します。
contracts/Greeter.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//SPDX-License-Identifier: Unlicense pragma solidity ^0.8.0; import "hardhat/console.sol"; contract Greeter { string private greeting; constructor(string memory _greeting) { console.log("Deploying a Greeter with greeting:", _greeting); greeting = _greeting; } function greet() public view returns (string memory) { return greeting; } function setGreeting(string memory _greeting) public { console.log("Changing greeting from '%s' to '%s'", greeting, _greeting); greeting = _greeting; } } |
scripts/sample-script.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// We require the Hardhat Runtime Environment explicitly here. This is optional // but useful for running the script in a standalone fashion through `node <script>`. // // When running the script with `npx hardhat run <script>` you'll find the Hardhat // Runtime Environment's members available in the global scope. const hre = require("hardhat"); async function main() { // Hardhat always runs the compile task when running scripts with its command // line interface. // // If this script is run directly using `node` you may want to call compile // manually to make sure everything is compiled // await hre.run('compile'); // We get the contract to deploy const Greeter = await hre.ethers.getContractFactory("Greeter"); const greeter = await Greeter.deploy("Hello, Hardhat!"); await greeter.deployed(); console.log("Greeter deployed to:", greeter.address); } // We recommend this pattern to be able to use async/await everywhere // and properly handle errors. main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); |
test/sample-test.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("Greeter", function () { it("Should return the new greeting once it's changed", async function () { const Greeter = await ethers.getContractFactory("Greeter"); const greeter = await Greeter.deploy("Hello, world!"); await greeter.deployed(); expect(await greeter.greet()).to.equal("Hello, world!"); const setGreetingTx = await greeter.setGreeting("Hola, mundo!"); // wait until the transaction is mined await setGreetingTx.wait(); expect(await greeter.greet()).to.equal("Hola, mundo!"); }); }); |
$ npx hardhat compile
Downloading compiler 0.8.4
Compiling 2 files with 0.8.4
Compilation finished successfully
$ ls -la artifacts/contracts/Greeter.sol/Greeter.*
-rw-r–r– artifacts/contracts/Greeter.sol/Greeter.dbg.json
-rw-r–r– artifacts/contracts/Greeter.sol/Greeter.json
ローカルネットワークの起動
$ npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/Accounts
========WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80Account #1: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690dAccount #2: 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc (10000 ETH)
Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
….
別コンソールでアカウント確認コマンド
$ npx hardhat accounts –network localhost
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
0x70997970C51812dc3A010C7d01b50e0d17dc79C8
0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
0x90F79bf6EB2c4f870365E785982E1f101E93b906
….
テスト実行
$ npx hardhat test –network localhost
Greeter
✓ Should return the new greeting once it’s changed (400ms)1 passing (405ms)
いきなりテスト実行でしたが、パスしていることを確認しました。
(やはりフレームワークの力はすごい)
次に、OpenZeppelinライブラリをインストールします。
$ npm install @openzeppelin/contracts
上にならって3ファイルを用意します。(参考サイトの引用、部分的に編集)
contracts/NFT1.sol
1 2 3 4 5 6 7 8 |
pragma solidity ^0.8.0; import "hardhat/console.sol"; import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; contract NFT1 is ERC721PresetMinterPauserAutoId { constructor() ERC721PresetMinterPauserAutoId("NFT TEST 1", "TestSym", "https://decode.red/tokens/") {} } |
scripts/nft_deploy.js(テストを実行するので使わないが一応用意。$ node scripts/nft_deploy.js で実行)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const hh = require("hardhat"); async function main() { const NFT = await hh.ethers.getContractFactory("NFT1"); const nft = await NFT.deploy(); await nft.deployed(); console.log("Nft deployed to:", nft.address); } main() .then(() => process.exit(0)) .catch(error => { console.error(error); process.exit(1); }); |
test/nft-test1.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
const { expect } = require("chai"); describe("NFT1", async function () { it("should be able to mint, transferFrom, burn. And it should return appropriate name, symbol, totalSupply, tokenURI, ownerOf, balanceOf", async function () { const signers = await ethers.getSigners(); //console.log(signers); const [signer, badSigner] = signers; const NFT = await ethers.getContractFactory("NFT1"); const nft = await NFT.deploy(); await nft.deployed(); console.log("signer: " + signer.address); console.log("batSigner: " + badSigner.address); console.log(`greeter: ${nft.address}`); // before initial minting expect(await nft.name()).to.equal("NFT TEST 1"); expect(await nft.symbol()).to.equal("TestSym"); expect(await nft.totalSupply()).to.equal(0); // mint tokenId = 0 const mint0Tx = await nft.connect(signer).mint(signer.address); await mint0Tx.wait(); // Assertion for token(tokenId = 0) expect(await nft.totalSupply()).to.equal(1); expect(await nft.tokenURI(0)).to.equal("https://decode.red/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(); // Assertion for token(tokenId = 1) and contract state expect(await nft.totalSupply()).to.equal(2); expect(await nft.tokenURI(1)).to.equal("https://decode.red/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(); // 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(); // 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://decode.red/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-test1.js –network localhost
NFT1
signer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
batSigner: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
greeter: 0x0165878A594ca255338adfa4d48449f69242Eb8F
transfer1FromSignerToAddressTx tx hash: 0x7063985c48f41ca93e15186c1b54dd54a9f9bc2940b56571c5c68e911605a466
transfer2FromBadSignerToSignerAddress tx hash: 0x27de7322ef4ce73e48961aa882873dca649b1bd31585e0a39579db192e064259
✓ should be able to mint, transferFrom, burn. And it should return appropriate name, symbol, totalSupply, tokenURI, ownerOf, balanceOf (1007ms)1 passing (1s)
テストパスしたことを確認。(解説は長くなるので省略。ローカルネットの出力メッセージを参考に理解)
ここでtokenURIにアクセスすると以下のようなJSON返すことも意図しましたが、動作には無関係でした。
1 2 3 4 5 6 |
{ "description": "Test NFT Description.", "external_url": "https://decode.red/tokens/0", "image": "https://decode.red/DECODE-Education.png", "name": "NFT Name" } |
URIはNFTとして登録できてもその内容が別のものに置き換わったり、変更されていもわからないのではという疑問がわきました。
もう少し調べてみると以下のサイトでipfsというP2Pファイルサーバに登録した画像ファイルの例がありました。
https://www.quicknode.com/guides/solidity/how-to-create-and-deploy-an-erc-721-nft
$ ipfs init
generating ED25519 keypair…done
peer identity: 12D3KooWJpA83HUNUQg9wwTj3qZZaqsCmfRLMb3k9oGUkghNFEGN
initializing IPFS node at /home/k/.ipfs
to get started, enter:ipfs cat /ipfs/QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc/readme
$ ipfs cat /ipfs/QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc/readme
Hello and Welcome to IPFS!██╗██████╗ ███████╗███████╗
██║██╔══██╗██╔════╝██╔════╝
██║██████╔╝█████╗ ███████╗
██║██╔═══╝ ██╔══╝ ╚════██║
██║██║ ██║ ███████║
╚═╝╚═╝ ╚═╝ ╚══════╝
….
$ ipfs daemon
Initializing daemon…
go-ipfs version: 0.11.0
Repo version: 11
System version: amd64/linux
Golang version: go1.16.12
….
別コンソールで画像ファイルをadd。
$ ipfs add DECODE-Education.png
added QmR7Tr4CvAp6cfnDDZnLjrQwD5zzRF4g44ASmNCVThhKp7 DECODE-Education.png
7.88 KiB / 7.88 KiB [=======================================================================] 100.00%
$ ipfs add DECODE-Education.png
added QmR7Tr4CvAp6cfnDDZnLjrQwD5zzRF4g44ASmNCVThhKp7 DECODE-Education.png
7.88 KiB / 7.88 KiB [=======================================================================] 100.00%
画像のaddを複数回実行しても同じハッシュなのでこのURL(https://ipfs.io/ipfs/[ハッシュ値])を登録に使えば同一の内容であることは保証されそうです。しかし実体の紛失には対応できないことになります。どこかにバックアップをとっておく必要はありそうです。
(これがSymbolのアグリゲーショントランザクションの機能で話題になっている、オンチェーン・フルデータの実装のことなのでしょう)
現在、NFTについてかなり話題になっていますが、このあたりの不完全ななしくみについてはあまり一般的には議論されていない気がします。
実際に動かしてみて、このあたりよく理解できました。