実用的なスマートコントラクトアプリを開発するための準備として,最も簡単なスマートコントラクトである greeter を使ったアプリを作ってみました。
環境
$ geth version
Geth
Version: 1.7.1-stable
Git Commit: 05101641455a754936acc5ddff92f35f5e33181c
Architecture: amd64
Protocol Versions: [63 62]
Network Id: 1
Go Version: go1.9.1
Operating System: darwin
GOPATH=/Users/arosh/go
GOROOT=/usr/local/Cellar/go/1.9.1/libexec
- MetaMask 3.10.9
- browser-solidity e6f4dc2
MetaMaskについて
MetaMask は Chrome や Firefox のブラウザ拡張として使うことができるウォレットアプリです。このウォレットの大きな特徴は,Mist のようにブラウザ上で動作する Dapp に対応していて,アカウントを設定済みの web3.js オブジェクトをグローバル空間に注入してくれることです。オープンソースで開発されていて,接続先のネットワークをプライベート・ネットに切り替えることもできます。
アカウント作成画面です。左上の「Private Network」の部分をクリックすると,接続先のネットワークを Mainnet,Ropsten,Korvan,Rinkeby,localhost:8545,Custom RPC から選ぶことができます。
アカウントの管理画面です。現在の残高や過去のトランザクションが確認できます。
コントラクトに変更を加えるような操作をした時に表示されるポップアップです。消費される ether をユーザが確認して許可するまで ether が使用されることはありません。
プライベート・ネットの準備
開発用のプライベート・ネットを構築するために,以下のコマンドを実行します。
$ geth --datadir . --dev --networkid 1337 --gasprice 18000000000 --rpc --rpccorsdomain "*"
--datadir
オプションはデータベースとキーストアの場所を指定するオプションで,ここではカレントディレクトリを指定しています。--dev
オプションは開発用のプライベート・ネットを構築するのに有用なオプションで,P2Pネットワークを構築しなくなったりマイニングが簡単になったりします(詳細は 過去の記事 を参照してください)。--networkid
オプションについては後述します。--gasprice
オプションは送金やスマートコントラクトの処理の大きさの単位 (gas) あたりの価格 (gas/wei) を指定するオプションで,--dev
オプションを指定したときには 0 gas/wei になっているのですが,現実のネットワークとの差異が大きすぎるので適当な値に設定しています。現在の gas/wei は ethstats.net などを参照してください。記事を執筆している時点では 21 gwei (21000000000 wei) でした。
--networkid 1337
を指定している理由ですが,このオプションを指定せずにMetaMaskでトランザクションに署名をしようとすると "rpc error with payload", "Error: invalid sender" というエラーが出てしまったからです。
このエラーは EIP 155 という Ethereum の仕様が有効化されていないネットワークでMetaMaskを使おうとしたときに発生したり,genesis.json
の config.chainID
と Geth の --networkid
が一致していないときに発生したり,様々な場面で発生します。
- Newer version of matamask throws errors when sending transaction. · Issue #2015 · MetaMask/metamask-extension
- Signing Transactions fail with Locally created Accounts. · Issue #932 · ethereum/web3.js
最初は --dev
オプションを使ったときに EIP 155 が有効になっていないのかと思ったのですが,ソースコードを見る限りでは0ブロック目から有効になっているように見えるのでこれは違うようでした (config.go#L88, genesis.go#L348)。よく見ると,gethを起動した画面でもその旨が表示されています。
あとは genesis.json
の config.chainID
と Geth の --networkid
が一致していないことによる問題が考えられるのですが,上記の画像によると chainID
は 1337 に設定されているようなので --networkid 1337
と指定するとうまく動きました。ただ,マイニングをしたりetherを送金したりするときにはこのエラーは表示されなかったので,以前に体験した不具合 とは性質が違うような気がしています。
アカウントの準備
アカウントを2つ用意します。
- マイニング用アカウント
- MetaMask 用アカウント
最初に 1. のアカウントでマイニングを行い適量の ether を入手します。次に,得られた ether を 1 ether ほど 2. のアカウントに送金します。
私は 1. のアカウントの作成と送金はMistを使って行いました(EthereumのDappをスマートフォンから利用する方法について)。
Solidityを使ったスマートコントラクトの記述
browser-solidity を使ってスマートコントラクトを記述します。今回は solidity-baby-steps の greeter.sol を改変したものを作成しました。作成したコントラクトを以下に示します。
pragma solidity ^0.4.17;
contract Greeter {
string public message;
event greetEvent(string _message);
function Greeter(string _message) public {
message = _message;
}
function setMessage(string _message) public {
message = _message;
greetEvent(_message);
}
function greet() public {
greetEvent(message);
}
}
このコントラクトは基本的には message
というプロパティを set して get するだけのものです。greetEvent
という event を用意しているので,web3.js でこの event を .watch()
すると setMessage(_message)
もしくは greet()
が呼ばれたときに何か処理を行うこともできます。ところが,後述しますが MetaMask の制限により event の機能は使えなかったので,set と get の機能しか使っていません。
コントラクトのデプロイには browser-solidity を使います。最初に browser-solidity の gh-pages ブランチ から remix-XXXXXXX.zip
をダウンロードして展開します。その中の index.html
を開けば browser-solidity が起動するのですが,MetaMask は http もしくは https でしか使えないという制約がある ので http-server などを使って http で開きます。
$ npm i -g http-server
$ http-server
browser-solidityが開いたら中央のペインにコントラクトのソースコードを貼り付けます。ファイル名は "Greeter.sol" などに変更しておきましょう。
次にコントラクトのデプロイを行います。右のペインの Run タブの Environment の項目の中から "Injected Web3" を選びます。これは MetaMask から供給される Web3 のアカウントを使用するという設定です。「Web3 が見つからない」という旨のエラーが出たときには MetaMask のアカウントの作成がうまくいっているか,MetaMask のネットワークの設定で localhost:8545 が選択されているかなどを確認してください。設定変更後は Chrome のタブを一度閉じて開き直さないと変更が読み込まれないようです。
Injected Web3 が選択できたら "Create" というボタンの右にあるテキストボックスに json 形式でコントラクトのコンストラクタの引数を与えます。今回は string 型の引数なのでダブルクォートで囲いましょう。
"Create" ボタンを押すとコントラクトのデプロイが行えます。MetaMask から確認が求められるので Submit を押します。
コントラクトがデプロイできたら,メモしておく必要がある項目が2つあります。1つめはコントラクトの ABI で,Web3 からコントラクトを利用する際に必要となります。browser-solidity の右のペインの Compile タブの Detail ボタンを押し,出てきたダイアログの中の INTERFACE という項目の部分にあるコピーボタンを押すとクリップボードにコピーできます。
コピーされた文字列は以下のようなものです。
[{"constant":false,"inputs":[{"name":"_message","type":"string"}],"name":"setMessage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"greet","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"message","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_message","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_message","type":"string"}],"name":"greetEvent","type":"event"}]
2つ目はコントラクトのアドレスです。これはコントラクトのデプロイ後に Run タブのコントラクト・インスタンスの一覧が表示されている部分でコピーボタンを押すとクリップボードにコピーできます。
コピーされた文字列はたとえば以下のようなものです。
0x7599c8900e6c9f57078d2248d2596fd3a4c4f8c2
React+Redux を使ったアプリケーション開発
作成したコントラクトをブラウザで利用するために以下のようなコントラクトを作成しました(クリックするとYouTubeが開きます)。
ソースコードは以下のリポジトリにあります。
詳細な実装はリポジトリのソースコードを見てもらうとして,ここでは要点だけ説明します。
Web3 のセットアップを行っている部分です。MetaMask Compatibility Guide に載っているものをコピペしています。MetaMask や Mist などで Web3 オブジェクトが供給されている場合にはそれを利用しています。
export function setupWeb3() {
if (typeof window.web3 !== 'undefined') {
window.web3 = new Web3(window.web3.currentProvider);
} else {
window.web3 = new Web3(
new Web3.providers.HttpProvider('http://localhost:8545')
);
}
}
コントラクトのインスタンスを取得する処理です。browser-solidity で取得したコントラクトの ABI とコントラクトアドレスが必要になります。自分で動かしたい場合にはコントラクトのアドレスを書き換えてください。
const contractAddress = '0x7599c8900e6c9f57078d2248d2596fd3a4c4f8c2';
// prettier-ignore
const contractABI = [{"constant":false,"inputs":[{"name":"_message","type":"string"}],"name":"setMessage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"greet","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"message","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_message","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_message","type":"string"}],"name":"greetEvent","type":"event"}];
export function getInstance() {
const web3 = window.web3;
const contract = web3.eth.contract(contractABI);
const instance = contract.at(contractAddress);
return instance;
}
トランザクションに署名を行うアカウントの設定です。MetaMask Compatibility Guide によれば,MetaMask を使う場合には web3.eth.accounts[0]
を使えば MetaMask で設定したアカウントが利用されるようになっているようなのですが,Mist など他のウォレットではそのような仕様にはなっていないので,別の処理を書く必要があります。
export function setupDefaultAccount() {
return new Promise((resolve, reject) => {
window.web3.eth.getAccounts((err, accounts) => {
if (err) {
reject(err);
return;
}
window.web3.eth.defaultAccount = accounts[0];
resolve();
});
});
}
コントラクトから message
を取り出す処理です。
export function getMessage(): Promise<string> {
const instance = getInstance();
return new Promise((resolve, reject) => {
instance.message((err, message) => {
if (err) {
reject(err);
return;
}
resolve(message);
});
});
}
message
の内容を書き換える処理です。instance.setMessage(_message)
を呼び出すだけで,署名を行うかどうかの確認などの処理は MetaMask がよろしくやってくれます。
export async function setMessage(message: string): Promise<void> {
await setupDefaultAccount();
const instance = getInstance();
return new Promise((resolve, reject) => {
instance.setMessage(message, err => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
最初に呼び出される部分です。setupWeb3()
の呼び出しのほかに,setInterval
で定期的に getMessage
する処理などを書いています。取得した値は Flux パターンの Store に dispatch しています。
// @flow
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './components/App';
import createStore from './store/create';
import * as eth from './infra/ethereum';
import { updateBlockNumber, updateMessage } from './reducers';
const store = createStore();
window.addEventListener('load', function() {
eth.setupWeb3();
eth.setOnGreet(() => {});
setInterval(async () => {
const blockNumber = await eth.getBlockNumber();
if (store.getState().blockNumber !== blockNumber) {
store.dispatch(updateBlockNumber(blockNumber));
}
const message = await eth.getMessage();
if (store.getState().message !== message) {
store.dispatch(updateMessage(message));
}
}, 100);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('react-root')
);
});
event の処理について
コントラクトの setMessage
や greet
を呼び出したときには greetEvent
という event を発生させるようなコントラクトを記述したのですが,MetaMask経由では event を取得することができませんでした。以下の画像のように geth のコンソールからは event が取得できています。
- Event Filters - Support loading Historical Events · Issue #503 · MetaMask/metamask-extension
- Historical events not working in certain cases · Issue #681 · MetaMask/metamask-extension
- Events in Metamask · Issue #792 · MetaMask/metamask-extension
- Metamask doesn't detect events. · Issue #1429 · MetaMask/metamask-extension
- Watching events: The request took too long to process · Issue #2114 · MetaMask/metamask-extension