Ethereum
solidity
truffle

Truffle Ethereum Tutorial 日本語訳(Ehereum Pet Shop)

はじめに

  • Ethereum
  • スマートコントラクト
  • dapp(分散型アプリ)
  • WEBサービス

のチュートリアルとして、Truffleが提供している、ETHEREUM PET SHOP
Ethereum上でスマートコントラクトのdappを作成し、WEBサービスとして提供するまでの一連を体験できます。
実際にやってみましたが中々良かったです。

拙い訳では有りますが、日本語訳(意訳多め、一部追記有り)しましたので、学習の手助けになればと思います。
以下、本文です。

Ethereumペットショップ

Petshop
このチュートリアルでは、ペットの飼い主を決定するためのペットショップのシステム構築を通して、dapp構築の第一歩を経験できます。

このチュートリアルはEthereum、スマートコントラクト、HTML、JavaScriptに関する知識を持っている、dappsの初心者を対象としています。

注意:Ethereumの基本については、Truffle Ethereum OverView チュートリアルを参考。

このチュートリアルでは、次の内容について説明します。

  1. 開発環境の設定
  2. Truffle Box を使った Truffle Project の作成
  3. スマートコントラクトを書く
  4. スマートコントラクトのコンパイルと移行
  5. スマートコントラクトのテスト
  6. スマートコントラクトを使用するためのユーザーインターフェイスの作成
  7. ブラウザでdappを使用する

0. 背景

Pete's ペットショップを経営するPete Scandlonは、ペットの飼い主決定を効率的に処理する方法として、Ethereumを使ってみたいと考えています。この店にはペット用のスペースが16匹分有り、またすでにペットのデータベースは持っています。オーナーのPeteはまず第一歩として、コンセプトを理解するために、ペットとEthereumのアドレスを紐付けるようなdappを見てみたいと考えています。

これを動かすためのウェブサイトは提供されます。私達の仕事は、スマートコントラクトと、それを使用するためのフロントエンドのロジックを作成することです。

1. 開発環境の設定

まずは技術的要件から。以下をインストールしてください。

上記がインストールできたら、次のコマンドでTruffleをインストールしてください。

npm install -g truffle

Truffleが正しくインストールされていることを確認するには、端末にtruffle versionと入力してください。エラーが表示された場合は、npmモジュールがパスに追加されていることを確認してください。

また、スマートコントラクトのデプロイ、アプリ開発、テスト実行に使用できる、Ethereum開発用の個人用ブロックチェーンであるGanacheも使用します。 Ganacheをダウンロードするには、 http://truffleframework.com/ganacheにアクセスし、[ダウンロード]ボタンをクリックします。

注意 :GUIのない環境で開発している場合は、Gauacheの代わりにTruffleの組み込み個人用ブロックチェーンであるTruffle Developを使用することもできます。 Truffle Developのチュートリアルに合わせるために、ブロックチェーンが動作するポートなどの設定を変更する必要があります。



訳者追記:1章振り返り

この章では以下のことを行いました。

- Node.js v6 および関連モジュールのインストール
- Truffleのインストール
- Ganacheのインストール

これで開発環境が整いました。

2.Truffle Box を使った Truffle Project の作成

(1) Truffleの初期化を行います。作業ディレクトリで初期化が行われるため、まず作業用のディレクトリを作成し、そのディレクトリに移動します。

mkdir pet-shop-tutorial 
cd pet-shop-tutorial

(2) このペットショップチュートリアルのために、専用のTruffle Boxを用意してあります。これには基本的なプロジェクトのベースと、ユーザインターフェース用のコードを含んでいます。このTruffle Boxを解凍するために、truffle unboxコマンドを使用してください。

truffle unbox pet-shop

注意 :トリュフの初期化の方法は、いくつかあります。 もう1つの便利な初期化コマンドは、Truffleのtruffle initでしょう 。これは、Truffleプロジェクトを作成しますが、スマートコントラクトのサンプルは含まれていません。 詳細については、プロジェクトの作成を参照してください。

ディレクトリ構成の解説

