LoginSignup
2
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-09-10

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.
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1