分散型アプリケーション(Dapp)はブロックチェーンを基盤に構築していくので、特定の管理者に権限が集中することのないアプリケーションとして注目を集めています。
そこで、実際に簡単なDappを作ることでDappはどのような仕組みで機能しているのか見ていきましょう。
ここではTruffleのチュートリアルであるETHEREUM PET SHOPの流れにそって、実際にウェブ上ペットショップの分散型アプリケーションを作っていきます。Truffleというイーサリアムの開発フレームワークを使用し、solidityという言語でスマートコントラクトを記述していきます。
完成図は以下の通りです。飼いたいと思ったペットの「adopt」というボタンを押すとMetaMaskが起動し、表示された金額と手数料を確認してトランザクションが作成されETHで支払うことができます。
通常のECサイトと違い、購入するために個人情報やクレジット番号を入力する必要がないことが分かります。また、購入データはトランザクションとしてイーサリアムのブロックチェーンに記録されるので改ざんされることはありません。
実装の具体的な流れは以下の通りです。
- 開発環境の設定
- Truffle Boxを使いTruffleプロジェクトを作成
- スマートコントラクトの記述
- スマートコントラクトのコンパイルとマイグレーション
- スマートコントラクトのテスト
- スマートコントラクトに紐付いたUIの作成
- ブラウザでのDappの使用
Truffleプロジェクトの作成
nodeとnpmを使える環境を用意しておきましょう。OSがubuntuであれば以下のようにnode.js 8.x系をインストールすることができます。
$ apt-get update
$ curl -sL https://deb.nodesource.com/setup_8.x | bash -
$ apt-get install -y nodejs
まずは、Truffleをインストールしていきます。
$ npm install -g truffle
Truffleとはイーサリアムの開発フレームワークです。ソースコードのコンパイルやデプロイなどを効率的に行うことができるのでスマートコントラクト開発でとても便利なフレームワークとなります。
pet-shop-tutorialというディレクトリを作り、このディレクトリ内で作業していきます。通常は truffle init で初期化を行い、空のプロジェクトを作成しますが、今回はチュートリアルのためTruffle Boxというあらかじめ用意されたプロジェクトから作成します。
$ mkdir pet-shop-tutorial
$ cd pet-shop-tutorial
$ truffle unbox pet-shop
すると、pet-shop-tutorial内にこのようなファイルとディレクトリが用意されます。
実際に使っていくディレクトリとファイルは以下の通りです。
- contarctsディレクトリ:スマートコントラクトを記述するSolidityのソースファイル群
- migrationsディレクトリ:スマートコントラクトのデプロイ時に使うマイグレーションシステム
- testディレクトリ:JavascriptとSolidityで記述されたテストファイル群
- truffle.js:Truffleの設定ファイル
solidityとはイーサリアムのスマートコントラクトを記述するプログラミング言語になります。
スマートコントラクトの記述
さっそくsolidityでスマートコントラクトを記述していきます。作成されたcontractsディレクトリにAdoption.solというファイルを作り、以下を記述していきます。
pragma solidity ^0.4.4;
contract Adoption {
address[16] public adopters;
function adopt(uint petId) public returns(uint) {
require(petId >= 0 && petId <= 15);
adopters[petId] = msg.sender;
return petId;
}
function getAdopters() public returns (address[16]) {
return adopters;
}
}
それぞれ順番に見ていきましょう。
pragma solidity ^0.4.4;
solidityコンパイラのバージョンを指定しています。また、solidityではjavascriptのように行末にセミコロン(;)をつけます。
contract Adoption {
・・・
}
Adoption という名前のコントラクトを宣言しています。この中に、コントラクトを実装していきます。
address[16] public adopters;
ここでは、 adopters という状態変数を宣言しています。solidityは静的言語なので変数のデータ型を宣言する必要があり、solidityには一般的なstringやuintなどのデータ型の他に、addressという独特なデータ型があります。addressはアカウントのアドレスを格納しています。
ここでのadressは配列でありadoptersという変数は16個のアドレスを持つことになります。
また、ここではadopters変数の前に public と記述されており、publicの変数はそのコントラクトにアクセスできるユーザーであれば誰でも閲覧することが可能になります。
このように変数を宣言した後にコントラクトのメソッドを宣言していきます。
adopt(uint petId) public returns (uint) {function
require(petId >= 0 && petId <= 15);
adopters[petId] = msg.sender;
return petId;
}
整数型である petId は adopters の配列長に合わせて0〜15にしておきます。(配列のインデックス番号は0から始まるので) reruire()を使うことでpetIDが0〜15になるようにしています。
msg.senderは、この関数を実行した人(またはスマートコントラクト)のアドレスを表しています。なので adopters[petId] = msg.sender; でadopters配列に実行者のアドレスを追加しています。
そして、petIdを戻り値として返しています。
以上のadopt関数で、petIdをadopters配列のkeyとしてひとつのアドレスを返すことができるようになりました。しかし、このままでは更新するたびに16回のAPIコールを行う必要があるので、以下のgetAdopters関数でadopters配列全体を返すようにしていきます。
getAdopters() public returns (address[16]) {function
return adopters;
}
すでに、変数adoptersは宣言されているので、ただデータ型を指定し戻り値を返すだけでOKです。
これでスマートコントラクトを記述することができました。内容をまとめると、「16匹いるペットに対して、もしユーザーがあるペットを飼おうとしたらそのユーザーのアドレスとそのペットIDが紐付けられる」というAdoptionコントラクトを作成しました。
続いてこのスマートコントラクトをコンパイルし、マイグレートしていきましょう。
コンパイル
コンパイルとは、プログラミング言語で書かれたソースコードをコンピューターが直接実行できる機械語に翻訳することです。つまり、solidity言語で書かれたコードをイーサリアムのバーチャルマシンであるEVM(Ethereum Virtual Machine)が実行できるようにバイトコードに変換していきます。
Dappを含んでいるディレクトリにおいて、ターミナル等でTruffle Developを立ち上げます。
$ truffle develop
そして、起動したtruffle developのコンソールでcompileと入力します。
$ truffle(develop)> compile
以下のような出力が表示されれば成功です。
Compiling ./contracts/Adoption.sol...
Compiling ./contracts/Migrations.sol...
Writing artifacts to ./build/contracts
publicであるwarningが出ることもありますがスルーしてOKです。(truffle developのコンソールはずっと起動したままにしておきましょう)
マイグレーション
マイグレーションとは、「移動」といった意味で既存のシステムなどを新しいプラットフォームに移行することを言います。
ここでは、作成したAdoptionコントラクトをイーサリアムのブロックチェーンネットワークへデプロイするのをマイグレーションファイルが行なってくれます。
migrationsディレクトリの中を見るとすでに1_initial_migration.jsというjavascriptのマイグレーションファイルがあるはずです。このファイルは、contractsディレクトリにあるMigrations.solをデプロイし、一連のスマートコントラクトを正しくマイグレートできるように管理してくれます。
2_deploy_contracts.jsというマイグレーションファイルをmigrationsディレクトリ内に作成します。このマイグレーションファイルには以下のように記述します。
Adoption = artifacts.require("Adoption");const
module.exports = (deployer) => {
deployer.deploy(Adoption);
};
そして、起動しているtruffle developのコンソールにmigrateとコマンドを打ちましょう。
$ truffle(develop)> migrate
そして、以下のような出力がされれば成功です。
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0xa1f5bc4affc464999763799648db42acae31772140af652d27f921ee11cb330d
Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying Adoption...
... 0xe46e604dce4322e0492be99b5d3744468e20f8a233e3da551dd42ad9272839b9
Adoption: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...
以上で、スマートコントラクトの作成し、それをコンパイルし、ローカル環境のテストブロックチェーンにデプロイできました。続いて、これが実際に正しく行われているのかテストしていきましょう。
スマートコントラクトのテスト
スマートコントラクトのテストを行うことは非常に重要なステップです。なぜなら、スマートコントラクトの設計ミスやバグはユーザーのトークン(資産)に関わることで、ユーザーの大きな損害につながるおそれがあるからです。
スマートコントラクトのテストは主に手動テストと自動テストがあります。これらはどちらか一方だけを行えばいい、というわけではなく手動でも自動でもしっかりと動作確認を行うようにしましょう。
手動テストの場合は、Ganacheなどのローカル開発環境のツールを使うことでアプリケーションの動作確認を行うことができます。これらはGUIで実際にトランザクションを参照することができるので分かりやすいです。
当記事では手動テストは省略して以下、自動テストを行なっていきます。
Truffleにおいてスマートコントラクトの自動テストはjavascriptとSolidityの両方で記述することができますが、ここではSolidityで書いていきます。作成されたtestディレクトリ内にTestAdoption.solというファイルを作り、以下を記述していきます。
pragma solidity ^0.4.11;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";
contract TestAdoption {
Adoption adoption = Adoption(DeployedAddresses.Adoption());
function testUserCanAdoptPet() {
uint returnedId = adoption.adopt(8);
uint expected = 8;
Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
}
function testGetAdopterAddressByPetId() {
address expected = this;
address adopter = adoption.adopters(8);
Assert.equal(adopter, expected, "Owner of pet ID 8 should be recorded.");
}
function testGetAdopterAddressByPetIdInArray() {
address expected = this;
address[16] memory adopters = adoption.getAdopters();
Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
}
}
少し長いですが分解して見ていきましょう。
pragma solidity ^0.4.11;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";
contract TestAdoption {
Adoption adoption = Adoption(DeployedAddresses.Adoption());
/*
*ここに後述の関数を追記
*/
}
まず最初に以下の3つのコントラクトをインポートしています。
- Assert.sol:テスト時のさまざまな確認作業
- DeployedAddresses.sol:テスト時にデプロイしたコントラクトのアドレスを入手
- Adoption.sol:テストをしたいスマートコントラクト
そして、TestAdoptionというコントラクトを作成し、変数adoptionを宣言します。adoptionには、テストをするAdoptionコントラクトで上述のアドレスを入手するためのスマートコントラクトDeployedAddressesをコールしたものを入れています。
以下、このTestAdoptionコントラクト内にテストに使う関数を定義していきます。
testUserCanAdoptPet() {function
uint returnedId = adoption.adopt(8);
uint expected = 8;
Assert.equal(returnedId, expected, "Adoption of pet ID 8 should be recorded.");
}
ここではAdoptionコントラクト内に定義したadopt()関数のテストを行なっていきます。もしadopt()関数が正しく機能していれば引数の数字と同じpetId(戻り値)を返してくれるはずです。
ここでは、8というpetIdをadopt()関数に入れて、Assert.equal()で戻り値のpetIdと一致しているか確かめています。
testGetAdopterAddressByPetId() {function
address expected = this;
address adopter = adoption.adopters(8);
Assert.equal(adopter, expected, "Owner of pet ID 8 should be recorded.");
}
ひとつのpetIdに対して正しい所有者のアドレスが紐付いているか確認しています。ここでは、ペットのIDが8である所有者のアドレスが正しいかテストしています。
ちなみにthisという変数は現在のコントラクトにおけるアドレスを表しています。
testGetAdopterAddressByPetIdInArray() {function
address expected = this;
address[16] memory adopters = adoption.getAdopters();
Assert.equal(adopters[8], expected, "Owner of pet ID 8 should be recorded.");
}
最後に、adoptersという全てのアドレスを持つ配列が正しく返されているか確認します。memoryという属性はコントラクトのストレージに保存するのではなく、一時的に記録する値であることを表しています。
以上でテストを記述することができたので、このテストファイルをTruffle Developで実行していきます。
$ truffle(develop)> test
以下のような出力が表示されたら無事テストを成功しています。
Using network 'develop'.
Compiling ./contracts/Adoption.sol...
Compiling ./test/TestAdoption.sol...
Compiling truffle/Assert.sol...
Compiling truffle/DeployedAddresses.sol...
TestAdoption
✓ testUserCanAdoptPet (133ms)
✓ testGetAdopterAddressByPetId (112ms)
✓ testGetAdopterAddressByPetIdInArray (196ms)
3 passing (1s)
UIを作る
ここまでで、スマートコントラクトを作成し、ローカル環境のテストブロックチェーンにデプロイし、正常に動作するかテストを行いました。続いて、実際にペットショップをブラウザ上で見るためのユーザーインターフェースを作っていきましょう。
基本的な構造はすでにTruffle Boxが作ってくれていますので、イーサリアムで特徴的な関数を追記していくだけです。
srcディレクトリはアプリケーションのフロントエンド部分であり、この中の/src/js/app.jsファイルを編集していきます。以下のappオブジェクトにおける①〜④にコードを追記していきます。
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() {
/*
*①ここにコードを追記
*/
return App.initContract();
},
initContract: function() {
/*
* ②ここにコードを追記
*/
return App.bindEvents();
},
bindEvents: function() {
$(document).on('click', '.btn-adopt', App.handleAdopt);
},
markAdopted: function(adopters, account) {
/*
* ③ここにコードを追記
*/
},
handleAdopt: function(event) {
event.preventDefault();
var petId = parseInt($(event.target).data('id'));
/*
* ④ここにコードを追記
*/
}
};
$(function() {
$(window).load(function() {
App.init();
});
});
それでは①〜④に導入すべきコードを見ていきましょう。
①web3のインスタンス化
if (typeof web3 !== 'undefined') {
App.web3Provider = web3.currentProvider;
} else {
App.web3Provider = new Web3.providers.HttpProvider('http://localhost:9545');
}
web3 = new Web3(App.web3Provider);
まずweb3のインスタンスがすでにアクティブになっているか確認します。もしアクティブであれば、作成したアプリケーションのweb3オブジェクトに置き換えます。アクティブでなければ、ローカル開発環境でのweb3オブジェクトを作成します。
②コントラクトのインスタンス化
web3を介してイーサリアムネットワークとやり取りできるようになったので、続いて作成したスマートコントラクトをインスタンス化していきます。そのためにはコントラクトがどこにあり、どのように動作しているのかweb3に教える必要があります。
$.getJSON('Adoption.json', function(data) {
var AdoptionArtifact = data;
App.contracts.Adoption = TruffleContract(AdoptionArtifact);
App.contracts.Adoption.setProvider(App.web3Provider);
return App.markAdopted();
});
truffleにはtruffle-contractという便利なライブラリがあります。これはweb3上のライブラリであり、コントラクトとより簡単に接続することが可能になります。例えば、truffle-contractはマイグレーション時にコントラクトの情報を同期してくれるので、デプロイしたアドレスを手動で変更する必要はありません。
また、ArtifactファイルはデプロイしたアドレスやABI(Application Binary Interface)についての情報です。ABIとは、コントラクトの変数や関数、パラメーターなどがどのようにやり取りするのかインターフェースの情報を表しています。
TruffleContract()関数にArtifactを入れ、コントラクトをインスタンス化します。そして、web3のインスタンス化で作ったApp.web3Providerをそのコントラクトにセットします。
さらに、以前にペットが飼われていた場合のためにmarkAdopted()をコールします。スマートコントラクトのデータが変更される度にUIをアップデートする必要があるので、以下③の異なる関数で定義しているのです。
③UIのアップデート
ここでは飼われたペットの状態が変化しUIが更新されるようにしていきます。
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()をコールします。call()関数はブロックチェーンの状態を変化させるのではなく、単にデータを読み取るだけなのでgasを支払う必要はありません。
そして、それぞれのpetIdに対してアドレスが紐付けられているかチェックします。もしアドレスが存在したら、ボタンを「Success」に変更しボタンを押せないようにします。
④adopt()関数の操作
adoptionInstance;var
web3.eth.getAccounts(function(error, accounts) {
if (error) {
console.log(error);
}
var account = accounts[0];
App.contracts.Adoption.deployed().then(function(instance) {
adoptionInstance = instance;
return adoptionInstance.adopt(petId, {from: account});
}).then(function(result) {
return App.markAdopted();
}).catch(function(err) {
console.log(err.message);
});
});
ここではweb3を使ってアカウントのエラーを確認した後、実際にトランザクションを送る処理をしています。トランザクションの実行はadopt()関数によって行われ、petIdとアカウントのアドレスを含むオブジェクトを引数に取ります。
そして、そのトランザクション実行結果は③で定義したmarkAdopted()によって、新しいデータとしてUIに反映されることになります。
以上でDappを使う準備ができたので実際に作ったDappをブラウザで使ってみましょう!
Dappの完成
Choromeの拡張機能であるMetaMaskを使用していくのであらかじめインストールをしていきましょう。その際、アカウントは以下のWallet Seedを使うことでTruffle Develop用のアカウントを使っていきます。このSeedはTruffle Develop実行時に表示されています。(共通のseedです)
candy maple cake sugar pudding cream honey rich smooth crumble sweet treat
(すでにMetaMaskを使用している方はメニューのLockから以下の画面にいけます。)
Truffle Developで作成したブロックチェーンにMetaMaskを接続させるため、左上の「Main Network」から「Custom RPC」に変更します。そして、Truffle Develop用のhttp://localhost:9545に変更しましょう。すると、「Main Network」から「Private Network」に表示が変更されます。
上記のseedから作ったアカウントは100ETH弱を持っているはずです。コントラクトのデプロイで消費したgas分だけ引かれています。
以上のようにMetaMaskの設定ができたら、以下をterminal等に打ちローカルウェブサーバーを起動させましょう。(すでにbs-config.jsonやpackage.jsonは作られているのでlite-serverライブラリが使えます)
$ npm run dev
すると、以下のようなDappがブラウザ上で表示できます。
好きなペットの「adopt」というボタンをクリックすると、MetaMaskによりトランザクションが送られ、ETH払いでペットをすることが購入できます!
今回はチュートリアルなのでTruffle Boxを使ってイーサリアムにおけるDappの特徴的な部分にフォーカスして実装することができました。実際に自分で作ってみると、イーサリアムブロックチェーンの詳しい知識がなくてもDappがどのように機能しているか理解できてくるはずです。
また、分散型アプリケーション(Dapp)の具体的な活用事例や開発における課題点は以下の記事で紹介しています。