React+Redux と web3.js を使ったスマートコントラクトアプリ開発

実用的なスマートコントラクトアプリを開発するための準備として，最も簡単なスマートコントラクトである [greeter](https://github.com/fivedogit/solidity-baby-steps/blob/master/contracts/05_greeter.sol) を使ったアプリを作ってみました。

## 環境

```console
$ 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](https://github.com/ethereum/browser-solidity/blob/247a44990f00e30a72e318a0cfa3750001c1dc4e/remix-e6f4dc2.zip)

## MetaMaskについて

[MetaMask](https://metamask.io/) は Chrome や Firefox のブラウザ拡張として使うことができるウォレットアプリです。このウォレットの大きな特徴は，Mist のようにブラウザ上で動作する Dapp に対応していて，アカウントを設定済みの web3.js オブジェクトをグローバル空間に注入してくれることです。オープンソースで開発されていて，接続先のネットワークをプライベート・ネットに切り替えることもできます。

アカウント作成画面です。左上の「Private Network」の部分をクリックすると，接続先のネットワークを Mainnet，Ropsten，Korvan，Rinkeby，localhost:8545，Custom RPC から選ぶことができます。

![スクリーンショット 2017-10-08 16.41.05.png](https://qiita-image-store.s3.amazonaws.com/0/154409/3fa828e2-7c68-7320-9ab0-71fc788c1e49.png)

アカウントの管理画面です。現在の残高や過去のトランザクションが確認できます。

![スクリーンショット 2017-10-09 22.34.42.png](https://qiita-image-store.s3.amazonaws.com/0/154409/f51accd6-b71e-9f4c-87c4-2aa0e045aa5e.png)

コントラクトに変更を加えるような操作をした時に表示されるポップアップです。消費される ether をユーザが確認して許可するまで ether が使用されることはありません。

![スクリーンショット 2017-10-09 22.32.57.png](https://qiita-image-store.s3.amazonaws.com/0/154409/79792837-7ca1-2568-16b7-89053be303f5.png)

## プライベート・ネットの準備

開発用のプライベート・ネットを構築するために，以下のコマンドを実行します。

```console
$ geth --datadir . --dev --networkid 1337 --gasprice 18000000000 --rpc --rpccorsdomain "*"
```

`--datadir` オプションはデータベースとキーストアの場所を指定するオプションで，ここではカレントディレクトリを指定しています。`--dev` オプションは開発用のプライベート・ネットを構築するのに有用なオプションで，P2Pネットワークを構築しなくなったりマイニングが簡単になったりします（詳細は [過去の記事](https://qiita.com/shora_kujira16/items/ab05f9d4a58bfa05a96c#--dev-%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E4%BD%BF%E3%81%86%E5%A0%B4%E5%90%88) を参照してください）。`--networkid` オプションについては後述します。`--gasprice` オプションは送金やスマートコントラクトの処理の大きさの単位 (gas) あたりの価格 (gas/wei) を指定するオプションで，`--dev` オプションを指定したときには 0 gas/wei になっているのですが，現実のネットワークとの差異が大きすぎるので適当な値に設定しています。現在の gas/wei は [ethstats.net](https://ethstats.net/) などを参照してください。記事を執筆している時点では 21 gwei (21000000000 wei) でした。

`--networkid 1337` を指定している理由ですが，このオプションを指定せずにMetaMaskでトランザクションに署名をしようとすると "rpc error with payload", "Error: invalid sender" というエラーが出てしまったからです。

![スクリーンショット 2017-10-09 23.14.07.png](https://qiita-image-store.s3.amazonaws.com/0/154409/a5307ccc-edcd-0d6d-37fb-cd289392be95.png)

このエラーは EIP 155 という Ethereum の仕様が有効化されていないネットワークでMetaMaskを使おうとしたときに発生したり，`genesis.json` の `config.chainID` と Geth の `--networkid` が一致していないときに発生したり，様々な場面で発生します。

* [Newer version of matamask throws errors when sending transaction. · Issue #2015 · MetaMask/metamask-extension](https://github.com/MetaMask/metamask-extension/issues/2015#issuecomment-327269942)
* [Signing Transactions fail with Locally created Accounts. · Issue #932 · ethereum/web3.js](https://github.com/ethereum/web3.js/issues/932#issuecomment-332063807)

最初は `--dev` オプションを使ったときに EIP 155 が有効になっていないのかと思ったのですが，ソースコードを見る限りでは0ブロック目から有効になっているように見えるのでこれは違うようでした ([config.go#L88](https://github.com/ethereum/go-ethereum/blob/7a045af05b93c323f63c7af41423ded701ce3890/params/config.go#L88), [genesis.go#L348](https://github.com/ethereum/go-ethereum/blob/7a045af05b93c323f63c7af41423ded701ce3890/core/genesis.go#L348))。よく見ると，gethを起動した画面でもその旨が表示されています。

![スクリーンショット 2017-10-10 11.51.25.png](https://qiita-image-store.s3.amazonaws.com/0/154409/72937fa4-2889-a6de-fcc2-e7a90df25548.png)

あとは `genesis.json` の `config.chainID` と Geth の `--networkid` が一致していないことによる問題が考えられるのですが，上記の画像によると `chainID` は 1337 に設定されているようなので `--networkid 1337` と指定するとうまく動きました。ただ，マイニングをしたりetherを送金したりするときにはこのエラーは表示されなかったので，[以前に体験した不具合](https://qiita.com/shora_kujira16/items/ab05f9d4a58bfa05a96c#%E8%87%AA%E5%88%86%E3%81%A7-genesisjson-%E3%82%92%E7%94%A8%E6%84%8F%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88) とは性質が違うような気がしています。

## アカウントの準備

アカウントを2つ用意します。

1. マイニング用アカウント
2. MetaMask 用アカウント

最初に 1. のアカウントでマイニングを行い適量の ether を入手します。次に，得られた ether を 1 ether ほど 2. のアカウントに送金します。

私は 1. のアカウントの作成と送金はMistを使って行いました（[EthereumのDappをスマートフォンから利用する方法について](https://qiita.com/shora_kujira16/items/fc143537a8773e6674a6#pc%E3%81%8B%E3%82%89%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88-mist)）。

![スクリーンショット 2017-10-08 16.47.59.png](https://qiita-image-store.s3.amazonaws.com/0/154409/99fa6288-1387-59fc-e739-00261f2387fd.png)

## Solidityを使ったスマートコントラクトの記述

[browser-solidity](https://github.com/ethereum/browser-solidity) を使ってスマートコントラクトを記述します。今回は [solidity-baby-steps の greeter.sol](https://github.com/fivedogit/solidity-baby-steps/blob/master/contracts/05_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 ブランチ](https://github.com/ethereum/browser-solidity/tree/gh-pages) から `remix-XXXXXXX.zip` をダウンロードして展開します。その中の `index.html` を開けば browser-solidity が起動するのですが，[MetaMask は http もしくは https でしか使えないという制約がある](https://github.com/MetaMask/faq/blob/49341c258787a0cce746bc6072c3375d2e61936e/DEVELOPERS.md#globe_with_meridians-https---web-server-required) ので [http-server](https://www.npmjs.com/package/http-server) などを使って http で開きます。

```console
$ npm i -g http-server
$ http-server
```

browser-solidityが開いたら中央のペインにコントラクトのソースコードを貼り付けます。ファイル名は "Greeter.sol" などに変更しておきましょう。

![スクリーンショット 2017-10-10 12.11.47.png](https://qiita-image-store.s3.amazonaws.com/0/154409/a6e46048-ef17-a157-e289-3f7171a26b20.png)

次にコントラクトのデプロイを行います。右のペインの Run タブの Environment の項目の中から "Injected Web3" を選びます。これは MetaMask から供給される Web3 のアカウントを使用するという設定です。「Web3 が見つからない」という旨のエラーが出たときには MetaMask のアカウントの作成がうまくいっているか，MetaMask のネットワークの設定で localhost:8545 が選択されているかなどを確認してください。設定変更後は Chrome のタブを一度閉じて開き直さないと変更が読み込まれないようです。

![スクリーンショット 2017-10-08 16.49.02.png](https://qiita-image-store.s3.amazonaws.com/0/154409/c9ea4ee4-3940-0da6-49b2-bd77576c5deb.png)

Injected Web3 が選択できたら "Create" というボタンの右にあるテキストボックスに json 形式でコントラクトのコンストラクタの引数を与えます。今回は string 型の引数なのでダブルクォートで囲いましょう。

![スクリーンショット 2017-10-08 16.52.34.png](https://qiita-image-store.s3.amazonaws.com/0/154409/cd6eede1-a0a8-aaa7-5c2e-a91767fe5eb6.png)

"Create" ボタンを押すとコントラクトのデプロイが行えます。MetaMask から確認が求められるので Submit を押します。

![スクリーンショット 2017-10-10 12.23.48.png](https://qiita-image-store.s3.amazonaws.com/0/154409/9596a8ed-36dc-557c-6662-a3c05a13f77b.png)

コントラクトがデプロイできたら，メモしておく必要がある項目が2つあります。1つめはコントラクトの ABI で，Web3 からコントラクトを利用する際に必要となります。browser-solidity の右のペインの Compile タブの Detail ボタンを押し，出てきたダイアログの中の INTERFACE という項目の部分にあるコピーボタンを押すとクリップボードにコピーできます。

![スクリーンショット 2017-10-10 12.22.34.png](https://qiita-image-store.s3.amazonaws.com/0/154409/38c67f4f-194e-cba5-bb4f-dc046388bc69.png)

![スクリーンショット 2017-10-10 12.22.44.png](https://qiita-image-store.s3.amazonaws.com/0/154409/b082dc6a-c77b-2189-0444-89fc21b55b14.png)

コピーされた文字列は以下のようなものです。

```js
[{"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 タブのコントラクト・インスタンスの一覧が表示されている部分でコピーボタンを押すとクリップボードにコピーできます。

![スクリーンショット 2017-10-10 12.23.57.png](https://qiita-image-store.s3.amazonaws.com/0/154409/d6e7cf51-1381-5de1-5003-8e6bcf5dc5ca.png)

コピーされた文字列はたとえば以下のようなものです。

```
0x7599c8900e6c9f57078d2248d2596fd3a4c4f8c2
```

## React+Redux を使ったアプリケーション開発

作成したコントラクトをブラウザで利用するために以下のようなコントラクトを作成しました（クリックするとYouTubeが開きます）。

[![React+Redux を使ったアプリケーション開発](http://img.youtube.com/vi/Su4EJEPcbFc/0.jpg)](http://www.youtube.com/watch?v=Su4EJEPcbFc)

ソースコードは以下のリポジトリにあります。

https://github.com/arosh/greeter

詳細な実装はリポジトリのソースコードを見てもらうとして，ここでは要点だけ説明します。

Web3 のセットアップを行っている部分です。[MetaMask Compatibility Guide](https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md) に載っているものをコピペしています。MetaMask や Mist などで Web3 オブジェクトが供給されている場合にはそれを利用しています。

```src/infra/ethereum.js
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 とコントラクトアドレスが必要になります。自分で動かしたい場合にはコントラクトのアドレスを書き換えてください。

```src/infra/ethereum.js
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](https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#raising_hand-account-list-reflects-user-preference) によれば，MetaMask を使う場合には `web3.eth.accounts[0]` を使えば MetaMask で設定したアカウントが利用されるようになっているようなのですが，Mist など他のウォレットではそのような仕様にはなっていないので，別の処理を書く必要があります。

```src/infra/ethereum.js
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` を取り出す処理です。

```src/infra/ethereum.js
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 がよろしくやってくれます。

```src/infra/ethereum.js
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 しています。

```src/index.js
// @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 が取得できています。

![スクリーンショット 2017-10-09 22.11.29.png](https://qiita-image-store.s3.amazonaws.com/0/154409/7e6aeba8-50a6-e886-fd10-cb9eb2ffa622.png)

* [Event Filters - Support loading Historical Events · Issue #503 · MetaMask/metamask-extension](https://github.com/MetaMask/metamask-extension/issues/503)
* [Historical events not working in certain cases · Issue #681 · MetaMask/metamask-extension](https://github.com/MetaMask/metamask-extension/issues/681)
* [Events in Metamask · Issue #792 · MetaMask/metamask-extension](https://github.com/MetaMask/metamask-extension/issues/792)
* [Metamask doesn't detect events. · Issue #1429 · MetaMask/metamask-extension](https://github.com/MetaMask/metamask-extension/issues/1429)
* [Watching events: The request took too long to process · Issue #2114 · MetaMask/metamask-extension](https://github.com/MetaMask/metamask-extension/issues/2114)

## 参考資料

* [Learning Solidity Part 1: Contract Dev with MetaMask](https://karl.tech/learning-solidity-part-1-deploy-a-contract/)
* [JavaScript API · ethereum/wiki Wiki](https://github.com/ethereum/wiki/wiki/JavaScript-API)
* [coopermaruyama/react-web3: Ensure web3 is available before your app renders](https://github.com/coopermaruyama/react-web3)
* [Ethereumでコントラクトの監視 - Event - - Qiita](https://qiita.com/satons/items/d9c2dabfbf513e44f78b)