見出し画像

第1回 solidity学習会 講義ノート(2/19 AM8:00~)

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

クリスペの会社です。

また、CryptoMaidsのアンバサダーも務めさせていただいております。

今回は、2/19 AM8:00から次の場所で勉強会を行いますので、その講義ノートの公開です。

場所 https://meet.google.com/zme-pohq-hcm

予習や復習などに役立てていただければ幸いです。

はじめる前に
・実施はテストアカウントで行うことを推奨いたします。

Rinkebyというテストネットで実施を行いますが、操作を誤ってしまったときに実際のETHが使われないためです。

・シークレットNFTを行いますが、技術の話と法律面の話は別物だ考えています。今回はあくまでも、技術的にどのように実現するかというのみの話になります。

1 メタデータとは

1ー1 はじめに

まずは、トークンID画像などのデータがどのように結びついているのかを見てみましょう。

例えば、次のプロジェクト(Azuki)を見てみましょう。

スクリーンショット 2022-02-17 19.57.08

このコントラクトには1万のNFTがあり、一つ一つの画像やnameが異なっております。

これらはどのように紐づいているのでしょうか?

1ー2 Etherscanを見てみよう

では、上のコントラクトをEtherscanで見てみましょう。

https://etherscan.io/address/0xed5af388653567af2f388e6224dc7c4b3241c544

スクリーンショット 2022-02-17 19.59.33

Etherscanからコントラクトの中身を読み取ることができます。

1ー3 コントラクトの中身を読んでみよう(Read Contract)

では、具体的にコントラクトの中身を読み取ってみましょう。

「ownerOf」に確認したいToken IDを入れて「Query」を押します。

スクリーンショット 2022-02-17 20.02.39

すると、上のように、tokenIdの所有者のウォレットアドレスが表示されました。

1ー4 tokenURIを読んでみよう

では、同じようにして、tokenURIも読んでみましょう。

スクリーンショット 2022-02-17 4.39.15

このように、tokenId:1には下のURIが紐づいていました。

https://ikzttp.mypinata.cloud/ipfs/QmQFkLSQysj94s5GvTHPyzTxrawwtjgiiYS2TBLgrvw8CW/1

では、このURIを見てみましょう。

下のように、色々な情報が入っています。

スクリーンショット 2022-02-17 4.45.39

では、実際のOpenSeaの表示と見比べてみましょう。

スクリーンショット 2022-02-17 4.52.44

このように、実際の表示と対応していることがわかりました。

1ー5 OpenSeaの仕様を見てみよう

では、TokenURIは、なぜこのような書き方になっているのでしょう。

OpenSeaの仕様を見てみましょう。

下のように、メタデータをどのようなデータ構成にするのかが書いてあります。

スクリーンショット 2022-02-17 5.03.39

基本的には、このOpenSeaの仕様を元にメタデータが作られています。

このように、何か疑問点があった場合は公式のドキュメントを見ると解決することが多いです。(英語がほとんどですが。)

3 IPFSについて

3ー1 tokenURIをもう一度見てみよう

先ほどのtokenURIを見てみると、下のように「pinata」「IPFS」という文字が見えます。

スクリーンショット 2022-02-17 5.19.11

そのため、「pinata」というサービスを使って「IPFS」上に保管していることが推測できました。

3ー2 pinataへのIPFS保管

3ー2ー1 pinataを見てみよう

私もまだまだ不勉強なので、IPFSの詳細は割愛します。

大まかには、ファイルをHTTPのような場所(どこにそのファイルがあるのか)ではなく、CID(コンテンツID)で指定するファイルシステムです。

この記事がとてもわかりやすいと思います。

では、簡単にIPFS保管ができる「pinata」を見てみましょう。

pinataは1GBまでであれば、無料で利用することができます。

私は無料プランを利用しています。

スクリーンショット 2022-02-17 5.59.41