デフォルトのTruffleディレクトリ構造には以下が含まれています:

  • contracts/ :スマートコントラクトのSolidityソースファイルを含みます。 ここにはMigrations.solと呼ばれる重要なコントラクトがあります。これについては後でお話します。
  • migrations/ :Truffleは、migrationシステムを使用してスマートコントラクトの展開を処理します。 migrationとは、変更を追跡する特別なスマートコントラクトです。
  • test/ :スマートコントラクトのJavaScriptとSolidityテストの両方を含んでいます
  • truffle.js :Truffle設定ファイル pet-shop Truffle Boxには余分なファイルやフォルダがありますが、まだそれらについては心配しません。


訳者追記:2章振り返り

この章では以下のことを行いました。

- チュートリアル用のTruffle boxを使用して、Truffle Projectの作成

ベースは用意されているので、これに追加で作業をしていくことになります。

またこの時、unboxがうまくいかない場合には、以下のような理由考えられます。

(1) npm,nodejsのバージョンが古い
【対策】
- nのインストール:npm install -g n
- n のアップデート:n Latest
- npmのアップデート:npm update -g

(2) インストール先に書き込み許可がない
【対策】
- sudo をつけて実行します

3. スマートコントラクトを書く

バックエンドのロジックとストレージとして機能するスマートコントラクトを書くことで、dappを開始します。

(1) contracts/ディレクトリにAdoption.solという名前の新しいファイルを作成します。

cd contracts
vim Adoption.sol

(2) ファイルに次の内容を追加します。

Adoption.sol
pragma solidity ^0.4.17;

contract Adoption {

}

解説:

  • 必要なSolidityの最小バージョンは、コントラクトのトップに記載されている0.4.17です。pragmaコマンドは「 コンパイラのみが気にする追加情報 」を意味し、キャレット記号(^)は「 それ以上のバージョン 」を意味します。
  • JavaScriptやPHPと同様に、ステートメントはセミコロンで終了します。

変数の設定

Solidityは静的プログラミング言語です。つまり、文字列、整数、配列などのデータ型を定義する必要があります。またSolidityにはアドレスと呼ばれるユニークなタイプがあります 。アドレスは、20バイトの値として格納されたEthereumアドレスです。Ethereumブロックチェーンのすべてのアカウントとスマートコントラクトにはアドレスがあり、このアドレスとの間でEtherを送受信できます。

