
【完全保存版】フルオンチェーンNFTをランダムで10,000個作る方法
こんにちは、CryptoGamesの高橋です。
クリスペの会社です。
今回はこちらの「OnChainMonkey」というNFTを元に作り方を学んでいきましょう。
https://opensea.io/collection/onchainmonkey
重要
EthereumでのフルオンチェーンNFTにはガス代がかなりかかります。そのことを十分認識の上、ご自身の判断で行ってください。
1 コードを見てみよう
こちらのEtherscanというサイトを見に行ってみましょう。
https://etherscan.io/address/0x960b7a6bcd451c9968473f7bbfd9be826efd549a#code
このようにコードを見ることができます。
何やら「background(背景)」やら「eyes(目)」やらにいろんな数値を入れていますね。
こちらはCSSの、色のコードが入っています。
2 パーツを見てみよう(長方形)
こちらのSVGが書かれているところも見てみましょう。
もし不明な場合は、これらの記事をぜひご参照ください。
どうやら目っぽいのが3つありますね。(「ey1」「ey2」「ey3」)
幅75、高さ15の長方形なので、なんとなくここかなーと思います。
3 パーツを見てみよう(円)
他のところも見てみましょう。
例えばこちらの「ea1」「ea2」「ea3」は中心(100,290)で半径の異なる3つの円です。
ということは、この辺りが「ea1」のようですね。
4 共通パーツを見てみよう。(形)
一方、この辺りのように形がまとめて書かれているところがありますね。
第2・3章とこちらのSVGの違いはなんでしょう?
それは「共通して使われるパーツか否か」です。
こちらを見てみるとわかりやすそうです。
例えば顔の丸の形はどのキャラクターも共通です。
一方、イヤリングや眉毛はあったりなかったりします。このように共通化した形は一つにまとめ、個別のパーツは分けています。
5 共通パーツを見てみよう。(色)
次に共通パートをさらに見てみましょう。
よく見ると、「fill:#」の後に数字があるものとないものがあります。
「fill」は色の塗りつぶしを意味し、ここの「#000」は黒色を表しています。
では見てみましょう。
よく見ると、顔の色はキャラごとに異なるものの、目の色は共通して黒色ということがわかりました。
つまり、共通する色はすでに書き込んでいるようです。
6 共通パーツを見てみよう。(構成)
次は共通パーツである「z」の構成を見てみましょう。
構成としては
string[] private z = ["中身①", "中身②", "中身③", "中身④",,,,];
という形になっています。
つまり、これは配列です。
そのため、z[0]で中身①を、z[1]で中身②を取り出すことができます。
7 個別の色の配列も見てみよう
では、他の配列も見てみましょう。
例えば、background(背景)はこのようになっています。
そのため、先ほどと同じく、background[0]で"656"を、background[1]で"dda"を取ることができます。
8 背景色をランダムで取得しよう①
「randomOne」という関数を作っていますね。
この中にabi.encodePackedという関数が入っています。
こちらは、文字を繋げる関数です。
例えば「Hello」と「 」と「World」を繋げて「Hello World」にするという意味です。
ここでは「ra1」 と「tokenId.toString()」という2つを繋げています。
ちなみに、「ra1」は下のように'A'のようです。
なお、「tokenId.toString()」はTokenIDの数値を文字列に変換しています。
9 背景色をランダムで取得しよう②
さて、random関数も見てみましょう。
ここでは「keccak256」という関数が使われています。
こちらは32バイトの文字列に暗号化する関数です。
なお、solidityで見かけるアルゴリズムには大きく
① SHA2アルゴリズム ⇨ sha256関数
② SHA3アルゴリズム ⇨ keccak256関数かsha3関数
の2つがあります。基本的には②SHA3アルゴリズムを用います。
また、アルゴリズムが同じなので、keccak256とsha3の結果は同じになります。
今回のrandom関数はkeccak256で32バイトにし、それをuint256で32バイトの正の値に変換しています。
そして、その値を「%8」で8で割った余り(0~7)にして、ape.bgに入れているようです。
10 ランダム数が格納される構造体を見よう
上の章で、Apeという構造体が使われたので、そちらも見ていきましょう。
構造体は異なる形の変数を入れるセットのようなものです。
今回は全て同じ型の7種類の変数(背景色から帽子まで)を入れられるようにしています。
改めてrandomOne関数を見てみると、tokenIdを渡して、それに基づいてランダムな値をApeの構造体のインスタンスに入れているようです。
11 確率付きランダム数の生成
上の「usew」を見てみましょう。
例 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 確率付きランダム数の結果例
では、一番数が小さい、こちらのイヤリングを見てみましょう。
「earring_w」に対して、usewが使われていますので、この配列に対して、確率付きランダム数が使われています。
つまり、この場合、「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つのパターンがあります。
① ノーマル
② 目が半分
③ ②+眉毛
④ ②+③+細目
下のようにhの値によって分岐しています。
hが4未満の時は何もない('')というのもポイントだと思います。
では②を見てみましょう。abi.encodePacked()は文字を繋げる関数でしたね。(第8章)
では、どんな文字を繋げているのでしょう。
sl1、sl2を見てみましょう。
rectと書いてあるので長方形のSVGですが、ポイントは「fill:#」で終わっているということです。
この後には色がきます。そのため、色の前までを繋げているのですね。
そのため、aの場所には色がきます。(bも同じです。)
このように「genEye」は色の変数を二つ、乱数を一つ渡して目のパーツを作ってくれるものだとわかりました。
14 パーツを組み立てる関数を見てみよう(genMouth)
次は口のパーツを見てみましょう。
大きく3つがあります。
① ノーマル(周りの丸がないのは色がたまたま同じ)
② ひげ
③ 棒
では、コードを見てみましょう。
hの値によって分岐しています。
ポイントは
① ノーマルの時は' 'を返す
② ひげと棒は両立しない(目の時とは違う)
あたりかと思います。
また、for文も見てみましょう。上が7回、下が6回まわしていますが、ひげをよくみて見てください。
上が7本、下が6本、そして点ではなく、棒になっていますね。
abi.encodePacked()でつなげているmo1,mo2を見てみると、線を引く「line」になっています。
長さ・間隔が同じ線を引くために、for文を使っていたのですね。
15 色の統一
ここで顔の色を見てみましょう。
実は完全にバラバラではなく、①肌の色 ②輪郭の色 がそれぞれ統一されています。
コードを見てみましょう。
実は2種類の色をそれぞれa,bという変数に格納し、a,bで色を使用しています。そのため、色に統一感が生まれています。
ちなみに、色はこんな風に格納されています。
15 Propertiesの設定
次はPropertiesの設定をしていきましょう。
この部分ですね。
コードはこんな感じです。
いつもと同じでabi.encodePacked()で文字を繋げていますね。
ポイントはあくまでも文字を繋げるものなので、数字の部分は「.toString()」で文字にしているというところだと思います。
中身はこのようになっています。
attributesで属性についての指定が始まり、各属性はtrait_typeで表していますね。
16 TokenURIの設定
randomOne()関数を使って、構造体に直接値を入れているのはすごいと思いました。
また、SVGはBase64形式にして返していました。
以上で、「OnChainMonkey」がどのように作られているのかを知ることができました。
ではこれを元に次章から具体的な作成方法を見ていきましょう。
17 実践編
こんなのを作ってみましょう。
① 上の四角はレア(必ず現れるわけではない)
② 下の丸は必ずある。
③ 色は背景色を含めて3種類
では、やっていきましょう。
18 ステップ1 構造体を作ろう
まずは、色を格納する構造体を作ります。
ここにランダムな色の番号が入ることになります。
後から加えることも可能です。
参考に、コードです。
struct Shape {
uint8 bg;
uint8 shikaku;
uint8 maru;
}
19 ステップ2 変数を作ろう
19ー1 クレーム数チェック用変数
これは作るだけです。claim関数などで使います。
19ー2 色の格納・確率用変数
①それぞれの色が入る変数
②確率を入れるための_wの配列の変数を作る
なお、少しでもコード数を減らすために、色のコードは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」という関数で使用する、ランダムを少し複雑にするための変数です。
なくても大丈夫ですし、他の文字でも大丈夫です。
参考に、コードです。
string private ra1='A';
string private ra2='B';
string private ra3='C';
19ー4 SVG簡素化用の変数
SVGを直に書いていくと、混乱の元になります。
少しでも混乱を避けるように、わかりやすい記号に変えましょう。
参考に、コードです。
string private zz='"/>';
string private co1=', ';
19ー5 属性設定用の変数
OpenSeaで「Properties」を設定するための変数を作りましょう。
不要な場合は、なくて大丈夫です。
参考に、コードです。
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を入れる用の変数を作りましょう。
参考に、コードです。
string private rl1='{"name": "OnChain Test #';
string private rl2='"}';
string private rl3='data:application/json;base64,';
19ー7 TokenURI用の変数(ノーマルsvg)
ここが一つのポイントです。
どのNFTにも必ず入るパーツなどはここに入れましょう。レアなNFTだけにあるパーツはここには入りません。
また、ランダムな色を入れたい場合、その色の部分は入れません。
参考に下がコードです。
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にあるわけではない、特別パーツを作る場合はこのように別に変数を作ります。
やはり色の部分は抜かします。
参考に、下がコードです。
string private mayu='<rect width="300" height="50" x="50" y="50" style="fill:#';
20 ステップ3 関数を作ろう
20ー1 ランダム作成用の関数を作ろう
ランダム用の関数として
① 通常ランダム ⇨ random関数
② 確率付きランダム ⇨ usew関数
を使用します。
ただ、ここはコピペでいいと思います。
参考に、下がコードです。
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 ランダム数を構造体に格納する関数を作ろう
ランダムの値を各構造体に入れるための関数を作りましょう。
ちなみに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文を使い、合致しないときには''を返すようになっているところです。
その他、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 属性確認用の関数を作ろう
属性を確認するための関数です。
コード内で使っているわけではないので、なくても大丈夫です。
参考に、下がコードです。
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に渡すために形を整えています。
参考に、下がコードです。
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化するための関数です。
ここでは特別パーツは含まれません。
参考に、以下がコードです。
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形式に変換しています。
ご不明な場合は、こちらで詳しく書いておりますので、ご参考ください。
参考に、以下がコードです。
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できる関数は必要ですね。
参考に、以下がコードです。
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度だけ行われる処理です。
コントラクトの名前とシンボル名を設定していますね。
参考に、以下がコードです。
constructor() ERC721("OnChainEyes", "OCE") Ownable() {}
いかがだったでしょうか。
今回は少し難しかったかもしれませんが、ぜひチャレンジしてみてください。
また、もし参考になったよーという方は作ったNFTの一つなどを私のウォレットに投げていただけると喜びます。
筆者のウォレット:0xB365BC5A0BA013e260B922cA2f27DC7C86279e3b
もちろん投げなくても大丈夫です。
以上です。
いいなと思ったら応援しよう!