このように自分の管理画面で、どのファイルがどのCID(コンテンツID)として保管されているのかを確認することができます。

3ー2ー2 pinataにファイルを保存しよう

では、実際にファイルを保存してみましょう。

次のような流れでファイルを指定することで、保存することができます。

スクリーンショット 2022-02-17 6.07.37

スクリーンショット 2022-02-17 6.08.17

このようにファイルが保存されました!

スクリーンショット 2022-02-17 6.08.41

見てみると、このように、ファイルが保存されていることを確認できました。

スクリーンショット 2022-02-17 6.08.54

3ー2ー3 CID(コンテンツID)についてもう少し詳しく 

せっかくなので、IPFSCIDについて、もう少し見てみましょう。

スクリーンショット 2022-02-17 6.26.25

仮にHTTPの場合はURLが変わるとアクセスすることができないと思います。

しかし、IPFSの場合、CID(コンテンツID)の前を次のように変更しても、アクセスすることができます。

スクリーンショット 2022-02-17 6.22.31

大まかに、こんな仕組みになっています。

スクリーンショット 2022-02-17 7.54.44

(参照)https://ipfs-book.decentralized-web.jp/building_public_gateway/

IPFSゲートウェイを通じてIPFSネットワークに行くのですが、そこで指定しているのはなんとCID(コンテンツID)だけです。

そのためIPFSはコンテンツ指向型プロトコルなどとも呼ばれます。

3ー2ー4 フォルダのIPFS保管

ファイルの時と同じような手順でフォルダのIPFS保管もすることができます。

下のようになります。

スクリーンショット 2022-02-17 8.04.06

フォルダ自体にCID(コンテンツID)が与えられ、各ファイルにも独自のCIDが与えられることになります。

そのため、このファイル(例えば、1.png)は次のような2パターンで指定することができます。

① ファイルのCIDの直接指定

スクリーンショット 2022-02-17 8.11.05

② フォルダを通じた指定

スクリーンショット 2022-02-17 8.10.51

そして、このフォルダを通じた指定が実際のメタデータとして使用される方法になります。

4 コンパイル〜ミントまでの流れ

ここでは、後に行うコンパイルからミントまでの流れをまずは簡単に見てみます。

① コンパイル

エンジニアさんが作ったsolidityファイルはそのままでは実行ができません。

そのため、コンピュータが理解できるためのバイトコード(とABI定義)を作成します。

スクリーンショット 2022-02-17 9.37.12

② デプロイ

①で作ったABI定義とバイトコードを用いて、ネットワーク上にコントラクトを作成します。

スクリーンショット 2022-02-17 9.37.59

ここではRinkebyテストネットワーク上に書き込むことになるため、ガス代が発生します。

ちなみに、大まかにではありますが、バイトコードABI定義は次のようなものです。(覚えなくも良いかもですが。)

① バイトコード ⇨ コントラクトそのものを変換したもの
② ABI定義 ⇨ コントラクトの関数を実行する時に必要

5 デプロイしよう

5ー1 Workspaceを作ろう

まずはRemixを立ち上げましょう。

Remixはオンライン上でコンパイル〜ミントなどができる便利なサービスです。

「+」ボタンを押して、任意の名前をつけましょう。

スクリーンショット 2022-02-17 10.13.18

これで作業スペースができました。

5ー2 solidityのファイルを作ろう

「contracts」「scripts」などのフォルダがありますが、コントラクトのコードを入れるフォルダは「contracts」になります。

まずは「contracts」内のサンプルコードとしては入っているファイルを全て消しましょう。

スクリーンショット 2022-02-17 10.13.40

もし、「artifacts」というフォルダもありましたら、そちらも消してください。(コンパイルされてできる生成物です。)

5ー3 solidityファイルを作ろう

まずは空のファイルを作りましょう。

下のように「contracts」フォルダ内で「PurpleEye.sol」というファイルを作ります。

スクリーンショット 2022-02-17 10.47.49