(1) contract Adoption { 後の次の行に次の変数を追加します。

address[16] public adopters;

解説:

  • 変数adoptersを定義しました。これはEthereumアドレスの配列です。配列には1つの型が含まれ、固定長または可変長を持つことができます。この場合、タイプはaddressで長さは16です。
  • また、adoptersがpublicなことに気づくでしょう。public変数には自動ゲッターメソッドがありますが、配列の場合はキーが必要であり、単一の値しか返しません。後で、UI全体で使用するために配列全体を返す関数を作成します。

1つ目のFunction : ペットの引取り

お客様がペットの引取を要求できるようにしましょう。

(1) 上記で設定した変数宣言のあとに、次の関数を追加します。

Adoption.sol
// Adopting a pet
function adopt(uint petId) public returns (uint) {
  require(petId >= 0 && petId <= 15);

  adopters[petId] = msg.sender;

  return petId;
}

解説:

  • Solidityでは、関数のパラメータと出力の両方の型を指定する必要があります。ここでは、パラメータとしてpetId(integer)を入力し、integerを出力する設定です。
  • petIdadopters配列の範囲内にあることを確認しています。Solinityの配列は0からインデックスされるため、ID値は0から15の間である必要があります。IDが範囲内にあることを確認するには、 require()ステートメントを使用します。
  • IDが範囲内にある場合は、呼び出しを行ったアドレスをadopters配列に追加します。この機能を呼び出した人、または呼び出したスマートコントラクトのアドレスは、 msg.senderによって示されます
  • 最後に、提供された petId を確認のために返します。

2つ目のFunction : Adoptersの取得

前述のように、配列ゲッターは与えられたキーから単一の値だけを返します。私たちのUIはすべてのペットの採用状況を更新する必要がありますが、16のAPI呼び出しを行うことは理想的ではありません。だから次のステップは、配列全体を返す関数を書くことです。

(1)上に追加したgetAdopters()関数の後に、次のgetAdopters()関数をスマートコントラクトに追加します。

Adoption.sol
// Retrieving the adopters
function getAdopters() public view returns (address[16]) {
  return adopters;
}

adoptersは既に宣言されているので、単にそれを返すことができます。 戻り値の型(この場合は、adoptersの型)をaddress[16]として明示します。



訳者追記:3章振り返り

この章では以下のことを行いました。

- スマートコントラクトの作成
 - 使用バージョン指定
 - ペットを管理する配列
 - ペット引取ファンクション
 - 配列取得ファンクション

スマートコントラクトはSolidityで書くことになりますが、
開発環境やエディタもそれなりにあるようです。

ですがこのチュートリアル程度ならVimなどのエディタで、
十分事足りると感じました。

4.スマートコントラクトのコンパイルと移行

スマートコントラクトが作成出来たので、次のステップは、それをコンパイルして移行することです。

Truffleには開発者コンソールが組み込まれています。これをTruffle Developと呼びます。これはデプロイしたスマートコントラクトをテストするために使用できる、開発ブロックチェーンを生成します。 また、コンソールからTruffleコマンドを直接実行することもできます。このチュートリアルでは、Truffle Developを使用して、スマートコントラクトのほとんどの処理を実行します。

コンパイル

Solidityはコンパイルされた言語です。つまり、Ethereum仮想マシン(EVM)を実行するためにSolinityをバイトコードにコンパイルする必要があります。人間が読めるSolidityをEVMが理解できるものに変換するものと考えてください。

(1)端末で、dappを含むディレクトリのルートにあることを確認して、次のように入力します。

truffle compile

注 :Windows上でこのコマンドを実行する際に問題が発生した場合は、Windowsでの名前の競合を解決するためのドキュメントを参照してください。



訳者追記

バージョンの関係か、私の環境では以下のエラーでうまく出来ませんでした。

Compilation warnings encountered:

[ディレクトリ構成]/Migrations.sol:11:3: Warning: Defining constructors as functions with the same name as the contract is deprecated. Use "constructor(...) { ... }" instead.
  function Migrations() public {
  ^ (Relevant source part starts here and spans across multiple lines).


つまり、Migrations.solにおいて、ConstructorがJavaのようにClass名をそのまま踏襲するのではなく、constructorという名前に変更しないとだめなようです。以下のように編集することで解決しました。

Migrations.sol
pragma solidity ^0.4.17;

contract Migrations {
  address public owner;
  uint public last_completed_migration;

  modifier restricted() {
    if (msg.sender == owner) _;
  }

  #ここをconstructorに修正する。
  #function Migrations() public {
  constructor() public {
    owner = msg.sender;
  }

  function setCompleted(uint completed) public restricted {
    last_completed_migration = completed;
  }

  function upgrade(address new_address) public restricted {
    Migrations upgraded = Migrations(new_address);
    upgraded.setCompleted(last_completed_migration);
  }
}

成功すれば次のような出力が表示されます。

Compiling ./contracts/Migrations.sol...
Compiling ./contracts/Adoption.sol...
Writing artifacts to ./build/contracts

移行

スマートコントラクトが完成したので、ブロックチェーンに移行しましょう!

移行とは、アプリケーションの契約の状態を変更し、ある状態から次の状態に移行するためのデプロイスクリプトです。 最初の移行では、新しいコードを導入しているだけかもしれませんが、他の場合にはデータを移動したり、契約を新しいものに置き換えたりする可能性があります。

注意:移行の詳細については、Truffleのドキュメントを参照してください。

1つのJavaScriptファイルがmigrations/ディレクトリにあることが確認できるでしょう(1_initial_migration.js)。 これは、Migrations.solコントラクトを展開し、その後のスマートコントラクトの移行を監視し、二重契約が起きないようにハンドルします。

これで、独自の移行スクリプトを作成する準備が整いました。

(1)migrations/ディレクトリに2_deploy_contracts.jsという名前の新しいファイルを作成します。

cd ../migrations
vim 2_deploy_contracts.js

(2)2_deploy_contracts.jsファイルに次のコンテンツを追加します。

2_deploy_contracts.js
var Adoption = artifacts.require("Adoption");

module.exports = function(deployer) {
  deployer.deploy(Adoption);
};

(3)コントラクトをブロックチェーンに移行する前に、ブロックチェーンを実行する必要があります。このチュートリアルでは、契約の展開、アプリケーションの開発、テストの実行に使用できるEthereum開発用の個人用ブロックチェーンであるGanacheを使用します。 Ganacheをまだダウンロードしていない場合は、 Ganacheをダウンロードしてアイコンをダブルクリックしてアプリケーションを起動してください。これにより、ポート7545でローカルで動作するブロックチェーンが生成されます。

注意:Ganacheについての詳細は、Truffleのドキュメントを参照してください。

初回起動時のGanache
初回起動時のGanache

(4)ターミナルに戻って、コントラクトをブロックチェーンに移行します。

truffle migrate

すると、次のような出力が表示されます。

Using network 'development'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0xcc1a5aea7c0a8257ba3ae366b83af2d257d73a5772e84393b0576065bf24aedf
  Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
  ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying Adoption...
  ... 0x43b6a6888c90c38568d4f9ea494b9e2a22f55e506a8197938fb1bb6e5eaa5d34
  Adoption: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
  ... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...

順番に移行が実行され、展開されたコントラクトのブロックチェーンアドレスが表示されます(アドレスは個人毎に異なります)。

(5)Ganacheでは、ブロックチェーンの状態が変わったことに注意してください。CURRENT BLOCKは0から4に。最初のアカウントはもともと100Etherを持っていましたが、移行のトランザクションコストのために今は低くなっています。後で取引費用について詳しく説明します。

移行後のGanache
移行後のGanache

最初のスマートコントラクトを書いて、それをローカルで実行しているブロックチェーンに配備しました。 私たちが望んでいることを確かめるために、スマートコントラクトとやり取りする準備が整いました。



訳者追記:4章振り返り

この章では以下のことを行いました。

- スマートコントラクトのコンパイル
- ローカルでのブロックチェーン立ち上げ
- ブロックチェーンへのコントラクトの移行

Ganacheを使用することで簡単にBlockChainをローカルで構築できました。またビジュアライズされているので直感的にわかりやすいのもいいと思います。

5.スマートコントラクトのテスト

Truffleはスマートコントラクトのテストの場合、JavaScriptやSolidityのいずれかでテストを書くことができるという点で非常に柔軟です。このチュートリアルでは、Solidityでテストを書いていきます。

(1) test/ディレクトリにTestAdoption.solという名前の新しいファイルを作成します。

cd ../test
vim TestAdoption.sol

(2) TestAdoption.solファイルに次のコンテンツを追加します。

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());

}

