見出し画像

【保存版】フルオンチェーンNFTの作り方_完全版(入門レベル)

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

↓私、大量のエアドロなどを行っています。よかったらぜひ。

今回は、私がフルオンチェーンNFTを最初に学んだときに参考にしたYouTubeをもとに解説していきます。

ちなみにこのYouTube、かなり無駄な工程も含まれていました(汗)

ただ、「なぜそれをやるのか」を考えることで、理解が深まるかと思いましたので、ぜひ一緒にみていきましょう。


1 SVGデータを作成しよう

フルオンチェーンNFTを作るには、データ量を抑える必要があります。

そして、その手段として多く使われるのが、SVGというデータ形式です。

画像を、ドットではなくXY座標で描こうという考え方です。

まずは、やってみましょう。

例えば、こんなサイトでSVGデータを作ることができます。

とりあえずこんな感じで作ってみました。

Width, Hightがサイズで、Colorで色を決められます。

左の「T」で文字を入力し。右の「Algin to canvas」で文字の位置を調節します。

スクリーンショット 2021-12-11 17.23.20

あと、図形も入れたかったので、なんとなくこんな感じにしました。

なんでもいいと思います。

スクリーンショット 2021-12-11 17.43.45

図形が完成しましたら、「File」 > 「Save Image」でSVGファイルがダウンロードされます。

左下にsvg(15).svgというものができていますね。

画像40


2 作成したSVGをコードで見てみよう

できたSVGファイルをVisual Studio Codeで開きましょう。

もちろん、SVGとして開けるようでしたら別のエディタでも構いません。

もし「Visual Studio Code」って何?という方がいらっしゃいましたら、とても使いやすいエディタなので、ググってインストールなどするのも良いかもしれません

スクリーンショット 2021-12-11 17.45.54

開くとこんな感じでSVGのコードを見ることができます。

そうです、もうSVGは作ることができました。そしてSVGというコードで作られています。

スクリーンショット 2021-12-11 17.46.46


3 作成したSVGをブラウザで見てみよう

ちなみに、ブラウザに先ほどのSVGを持っていくと、こんな感じに画像としてもできていることがわかります。

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


4 SVGをBase64形式にしよう

SVGはそのままではブロックチェーンに記載することができません。Base64という記載方法でブロックチェーンに記すことになります。

では、その方法を見てみましょう。

例えば、こんな変換サイトに行ってみましょう。(もちろん別の変換サイトでも大丈夫です。)

左のように画像を取り組むと、右のように何やら難しそうな文字に変換されました。

これがBase64です。

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


5 Base64形式でのブラウザに表示しよう

ここで、一つ実験をしてみましょう。

左が右に変換されたということは、左と右はイコールです。

では、右のコードをコピペして、ブラウザに貼り付けてみたらどうなるでしょう??

スクリーンショット 2021-12-11 17.58.15

こんな感じになりました。

これでBase64は結局最初のSVGとイコールであり、Base64の状態でもブラウザで表示されるということがわかりました。


6 HashLipsのコードをコピーしよう

こちらの「NFT_START.sol」というコードを用います。

スクリーンショット 2021-12-11 18.04.47

★重要★
今回はサンプルとして、コードをそのまま用いますが、実際に行うときは修正すべき点はないか、おかしなところはないかを確認の上、ご自身の責任の上、行ってください。

こちらのコードをコピーします。

スクリーンショット 2021-12-11 18.07.16


7 Remixでsolファイルを作ろう(メインコントラクト)

Remixにつきましては、こちらの記事の13章をご確認ください。

ここでは、違う部分のみ取り上げます。

任意の名前でワークスペースを作ります。

スクリーンショット 2021-12-11 18.12.26

使わないコントラクトを消し、

スクリーンショット 2021-12-11 18.10.53

任意の名前の.solファイルを作り(ここではOnChainNFT.sol)先ほどのコードをコピペします。

スクリーンショット 2021-12-11 18.13.34


8 Remixでsolファイルを作ろう(Base64コントラクト)

また、同じフォルダ内に、「Base64.sol」というファイルも作り、

スクリーンショット 2021-12-11 18.15.24

HashLipsのこちらのコードもコピペして、今作ったファイルに貼り付けましょう。

スクリーンショット 2021-12-11 18.16.28


9 Base64のimportとコントラクト名の設定

では、これでコントラクトの準備が整いましたので、あとは調整していきましょう。