このようにsolidityファイルの拡張子(.〜の部分)は「~.sol」になります。

今回はこちらのHashLipsというチームが作ったコントラクトをもとにテストで作っていきましょう。

こちらのコードを使います。

右の「Copy raw contents」でコピーを行ってください。

スクリーンショット 2022-02-17 10.20.17

下のように貼り付けましょう。

スクリーンショット 2022-02-17 10.53.34

5ー4 コントラクトの大まかな構成について

コントラクトは大まかに、次のような構成になっています。

スクリーンショット 2022-02-17 17.28.51

5ー4ー1 コンストラクタについて

コンストラクタはとても大事です。

コントラクトがデプロイ(作られる)される時1度だけ最初に実行される処理です。

スクリーンショット 2022-02-17 17.36.25

ここでは

① _name(名前)
② _symbol(シンボル)
③ _initBaseURI(公開後のURI)
④ _initNotRevealedUri(公開前のURI)

の4つの値をデプロイ時にコントラクトに渡すことになります。

5ー4ー2 functionについて

functionは何かしらの処理を行うものです。

ざっくりと、こんな感じです。

スクリーンショット 2022-02-17 17.49.38

スクリーンショット 2022-02-17 17.49.48

細かい内容はまた次回にしましょう。

5ー5 コントラクトの修正をしてみよう

5ー5ー1 コントラクト名の修正

では、コントラクト名を修正してみましょう。

solidityのファイル名コントラクト名は基本的に一致させます。

そのため、コントラクト名を「PurpleEye」に修正しましょう。

スクリーンショット 2022-02-17 17.14.25

5ー4ー2 微修正をしてみよう

ここでは「maxSupply」(総供給量)と「maxMinAmount」 (一度にミントできる数)を変えてみましょう。

スクリーンショット 2022-02-17 17.57.45

今回は
「maxSupply」(総供給量)を5個
「maxMinAmount」 (一度にミントできる数)を3個にしてみましょう。

6 コンパイル・デプロイをしてみよう!

6ー1 設定を行う

では、コンパイルを行いましょう!

下のように緑になっていたら準備OKです。

スクリーンショット 2022-02-17 18.04.27

赤くなっていたら、何か打ち間違いがあると思います。(どうしてもわからなければ、末尾にコードがあるので、使ってください。)

次に、下のように設定していきます。

スクリーンショット 2022-02-17 18.08.16

ポイントはRinkebyになっているかです。

これがなっていないと、実際のETHが使われてしまいますので、十分ご注意ください。

6ー2 コンストラクタの設定・デプロイ

では、コンストラクタに4つの値を入れていきましょう。

今回はこちらの4つを入れてください。

・_NAME(名前) 
 ⇨ PurpleEye
・_SYMBOL(シンボル)
 ⇨ PE
・_INITBASEURI(公開後URI)
 ⇨ ipfs://QmYJhYes1kzp2soWYEYKzvA84V8YivL8BCpsnN773xyufr/
・_INITNOTREVEALEDURI(公開前URI)
 ⇨ ipfs://QmVgBb7rK8RDsWa44pRBDSdyXU47qix8QayLMFmRVye8Fp

なお、URIは次のような画像を設定ています。

①公開前の画像

スクリーンショット 2022-02-17 20.24.16

②公開後の画像

スクリーンショット 2022-02-17 20.24.33

できましたら、「transact」を押してください。

スクリーンショット 2022-02-17 18.52.32

メタマスクを進めて待ってみると。。

スクリーンショット 2022-02-17 18.54.22

このようにデプロイが完了しました!

これでコントラクト完成です。

7 ミントをしてみる

では、下のように1個ミントを行いましょう。

スクリーンショット 2022-02-17 18.56.32

できたかどうか、OpenSeaのテストネットで見てみましょう。

https://testnets.opensea.io/

スクリーンショット 2022-02-17 19.30.54

こんな感じで、無事NFTができていました。

