Truffle+VueでDApp開発入門
以前 geth を使って開発環境作りみたいなことしましたが,Truffle と Ganache を使ってより簡単に DApp を作っていきたいと思います.
今回作ったもの
初めての DApp 開発なのでアイディアを書き込んで参照するだけの簡単なものを作ってみます.
デモアプリ は Heroku で公開しています.Free Dyno なので,最初の接続に時間がかかります.試す際は必ず Ropsten ネットワークに切り替えてください.
mktia/truffle-test | GitHub にも上げています.
開発環境
- macOS High Sierra
- Truffle: v4.1.14
- Solidity: v0.4.24
- Vue: v2.9.6
- Ganache: v1.2.1
あらかじめ Vue (vue-cli) を入れておきます.macOS に Vue を入れる方法は以下の記事でご確認ください.
Truffle インストール
Truffle を入れるとプロジェクトの管理が楽になります.
terminal
$ npm install -g truffle
Ganache インストール
geth は色々設定することがありますが,Ganache (GUI 版) は起動するとアカウントが最初から用意されていて楽に開発できます.
プロジェクトの初期化
terminal
$ truffle init
Truffle での開発に必要なディレクトリが作られます.
コントラクト
コーディング
Solidity で Ethereum ブロックチェーン上に置くコントラクトのコードを書きます.コーディングに使う言語は他にも Vyper とかあります.
一応コーディング規約もあります.誰でも使えるようなコントラクトならやはり見やすいほうが望ましいので,命名規則などは統一すべきと思われます.JS ライクなのでだいたい CapsWords か mixedCase です.
コントラクトのコードは contracts ディレクトリに保存します.
pragma solidity ^0.4.24;
contract IdeaFactory {
event NewIdea(uint id, string content);
struct Idea {
uint ideaId;
string content;
}
Idea[] public ideas;
mapping(uint => address) public ideaToOwner;
function comeUpWithIdea(string _content) public returns (uint) {
uint id = ideas.length;
ideas.push(Idea(id, _content));
ideaToOwner[id] = msg.sender;
emit NewIdea(id, _content);
return id;
}
function getIdea(uint _id) public view returns (string) {
return ideas[_id].content;
}
function getIdeaCount() public view returns (uint) {
return ideas.length;
}
}
Solidity のコーディングで注意すべき点
- Struct は返せないので,複数の戻り値を使うなどして対応する
- string の配列も返せない
- Struct 外で uint32 など変数の使用領域を指定するのは意味がない
Struct の配列を返したかったら基本型の配列を作って返すことで対応できますが,string が含まれる場合は詰みます.その場合は JS 側で片付けましょう.
コンパイル
terminal
$ truffle compile
コンパイルすると build/contracts に JSON ファイルが作られます.
ローカルネットワークへのデプロイ
予め起動した Ganache にデプロイします.
設定
truffle init
した状態では白紙になっている truffle.js に設定を書き込みます.
truffle.js
module.exports = {
networks: {
development: {
host: '127.0.0.1',
port: 7545,
network_id: '*'
}
}
};
development
というネットワークの環境を設定します.ホストとポートは Ganache のものを書き込みます.network_id
はワイルドカードになっています.
127.0.0.1
の代わりに localhost
で設定すると何らかの問題が起きる的なことをどこかで見た気がするので,前者で書いています.
マイグレーションファイルの作成
var IdeaFactory = artifacts.require("./IdeaFactory.sol");
module.exports = function (deployer) {
deployer.deploy(IdeaFactory);
};
migrations ディレクトリに保存します.Truffle はファイル名の最初の数字の昇順にマイグレーションを実行します.
デプロイ
terminal
$ truffle migrate
ネットワークを選択する場合は --network development
のようにオプションで指定できます.truffle.js の設定を参照しています.
マイグレーションをし直したい場合は --reset
オプションを付けます.
テスト
test ディレクトリにテストコードを保存し,想定通りの動きをしているか確認してみます.
pragma solidity ^0.4.24;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/IdeaFactory.sol";
contract TestIdea {
IdeaFactory ideaFactory = IdeaFactory(DeployedAddresses.IdeaFactory());
function testComeUpWithIdea() public {
uint returned = ideaFactory.comeUpWithIdea("Get more money!");
uint expected = 0;
Assert.equal(returned, expected, "Idea ID 0 should be recorded.");
}
function testGetIdea() public {
string memory expected = "Get more money!";
string memory returned = ideaFactory.getIdea(0);
Assert.equal(returned, expected, "'Get more money!' should be recorded.");
}
}
testGetIdea()
では変数を memory
にしています.明示的に宣言しなかったら Type error: memory is not implicity convertible to expected type
的な感じで怒られました.
ちなみに変数は格納領域が三つあり,それぞれコストのかかり方が異なります.
- storage: 参照型の変数 (Struct, 配列, mapping)
- memory: 関数の引数
- stack: 基本型の変数 (uint, string etc)### ###
参考:Solidity の変数の格納領域 (storage, memory, stack) | Qiita
terminal
$ truffle test test/testIdeaFactory.sol
ファイル名を指定しなければ,ディレクトリ内のすべてのファイルに対してテストします.
Vue
デプロイが成功したら,フロントエンドも用意します.今回は Vue を使います.
Vue プロジェクトの初期化
terminal
$ vue init
vue-cli を使って Vue のプロジェクトを作ります.全て初期設定です.
Truffle のルートディレクトリで実行します.ディレクトリが空でないため,注意が表示されますがそのまま実行できます.
ちなみに, truffle init
は空のディレクトリじゃないと実行できないので, vue init
からの truffle init
はできません.
必要なパッケージをインストール
terminal
$ npm install web3
$ npm install truffle-contract
フロントエンドでブロックチェーンを扱うためのパッケージを入れます.
Vue Component を書く
vue-cli を使うと基本形が用意されるので,それを少しいじって作ることにします.コントラクトを使って動いていることが最低限分かればいいので,ここは適当に書きます.
<template>
<div class="app">
<h2>POST YOUR IDEA</h2>
<p v-if="account">アカウント: {{ account }}</p>
<p v-if="!account">アカウントが見つからないよ</p>
<input
v-model="newIdea"
type="text"
name=""
value=""
placeholder="なんか書いてみたら?"
/>
<button @click="postIdea">投稿</button>
<ul>
<li class="idea-box" v-for="idea in ideas" :key="idea">
{{ idea }}
</li>
</ul>
<p v-if="contractAddress">コントラクトアドレス: {{ contractAddress }}</p>
<p v-if="!contractAddress">コントラクトアドレスが見つからないよ</p>
</div>
</template>
<script>
import Web3 from "web3";
import contract from "truffle-contract";
import artifacts from "../../build/contracts/IdeaFactory.json";
const IdeaFactory = contract(artifacts);
export default {
name: "TopPage",
data() {
return {
contractAddress: null,
account: null,
newIdea: null,
ideas: [],
};
},
created() {
if (typeof web3 !== "undefined") {
web3 = new Web3(web3.currentProvider);
} else {
console.warn(
"No web3 detected. Falling back to http://127.0.0.1:7545. You should remove this fallback when you deploy live, as it's inherently insecure. Consider switching to Metamask for development. More info here: http://truffleframework.com/tutorials/truffle-and-metamask"
);
web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:7545"));
}
IdeaFactory.setProvider(web3.currentProvider);
web3.eth.getCoinbase().then((coinbase) => {
IdeaFactory.defaults({ from: coinbase });
});
web3.eth.getAccounts((error, accounts) => {
if (error != null) {
console.error(error);
this.message =
"There was an error fetching your accounts. Do you have Metamask, Mist installed or an Ethereum node running? If not, you might want to look into that.";
return;
}
if (accounts.length === 0) {
this.message =
"Couldn't get any accounts! Make sure your Ethereum client is configured correctly.";
return;
}
this.account = accounts[0];
});
IdeaFactory.deployed().then((instance) => {
var event = instance.NewIdea();
event.watch((error, result) => {
if (!error) {
this.ideas.unshift(result.args.content);
this.newIdea = null;
console.log(result.args.content);
}
});
this.contractAddress = instance.address;
});
},
beforeMount() {
IdeaFactory.deployed().then((instance) => {
var contract = instance;
contract.getIdeaCount().then((count) => {
for (var i = 0; i < parseInt(count.toString(10)); i++) {
contract.getIdea(i).then((idea) => this.ideas.unshift(idea));
}
});
});
},
methods: {
postIdea() {
return IdeaFactory.deployed()
.then((instance) => {
var contract = instance;
contract
.comeUpWithIdea(this.newIdea)
.then((id) => contract.getIdeaCount())
.then((count) => contract.getIdea(count.toString(10)))
.then((idea) => this.ideas.unshift(idea));
})
.catch((error) => {
console.error(error);
});
},
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
li.idea-box {
border: 1px solid;
display: block;
margin: 0.5em auto;
padding: 0.5em;
width: 320px;
}
ul {
padding-left: 0;
}
</style>
コンポーネント名を変更したので src/router/index.js も書き換えます.
src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import TopPage from '@/components/TopPage'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'TopPage',
component: TopPage
}
]
})
JS 側で注意すべき点
- 非同期なので Promise で返ってくる
- JSON RPC エラーで泣くことがある
- BigNumber で返ってくる
- Metamask のエラーが出て混乱することがある (これは無視)
JSON RPC エラーはパッケージの使い方が正しくなかったりプロバイダ指定が間違っていたりすると起きるようですが,詳細不明です.前者に関しては正しくなくても一見動いているように見えるので,要確認です.
returns (uint)
だから int で返ってくるのかと思いきや,BigNumber という型で返ってくることがあります. obj.toString(10)
でいいよって Q&A サイトには書かれていましたが,望ましい書き方かどうかはわかりません. obj.toNumber()
かもしれませんし,int 型にパースしたほうがいいのかもしれません.
利用環境の準備
Chrome ブラウザで Metamask が使える環境を作っておきます.Chrome ウェブストアから拡張機能としてインストールできます.
Metamask を開き,Ganache 上のアカウントをインポートして使います.Main Ethereum Network になっているので Custom RPC に切り替え,Ganache の動いているポートを指定します.初期設定では http://localhost:7545 です.
ネットワークを切り替えたら Import existing DEN で進みます.12 単語を入力する画面が出るので,Ganache の MNEMONIC をコピペするとインポートできます.
開発環境で実行
terminal
$ npm run dev
webpack-dev-server を使ってローカルサーバで起動します.ポートは 8080 です.(http://localhost:8080)
Ropsten ネットワークへのデプロイ
現時点では Ganache を使ってローカルで動いています.本番環境ではメインネットを使いますが,デプロイ後はコントラクトを書き換えられないのでテストネットを使ってテストします.
Infura の登録
はじめに Infura に登録します.CREATE NEW PROJECT でエンドポイントが生成されます.
設定の更新
設定に使うパッケージを入れます.
terminal
$ npm install truffle-hdwallet-provider
truffle.js に Ropsten の設定を加えます.
truffle.js
var HDWalletProvider = require('truffle-hdwallet-provider');
const MNEMONIC = 'Truffleからコピペ'
module.exports = {
networks: {
development: {
...
},
ropsten: {
provider: function() {
return new HDWalletProvider(MNEMONIC, 'https://ropsten.infura.io/v3/xxxxxxx')
},
network_id: '3'
}
}
}
HDWalletProvider()
の第二引数には Infura で発行されたエンドポイントを入れます.エンドポイントはユーザーの固有値となるため,誤って Git の管理下にしないように .gitignore で設定して下さい.
network_id
は 3 を指定すると Ropsten ネットワークが選択されます.その他の ID にも対応するネットワークがあり,1 がメインネットです.
テスト用 ETH の受け取り
ETH がないと何もできないので,テスト用に配布されているものを受け取ります.
アカウント画面で BUY > ROPSTEN TEST FAUCET > request 1 ether from faucet で Ropsten ネットワークで使える 1ETH が送られてきます.テストネットでブロックが承認されたら受け取れるので,数十秒かかります.
デプロイ
terminal
$ truffle migrate --network ropsten
デプロイした後は Metamask を Ropsten ネットワークに切り替えても DApp が動くようになります.
デプロイ時のエラー
Error encountered, bailing. Network state unknown. Review successful transactions manually. insufficient funds for gas * price + value
このエラーはデプロイに使うアカウントに必要な分のガスがなかったときに出ます.恐らく初期設定では一つ目のアカウントが coinbase になっているので,一つ目のアカウントに ETH を入れておくとデプロイできます.
参考:Ropsten デプロイ時に躓いたことメモ | Qiita
Node.js サーバへのデプロイ
ローカルサーバで動かしているものを公開するためには Node.js に対応する Web サーバに上げる必要があります.GCP や Heroku を使うと楽です.
Express の導入
今回は Express で動く環境を作ります.
terminal
$ npm install --save express
Express の設定
設定ファイルを書きます.
var express = require("express");
var path = require("path");
var serveStatic = require("serve-static");
app = express();
app.use(serveStatic(__dirname + "/dist"));
var port = process.env.PORT || 5000;
app.listen(port);
console.log("server started " + port);
npm でビルドしたファイルは dist ディレクトリ下に生成されるので,Express がそこを参照するように設定します.
ビルド
terminal
$ npm run build
ビルドすると Vue で書いた内容をコンパイルしてくれます.
起動
terminal
$ node server.js
起動コマンドの設定
上記のコマンドでも動きますが,package.json に設定を加えておくと良いと思います.
初期設定では "dev"
と "start"
の内容が同じなので,後者を書き換えます.
package.json
{
...
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "node server.js",
...
},
...
}
これ以降は次のコマンドで起動できます.
terminal
$ npm run start
Heroku へのデプロイ
書くのに疲れてきてしまったので,ざっくりとした説明になります....
Git の初期化
以下の記事の「1.開発環境の構築」が終わってるものとして,プロジェクトのルートディレクトリで以下のコマンドを実行します.
terminal
$ git init
.gitignore の編集
初期設定では動かなくなるので,必要に応じてデプロイするディレクトリ,ファイルを変更します.
.gitignore
.DS_Store
node_modules/
dist/ # 消す
npm-debug.log*
yarn-debug.log*
yarn-error.log*
test/unit/coverage
test/e2e/reports
selenium-debug.log
truffle.js # 追加する
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
デプロイ
terminal
$ git add .
$ git commit -m "init"
$ heroku create --name YOURAPPNAME
$ heroku git:remote --app YOURAPPNAME
$ git push heroku master
Heroku 上にアプリを作成し,Git をデプロイします.
参考
Easily deploy a Vue + Webpack App to Heroku in 5 Steps tutorial | Medium