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で、ローカル環境でブロックチェーンを起動。
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のインストール
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.