まずはメインのコントラクトに先ほどのBase64.solを読み込ませましょう(インポート)

20行目のように書きました。

import "./Base64.sol";

上の章で作りましたBase64.solを読み込みますので、ファイルがある場所を指定して、「import」を行います。

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

では、続いてコントラクトの名前を設定しましょう。

22行目のように「OnChainNFT」としました。

基本的にファイル名と一致させるようにしましょう。

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


10 オンチェーンに不要な機能を削る

ここからは、、正直めんどくさいと思います。

元々こちらはOnChain用のコントラクトではないようで、不要な部分がたくさんありました。

ただ、削除の過程で、なぜそれがいらないのかを学ぶことができると思います。

それを考えながら、消していきましょう。

10ー① baseURIを削る

baseURIはNFTとメタデータを結びつけるものです。

オンチェーンNFTはすでに画像などの情報が全てブロックチェーン上にあり、メタデータを参照しないので、baseURI関連のものは使用しません。

baseURIと書かれているあたりを検索(Command+「F」など)しながら消していきましょう。

baseURIと書かれているものは容赦なく消していきます。

例えば下の場合、89行目から92行目までをごっそり消します。(93行目のかっこは全体にかかっているので、こちらを消したらエラーになります。)

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

最終的に右のように「baseURI」で「No result」となればOKです。

スクリーンショット 2021-12-11 18.30.21


10ー② baseExtensionを削る

こちらは、メタデータとして、どんな拡張子のものを取得するかを指定しています。

やはりメタデータは使わなくなりましたので、①と同様、全て消しましょう。


10ー③ 「maxMintAmount」「paused」「revealed」「notRevealedUri」を削る

これらは、上の二つと異なり、あっても良いが、なくても構わないといったものです。

なぜこれらを消すのでしょう?

それはフルオンチェーンの場合、元々コード量が多くなり、デプロイ時にガス代(お金)が多くかかってしまう傾向にあるため、少しでもコード量を減らしたいためです。

ちなみに上の4つは

①「maxMintAmount」 
 > 一人当たりのミント量を制限
②「paused」 
 > コントラクトを止める
③「revealed」 
 > NFTを隠す(すみません、これはちょと自信ないです。)
④「notRevealedUri」 
 > 「?」状態の時に表示させるメタデータのURI

のようです。

なくても構わないので、消していきましょう。

ちなみに、細かい部分ですが、消す過程でこんなエラーが出ました。

こちらは_symbolの後に「,」はいらないよと言っているだけですので、慌てず「,」を消してみてください。

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


11 コンストラクタもすっきりさせましょう。

元々のコードでは、デプロイ時に外からコントラクト名やシンボル名を渡して、それを書き込んでいました。

これを直し、直接ブロックチェーンの中に書き込んでみましょう。

スクリーンショット 2021-12-11 18.57.00

これを

こんな感じにしました。

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

とってもシンプルですね。

「On Chain NFT」がコントラクト名、「OCN」がシンボル名になります。


12 SVGもすっきりさせよう

スクリーンショット 2021-12-11 19.04.53

では、次にSVGの無駄を削っていきましょう。

①<g> <title>

 <g>  ⇨ グループとして囲っています。
<title> ⇨ その名の通り、タイトルです。

ともにいらないので、消していきます。

②stroke属性、stroke-width属性

図形の外側の色を定義しているようです。今回は、使っていないので、消しましょう。

③id

こちらも、使っていないので、消しましょう。

最終的にこんな感じになりました。

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

だいぶすっきりしました。


13 SVGをBase64形式にして取得しよう

今回が目玉の一つです。

大まかにこんなことをやってみようと思います。

①SVGを一つなぎの文字列にする
②繋がったSVGをBase64の形式に変換する
②Base64をブロックチェーンに書き込む


13ー① SVGを一つなぎの文字列にする

こんな関数をコントラクトに追加します。

 function buildImage() public pure returns(string memory) {
   return string(abi.encodePacked());
   }

なんだか難しそうですが、ざっくりと説明しますね。

今回のSVGは下のように7行で構成されていました。

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

でもこの7行のままではBase64というコントラクトで変換ができないので、一つなぎの文字列にするにします。実はこれだけです。

では見ていきましょう。

 13ー①(1) 関数についての大まかな説明

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

ここでは

function buildImage()

まずは、この部分でbuildImage()という、引数を持たない関数ということを宣言しています。

