Truffleのpetshopチュートリアルの自分用の要点まとめ

Truffleのペットショップのチュートリアルをやって分かった事のメモです。
16匹のペットがいるペットショップで、それぞれ買い手がつく。という想定です。
他の方がやられているTruffleを使ってEthereumでペットショップアプリを作る〜前半:コントラクトの実装&テスト〜が参考になります。

システム概要

  • 機能を限定したペットショップのシステム。
  • ペットは16匹いる。
  • ブラウザでペット一覧が見れて、ボタンをクリックすると買える。
  • ペットの代金は無料。
  • 販売のみで買取はしない。
  • 飼い主の区別はアカウント(アドレス?)で行う。

環境構築

  • truffle initではなく、truffle unbox pet-shopで、Truffle Boxに登録済みのひな形を使って初期化する。

スマートコントラクトの作成

  • コントラクト内の関数は2つだけ。
    • 飼い主を登録
    • 全てのペットの飼い主を取得

contract文

  • contracts/Adoption.solを作成して、以下を記述。
pragma solidity ^0.4.17;

contract Adoption {

}
  • pragmaは、コンパイラに渡す情報。
  • 文はセミコロン;で終わる。

変数の宣言

  • コントラクトの{}内に以下を書いて変数宣言。
    • address型
    • publicなので、外部?から参照可。自動的にgetterメソッドが作成される。(ので、変数名そのままで呼べば中身が取得できる)
address[16] public adopters;    // 飼い主。address型が16個の配列

関数を書く

  • ペットIDを整数で渡すと、トランザクション実行者のIDを配列内の該当位置に代入し、ペットIDを返す関数。
    • publicなので、外部から呼び出し可能。
    • 引数だけでなく、戻り値の型も書く。returns (uint)の部分。
    • require()で、動作に必要な条件を書く。条件に一致しなければ何もせずreturnする。
    • msg.senderに、関数を呼び出した人のアドレスが入っている。グローバル変数?
  // Adopting a pet(ペットを買う)
  function adopt(uint petId) public returns (uint) {
    require(petId >= 0 && petId <= 15); // ペットは16匹なので、範囲外のpetIdの場合は何もしない

    adopters[petId] = msg.sender;   // トランザクション実行者のアドレスを代入        

    return petId;
  }
  • 飼い主一覧を返す関数。
    • viewを指定して、読み出し専用の関数にしている。(以前はconstantだったらしい)
    • 戻り値の型に、配列の長さまで書いているのに注意。 
  // Retrieving the adopters(飼い主一覧の配列を返す)
  function getAdopters() public view returns (address[16]) {
    return adopters;
  }

コンパイルとデプロイ(マイグレーション)

コンパイル

  • truffle compileでコンパイル
    • ブロックチェーン常にデプロイされたバイトコードは誰でも見れるので、秘密の情報をハードコーディングしないように注意。

デプロイ(マイグレーション)

  • migrations/2_deploy_contracts.jsファイルを作成し、以下を記述。
var Adoption = artifacts.require("Adoption");

module.exports = function(deployer) {
  deployer.deploy(Adoption);
};
  • MacアプリのGanacheで、ローカル環境でブロックチェーンを起動。

スクリーンショット 2018-09-10 14.24.42.png

スクリーンショット 2018-09-10 14.26.07.png

  • truffle migrateでマイグレーション実施。

  • Ganacheのアプリ上で、ブロック高が0から4になったのを確認。上部メニューのBlocksやTransactionsで概要も確認。

テスト

テストをSolidity言語で書く。JavaScriptでも書ける。

テストの作成

  • test/TestAdoption.solを作成し、以下を記述。
pragma solidity ^0.4.17;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";

contract TestAdoption {
  Adoption adoption = Adoption(DeployedAddresses.Adoption());   // テスト用インスタンス

}

テストのたびにデプロイが行われるため、import "truffle/DeployedAddresses.sol";で、デプロイ先アドレスを取得する。(DeployedAddressesという特殊な変数?に入る)

adopt()関数のテスト

  // Testing the adopt() function
  function testUserCanAdoptPet() public {
    uint returnedId = adoption.adopt(8);

    uint expected = 8;

    Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
  }
  • Assert.equal(戻り値、期待値、失敗した時のメッセージ)で判定。
  • ペットIDを渡すと(実行者のアドレスを変数に入れて)ペットIDを返す関数なので、8を渡したら8が返る。

ある一匹のペットの飼い主を確認するテスト

  • 上述のテスト関数で、8番のペットを買っているので、その状態を利用して、8番のペットの飼い主が自分であることを確認する。
  • thisは、テスト実行者のアドレスが入っている。グローバル変数?
  // Testing retrieval of a single pet's owner(ある一匹のペットの飼い主を確認するテスト)
  function testGetAdopterAddressByPetId() public {
    // Expected owner is this contract
    address expected = this;

    address adopter = adoption.adopters(8);

    Assert.equal(adopter, expected, "Owner of pet ID 8 should be recorded.");
  }

