MonoPixelNFTのコード解説
今回作ったコントラクトをざっと解説していきます。
ライセンス表記
// SPDX-FileCopyrightText: 2021 @NowAndNawoo // // SPDX-License-Identifier: MIT
他の人のコントラクトを見ていると、こんなCopyright表記をしている人がいたのでマネしてみました。
ERC721EnumerableとOwnableを継承
contract MonoPixelNft is ERC721Enumerable, Ownable {
Ownableを継承しているのは、以前の記事で書きましたがOpenSea対策です。
ERC721Enumerableを継承しているのは、tokenOfOwnerByIndex関数を使うためです。tokenOfOwnerByIndex関数ではownerとindexを指定してtokenIDを取得できます。 これを使って、UIサイトでユーザーの持っているNFTを一覧表示しています。
アスキーアートのロゴ
/* __ __ _____ _ _ _ _ ______ _______ | \/ | | __ (_) | | | \ | | ____|__ __| | \ / | ___ _ __ ___ | |__) |__ _____| | | \| | |__ | | | |\/| |/ _ \| '_ \ / _ \ | ___/ \ \/ / _ \ | | . ` | __| | | | | | | (_) | | | | (_) | | | | |> < __/ | | |\ | | | | |_| |_|\___/|_| |_|\___/ |_| |_/_/\_\___|_| |_| \_|_| |_| */
ソースコードにアスキーアートでロゴを書くのに憧れていたので、やってみました。
アスキーアートはこのサイトで作成しました。→ Text to ASCII Art Generator (TAAG)
version
uint8 public constant version = 5;
テストネットやメインネットへデプロイした後に修正したくなって再デプロイ、というのを繰り返していると どれが最新のコントラクトだっけ?となってしまうので、自分で区別しやすいように version を付けています。
claim関数
function claim(uint256 tokenId) external { _safeMint(_msgSender(), tokenId); }
NFTを発行するためのclaim関数です。ただ_safeMint
を呼んでいるだけです。
exists関数
function exists(uint256 tokenId) public view returns (bool) { return _exists(tokenId); }
tokenIDが発行済かどうかをチェックする関数です。 UIサイトで[Claim NFT]ボタンをクリックしたときに、まずexists関数を呼び出して、 tokenIDが未発行の場合のみclaimを実行します。 (無駄なトランザクションを発行しないため。)
tokenURI関数
function tokenURI(uint256 tokenId) public view override returns (string memory) { require( _exists(tokenId), "ERC721Metadata: URI query for nonexistent token" ); string memory svg = getSVG(tokenId); bytes memory json = abi.encodePacked( '{"name": "MonoPixelNFT ', tokenId.toHexString(32), '", \ "description": "MonoPixelNFT is a full on-chain NFT. You can store black and white 16x16 pixel art on the blockchain. created by @NowAndNawoo", \ "image": "data:image/svg+xml;base64,', Base64.encode(bytes(svg)), '"}' ); return string( abi.encodePacked( "data:application/json;base64,", Base64.encode(json) ) ); }
tokenURI関数はいつもどおりで、特に書くことはないですが、 name, description, imageの3つを含むJSONをBase64エンコードして返します。 imageは次のgetSVG関数で作成します。
getSVG関数
function getSVG(uint256 data) private pure returns (string memory) { bytes memory rects; for (uint256 y = 0; y < 16; y++) { for (uint256 x = 0; x < 16; x++) { uint256 index = 255 - y * 16 - x; // (0,0)=>255, (1,0)=>254, (15,15)=>0 if (data & (1 << index) != 0) { rects = abi.encodePacked( rects, '<rect x="', (x * 10 + 1).toString(), '" y="', (y * 10 + 1).toString(), '"/>' ); } } } return string( abi.encodePacked( '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 162 162">\ <style>rect{shape-rendering:crispEdges;width:10px;height:10px;fill:white;}</style>\ <rect style="width:162px;height:162px;fill:black;"/>', rects, "</svg>" ) ); } }
getSVG関数では、tokenIDをdataとして受け取って、ピクセルアートのSVGを作成します。
まず、forループで256bitを順にチェックします。
if (data & (1 << index) != 0)
の部分で、index番目のbitが0か1かを判定します。
bitが1ならば白ピクセル用の<rect>を作成し、rects変数に追加していきます。
ビット演算についてはこちら → Solidityメモ: ビット演算 - Now and Nawoo
生成するSVGを短くするために、白ピクセル用<rect>のwidth、height、fillは<style>タグでまとめて指定しています。
最後にsvgタグ全体を作ります。 svgは162x162サイズにしています。 これは1ピクセルのサイズが10x10で、それが縦と横に16個並び、さらに全体の周囲に幅1の黒い枠線を付けるためです。 (OpenSeaで見ると黒い枠線があったほうがキレイだったので。)
背景用の162x162サイズの黒い<rect>の後に、先程作った白ピクセルのrects変数を追加します。
shape-rendering:crispEdges について
<style>タグでshape-rendering:crispEdges;
を指定しておくと、四角形の周囲がくっきりと表示されるのでピクセルアートには必須です。
(これはCryptoPunksを参考にしました。)
おわりに
間違い、バグ、改善点などありましたら、どうぞご指摘ください。