今日のテーマ
- ERC-721(NFT)を発行する独自コントラクトを作ってみようハンズオン
- Remixを使ってNFTを発行する独自コントラクトを作りながらSolidityを学びます。
- https://solidity-jp.connpass.com/event/237306/
環境
- macOS 12.0
- GoogleChrome 97.0.4692.99
- Remix 0.20.3
準備
- GoogleChromeのインストール
- Metamaskのインストール
コントラクト開発
- Remixを開く
- http://remix.ethereum.org/
-
contracts
フォルダにSushiNeko.sol
ファイルを作成する
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.4.2/contracts/token/ERC721/ERC721.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.4.2/contracts/utils/Counters.sol";
contract SushiNeko is ERC721 {
using Counters for Counters.Counter;
Counters.Counter private _tokenCounter;
constructor () ERC721 ("SushiNeko", "SNEKO") {}
function mint() public returns (uint256) {
// 0 -> 1, 1 -> 2 .....
_tokenCounter.increment();
uint256 newItemId = _tokenCounter.current();
// NFTを発行する処理
_safeMint(msg.sender, newItemId);
return newItemId;
}
}
コンパイル
- ブロックチェーン上にデプロイするためにコンパイルを行います。
- Control(Command)+S で保存&コンパイルが走ります。もしくは手動でコンパイルボタンからコンパイル
ブラウザ上のローカルブロックチェーンにデプロイ
- ブラウザ上のローカルブロックチェーンにデプロイする
- mintボタンからNFTの発行ができる。
- ownerOfの横にtokenIDを入力してownerOfボタンを押すと tokenIDが1のNFTをどのアドレスが所有しているか表示される
- テストネット上でデプロイしてみよう
rinkeby テストネット上にデプロイ
- 準備
- rinkebyテストネット上にデプロイするためのethをもらう
- https://nnnnn15z.hatenablog.jp/entry/feucet-rinkeby-eth
- ENVIRONMENTをInjected Web3にメタマスクの接続先ネットワークをRinkebyにする
- デプロイする
- デプロイしたコントラクトのアドレスを元にetherscanを見てみる
https://rinkeby.etherscan.io/address/コントラクトのアドレス
- 例:
https://rinkeby.etherscan.io/address/0x19a887b4984dca2b1ca293e03ec11a7787189215
- remixからmintしてみる
- rinkebyテストネットのetherscanを見てみる
- openseaテストネットを見てみる
https://testnets.opensea.io/assets/コントラクトのアドレス/トークンID
- 例:
https://testnets.opensea.io/assets/0x2949c658ED864a79B6DdbbDA3BeEB6Bb4a80ACDA/1
- 出品されているのを確認。ただしデータがなにもないのを確認
- これはtokenURIが正しく設定されていないため。
メタデータを入れてみよう
- mint時にtokenURIを書き込む方法を試す
- コントラクトを書き換えよう。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.4.2/contracts/token/ERC721/ERC721.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.4.2/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.4.2/contracts/utils/Counters.sol";
contract SushiNeko is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenCounter;
constructor () ERC721 ("SushiNeko", "SNEKO") {}
function mint(string memory tokenURI) public returns (uint256) {
_tokenCounter.increment();
uint256 newItemId = _tokenCounter.current();
_safeMint(msg.sender, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
}
- rinkebyテストネットにデプロイしておく
メタデータの用意
- メタデータを用意する。今回はpinataで用意する
- 画像を用意し、pinata経由でipfsにアップロード
- アップロードした画像のURLを取得し、以下のjsonファイルを作成する
{
"name": "すしねこ #1",
"description": "まぐろ",
"image": "https://gateway.pinata.cloud/ipfs/QmWyX6q28LTkGBkjidn1bKKv1gFc9NjqoG3MjvU3FwYnAg"
}
- このjsonファイルをpinata経由でipfsにアップロードし、URLをコピーする
// 今回のサンプルメタデータjson URL
https://gateway.pinata.cloud/ipfs/QmbsrWq6FBpcbn4zoZKFZ6qPwqud2QtJ21gBkQ1fiibaFT
- mintボタンのところにあるテキストボックスにメタデータのURLをコピペし、mintボタンを押す
- デプロイしたコントラクトのアドレスを元にetherscanを見てみる
https://rinkeby.etherscan.io/address/コントラクトのアドレス
- 例:
https://rinkeby.etherscan.io/address/0x19a887b4984dca2b1ca293e03ec11a7787189215
- openseaテストネットを見てみる
https://testnets.opensea.io/assets/コントラクトのアドレス/トークンID
- 例:
https://testnets.opensea.io/assets/0x2949c658ED864a79B6DdbbDA3BeEB6Bb4a80ACDA/1
- 確認できたら成功〜〜!!
まとめ
- Remixを使ってスマートコントラクトを開発した
- ERC721を継承させ、デプロイし、NFTをmintした
- etherscanとopenseaテストネットからmintしたNFTを確認した
おすすめの記事
FAQ
てっきり、openseaからNFTを発行すると他のマケプレとの互換性がないって印象を持っていたのですが、正確にはメタデータとフロントエンドの関係が合わない場合は正しい情報が表示されないってことでしょうか?
- はい、そのとおりです!
- OpenSeaの場合は https://docs.opensea.io/docs/metadata-standards#metadata-structure に表示できるメタデータが記載されています。
- OpenSeaの場合には正しく表示できていても、他のマケプレではその形式は対応していないから表示に失敗する、みたいなのはあります。
- 例えば、OpenSeaでは
animation_url
にHTMLを表示するURLを記載しても正しく表示してますが、他のマケプレだと正しく表示される保証はないです!
tokenURIを提供するサーバが落ちたらNFTアートにアクセスできなくなっちゃうんでしょうか.
- 直でアクセスした場合はNFTアートにアクセスできなくなります。
- OpenSeaからアクセスした場合にOpenSea側のサーバーのキャッシュが存在していれば表示できますが、キャッシュが存在しない場合は表示に失敗します。
- キャッシュの保持期間はマケプレによって実装が異なりますが、大体のマケプレはキャッシュがあると思うので、一時的なサーバーダウンの場合なら特に問題ないと思います。
- 永久的なサーバーダウンであれば、アクセスできなくなります。
NFTの本質はトークンそのものであって、画像などは外にあるものを読み込んでる、、っていう理解で、正しいんでしょうか。
世間のイメージと実態はだいぶ違うのかもしれません、勉強になりました。
- はい、ERC-721の場合はそうなります!
PolygonNetでNFTを出品するときのデメリット
- PolygonチェーンはEthereumのL2ソリューションなのでEthereumがしぬとしにます。
- ERC-721の基本的な実装の場合は、Polygonチェーンで発行したNFTはEthereumメインネットにブリッジできないです。
- ただ、実装すればブリッジできるようになります(試してないので違ったらすみません)
ERCからはじまる数字はどのように決まっているんですか?
- GitHub上のEIPsリポジトリのissue番号と紐付いています!
- https://github.com/ethereum/EIPs/issues?q=is%3Aopen+is%3Aissue
- 例えば、ERC-721は https://github.com/ethereum/EIPs/issues/721 です!
ERC20や721はトークンに関する規格だと思うのですが、他の番号のものはトークン以外のものもあるのでしょうか?
- ここにまとまったEIPの一覧があります!
- https://eips.ethereum.org/all
- 例えば、コントラクトのコードサイズ制限なんかは https://eips.ethereum.org/EIPS/eip-170 で定義されています!
空き時間で作ったフルオンチェーンNFT
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.4.2/contracts/token/ERC721/ERC721.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.4.2/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.4.2/contracts/utils/Counters.sol";
import "https://github.com/Brechtpd/base64/blob/main/base64.sol";
contract Kisekitaiken is ERC721 {
using Counters for Counters.Counter;
Counters.Counter private _tokenCounter;
struct KisekitaikenToken {
string text;
}
KisekitaikenToken[] public tokens;
constructor() ERC721("Kisekitaiken", "KSK") {}
function mint(string memory text) public {
_tokenCounter.increment();
uint256 newItemId = _tokenCounter.current();
_safeMint(msg.sender, newItemId);
tokens.push(KisekitaikenToken(text));
}
function tokenURI(uint256 tokenId)
public
view
override
returns (string memory)
{
require(
_exists(tokenId),
"ERC721Metadata: URI query for nonexistent token"
);
KisekitaikenToken memory token = tokens[tokenId - 1];
string memory svg = getSVG(token);
bytes memory json = abi.encodePacked(
'{"name": "',
token.text,
'", "description": "aaaa", "image": "data:image/svg+xml;base64,',
Base64.encode(bytes(svg)),
'"}'
);
string memory _tokenURI = string(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode(json)
)
);
return _tokenURI;
}
// OpenSea等で表示される実体
function getSVG(KisekitaikenToken memory token)
private
pure
returns (string memory)
{
return
string(
abi.encodePacked(
'<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350">',
"<style>text{fill:black;font-family:serif;}</style>",
'<rect width="100%" height="100%" fill="#a9ceec" />',
'<text x="10%" y="45%" font-size="50px">',
token.text,
"</text>",
"</svg>"
)
);
}
}