全てのペットの飼い主を得るテスト

  • memory属性がつくと、メモリ上に一時的に保存する変数となる。(つけないとコントラクト上の変数になる(ので書き込みにgas代がかかる?))
  // Testing retrieval of all pet owners(全てのペットの飼い主を得るテスト)
  function testGetAdopterAddressByPetIdInArray() public {
    // Expected owner is this contract
    address expected = this;

    // Store adopters in memory rather than contract's storage
    address[16] memory adopters = adoption.getAdopters();

    Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
  }

テストの実行

  • truffle testでテストを実行。
    • 3つのテスト全てにチェックマークがつけばOK。

ユーザーインタフェースの作成

  • src ディレクトリに、index.htmlと画像やcssなどが一式入っている。 
  • ブラウザ画面の主な機能は以下の3つ。

    • ファイルからペットの名前などの情報を読み込んで表示。
    • ブロックチェーン上のコントラクト内の変数を取り出して、飼い主がいるペットはボタンを押せなくする。
    • ボタンが押されたら、ペットを買った飼い主の情報をブロックチェーン上に書き込み、ボタンを押せなくする。
  • src/js/app.js を書き換えていく。

    • Appオブジェクトでアプリを操作できる。(グローバル変数?)
    • init関数で、jsonファイルからペット情報を読み出して、initWeb3関数を呼び出している。
    • markAdopted関数で、買い手のついたペットを調べ、表示を変えている。
    • handleAdopt関数で、ボタンを押してペットを買った時の処理を行っている。

web3のインストール

  • web3は、EthereumのノードとHTTPなどでやり取りするためのJavaScriptライブラリ。 (ドキュメントがあるので参照)

  • npm install -g web3でインストールしておく。

initWeb3関数の実装

  • コメントが入っているのを消して、以下に書き換える。
    • web3インスタンスがあればそれを使い、なければ実体化する処理。
  initWeb3: function() {
    // Is there an injected web3 instance?
    if (typeof web3 !== 'undefined') {
      App.web3Provider = web3.currentProvider;
    } else {
      // If no injected web3 instance is detected, fall back to Ganache
      App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
    }
    web3 = new Web3(App.web3Provider);

    return App.initContract();
  },

コントラクトの実体化

  • TruffleContract関数があるので、デプロイ先のアドレスをいちいち書き換えないですむ(?)
  • initContract関数を、以下のように書き換え。
    • 関数内で読み込んでいるAdoption.jsonはbuild/contracts/Adoption.jsonにある。
    • Application Binary Interface (ABI)は、コントラクトと対話(関数や変数へのアクセス)するためのアドレス。
    • インスタンス化されると、上述のApp.web3Providerを使ってアクセスできるようになる?
    • 最後の行のApp.markAdopted();は、すでに買い手のついた(=配列内の所定の位置に飼い主のアドレスが入っている?)ペットを、表示を変えるための関数。(次の段落で実装)
  initContract: function() {
    $.getJSON('Adoption.json', function(data) {
      // Get the necessary contract artifact file and instantiate it with truffle-contract
      var AdoptionArtifact = data;
      App.contracts.Adoption = TruffleContract(AdoptionArtifact);

      // Set the provider for our contract
      App.contracts.Adoption.setProvider(App.web3Provider);

      // Use our contract to retrieve and mark the adopted pets
      return App.markAdopted();
    });

    return App.bindEvents();
  },

買い手のついたペットを調べ、表示を変える

  • markAdopted関数を、コメントを消して以下に書き換える。
    • call()を使ってコントラクトの変数を取り出している(gas代はかからない)
    • Ethereumでは最初に変数をゼロクリアするので、買い手がついた(アドレスが入った)かどうかを、0と比較して判断できる。
    • エラーがあれば、(ブラウザの)consoleログに出力する。
  markAdopted: function(adopters, account) {
    var adoptionInstance;

    // デプロイしたコントラクトのgetAdopters関数をcallする
    App.contracts.Adoption.deployed().then(function(instance) {
      adoptionInstance = instance;

      return adoptionInstance.getAdopters.call();
    }).then(function(adopters) {
      // adoptersを順番に取り出し、0以外(=すでに買い手がついている)の場合は、
      // ボタンの文字列を'Success'にして、disabled表示にする
      for (i = 0; i < adopters.length; i++) {
        if (adopters[i] !== '0x0000000000000000000000000000000000000000') {
          $('.panel-pet').eq(i).find('button').text('Success').attr('disabled', true);
        }
      }
    }).catch(function(err) {
      console.log(err.message);
    });
  },

