見出し画像

【完全保存版】フルオンチェーンNFTをランダムで10,000個作る方法

こんにちは、CryptoGamesの高橋です。

クリスペの会社です。

今回はこちらの「OnChainMonkey」というNFTを元に作り方を学んでいきましょう。

https://opensea.io/collection/onchainmonkey

重要
EthereumでのフルオンチェーンNFTにはガス代がかなりかかります。そのことを十分認識の上、ご自身の判断で行ってください。

1 コードを見てみよう

こちらのEtherscanというサイトを見に行ってみましょう。

https://etherscan.io/address/0x960b7a6bcd451c9968473f7bbfd9be826efd549a#code

スクリーンショット 2021-12-24 7.05.57

このようにコードを見ることができます。

何やら「background(背景)」やら「eyes(目)」やらにいろんな数値を入れていますね。

こちらはCSSの、色のコードが入っています。

2 パーツを見てみよう(長方形)

こちらのSVGが書かれているところも見てみましょう。

もし不明な場合は、これらの記事をぜひご参照ください。

どうやら目っぽいのが3つありますね。(「ey1」「ey2」「ey3」

スクリーンショット 2021-12-24 7.15.25

幅75、高さ15の長方形なので、なんとなくここかなーと思います。

スクリーンショット 2021-12-24 7.21.08

3 パーツを見てみよう(円)

他のところも見てみましょう。

スクリーンショット 2021-12-24 7.32.34

例えばこちらの「ea1」「ea2」「ea3」は中心(100,290)で半径の異なる3つの円です。

スクリーンショット 2021-12-24 7.40.23

ということは、この辺りが「ea1」のようですね。

4 共通パーツを見てみよう。(形)

一方、この辺りのように形がまとめて書かれているところがありますね。

スクリーンショット 2021-12-24 7.42.50

第2・3章とこちらのSVGの違いはなんでしょう?

それは「共通して使われるパーツか否か」です。

スクリーンショット 2021-12-24 7.47.50

こちらを見てみるとわかりやすそうです。

例えば顔の丸の形はどのキャラクターも共通です。

一方、イヤリングや眉毛はあったりなかったりします。このように共通化した形は一つにまとめ、個別のパーツは分けています。

5 共通パーツを見てみよう。(色)

次に共通パートをさらに見てみましょう。

よく見ると、「fill:#」の後に数字があるものとないものがあります。

スクリーンショット 2021-12-24 7.51.44

「fill」は色の塗りつぶしを意味し、ここの「#000」は黒色を表しています。

では見てみましょう。

スクリーンショット 2021-12-24 7.55.23

よく見ると、顔の色はキャラごとに異なるものの、目の色は共通して黒色ということがわかりました。

つまり、共通する色はすでに書き込んでいるようです。

6 共通パーツを見てみよう。(構成)

次は共通パーツである「z」の構成を見てみましょう。

構成としては

string[] private z = ["中身①", "中身②", "中身③", "中身④",,,,];

という形になっています。

スクリーンショット 2021-12-24 8.02.37

つまり、これは配列です。

そのため、z[0]で中身①を、z[1]で中身②を取り出すことができます。

7 個別の色の配列も見てみよう

では、他の配列も見てみましょう。

例えば、background(背景)はこのようになっています。

スクリーンショット 2021-12-24 8.10.17

そのため、先ほどと同じく、background[0]で"656"を、background[1]で"dda"を取ることができます。

8 背景色をランダムで取得しよう①

randomOne」という関数を作っていますね。

この中にabi.encodePackedという関数が入っています。

スクリーンショット 2021-12-24 8.18.15

こちらは、文字を繋げる関数です

例えば「Hello」と「 」と「World」を繋げて「Hello World」にするという意味です。

ここでは「ra1」 と「tokenId.toString()」という2つを繋げています。

ちなみに、「ra1」は下のように'A'のようです。

スクリーンショット 2021-12-24 8.25.43

なお、「tokenId.toString()」はTokenIDの数値を文字列に変換しています。

9 背景色をランダムで取得しよう②

さて、random関数も見てみましょう。

スクリーンショット 2021-12-24 9.35.12

ここでは「keccak256」という関数が使われています。

こちらは32バイトの文字列に暗号化する関数です。

なお、solidityで見かけるアルゴリズムには大きく

① SHA2アルゴリズム ⇨ sha256関数
② SHA3アルゴリズム ⇨ keccak256関数かsha3関数

の2つがあります。基本的には②SHA3アルゴリズムを用います。

また、アルゴリズムが同じなので、keccak256とsha3の結果は同じになります。

今回のrandom関数はkeccak256で32バイトにし、それをuint256で32バイトの正の値に変換しています。

スクリーンショット 2021-12-24 8.18.15

そして、その値を「%8」で8で割った余り(0~7)にして、ape.bgに入れているようです。

10 ランダム数が格納される構造体を見よう

上の章で、Apeという構造体が使われたので、そちらも見ていきましょう。

構造体は異なる形の変数を入れるセットのようなものです。

スクリーンショット 2021-12-24 9.53.34

今回は全て同じ型の7種類の変数(背景色から帽子まで)を入れられるようにしています。

改めてrandomOne関数を見てみると、tokenIdを渡して、それに基づいてランダムな値をApeの構造体のインスタンスに入れているようです。

スクリーンショット 2021-12-24 9.58.00

11 確率付きランダム数の生成

上の「usew」を見てみましょう。

スクリーンショット 2021-12-24 18.35.36

例 w = [40, 32, 15, 10, 3](wの合計100) i = 0~ 99(iはランダム

この場合、次のような結果が得られます。

① 100分の40の確率で0
② 100分の32の確率で1
③ 100分の15の確率で2
④ 100分の10の確率で3
⑤ 100分の3の確率で4

具体的に考えましょう。(飛ばしても大丈夫)

i = 0 ~39 の40通りの場合、最初のwhileに辿り着かないので、indは0であり、0が返ります。

12 確率付きランダム数の結果例

では、一番数が小さい、こちらのイヤリングを見てみましょう。

スクリーンショット 2021-12-24 18.53.13

「earring_w」に対して、usewが使われていますので、この配列に対して、確率付きランダム数が使われています。

スクリーンショット 2021-12-24 18.54.56

つまり、この場合、「earring_w」の合計である358分の251の確率で"999"という番号になります。

① 358分の251 ⇨ "999" 
② 358分の32 ⇨ "fe7"
③ 358分の29 ⇨ "999"
④ 358分の17 ⇨ "999"
⑤ 358分の16 ⇨ "999"
⑥ 358分の8 ⇨ "fe7"
⑦ 358分の5 ⇨ "bdd"

13 パーツを組み立てる関数を見てみよう(genEye)

まず最初に目にはどのような種類があるのかを見てみましょう。

次のように4つのパターンがあります。

スクリーンショット 2021-12-24 20.55.45

① ノーマル
② 目が半分
③ ②+眉毛
④ ②+③+細目

下のようにhの値によって分岐しています。

hが4未満の時は何もない('')というのもポイントだと思います。

スクリーンショット 2021-12-24 21.02.25

では②を見てみましょう。abi.encodePacked()は文字を繋げる関数でしたね。(第8章)

では、どんな文字を繋げているのでしょう。

sl1、sl2を見てみましょう。

スクリーンショット 2021-12-24 21.07.37

rectと書いてあるので長方形のSVGですが、ポイントは「fill:#」で終わっているということです。

この後には色がきます。そのため、色の前までを繋げているのですね。

スクリーンショット 2021-12-24 21.02.2525

そのため、aの場所には色がきます。(bも同じです。)

このように「genEye」は色の変数を二つ、乱数を一つ渡して目のパーツを作ってくれるものだとわかりました。

14 パーツを組み立てる関数を見てみよう(genMouth)

次は口のパーツを見てみましょう。

大きく3つがあります。

① ノーマル(周りの丸がないのは色がたまたま同じ)
② ひげ
③ 棒

スクリーンショット 2021-12-24 21.25.01

では、コードを見てみましょう。

hの値によって分岐しています。

スクリーンショット 2021-12-24 21.30.17

ポイントは

① ノーマルの時は' 'を返す
② ひげと棒は両立しない(目の時とは違う)

あたりかと思います。

また、for文も見てみましょう。上が7回下が6回まわしていますが、ひげをよくみて見てください。

スクリーンショット 2021-12-24 21.36.28

上が7本、下が6本、そして点ではなく、棒になっていますね。

abi.encodePacked()でつなげているmo1,mo2を見てみると、線を引く「line」になっています。

長さ・間隔が同じ線を引くために、for文を使っていたのですね。

スクリーンショット 2021-12-24 21.38.22

15 色の統一

ここで顔の色を見てみましょう。

実は完全にバラバラではなく、①肌の色 ②輪郭の色 がそれぞれ統一されています。

スクリーンショット 2021-12-24 21.46.26

コードを見てみましょう。

スクリーンショット 2021-12-24 21.50.29

実は2種類の色をそれぞれa,bという変数に格納し、a,bで色を使用しています。そのため、色に統一感が生まれています。

ちなみに、色はこんな風に格納されています。

スクリーンショット 2021-12-24 21.56.37

15 Propertiesの設定

次はPropertiesの設定をしていきましょう。

この部分ですね。

スクリーンショット 2021-12-24 22.04.19

コードはこんな感じです。

いつもと同じでabi.encodePacked()で文字を繋げていますね。

スクリーンショット 2021-12-24 22.00.31

ポイントはあくまでも文字を繋げるものなので、数字の部分は「.toString()」で文字にしているというところだと思います。

中身はこのようになっています。

スクリーンショット 2021-12-24 22.08.07

attributesで属性についての指定が始まり、各属性はtrait_typeで表していますね。

16 TokenURIの設定

randomOne()関数を使って、構造体に直接値を入れているのはすごいと思いました。

スクリーンショット 2021-12-24 22.17.26

また、SVGはBase64形式にして返していました。

以上で、「OnChainMonkey」がどのように作られているのかを知ることができました。

ではこれを元に次章から具体的な作成方法を見ていきましょう。

17 実践編

こんなのを作ってみましょう。

スクリーンショット 2021-12-25 15.45.21

① 上の四角はレア(必ず現れるわけではない)
② 下の丸は必ずある。
③ 色は背景色を含めて3種類

では、やっていきましょう。

18 ステップ1 構造体を作ろう

まずは、色を格納する構造体を作ります。

スクリーンショット 2021-12-25 13.19.18

ここにランダムな色の番号が入ることになります。

後から加えることも可能です。

参考に、コードです。

struct Shape { 
 uint8 bg;
 uint8 shikaku;
 uint8 maru;
}

19 ステップ2 変数を作ろう

19ー1 クレーム数チェック用変数

これは作るだけです。claim関数などで使います。

スクリーンショット 2021-12-25 14.02.53

19ー2 色の格納・確率用変数

①それぞれの色が入る変数
②確率を入れるための_wの配列の変数を作る

スクリーンショット 2021-12-25 14.04.57

なお、少しでもコード数を減らすために、色のコードは3桁で指定しています。

参考に、コードです。

string[] private background = ["656","dda","e92","1eb","663","9de","367","ccc"];
string[] private shape1 = ["abe","0a0","653","888","be7","abe","0a0","653","888","be7","cef","abe","0a0","653","888","be7","cef","abe","0a0","653","888","be7","cef"];
uint8[] private shape1_w = [245, 121, 107, 101, 79, 78, 70, 68, 62, 58, 56, 51, 50, 48, 44, 38, 35, 33, 31, 22, 15, 10, 7];
string[] private shape2 = ["653","ffc","f89","777","049","901","bcc","d04","fd1","ffc","653","f89","777","049","bcc","901","901","bcc","653","d04","ffc","f89","777","049","fd1","f89","777","bcc","d04","049","ffc","901","fd1"];
uint8[] private shape2_w = [252, 172, 80, 79, 56, 49, 37, 33, 31, 30, 28, 27, 26, 23, 22, 18, 15, 14, 13, 12, 11, 10, 10, 10, 9, 8, 7, 7, 6, 5, 5, 4, 3];

19−3 ランダムの複雑化用の変数

「randomOne」という関数で使用する、ランダムを少し複雑にするための変数です。

なくても大丈夫ですし、他の文字でも大丈夫です。

スクリーンショット 2021-12-25 14.08.26

参考に、コードです。

string private ra1='A';
string private ra2='B';
string private ra3='C';

19ー4 SVG簡素化用の変数

SVGを直に書いていくと、混乱の元になります。

少しでも混乱を避けるように、わかりやすい記号に変えましょう。

スクリーンショット 2021-12-25 14.12.26

参考に、コードです。

string private zz='"/>';
string private co1=', ';

19ー5 属性設定用の変数

OpenSeaで「Properties」を設定するための変数を作りましょう。

不要な場合は、なくて大丈夫です。

スクリーンショット 2021-12-25 14.17.17

スクリーンショット 2021-12-25 14.15.16

参考に、コードです。

string private tr1='", "attributes": [{"trait_type": "Background","value": "';
string private tr2='"},{"trait_type": "Mayu","value": "';
string private tr3='"}],"image": "data:image/svg+xml;base64,';

19ー6 TokenURI用の変数(svgの前まで)

nameやdescriptionを入れる用の変数を作りましょう。

スクリーンショット 2021-12-25 14.20.16

参考に、コードです。

string private rl1='{"name": "OnChain Test #';
string private rl2='"}';
string private rl3='data:application/json;base64,';

19ー7 TokenURI用の変数(ノーマルsvg)

ここが一つのポイントです。

どのNFTにも必ず入るパーツなどはここに入れましょう。レアなNFTだけにあるパーツはここには入りません。

また、ランダムな色を入れたい場合、その色の部分は入れません。

スクリーンショット 2021-12-25 14.23.19

参考に下がコードです。

string[] private z = ['<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 500 500"><rect x="0" y="0" width="500" height="500" style="fill:#',
'"/><circle cx="200" cy="300" r="150" style="fill:#',
'<circle cx="200" cy="300" r="50" style="fill:#000"/></svg>'];

19ー8 TokenURI用の変数(特別svg)

ここもポイントです。

全てのNFTにあるわけではない、特別パーツを作る場合はこのように別に変数を作ります。

やはり色の部分は抜かします。

スクリーンショット 2021-12-25 14.30.12

参考に、下がコードです。

string private mayu='<rect width="300" height="50" x="50" y="50" style="fill:#';

20 ステップ3 関数を作ろう

20ー1 ランダム作成用の関数を作ろう

ランダム用の関数として

① 通常ランダム ⇨ random関数
② 確率付きランダム ⇨ usew関数

を使用します。

ただ、ここはコピペでいいと思います。

スクリーンショット 2021-12-25 14.34.58

参考に、下がコードです。

function usew(uint8[] memory w,uint256 i) internal pure returns (uint8) {
 uint8 ind=0;
 uint256 j=uint256(w[0]);
 while (j<=i) {
   ind++;
   j+=uint256(w[ind]);
 }
 return ind;
}
function random(string memory input) internal pure returns (uint256) {
 return uint256(keccak256(abi.encodePacked(input)));
}

20ー2 ランダム数を構造体に格納する関数を作ろ

ランダムの値を各構造体に入れるための関数を作りましょう。

スクリーンショット 2021-12-25 14.40.18

ちなみに19−3で作った、少しランダムを複雑にする変数がここで使われます。

右の方にある、数字は19ー2で作った「_w」の配列の合計数を入れます。

これにより、例えば「shikaku」用に用意した「shape1」には23個が格納されているため、「shape.shikaku」には「0~22」のどれかの値が入ります。

参考に、下がコードです。

function randomOne(uint256 tokenId) internal view returns (Shape memory) {
 Shape memory shape;
 shape.bg = uint8(random(string(abi.encodePacked(ra1,tokenId.toString()))) % 8);
 shape.shikaku = usew(shape1_w,random(string(abi.encodePacked(ra2,tokenId.toString())))%1429);
 shape.maru = usew(shape2_w,random(string(abi.encodePacked(ra3,tokenId.toString())))%1112);
 return shape;
}

20ー3 特別パーツ用の関数を作ろう

全てのNFTに共通ではない、特別なパーツ用の関数を作りましょう。

ポイントはif文を使い、合致しないときには''を返すようになっているところです。

スクリーンショット 2021-12-25 14.51.45

その他、abi.encodePaccked()を使って文字もつなげていますね。

参考に、下がコードです。


function genMayu(string memory a,uint8 h) internal view returns (string memory) {
 string memory out = '';
 if (h>8) { out = string(abi.encodePacked(mayu,a,zz)); }
 return out;
}

20ー4  属性確認用の関数を作ろう

属性を確認するための関数です。

コード内で使っているわけではないので、なくても大丈夫です。

スクリーンショット 2021-12-25 15.05.31

参考に、下がコードです。

function getAttributes(uint256 tokenId) public view returns (string memory) {
 Shape memory shape = randomOne(tokenId);
 string memory o=string(abi.encodePacked(uint256(shape.shikaku).toString(),co1));
 return string(abi.encodePacked(o,uint256(shape.bg).toString()));
}

20ー5 属性部分を作るための関数を作ろう

同じ、属性関連でも、こちらは20ー4と異なり、必ず必要な関数になります。

TokenURIに渡すために形を整えています。

スクリーンショット 2021-12-25 15.09.22

参考に、下がコードです。

function getTraits(Shape memory shape) internal view returns (string memory) {
 string memory o=string(abi.encodePacked(tr1,uint256(shape.bg).toString(),tr2,uint256(shape.shikaku).toString()));
 return string(abi.encodePacked(o,tr3));
}

20ー6 共通パーツのSVG化の関数を作ろう

共通パーツをSVG化するための関数です。

ここでは特別パーツは含まれません。

スクリーンショット 2021-12-25 15.15.32

参考に、以下がコードです。

function genSVG(Shape memory shape) internal view returns (string memory) {
 string memory a=shape1[shape.shikaku];
 string memory output = string(abi.encodePacked(z[0],background[shape.bg],z[1],a,zz,z[2]));
 return output;
}

20ー7 Base64形式にして、TokenURIに渡す関数を作ろう

ここでは、Base64.encode()という関数を使って、SVGをBase64形式に変換しています。

スクリーンショット 2021-12-25 15.20.22

ご不明な場合は、こちらで詳しく書いておりますので、ご参考ください。

参考に、以下がコードです。

function tokenURI(uint256 tokenId) override public view returns (string memory) {
 Shape memory shape = randomOne(tokenId);
 return string(abi.encodePacked(rl3,Base64.encode(bytes(string(abi.encodePacked(rl1,tokenId.toString(),getTraits(shape),Base64.encode(bytes(genSVG(shape))),rl2))))));
}

20ー8 Claim用の関数を作ろう

今回の直接のテーマではないですが、claimできる関数は必要ですね。

スクリーンショット 2021-12-25 15.24.42

参考に、以下がコードです。

unction claim() public nonReentrant {
 require(numClaimed >= 0 && numClaimed < 9500, "invalid claim");
 _safeMint(_msgSender(), numClaimed + 1);
 numClaimed += 1;
}
 
function ownerClaim(uint256 tokenId) public nonReentrant onlyOwner {
 require(tokenId > 9500 && tokenId < 10001, "invalid claim");
 _safeMint(owner(), tokenId);
}

21 コンストラクタを作ろう

コンストラクタはコントラクトがデプロイされる時に1度だけ行われる処理です。

コントラクトの名前とシンボル名を設定していますね。

スクリーンショット 2021-12-25 15.29.27

参考に、以下がコードです。

constructor() ERC721("OnChainEyes", "OCE") Ownable() {}


いかがだったでしょうか。

今回は少し難しかったかもしれませんが、ぜひチャレンジしてみてください。

また、もし参考になったよーという方は作ったNFTの一つなどを私のウォレットに投げていただけると喜びます。

筆者のウォレット:0xB365BC5A0BA013e260B922cA2f27DC7C86279e3b

もちろん投げなくても大丈夫です。

以上です。


サポートをしていただけたらすごく嬉しいです😄 いただけたサポートを励みに、これからもコツコツ頑張っていきます😊