私たちは、3つのインポートと共に、契約を始めます。
- Assert.sol:テストで使用するさまざまなアサーションを提供します。テストにおいて、アサーションは、テストが合格/不合格かを判断するために、値が等しい、等しくない、空である、などをチェックしますTruffleに含まれるアサーションの完全なリストはここにあります
- DeployedAddresses.sol:テストを実行すると、Truffleはテスト対象のコントラクトの新しいインスタンスをブロックチェーンに展開します。このスマートコントラクトは、展開されたコントラクトのアドレスを取得します。
- Adoption.sol:私たちがテストしたいスマートコントラクトの本体。

注意:最初の2つのインポートは、truffleディレクトリではなく、グローバルなTruffleファイルを参照しています。あなたはあなたのtest/ディレクトリの中のtruffleディレクトリの中を見る必要はありません。

次に、テスト対象のスマートコントラクトを含むコントラクトワイド変数を定義し、DeployedAddressesというスマートコントラクトを呼び出して、そのアドレスを取得します。

adopt()Functionのテスト

adopt()Functionをテストするために、この関数は正しく動作すると、指定されたpetId返すことを思い出してください。私たちは、渡されたIDと、adopt()からの戻り値を比較することで、正しく動作しているかどうかをテストできます。

(1) TestAdoption.sol内に次の関数を追加します。

TestAdoption.sol
// 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.");
}

