CryptoZombies でゲーム作りながらブロックチェーンとNFTについて勉強してみる
CryptoZombies とは?
CryptoZombies is an interactive school that teaches you all things technical about blockchains. Learn to make smart contracts in Solidity by making your own crypto-collectibles game.
CryptoZombiesはブロックチェーンに関する技術的なことをすべて教えてくれるインタラクティブな学校です。独自の暗号収集ゲームを作ることで、Solidityでスマートコントラクトを作ることを学びます。
とあるように、ゲームを作りながらブロックチェーンやスマートコントラクトなどの技術について学ぶことができるコンテンツらしい。
コンテンツ自体は GitHub 上で Open になっているので、日本語化まわりで問題を見つけたら Pull Request を投げてみると良さそう。
コンテンツの構成
- Course
- Lesson
- Chapter
- Lesson
という形で構成されている
現時点で用意されている Cource は以下の 4 つで日本語化されているのは 1. Solidity のみ。
- Solidity
- Advanced Solidity
- Chainlink
- Beyond Ethereum
Cource.1: Solidity Path の構成
コースのサブタイトルは以下のようになっている
Solidity Path: Beginner to Intermediate Smart Contracts
Solidityの道。スマートコントラクトの初級から中級
コースは以下の 6 つのレッスンで構成されている
- ゾンビファクトリーの作成 (Capter 1 ~ 15)
- ゾンビが人間を襲う ( Chapter 1 ~ 15 )
- Solidity の高度なコンセプト
- ゾンビのバトルシステム
- ERC721 とクリプト収集物
- アプリのフロントエンドと Web3.js
Cource 1. / Lesson 1. : ゾンビファクトリーの作成
コントラクト
pragma solidity ^0.4.19;
contract HelloWorld {
}
状態変数
contract Example {
// この部分がブロックチェーン上に記載される
uint myUnsignedInteger = 100;
}
構造体
struct Person {
uint age;
string name;
}
// 構造体を生成する
Person satoshi = Person(172, "Satoshi");
配列
// 2要素の固定長の配列
uint[2] fixedArray;
// 可変長配列
uint[] dynamicArray;
// 構造体の配列
Person[] people;
// Public な配列 (自動的に getter メソッドが作成される)
Person[] public people;
// 配列に値を追加するには `push` メソッドを使用する
dynamicArray.push(1);
dynamicArray.push(4);
// 構造体配列に値を追加する
people.push(Person(123, "hoge");
関数
// 関数はデフォルトで public
function eatHamburgers(string _name, uint _amount) {
// グローバル変数と区別するため、引数の名前は `_` で始める(という慣習らしい)
}
// private な関数は関数名の後に `private` を付与する
// 関数名を `_` で始めるのが慣習となっている
function _doPrivateThing(uint _number) private {
// (なにか処理)
}
// 関数は戻り値を返すことができ、関数宣言には戻り値の型を指定する
function sayHello() public returns (string) {
return greeting;
}
// 関数の修飾子 view を指定するとコントラクトが持っているデータの読み取りしか行えない関数となる
function sayHello() public view returns (string) {
}
// pure 関数はコントラクトのデータへのアクセスが一切できない関数になる
function _multiply(uint a, uint b) private pure returns (uint) {
return a * b;
}
Keccak256
イーサリアムにはSHA3のバージョンの一つであるkeccak256が組み込まれている。
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5
keccak256("aaaab");
//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
keccak256("aaaac");
イベント
コントラクト側 ( イベントの宣言と適切なタイミングでの発火を行う )
// イベントの宣言 (contract)
event IntegersAdded(uint x, uint y, uint result);
function add(uint _x, uint _y) public {
uint result = _x + _y;
// 関数が呼ばれたことをアプリに伝えるためにイベントを発生させる:
IntegersAdded(_x, _y, result);
return result;
}
フロントエンド側 ( 特定のイベントをリッスンして発火されたら対応する処理を行う )
// イベントのリッスン (frontend)
YourContract.IntegersAdded(function(error, result) {
// 結果について何らかの処理をする
})
// コントラクトのメソッドを呼び出す
YourContract.add(1, 2)
こんなイメージ? (シーケンス図)
今の時点で、いろいろわからないことはある
- イベントを発火する起因となった frontend と、イベントを受け取る frontend が必ず同一であるべきなのか?
- そもそも、イベントを受け取るのは frontend だけなのか?
Cource 1 / Lesson 2 : ゾンビが人間を襲う
Address
ブロックチェーンのネットワーク上でコントラクトの実態に対して、それをユニークに識別するために与えられたアドレスというイメージ?
Mapping
key の型と value の型を指定して作る key-value store 的なイメージ?
以下のような mapping の定義は、 address
型を key として uint
型の value を格納する accountBalance
という key-value store を作っているイメージ?
mapping (address => uint) public accountBalance;
Msg.sender
msg.sender
はすべての関数で利用できるグローバル変数で、その関数を呼び出したユーザー(あるいはコントラクト) を指す address
を参照することができる。
mapping (address => uint) favoriteNumber;
function setMyNumber(uint _myNumber) public {
// ここでは`favoriteNumber` mappingを更新して、`msg.sender`下に`_myNumber`を格納するぞ。
favoriteNumber[msg.sender] = _myNumber;
// mappingにデータを格納するのは、こう書くのだ
}
function whatIsMyNumber() public view returns (uint) {
// 送信者のアドレスに格納されている値を受け取る
// もし送信者が`setMyNumber`を呼び出さなかった場合は`0`だ
return favoriteNumber[msg.sender];
}
Require
アサーションのように、「ある条件」を満たしていないとそこでエラーを投げて実行を止めることができる。
function sayHiToVitalik(string _name) public returns (string) {
// まず_nameが"Vitalik"と同じかどうか比較する。真でなければエラーを吐いて終了させる。
// (注:Solidityはネイティブで文字列比較ができない。そこで文字列の比較を
// するためにkeccak256 を使ってハッシュ同士を比較する方法を使うのだ。
require(keccak256(_name) == keccak256("Vitalik"));
// もし真ならば、関数を処理する:
return "Hi!";
}
継承
contract Doge {
function catchphrase() public returns (string) {
return "So Wow CryptoDoge";
}
}
// `BabyDoge` は `Doge` を継承しているため、 `Doge` で定義された public 関数にアクセスが可能である。
contract BabyDoge is Doge {
function anotherCatchphrase() public returns (string) {
return "Such Moon BabyDoge";
}
}
Import
// このコントラクトと同じディレクトリに存在する someothercontract.sol というファイルを読み込む
import "./someothercontract.sol";
contract newContract is SomeOtherContract {
}
Storage
- 関数外で宣言された状態変数
- ブロックチェーン上で永続化される
Memory
- 関数内で宣言された変数
- 関数の呼び出しが終われば消える
変数宣言で strage か memory かを明示することを求められる場合
関数外で宣言された状態変数 (Storage) を関数内で宣言した変数で参照する場合に、その参照先が Storage なのか一時的にコピーされた Memory として扱うのか、をコンパイラに対して指示する必要があるっぽい。
Sandwich[] sandwiches;
function eatSandwich(uint _index) public {
// Sandwich mySandwich = sandwiches[_index];
// ^ かなり簡単に見えるが、この場合Solidityが明示的に`storage` や `memory`を
// 宣言するように警告が出るはずだ。
// そこで、`storage`と宣言してみるぞ:
Sandwich storage mySandwich = sandwiches[_index];
//...この場合`mySandwich`がstorage内の`sandwiches[_index]`を
// 示すポインタだから...
mySandwich.status = "Eaten!";
// ...これで`sandwiches[_index]`をブロックチェーン上でも永久に変更することになる。
// コピーしたいだけなら、`memory`の方が便利だ:
Sandwich memory anotherSandwich = sandwiches[_index + 1];
// ...この場合`anotherSandwich`は memory内のデータを
// コピーすることになり...
anotherSandwich.status = "Eaten!";
// ...一時的な変数を変更するだけで、`sandwiches[_index + 1]`には
// なんの影響もない。次のようにすることも可能だ:
sandwiches[_index + 1] = anotherSandwich;
// ...ブロックチェーンのstorageに変更したい場合はこうだ。
}
関数の可視性としての Internal と External
contract Sandwich {
uint private sandwichesEaten = 0;
// この関数は private な関数とは違って継承したコントラクト側からも呼び出せる
function eat() internal {
sandwichesEaten++;
}
// external は宣言したコントラクト以外からのみ呼び出せる(?)
function xxx() external {
}
}
contract BLT is Sandwich {
uint private baconSandwichesEaten = 0;
function eatWithBacon() public returns (string) {
baconSandwichesEaten++;
// `eat`メソッドはinternalで宣言されているから呼び出すことが可能だ
eat();
}
}
Interface
コントラクトから他のコントラクトの関数を呼び出すことができる、そのときに呼び出す先のコントラクトの情報として Interface を定義する。
// こういうコントラクトがすでに存在するとして
contract LuckyNumber {
mapping(address => uint) numbers;
function setNum(uint _num) public {
numbers[msg.sender] = _num;
}
function getNum(address _myAddress) public view returns (uint) {
return numbers[_myAddress];
}
}
// 呼び出す側で以下のように interface を定義する
contract NumberInterface {
// interface では関数のシグネチャだけを宣言してその中身については記述していないことに注目
function getNum(address _myAddress) public view returns (uint);
}
// インターフェースを利用するコントラクト
contract MyContract {
address NumberInterfaceAddress = 0xab38...;
// ここは、イーサリアム上のFavoriteNumberコントラクトのアドレスが入る。
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// `numberContract`は他のコントラクトを指し示すものになっているぞ
function someFunction() public {
// コントラクトから`getNum`を呼び出せるぞ:
uint num = numberContract.getNum(msg.sender);
// ...よし、`num`を操作するぞ。
}
}