Open5

CryptoZombies でゲーム作りながらブロックチェーンとNFTについて勉強してみる

snakasnaka

CryptoZombies とは?

https://cryptozombies.io/

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でスマートコントラクトを作ることを学びます。

とあるように、ゲームを作りながらブロックチェーンやスマートコントラクトなどの技術について学ぶことができるコンテンツらしい。

https://github.com/loomnetwork/cryptozombie-lessons

コンテンツ自体は GitHub 上で Open になっているので、日本語化まわりで問題を見つけたら Pull Request を投げてみると良さそう。

snakasnaka

コンテンツの構成

  • Course
    • Lesson
      • Chapter

という形で構成されている

現時点で用意されている Cource は以下の 4 つで日本語化されているのは 1. Solidity のみ。

  1. Solidity
  2. Advanced Solidity
  3. Chainlink
  4. Beyond Ethereum
snakasnaka

Cource.1: Solidity Path の構成

コースのサブタイトルは以下のようになっている

Solidity Path: Beginner to Intermediate Smart Contracts
Solidityの道。スマートコントラクトの初級から中級

コースは以下の 6 つのレッスンで構成されている

  1. ゾンビファクトリーの作成 (Capter 1 ~ 15)
  2. ゾンビが人間を襲う ( Chapter 1 ~ 15 )
  3. Solidity の高度なコンセプト
  4. ゾンビのバトルシステム
  5. ERC721 とクリプト収集物
  6. アプリのフロントエンドと Web3.js
snakasnaka

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 だけなのか?
snakasnaka

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`を操作するぞ。
  }
}