public pure

まず、publicはこの関数を外からも見ることができることを示しています。

pureはこの関数はブロックチェーンの情報を

①書き換えない
②見にもいかない

ことを示しています。

returns(string memory)

stringは文字列で、memoryは一時的に保管する場所の意味です(ブロックチェーンに書き込むのはstorage

一時的に保管した文字列を返しますという意味です。

 13ー①(2) abi.encodePacked() (大事)


abi.encodePacked()

ここは文字列を繋げるという意味です。

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

それをStringで囲んで、文字列にして返すということです。

なんとなくやることは伝わったのではないでしょうか。もともとやりたかったのは7行のSVGを1行にすることでしたね

 13ー①(3)  SVGを入れ込む


では、中身を埋めていきましょう。

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

こんな感じで、SVGのそれぞれの行を「’」で囲っているだけですね。(最後以外は「,」で繋げています。)

これは7行を1行にしているだけでした。そして、この1行にした文字列を返しています。

13ー②繋がったSVGをBase64の形式に変換する


では、次にSVGをBase64形式にしてみましょう。

Base64形式にするために、インポートしたBase64コントラクトを見てみましょう。

 13ー②(1)Base64.encodeの構成


ここも難しそうですが、落ち着いて見てみれば大丈夫です。

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

「Base64」というライブラリの中に「encode」という関数が入っています。

この場合、使い方は

 Base64.encode( 中身 );

という感じになります。

Base64ライブラリの(.)encode関数というとてもわかりやすい書き方ですね。

では先ほどの一行の部分をこの中に入れたいので、先ほどの全体をこれで囲ってみましょう。

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

これでよし!と思いましたが、なんか左に赤く「!」と出ていますね。

エラーのようです。

なんでしょう。。。

 13ー②(2)Base64.encodeの引数(bytes)


encodeの関数を見てみると、必要なのは

bytes memory dataとありますね。

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

つまり、bytes型の一時保管のデータが欲しいのに、String型(文字型)の一時保管のデータを渡されたので、違うよと言われていたのです。

では下のように、Base64.encodeに渡す型をStringからBytesに直してみましょう。

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

これで無事、エラーが無くなりました。

14 Token URI

次はToken URIをやってみましょう。

14ー① OpenSeaを見てみよう

まず前提として、どのようなデータが必要かをOpenSeaのドキュメントで確認してみましょう。

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

この中で、特に重要なデータは

①name
②description
③image

の3つです。

14ー② TokenURIを書いてみよう

 14ー②(1)string(abi.encodePacked()) について

では、TokenURIの箇所に次のように書いてみましょう。

return string(abi.encodePacked(
       
));

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

まず、TokenURIとして、文字列を返すので、全体をStringで囲んでいます。

次に、トークンURIは繋がった文字列で返すので、「abi.encodePacked()」で囲み、その中のものを繋げています。

 14ー②(2)data:application/json;base64について

では、中身を追加してみましょう。

'data:application/json;base64,', Base64.encode(bytes(abi.encodePacked() ​

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

まず、大まかな構成としては

' ① ' , Base64.encode( ② )

となっており、①とBase64.encode( ② )を繋げています。

まず、①を見てみましょう。

'data:application/json;base64,'

これは、どのようにデータとして、トークンURIを設定しているかを示しています。

先ほどのOpenSeaのページでの書き方はJSON形式です。

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

そのJSON形式をBase64のフォーマットに変換してTokenURIとして設定するので、このような書き方がされています。

②の方の構成も見てみましょう。

Base64.encode(②) ​

Base64.encode()で、かっこの中をBase64ライブラリのencode関数で変換するという意味でしたね。

全体を見てみましょう。

Base64.encode(bytes(abi.encodePacked() ​

Base64.encode()は前章で見たようにbytes型を受け取るので、abi.encodePacked()で繋げたものをbytes型に変換してから、もらうという意味合いになります。

 14ー②(3)中身を書いてみよう

では、最後です。

最初はめんくらうかもしれません。

'{"name": "OnChainTest',
'", "description": "First test'
'", "image": "data:image/svg+xml;base64,',
buildImage(),
'"}'

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

大まかにみると、こんな感じになっています。

abi.encodePacked('①name群', '②description', '③image', buildImage(), '④}')

結局は「'」で文字を囲って「,」で繋げているというだけですね。

data:image/svg+xml;base64

こちらに着目してみましょう。

多くの場合、imageはどこかの場所に保管されているpngデータなどを確認します。今回はそうでないため、そうでないことを書きます。

imageのデータはSVG形式、もしくはXML形式のものをBase64に変換したものだということを示しています。

そして

buildImage(),

こちらで13章で作ったbuildImage()関数を呼び出しているのですね。


長かったですが、これで完了です。

15 作ったものを見てみよう

では、コンパイル・デプロイ・OpenSeaでの確認を行ってみましょう。

以前の記事のやり方と全く同じですので、ここでは省略します。

やり方が不明な場合は、こちらをご参照ください。

なお、特にデプロイ時には意図せずにEthereumにならないよう、十分注意して実行してください。

では、結果をみてみましょう。

https://testnets.opensea.io/assets/0xa5f7057fa42ebe9b6fab29523a2510b1ddf63116/1

スクリーンショット 2021-12-11 22.23.50

ちなみに、Token URIはこんな感じになっていました。

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

こちらのBase64をデコードすると。。

スクリーンショット 2021-12-11 22.26.12

こうなりました。

さらにimage部分をデコードすると。。

スクリーンショット 2021-12-11 22.27.42

このように変換されました。

これでSVG形式のものがBase64に変換され、それがブロックチェーン上に直接書かれ、表示されていることがわかりました。


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

意外と簡単だったのではないでしょうか?

よかったらぜひ作ってみてくださいね。

以下、SVG以外の箇所の参考です。

// SPDX-License-Identifier: MIT
// Amended by HashLips
/**
   !Disclaimer!
   These contracts have been used to create tutorials,
   and was created for the purpose to teach people
   how to create smart contracts on the blockchain.
   please review this code on your own before using any of
   the following code for production.
   HashLips will not be liable in any way if for the use 
   of the code. That being said, the code has been tested 
   to the best of the developers' knowledge to work as intended.
*/
pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./Base64.sol";
contract OnChainNftTest is ERC721Enumerable, Ownable {
 using Strings for uint256;
 uint256 public cost = 0.05 ether;
 uint256 public maxSupply = 10000;
 constructor() ERC721("OnChainNftTest", "OCNT") {}

 // public
 function mint(uint256 _mintAmount) public payable {
   uint256 supply = totalSupply();
   require(_mintAmount > 0);
   require(supply + _mintAmount <= maxSupply);
   if (msg.sender != owner()) {
     require(msg.value >= cost * _mintAmount);
   }
   for (uint256 i = 1; i <= _mintAmount; i++) {
     _safeMint(msg.sender, supply + i);
   }
 }
 function walletOfOwner(address _owner)
   public
   view
   returns (uint256[] memory)
 {
   uint256 ownerTokenCount = balanceOf(_owner);
   uint256[] memory tokenIds = new uint256[](ownerTokenCount);
   for (uint256 i; i < ownerTokenCount; i++) {
     tokenIds[i] = tokenOfOwnerByIndex(_owner, i);
   }
   return tokenIds;
 }
 function tokenURI(uint256 tokenId)
   public
   view
   virtual
   override
   returns (string memory)
 {
   require(
     _exists(tokenId),
     "ERC721Metadata: URI query for nonexistent token"
   );
   return string(abi.encodePacked(
     'data:application/json;base64,', Base64.encode(bytes(abi.encodePacked(
       '{"name": "OnChainTest',
     '", "description": "First test'
     '", "image": "data:image/svg+xml;base64,',
     buildImage(),
     '"}'
     )))));
 }
 
 function setCost(uint256 _newCost) public onlyOwner {
   cost = _newCost;
 }
 function withdraw() public payable onlyOwner {
   // This will pay HashLips 5% of the initial sale.
   // You can remove this if you want, or keep it in to support HashLips and his channel.
   // =============================================================================
   (bool hs, ) = payable(0x943590A42C27D08e3744202c4Ae5eD55c2dE240D).call{value: address(this).balance * 5 / 100}("");
   require(hs);
   // =============================================================================
   
   // This will payout the owner 95% of the contract balance.
   // Do not remove this otherwise you will not be able to withdraw the funds.
   // =============================================================================
   (bool os, ) = payable(owner()).call{value: address(this).balance}("");
   require(os);
   // =============================================================================
 }
function buildImage() public pure returns(string memory) {
  return Base64.encode(bytes(abi.encodePacked(
  )));
  }
}

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