ペットを買った時の処理(ブラウザからボタンを押して買う)

  • handleAdopt関数を、コメントを消して以下に書き換える。(一番後ろの関数なので、最後の波閉じカッコの後にカンマをつけない)
    • コントラクトのadopt関数を、petIdと自分のアドレスをつけて呼び出している(買った時に、コントラクト内の配列に値を入れるため)(gas代がかかる)
    • うまくいったら、表示を書き換える。
  handleAdopt: function(event) {
    event.preventDefault();

    // 選択した(押されたボタンの)idをpetIdにする
    var petId = parseInt($(event.target).data('id'));

    var adoptionInstance;

    // (実行者の?)アカウントを取得
    web3.eth.getAccounts(function(error, accounts) {
      if (error) {
        console.log(error);
      }

      var account = accounts[0];

      App.contracts.Adoption.deployed().then(function(instance) {
        adoptionInstance = instance;

        // Execute adopt as a transaction by sending account
        // 現在のアカウントを使って、コントラクトのadopt関数を呼び出し、配列にアドレスを入れる。
        return adoptionInstance.adopt(petId, {from: account});
      }).then(function(result) {
        // 表示を変更(ボタンの文字列を'Success'にして、disabled表示にする)
        return App.markAdopted();
      }).catch(function(err) {
        console.log(err.message);
      });
    });
  }

ブラウザから動作確認

Metamaskのインストールと設定

  • chromeブラウザにMetamaskエクステンションを入れる

  • Ganacheの「Accounts」タブの上の方にあるニーモニックをコピー。

  • Metamaskの「Restore from seed phrase」をクリック

  • 「Wallet seed」欄に入力し、パスワードも入力

  • 左上のセレクトボックスで「Custom RPC」を選択。Ganacheで立ち上げたサーバの「http://127.0.0.1:7545/ 」を入力してSave。(末尾に「/」を付けないと、再起動時に「Connecting to Unknown Private Network」で待たされる)

  • 左矢印ボタンで戻ると、Ganacheの一番上にあるアカウントの残高がMetamask側にも反映されている。

lite-serverの起動

  • npm run devで、サーバが立ち上がり、自動的にブラウザが開く。

    • すでにlite-serverモジュールがnode_modulesディレクトリにインストールされている。
    • bs-config.jsonファイルで、htmlソースの場所や、コントラクトの場所を指定する。
    • package.jsonファイルで、npmのdevコマンドが定義されている。
  • デフォルトでSafariが起動するので、手動でChromeからhttp://localhost:3000 を開く。

  • JavaScriptコンソールを開いて、エラーが出ていないか確認する。

  • Adoptボタンを押すと、Metamaskの画面がポップアップするので、トランザクション内容を確認して、SUBMITを押す。

    • Ganacheで、ブロックが進んでいるのも確認する。

Macを再起動した場合

  • Ganache起動
  • truffle migrate --reset (「–reset」で、build配下のバイトコードとかのjsonをクリアしてから実行してくれる)
  • Metamaskの画面を開いて、Account1を表示する。
    • 「Connecting to Unknown Private Network」のままの場合は、左上のネットワーク設定を、一旦別なネットワークを選んで、再度「http://127.0.0.1:7545/ 」を選択
    • パスワードを入れる(「Restore from seed phrase」で登録した時に使ったパスワード)
  • Metamaskのアカウントをリセットする。「Account1」が表示されている状態で、右上のメニューから「Settings」で、下の方にある「Reset Account」をクリック。
  • npm run dev でサーバ起動
  • Chromeで http://localhost:3000 を開く

初回のAdoptで?、ボタンが’Success’にならない

  • 起動直後に「Adopt」ボタンを押すと、ブロックチェーンに書き込まれたのがGanacheのBLOCKSから確認できますが、画面上のボタンがすぐには’Success’になりません。リロードすると反映されます。2回目以降のAdoptは即反映される場合もあれば、リロードしないと反映されない場合もあります。setIntervalで15秒待ってからApp.markAdopted();を呼ぶようにしたら反映されました。adoptionInstance.adopt()でブロックチェーンに書き込むのが非同期な気がしています。

nonceが違うというエラーが出た場合

  • Adoptをクリックした際に、「Error: Error: [ethjs-rpc] rpc error with payload {…once. account has nonce of: 8 tx has nonce of: 46」や「Error: the tx doesn't have the correct nonce. account has nonce of: 4 tx has nonce of: 10」のように、nonceが違うと出る場合は、Metamask上のアカウントをリセットする。Resetting an Account (New UI)

遭遇したエラー

  • デプロイ用のスクリプトで、deployの引数に変数を渡しているが、deployer.deploy(Adoption);と書くべきをdeployer.deploy("Adoption");と書いていた。
Running migration: 2_deploy_contracts.js
Error encountered, bailing. Network state unknown. Review successful transactions manually.
  • テストでAssert.equal()をAssert()と書いててエラー。
test/TestAdoption.sol:28:5: TypeError: Exactly one argument expected for explicit type conversion.
    Assert(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
    ^--------------------------------------------------------------------^
Compilation failed. See above.

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です