解説:

  • まずID=8と宣言するスマートコントラクトを呼びます。
  • 次に期待値8を宣言します。
  • 最後にAssert.equal()に、実際の値、期待値、(テストに失敗した場合にコンソールに出力される)失敗メッセージを設定します。

1人のペットのオーナーの検索のテスト

上記から、パブリック変数には自動ゲッターメソッドがあることを思い出すと、上記の採用テストで格納されたアドレスを取得できそうです。保存されたデータはテスト期間中も保持されるため、上記で定義されたペット(ID=8)に対応するオーナーアドレスは、他のテストでも取得できます。

(1)TestAdoption.sol内に次の関数を追加します。

TestAdoption.sol
// 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.");
}

TestAdoptionコントラクトがトランザクションを送信するので、期待値をthis(契約全体のアドレスを取得する契約全体の変数)に設定します。その上で、上記のように取得したアドレスと期待値が等しいことをアサーションします。

すべてのペットのオーナーの検索のテスト

配列はそのままでは、単一のキーで単一の値しか返すことができないので、配列全体を取得するための独自のゲッターを作成します。

(1)TestAdoption.sol内に次の関数を追加します。

TestAdoption.sol
// 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.");
}

adoptersの前に書かれたmemoryは、メモリ属性を意味します。 メモリ属性は、コントラクトのストレージに保存するのではなく、メモリに一時的に値を格納するようSolinityに指示するものです。adoptersは配列であり、最初のadopt()Functionのテストで、ID=8にアドレス格納していることがわかっているので、テストコントラクトのアドレス(this)とadopters[8]を比較するアサーションを行います。



訳者追記:テストコード完成版

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());

// 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.");
}

// 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.");
}

// 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.");
}


}


テストの実行

(1) テストを以下のコマンドで実行します。

truffle test

(2) 成功すると、以下のように出力されます。

  Using network 'development'.

   Compiling ./contracts/Adoption.sol...
   Compiling ./test/TestAdoption.sol...
   Compiling truffle/Assert.sol...
   Compiling truffle/DeployedAddresses.sol...

     TestAdoption
       ✓ testUserCanAdoptPet (91ms)
       ✓ testGetAdopterAddressByPetId (70ms)
       ✓ testGetAdopterAddressByPetIdInArray (89ms)


     3 passing (670ms)


訳者追記:5章振り返り

この章では以下のことを行いました。

- スマートコントラクトのテストコードの作成
- スマートコントラクトのテスト

それぞれのfunctionをテストできました。
スマートコントラクトはBlockChain上に展開したら戻せないので、テストをしっかりすることが非常に大切のようです。

6.スマートコントラクトを使用するためのユーザーインターフェイスの作成

スマートコントラクトを作成し、それをローカルのテストブロックチェーンに配備し、コンソール経由で対話できることを確認したので、Peteが自分のペットショップに使用できるようにUIを作成しましょう。

pet-shop Truffle Boxには、アプリのフロントエンドのコードが含まれていました。そのコードはsrc/ディレクトリ内に存在します。

フロントエンドは、できるだけ簡単に起動できるようにビルドシステム(webpack、gruntなど)を使用しません。アプリの基本構造は既にあります。Ethereum上にFunctionを追加し、機能を充実させていきます。これにより、(このチュートリアル終了後に)このスマートコントラクト開発への知識を、自分のフロントエンド開発に適用することができるでしょう。

web3のインスタンス化

(1) [pet-shopを展開したルート]/src/js/app.jsをテキストエディタで開きます。
(2) ファイルの中身を見ていきましょう。アプリケーションを管理し、 init()内のペットデータをロードし、initWeb3()関数を呼び出すためのグローバルなAppオブジェクトがあることに注意してください。web3 JavaScriptライブラリは、Ethereumブロックチェーンと相互作用します。ユーザーアカウントの取得、トランザクションの送信、スマートコントラクトとの対話などが可能です。

