第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)を見てみましょう。
このコントラクトには1万のNFTがあり、一つ一つの画像やnameが異なっております。
これらはどのように紐づいているのでしょうか?
1ー2 Etherscanを見てみよう
では、上のコントラクトをEtherscanで見てみましょう。
https://etherscan.io/address/0xed5af388653567af2f388e6224dc7c4b3241c544
Etherscanからコントラクトの中身を読み取ることができます。
1ー3 コントラクトの中身を読んでみよう(Read Contract)
では、具体的にコントラクトの中身を読み取ってみましょう。
「ownerOf」に確認したいToken IDを入れて「Query」を押します。
すると、上のように、tokenIdの所有者のウォレットアドレスが表示されました。
1ー4 tokenURIを読んでみよう
では、同じようにして、tokenURIも読んでみましょう。
このように、tokenId:1には下のURIが紐づいていました。
https://ikzttp.mypinata.cloud/ipfs/QmQFkLSQysj94s5GvTHPyzTxrawwtjgiiYS2TBLgrvw8CW/1
では、このURIを見てみましょう。
下のように、色々な情報が入っています。
では、実際のOpenSeaの表示と見比べてみましょう。
このように、実際の表示と対応していることがわかりました。
1ー5 OpenSeaの仕様を見てみよう
では、TokenURIは、なぜこのような書き方になっているのでしょう。
OpenSeaの仕様を見てみましょう。
下のように、メタデータをどのようなデータ構成にするのかが書いてあります。
基本的には、このOpenSeaの仕様を元にメタデータが作られています。
このように、何か疑問点があった場合は公式のドキュメントを見ると解決することが多いです。(英語がほとんどですが。)
3 IPFSについて
3ー1 tokenURIをもう一度見てみよう
先ほどのtokenURIを見てみると、下のように「pinata」「IPFS」という文字が見えます。
そのため、「pinata」というサービスを使って「IPFS」上に保管していることが推測できました。
3ー2 pinataへのIPFS保管
3ー2ー1 pinataを見てみよう
私もまだまだ不勉強なので、IPFSの詳細は割愛します。
大まかには、ファイルをHTTPのような場所(どこにそのファイルがあるのか)ではなく、CID(コンテンツID)で指定するファイルシステムです。
この記事がとてもわかりやすいと思います。
では、簡単にIPFS保管ができる「pinata」を見てみましょう。
pinataは1GBまでであれば、無料で利用することができます。
私は無料プランを利用しています。
このように自分の管理画面で、どのファイルがどのCID(コンテンツID)として保管されているのかを確認することができます。
3ー2ー2 pinataにファイルを保存しよう
では、実際にファイルを保存してみましょう。
次のような流れでファイルを指定することで、保存することができます。
このようにファイルが保存されました!
見てみると、このように、ファイルが保存されていることを確認できました。
3ー2ー3 CID(コンテンツID)についてもう少し詳しく
せっかくなので、IPFSのCIDについて、もう少し見てみましょう。
仮にHTTPの場合はURLが変わるとアクセスすることができないと思います。
しかし、IPFSの場合、CID(コンテンツID)の前を次のように変更しても、アクセスすることができます。
大まかに、こんな仕組みになっています。
(参照)https://ipfs-book.decentralized-web.jp/building_public_gateway/
IPFSゲートウェイを通じてIPFSネットワークに行くのですが、そこで指定しているのはなんとCID(コンテンツID)だけです。
そのためIPFSはコンテンツ指向型プロトコルなどとも呼ばれます。
3ー2ー4 フォルダのIPFS保管
ファイルの時と同じような手順でフォルダのIPFS保管もすることができます。
下のようになります。
フォルダ自体にCID(コンテンツID)が与えられ、各ファイルにも独自のCIDが与えられることになります。
そのため、このファイル(例えば、1.png)は次のような2パターンで指定することができます。
① ファイルのCIDの直接指定
② フォルダを通じた指定
そして、このフォルダを通じた指定が実際のメタデータとして使用される方法になります。
4 コンパイル〜ミントまでの流れ
ここでは、後に行うコンパイルからミントまでの流れをまずは簡単に見てみます。
① コンパイル
エンジニアさんが作ったsolidityファイルはそのままでは実行ができません。
そのため、コンピュータが理解できるためのバイトコード(とABI定義)を作成します。
② デプロイ
①で作ったABI定義とバイトコードを用いて、ネットワーク上にコントラクトを作成します。
ここではRinkebyテストネットワーク上に書き込むことになるため、ガス代が発生します。
ちなみに、大まかにではありますが、バイトコードとABI定義は次のようなものです。(覚えなくも良いかもですが。)
① バイトコード ⇨ コントラクトそのものを変換したもの
② ABI定義 ⇨ コントラクトの関数を実行する時に必要
5 デプロイしよう
5ー1 Workspaceを作ろう
まずはRemixを立ち上げましょう。
Remixはオンライン上でコンパイル〜ミントなどができる便利なサービスです。
「+」ボタンを押して、任意の名前をつけましょう。
これで作業スペースができました。
5ー2 solidityのファイルを作ろう
「contracts」「scripts」などのフォルダがありますが、コントラクトのコードを入れるフォルダは「contracts」になります。
まずは「contracts」内のサンプルコードとしては入っているファイルを全て消しましょう。
もし、「artifacts」というフォルダもありましたら、そちらも消してください。(コンパイルされてできる生成物です。)
5ー3 solidityファイルを作ろう
まずは空のファイルを作りましょう。
下のように「contracts」フォルダ内で「PurpleEye.sol」というファイルを作ります。
このようにsolidityファイルの拡張子(.〜の部分)は「~.sol」になります。
今回はこちらのHashLipsというチームが作ったコントラクトをもとにテストで作っていきましょう。
こちらのコードを使います。
右の「Copy raw contents」でコピーを行ってください。
下のように貼り付けましょう。
5ー4 コントラクトの大まかな構成について
コントラクトは大まかに、次のような構成になっています。
5ー4ー1 コンストラクタについて
コンストラクタはとても大事です。
コントラクトがデプロイ(作られる)される時に1度だけ最初に実行される処理です。
ここでは
① _name(名前)
② _symbol(シンボル)
③ _initBaseURI(公開後のURI)
④ _initNotRevealedUri(公開前のURI)
の4つの値をデプロイ時にコントラクトに渡すことになります。
5ー4ー2 functionについて
functionは何かしらの処理を行うものです。
ざっくりと、こんな感じです。
細かい内容はまた次回にしましょう。
5ー5 コントラクトの修正をしてみよう
5ー5ー1 コントラクト名の修正
では、コントラクト名を修正してみましょう。
solidityのファイル名とコントラクト名は基本的に一致させます。
そのため、コントラクト名を「PurpleEye」に修正しましょう。
5ー4ー2 微修正をしてみよう
ここでは「maxSupply」(総供給量)と「maxMinAmount」 (一度にミントできる数)を変えてみましょう。
今回は
「maxSupply」(総供給量)を5個
「maxMinAmount」 (一度にミントできる数)を3個にしてみましょう。
6 コンパイル・デプロイをしてみよう!
6ー1 設定を行う
では、コンパイルを行いましょう!
下のように緑になっていたら準備OKです。
赤くなっていたら、何か打ち間違いがあると思います。(どうしてもわからなければ、末尾にコードがあるので、使ってください。)
次に、下のように設定していきます。
ポイントはRinkebyになっているかです。
これがなっていないと、実際のETHが使われてしまいますので、十分ご注意ください。
6ー2 コンストラクタの設定・デプロイ
では、コンストラクタに4つの値を入れていきましょう。
今回はこちらの4つを入れてください。
・_NAME(名前)
⇨ PurpleEye
・_SYMBOL(シンボル)
⇨ PE
・_INITBASEURI(公開後URI)
⇨ ipfs://QmYJhYes1kzp2soWYEYKzvA84V8YivL8BCpsnN773xyufr/
・_INITNOTREVEALEDURI(公開前URI)
⇨ ipfs://QmVgBb7rK8RDsWa44pRBDSdyXU47qix8QayLMFmRVye8Fp
なお、URIは次のような画像を設定ています。
①公開前の画像
②公開後の画像
できましたら、「transact」を押してください。
メタマスクを進めて待ってみると。。
このようにデプロイが完了しました!
これでコントラクト完成です。
7 ミントをしてみる
では、下のように1個ミントを行いましょう。
できたかどうか、OpenSeaのテストネットで見てみましょう。
こんな感じで、無事NFTができていました。
2個のミントも。。
できました!
3個のミントは。。。できません!
これは最大供給量を5個と設定したためですね。3個ミントしたら合計6個になっちゃいます。
つまり、設定はうまく行っているようですね。
8 画像を更新しよう
では、reveal(公開)を実行して、データを更新してみましょう。
OpenSeaの「Refresh metadata」を押して、少し待ってからリロードすると。。
こんな感じで、うまくNFTが切り替わりました!
さりげなく、nameやdescriptionも変わっていますので、見比べてみてください。
プロパティもうまく行っていそうですね。
他にも、色々と試してみてください。
ちょっと長くなってしまったので、本日は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);
// =============================================================================
}
}
サポートをしていただけたらすごく嬉しいです😄 いただけたサポートを励みに、これからもコツコツ頑張っていきます😊