スクリーンショット 2022-02-17 18.57.29

2個のミントも。。

スクリーンショット 2022-02-17 18.58.09

できました!

スクリーンショット 2022-02-17 19.00.27

3個のミントは。。。できません!

スクリーンショット 2022-02-17 18.59.49

これは最大供給量を5個と設定したためですね。3個ミントしたら合計6個になっちゃいます。

つまり、設定はうまく行っているようですね。

8 画像を更新しよう

では、reveal(公開)を実行して、データを更新してみましょう。

スクリーンショット 2022-02-17 19.00.07

OpenSeaの「Refresh metadata」を押して、少し待ってからリロードすると。。

スクリーンショット 2022-02-17 19.00.35

こんな感じで、うまくNFTが切り替わりました!

スクリーンショット 2022-02-17 19.00.59

さりげなく、namedescriptionも変わっていますので、見比べてみてください。

プロパティもうまく行っていそうですね。

スクリーンショット 2022-02-17 19.38.48

他にも、色々と試してみてください。

スクリーンショット 2022-02-17 19.01.33

ちょっと長くなってしまったので、本日はverifyまで行かずに、この辺りで終了したいと思います。

ありがとうございました!

9 想定質問

9ー1 Rinkebyはどうやって取得するの?

こちらをご参照ください。

9ー2 Rinkebyネットワークがメタマスクにない

9ー3 画像が表示されない。

コントラクトに渡した値が違っている可能性があります。コピペミスの可能性があるのでスラッシュなどに気をつけながらもう一度デプロイしてみてください。

9ー4 Remixでメタマスクのポップアップが出ない

こちらでお試しください。


9ー5 コードのエラーが直らない

全角になっていないか、などもう一度みてみて下さい。

それでもわからない場合は、下のコードを利用して下さい。

// 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";
contract PurpleEye is ERC721Enumerable, Ownable {
 using Strings for uint256;
 string baseURI;
 string public baseExtension = ".json";
 uint256 public cost = 0.05 ether;
 uint256 public maxSupply = 20;
 uint256 public maxMintAmount = 3;
 bool public paused = false;
 bool public revealed = false;
 string public notRevealedUri;
 constructor(
   string memory _name,
   string memory _symbol,
   string memory _initBaseURI,
   string memory _initNotRevealedUri
 ) ERC721(_name, _symbol) {
   setBaseURI(_initBaseURI);
   setNotRevealedURI(_initNotRevealedUri);
 }
 // internal
 function _baseURI() internal view virtual override returns (string memory) {
   return baseURI;
 }
 // public
 function mint(uint256 _mintAmount) public payable {
   uint256 supply = totalSupply();
   require(!paused);
   require(_mintAmount > 0);
   require(_mintAmount <= maxMintAmount);
   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"
   );
   
   if(revealed == false) {
       return notRevealedUri;
   }
   string memory currentBaseURI = _baseURI();
   return bytes(currentBaseURI).length > 0
       ? string(abi.encodePacked(currentBaseURI, tokenId.toString(), baseExtension))
       : "";
 }
 //only owner
 function reveal() public onlyOwner {
     revealed = true;
 }
 
 function setCost(uint256 _newCost) public onlyOwner {
   cost = _newCost;
 }
 function setmaxMintAmount(uint256 _newmaxMintAmount) public onlyOwner {
   maxMintAmount = _newmaxMintAmount;
 }
 
 function setNotRevealedURI(string memory _notRevealedURI) public onlyOwner {
   notRevealedUri = _notRevealedURI;
 }
 function setBaseURI(string memory _newBaseURI) public onlyOwner {
   baseURI = _newBaseURI;
 }
 function setBaseExtension(string memory _newBaseExtension) public onlyOwner {
   baseExtension = _newBaseExtension;
 }
 function pause(bool _state) public onlyOwner {
   paused = _state;
 }
 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);
   // =============================================================================
 }
}


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