app.js
App = {
  web3Provider: null,
  contracts: {},

  init: function() {
    // Load pets.
    $.getJSON('../pets.json', function(data) {
      var petsRow = $('#petsRow');
      var petTemplate = $('#petTemplate');

      for (i = 0; i < data.length; i ++) {
        petTemplate.find('.panel-title').text(data[i].name);
        petTemplate.find('img').attr('src', data[i].picture);
        petTemplate.find('.pet-breed').text(data[i].breed);
        petTemplate.find('.pet-age').text(data[i].age);
        petTemplate.find('.pet-location').text(data[i].location);
        petTemplate.find('.btn-adopt').attr('data-id', data[i].id);

        petsRow.append(petTemplate.html());
      }
    });

    return App.initWeb3();
  },

  initWeb3: function() {
    //次の手順でここを書き換えます
    /*
     * Replace me...
     */

    return App.initContract();
  },

  initContract: function() {
    //次の次でここを書き換えます
    /*
     * Replace me...
     */

    return App.bindEvents();
  },

  bindEvents: function() {
    $(document).on('click', '.btn-adopt', App.handleAdopt);
  },

  markAdopted: function(adopters, account) {
    //更にその後、ここを書き換えます
    /*
     * Replace me...
     */
  },

  handleAdopt: function(event) {
    event.preventDefault();

    var petId = parseInt($(event.target).data('id'));
   //最後にここを書き換えて完成です
    /*
     * Replace me...
     */
  }

};

$(function() {
  $(window).load(function() {
    App.init();
  });
});

(3) app.js の initWeb3部分の複数行のコメントを削除し、次のように置き換えます。

app.js
// 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);

解説

  • まず、すでにアクティブなWeb3インスタンスがあるかどうかを確認します。(EthereumブラウザのMistや、MetaMask拡張機能を備えたChromeなどのブラウザでは、独自のweb3インスタンスが使用されます)。既存のweb3インスタンスが存在する場合、そのプロバイダを取得してWeb3オブジェクトを作成します。
  • 既存のweb3インスタンスが存在しない場合は、ローカルプロバイダに基づいてweb3オブジェクトを作成します。(この代替は開発環境では問題ありませんが、安全ではなく、本番環境には適していません)。

コントラクトのインスタンス化

Web3を介してEthereumとやりとりすることができるようになったので、次はスマートコントラクトをインスタンス化して、web3にその場所とその動作方法を知らせる必要があります。Truffleにはtruffle-contractと呼ばれる、これを手助けするライブラリがあります。
コントラクトに関する情報を移行と同期させるので、契約の展開アドレスを手動で変更する必要はありません。
(1) /src/js/app.js内のinitContractから複数行のコメントを削除し、次のように置き換えます。

app.js
$.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();
});

解説:

  • まず、スマートコントラクト用のアーティファクトファイルを取得します。アーティファクトは、展開されたアドレスやアプリケーションバイナリインターフェイス(ABI)など、契約に関する情報です。ABIは、変数、関数、およびそれらのパラメータを含むコントラクトとの対話方法を定義するJavaScriptオブジェクトです
  • コールバックにアーティファクトがあると、それをTruffleContract()に渡します。これにより、私たちがやり取りできるコントラクトのインスタンスが作成されます。
  • インスタンス化されたコントラクトでは、以前web3を設定するときに保存したApp.web3Providerの値を使用してweb3プロバイダを設定します。
  • 前回の訪問時以降にペットが既に引き取られている場合は、アプリのmarkAdopted()関数を呼び出します。スマートコントラクトのデータを変更するたびにUIを更新する必要があるため、これを別の関数でカプセル化しました。

採用されたペットの取得とUIの更新

(1)/src/js/app.js内のmarkAdoptedから複数行のコメントを削除し、次のように置き換えます。

app.js
  var adoptionInstance;

  App.contracts.Adoption.deployed().then(function(instance) {
    adoptionInstance = instance;
    return adoptionInstance.getAdopters.call();
  }).then(function(adopters) {
    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);
  });

解説:

  • 導入されたAdoptionコントラクトにアクセスし、そのインスタンスでgetAdopters()を呼び出します。
  • スマート・コントラクト・コールの外で、まずadoptionInstance変数を宣言して、最初にインスタンスを取得した後にインスタンスにアクセスできるようにします。
  • call()を使うと完全なトランザクションを送ることなくブロックチェーンからデータを読み取ることができます。つまり、Etherを使う必要はありません。
  • getAdopters()呼び出した後、すべてのループをループして、ペットごとにアドレスが格納されているかどうかを確認します。 配列にはアドレス型が含まれているため、Ethereumは16個の空アドレスで配列を初期化します。このため、nullやその他のFalseの値ではなく、空のアドレス文字列をチェックします。
  • 対応するアドレスのpetIdが見つかると、採用ボタンを無効にしてボタンのテキストを「成功」に変更するので、ユーザーはいくつかのフィードバックを得ることができます。
  • エラーはコンソールに記録されます。

adopt()Functionの処理

(1)/src/js/app.js内のhandleAdoptから複数行のコメントを削除し、次のように置き換えます。

app.js
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
    return adoptionInstance.adopt(petId, {from: account});
  }).then(function(result) {
    return App.markAdopted();
  }).catch(function(err) {
    console.log(err.message);
  });
});

解説:

  • web3を使用してユーザーのアカウントを取得します。 エラーチェック後のコールバックでは、最初のアカウントを選択します。
  • そこから、上記のようにデプロイされたコントラクトを取得し、そのインスタンスをadoptionInstanceに格納します。今度は、コールの代わりにトランザクションを送信します。取引には「差出人」アドレスが必要で、取引に関連してコストがかかります。この費用は、Etherで支払われ、gas(ガス)と呼ばれます。このガスとは、計算を実行し、および/またはスマートコントラクトでデータを記憶するための料金です。私たちは、ペットのIDと、先にaccountで格納したアカウントアドレスを含むオブジェクトの両方を使用してadopt()関数を実行するトランザクションを送信します 。
  • トランザクションを送信した結果がトランザクションオブジェクトです。エラーがなければ、markAdopted()関数を呼び出してUIを新しく格納されたデータと同期させます。


訳者追記:app.js完成版

app.js
App = {
  web3Provider: null,
  contracts: {},

  init: function() {
    // Load pets.
    $.getJSON('../pets.json', function(data) {
      var petsRow = $('#petsRow');
      var petTemplate = $('#petTemplate');

      for (i = 0; i < data.length; i ++) {
        petTemplate.find('.panel-title').text(data[i].name);
        petTemplate.find('img').attr('src', data[i].picture);
        petTemplate.find('.pet-breed').text(data[i].breed);
        petTemplate.find('.pet-age').text(data[i].age);
        petTemplate.find('.pet-location').text(data[i].location);
        petTemplate.find('.btn-adopt').attr('data-id', data[i].id);

        petsRow.append(petTemplate.html());
      }
    });

    return App.initWeb3();
  },

  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();
  },

  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();
  },

  bindEvents: function() {
    $(document).on('click', '.btn-adopt', App.handleAdopt);
  },

  markAdopted: function(adopters, account) {
    var adoptionInstance;

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

      return adoptionInstance.getAdopters.call();
    }).then(function(adopters) {
      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: function(event) {
    event.preventDefault();

    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
        return adoptionInstance.adopt(petId, {from: account});
      }).then(function(result) {
        return App.markAdopted();
      }).catch(function(err) {
        console.log(err.message);
      });
    });
  }

};

$(function() {
  $(window).load(function() {
    App.init();
  });
});


訳者追記:6章振り返り

この章では、以下のことを行いました。

- ユーザ用のフロントエンドシステムをJavaScriptで作成する。

基本的な外側は出来ていたので、順を追って機能毎に、中身を用意しました。

7.ブラウザでdappと対話する

これでdappを使う準備が整いました!

MetaMaskのインストールと設定

ブラウザでdappとやり取りする最も簡単な方法は、ChromeとFirefoxの両方のブラウザ拡張機能であるMetaMaskを使用することです。

(1) ブラウザにMetaMaskをインストールします。
(2) インストールが完了すると、アドレスバーの横にMetaMask Foxアイコンが表示されます。アイコンをクリックすると、次の画面が表示されます。
プライバシー通知
プライバシー通知
(3) 個人情報保護方針に同意するには、Acceptをクリックします。
(4) 次に利用規約が表示されます。 それらを読んで下にスクロールして、Acceptをクリックします。
利用規約
利用規約
(5) これで、最初のMetaMask画面が表示されます。Import Existing DENをクリックします。
MetaMask初期画面
MetaMask初期画面
(6) Wallet Seedと記されたボックスに、Ganacheに表示されるMnemonicを入力します。
GanacheのMnemonic
GanacheのMnemonic

警告 :メインのEthereumネットワーク(メインネット)ではこのMnemonicを使用しないでください。このMnemonicから生成されたアカウントにETHを送信すると、すべて失われます。

その下にパスワードを入力し、OKをクリックします。
Mnemonic入力画面
Mnemonic入力画面(このMnemonicは説明用です)
(7) ここでGanacheが作成したブロックチェーンにMetaMaskを接続する必要があります。MainNetworkと表示されているメニューをクリックし、CustomRPCを選択します。
MetaMaskネットワークメニュー
MetaMaskネットワークメニュー
(8) New RPC URLというボックスにhttp://127.0.0.1:7545と入力し、Saveをクリックします 。
MetaMask CustomRPC
MetaMask CustomRPC
すると、一番上のネットワーク名が"Private Network"と切り替わります。
(9) Settingsの横にある左矢印をクリックすると、ページが閉じてAccountページに戻ります。
Ganacheによって作成された各アカウントには10​​0Etherが与えられます。コントラクト本体が展開されたときと、テストが実行されたときにgasが使用されたため、最初のアカウントではそれよりやや少なくなっています。
MetaMaskアカウントの設定
MetaMaskアカウントの設定
これで設定は完了です。

lite-serverのインストールと設定

これで、ローカルのWebサーバーを起動し、dappを使用できるようになりました。私たちは静的ファイルを提供するためにlite-serverライブラリを使用しています。これはpet-shopTruffle Boxに同梱されていますが、どのように動作するかを見てみましょう。

(1) bs-config.json(プロジェクトのルートディレクトリにある)をテキストエディタで開き、内容を確認します。

bs-config.json
{
  "server": {
    "baseDir": ["./src", "./build/contracts"]
  }
}

これはlite-serverに、ベースディレクトリにどのファイルを含めるかを指示します。Webサイトファイルには./build/contractsディレクトリを、契約成果物には./srcディレクトリを追加します。

また、プロジェクトのルートディレクトリにあるpackage.jsonファイルのscriptsオブジェクトにdevコマンドを追加しました。scriptsオブジェクトを使用すると、コンソールコマンドを1つのnpmコマンドにエイリアスすることができます。この場合、単一のコマンドを実行するだけですが、より複雑な設定を行うことも可能です。今回は次のようになります:

package.json`
"scripts": {
  "dev": "lite-server",
  "test": "echo \"Error: no test specified\" && exit 1"
},

dappの使用

(1) ローカルWebサーバーを起動します。

npm run dev

devサーバーが起動し、dappを含む新しいブラウザー・タブが自動的に開きます。

Pete'sペットショップ
Pete'sペットショップ

(2) dappを使用するには、お好みのペットのAdoptボタンをクリックします。
(3) MetaMaskによるトランザクションの承認を求めるメッセージが自動的に表示されます。トランザクションを承認するには、Sendをクリックします。
取引レビュー
取引レビュー
(4) 引き取ったペットボタンがSuccessとなり、設定したとおりにボタンが無効になります。これはペットが引き取られたためです。
引取成功
引取成功

注意 :ボタンがSuccessと自動的に変わらない場合は、ブラウザでページを更新すると、反映されます。

そして、MetaMaskにはトランザクションがリストされています:
MetaMaskトランザクション
MetaMaskトランザクション

また、GanacheのTransactionに同じ取引が表示されます。



訳者追記:7章振り返り

この章では、以下のことを行いました。

- 実際にDappをブラウザ経由で使用してみる

Chrome/Firefoxの拡張機能であるMetaMaskを使用してトランザクションを送りました。

最後に

おめでとうございます!あなたは本格的なdapp開発者になるための大きな一歩を踏み出しました。ローカルで開発する場合は、より高度なdappを作成するために必要なすべてのツールが既に用意されています。dappを他の人が利用できるようにしたい場合は、Ropsten testnetへの配備についての今後のチュートリアルのために準備しておいてください!



以上、Truffleが提供している、ETHEREUM PET SHOPの日本語訳でした。