drizzleでブロックチェーンTodoアプリを作る

Ethereum Advent Calendar 2018 の7日目です!

よろしくお願いします!


はじめに

drizzleはethereum上でDAppsを作るためのフロントエンドライブラリです。

Redux上に構築されており、Reduxを通してコントラクトやトランザクションのステータスにアクセスできます。

今回は簡単なTodoアプリを作ってみます!

チュートリアルなのでテストとか権限・セキュリティ関係は割愛です。

公式サイトにもチュートリアルがあり、もっとシンプルな内容を扱っています。

公式の後でこちらをやってみてもらえると、より深く理解できるかと思います。

完成イメージはこちら。

完成イメージ.gif


ethereum側の構築


準備

まずtruffleganacheをインストールしていない方はインストールしましょう。

$ npm install -g truffle

$ npm install -g ganache-cli

次にプロジェクトを新たに作成します。

$ mkdir todo-chain

$ cd todo-chain
$ truffle init


コントラクトの作成

次にコントラクトを作成していきましょう。


contracts/TodoStore.sol


pragma solidity ^0.4.23;

contract TodoStore {
struct Todo {
string title;
bool isDone;
}

Todo[] todoList;

function addTodo(string x) public {
todoList.push(Todo(x, false));
}

function showTodo(uint _index) public view returns(uint, string, bool) {
if (todoList.length > _index) {
string memory title = todoList[_index].title;
return (_index, title, todoList[_index].isDone);
}
return (0, "Wrong index", false);
}

function doneTodo(uint _index, bool result) public {
if (todoList.length > _index) {
todoList[_index].isDone = result;
}
}

function getTodoLength() public view returns(uint) {
return todoList.length;
}
}


シンプルなので、読めば何をやっているかわかってもらえるかと思います。

クライアントからtodoを取得するメソッドがありますが、1件ずつ取得のものしか準備されておらず、イケてないですね…

本当は全件or複数件取得したいのですが、stringの配列を返せないというSolidityの素敵仕様のため、こうしています。

誰かいいHackをご存知でしたら教えてください(泣)

migrationファイルはこんな感じに。超シンプル。


migrations/2_deploy_contracts.js


const TodoStore = artifacts.require("TodoStore");

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


また、ganache-cli上に作るのでネットワーク設定もしていきます。


truffle.js


module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*",
}
}
};


マイグレーション

ここまできたら、migrateしてみましょう!

$ ganache-cli -b 3

$ truffle compile
$ truffle migrate

これでethereum側はOKです!


フロントエンド側の構築

次にフロントエンド側の実装です。


準備

前述の通りdrizzleはReduxをベースに構築されているので、様々なJSフレームワークを乗せることができます。

公式はReact推しみたいで、drizzle-reactというライブラリも準備されているので、Reactを使っていきましょう!

$ npx create-react-app client

いろいろできましたね。npxもcreate-react-appも最高!

次に必要なパッケージを追加でインストールしていきます。

チュートリアルなのでなるべく余計なものは入れたくないんですが、ちょっとデザインかっこつけたいのでmaterial-uiだけは入れます。

$ cd client

$ npm install -S @material-ui/core drizzle drizzle-react

(チュートリアルだしnpmにしたけど、やっぱyarnの方が早いな。。)

clientの内部にコントラクトへのシンボリックリンクを貼っておきます。

$ cd src 

$ ln -s ../../build/contracts contracts


drizzleインスタンスの実装

まずはおおもとであるindex.jsから。

こんな感じにしましょう!じゃーん!


client/src/index.js


import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App.jsx';
import * as serviceWorker from './serviceWorker';

import { Drizzle, generateStore } from 'drizzle';
import { DrizzleContext } from 'drizzle-react';
import TodoStore from './contracts/TodoStore.json';
import TodoKeyContext from './contexts/todoKey';

const options = { contracts: [TodoStore] };
const drizzleStore = generateStore(options);
const drizzle = new Drizzle(options, drizzleStore);

ReactDOM.render(
<DrizzleContext.Provider drizzle={drizzle}>
<TodoKeyContext.Provider>
<App />
</TodoKeyContext.Provider>
</DrizzleContext.Provider>,
document.getElementById('root'));
serviceWorker.unregister();


ポイントはこの辺ですね。

import TodoStore from './contracts/TodoStore.json';

import { Drizzle, generateStore } from 'drizzle';
...

const options = { contracts: [TodoStore] };
const drizzleStore = generateStore(options);
const drizzle = new Drizzle(options, drizzleStore);

drizzleがTodoStore.jsonを参照してstoreに格納します。

それによって、drizzleインスタンスからコントラクトのメソッドを呼び出せるようになります。

import { DrizzleContext } from 'drizzle-react';

...

ReactDOM.render(
<DrizzleContext.Provider drizzle={drizzle}>
...
</DrizzleContext.Provider>,

Context APIを通してdrizzleインスタンスを渡し、子コンポーネントからもdrizzleインスタンスを操作できるようにします。


Contextの作成

ではどうやってTodoを呼び出すのでしょう?

今回は独自のDrizzleContextの他に、独自のContextを通して呼び出すことにしました。

それがTodoKeyContextです。


client/src/contexts/todoKey.js


import React from 'react';

const Context = React.createContext();

class Provider extends React.Component {
constructor(props) {
super(props);

this.fetchTodoKeys = async (contract) => {
const todoLength = await contract.methods.getTodoLength().call();
const dataKeys = [];
for (let i = 0; i < todoLength; i++) {
await dataKeys.push(contract.methods.showTodo.cacheCall(i));
}
this.setState({
...this.state,
dataKeys,
});
};

this.turnFetchStatus = (newState) => {
this.setState({
...this.state,
isFetchingTodo: newState,
});
}

this.state = {
dataKeys: [],
fetchTodoKeys: this.fetchTodoKeys,
isFetchingTodo: false,
turnFetchStatus: this.turnFetchStatus,
};
}

render() {
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
);
}
}

export default {
Consumer: Context.Consumer,
Provider
};


drizzleでは各メソッドをcacheCallで呼び出し、その戻り値をトラッキングします。

たとえばshowTodoメソッドでi番目のtodoを呼び出しておくと、ethereum上でi番目のtodoが変更されたことを検知して、stateが更新されるんですね。

ここでいう「i番目のtodo」はstore上にkey値で管理されているので、そのkeyを取得するのがこのTodoKeyContextコンテキストのおもな役割です。

また、keyの取得中に無駄なレンダリングが走らないよう、状態管理もしています。


Containerの作成

いよいよComponentを作成していきます!

まずはApp.jsxから。

なんとなくComponentは拡張子を.jsxにしたいのでそうします。既存のApp.jsは消してください。


client/src/App.jsx


import React from 'react';
import { DrizzleContext } from 'drizzle-react';
import logo from './logo.svg';
import './App.css';

const App = initialized => {
if(!initialized) return 'Loading...';
return (
<div className='App'>
<header className='App-header'>
<img src={logo} className='App-logo' alt='logo' />
<p>dummy</p>
</header>
</div>
);
}

const withContext = () => (
<DrizzleContext.Consumer>
{({ initialized }) => App(initialized)}
</DrizzleContext.Consumer>
);
export default withContext;


logoとかstyleとかはcreate-react-appのものを流用。だってかっこいいし。

中身は特に難しいことをしていないですね。

気になるのはちょくちょく出てくるinitializedでしょうか。DrizzleContextを通して持ってきてますね。

DrizzleContextではdrizzle, drizzleState, initializedを返します。

drizzleStatedrizzle.store.getState()で取得されるstoreに保持されたstateで、 initializeddrizzleState内のプロパティです。

drizzle > drizzleState > initializedというイメージですね。

drizzleインスタンスさえあれば全部引っ張り出せますが、よく使うのでContextに乗っけてるんでしょう。

詳細を知りたい方はdrizzle-reactのソースを読んでみるといいかと!中身はとてもシンプルです。

ここまできたら一旦アプリを立ち上げてみましょう!client/ディレクトリで

$ npm start

「Loading...」という画面が表示されてから、MetaMaskとの接続確認があり、承認後に以下のような画面が出れば、とりあえずOKです!

dummy.png


Todoの追加

Todoの追加機能を作っていきます。まずはApp.jsxの修正。


client/App.jsx


import logo from './logo.svg';
import './App.css';
+import AddTodo from './components/AddTodo';

...

<img src={logo} className='App-logo' alt='logo' />
- <p>dummy</p>
+ <AddTodo />
</header>


次にAddTodoコンポーネントを作ります。どん!


client/components/AddTodo.jsx


import React, { Component } from 'react';
import TextField from '@material-ui/core/TextField';
import { DrizzleContext } from 'drizzle-react';
import TodoKeyContext from '../contexts/todoKey';

class AddTodo extends Component {
state = { stackId: null };

handleKeyDown = e => {
if (e.keyCode === 13) {
this.addTodo(e.target.value);
}
};

addTodo = value => {
const { drizzle, drizzleState } = this.props;
const contract = drizzle.contracts.TodoStore;

const stackId = contract.methods.addTodo.cacheSend(value, {
from: drizzleState.accounts[0]
});
this.setState({ stackId });
this.props.turnFetchStatus(true);
};

getTxStatus = () => {
const { transactions, transactionStack } = this.props.drizzleState;
const txHash = transactionStack[this.state.stackId];

if (!txHash) return null;
if (
transactions[txHash].status === 'success' &&
this.props.isFetchingTodo
) {
this.props.turnFetchStatus(false);
this.props.fetchTodoKeys(this.props.drizzle.contracts.TodoStore);
}
return transactions[txHash].status === 'success'
? 'Todo追加に成功しました!'
: 'Todo追加中…';
};

render() {
return (
<div style={{ textAlign: 'center', margin: '50px auto' }}>
<TextField
type='text'
placeholder='Todoを入力してください'
onKeyDown={this.handleKeyDown}
style={{ width: '500px', backgroundColor: 'white' }}
/>
<div>{this.getTxStatus()}</div>
</div>
);
}
}

const withContext = () => (
<DrizzleContext.Consumer>
{({ drizzle, drizzleState }) => (
<TodoKeyContext.Consumer>
{({ fetchTodoKeys, isFetchingTodo, turnFetchStatus }) => (
<AddTodo
drizzle={drizzle}
drizzleState={drizzleState}
fetchTodoKeys={fetchTodoKeys}
isFetchingTodo={isFetchingTodo}
turnFetchStatus={turnFetchStatus}
/>
)}
</TodoKeyContext.Consumer>
)}
</DrizzleContext.Consumer>
);
export default withContext;


結構ずっしりですね…2箇所解説します。

  addTodo = value => {

const { drizzle, drizzleState } = this.props;
const contract = drizzle.contracts.TodoStore;

const stackId = contract.methods.addTodo.cacheSend(value, {
from: drizzleState.accounts[0]
});
this.setState({ stackId });
this.props.turnFetchStatus(true);
};

drizzleインスタンスからコントラクトを呼び出し、cacheSendメソッドを使ってコントラクト内のメソッドaddTodoに送信しています。引数はaddTodoメソッドの引数と送信者のアカウントですね。後者はdrizzleStateから取得。

cacheSendを実行すると、送信したトランザクションスタックのIDが返ってくるので、そちらをAddTodoのstateに保持しておきます。

次にトランザクション管理。


getTxStatus = () => {
const { transactions, transactionStack } = this.props.drizzleState;
const txHash = transactionStack[this.state.stackId];

if (!txHash) return null;
if (
transactions[txHash].status === 'success' &&
this.props.isFetchingTodo
) {
this.props.turnFetchStatus(false);
this.props.fetchTodoKeys(this.props.drizzle.contracts.TodoStore);
}
return transactions[txHash].status === 'success'
? 'Todo追加に成功しました!'
: 'Todo追加中…';
};

こちらはdrizzleStateからトランザクション情報を取得して、対応する文字列を返すようにしています。

トランザクション情報の識別には先ほどstateに保持したトランザクションスタックのIDが必要です。

また、Todo追加のトランザクションが完了したタイミングで、Todoリスト(TodoKeys)を取得し直す役割もあります。

実行するとこんな感じになるはずです!

AddTodo.png


Todoを見る

次は追加したTodoを確認していきましょう〜


client/App.jsx


import AddTodo from './components/AddTodo';
+import ShowTodo from './components/ShowTodo';

...

<img src={logo} className='App-logo' alt='logo' />
<AddTodo />
+ <ShowTodo />
</header>



client/components/ShowTodo.jsx


import React, { Component } from 'react';
import { DrizzleContext } from 'drizzle-react';
import TodoKeyContext from '../contexts/todoKey';
import TodoTable from './TodoTable';

class ShowTodo extends Component {
async componentDidMount() {
await this.props.fetchTodoKeys(this.props.drizzle.contracts.TodoStore);
}

render() {
const { TodoStore } = this.props.drizzleState.contracts;
const todos = this.props.dataKeys.map(key =>
TodoStore.showTodo[key] ? TodoStore.showTodo[key].value : []
);
return <TodoTable todos={todos} />;
}
}

const withContext = () => (
<DrizzleContext.Consumer>
{({ drizzle, drizzleState }) => (
<TodoKeyContext.Consumer>
{({ dataKeys, fetchTodoKeys }) => (
<ShowTodo
drizzle={drizzle}
drizzleState={drizzleState}
dataKeys={dataKeys}
fetchTodoKeys={fetchTodoKeys}
/>
)}
</TodoKeyContext.Consumer>
)}
</DrizzleContext.Consumer>
);
export default withContext;


このコンポーネントはシンプルですね。

drizzleState内のコントラクトプロパティに、showTodoメソッドの値が入っているので、TodoKeyContextで持っておいたキーをもとに取得します。

そして、それを次のTodoTableに渡すだけです。

Todoテーブルはこちら。


client/components/TodoTable.jsx


import React, { Component } from 'react';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Checkbox from '@material-ui/core/Checkbox';
import Paper from '@material-ui/core/Paper';
import { withStyles } from '@material-ui/core/styles';
import { DrizzleContext } from 'drizzle-react';

class TodoTable extends Component {

render() {
const CustomTableCell = withStyles(theme => ({
head: {
backgroundColor: theme.palette.common.black,
color: theme.palette.common.white
},
body: {
fontSize: 14
}
}))(TableCell);

const rows = this.props.todos.map(todo => (
<TableRow key={todo[0]}>
<CustomTableCell>{todo[0]}</CustomTableCell>
<CustomTableCell>{todo[1]}</CustomTableCell>
<CustomTableCell>
<Checkbox checked={todo[2]} />
</CustomTableCell>
</TableRow>
));

return (
<Paper style={{ width: '70%', margin: '0 auto 100px' }}>
<Table>
<TableHead>
<TableRow>
<CustomTableCell>No</CustomTableCell>
<CustomTableCell>title</CustomTableCell>
<CustomTableCell>status</CustomTableCell>
</TableRow>
</TableHead>
<TableBody>{rows}</TableBody>
</Table>
</Paper>
);
}
}

const withContext = props => (
<DrizzleContext.Consumer>
{({ drizzle, drizzleState }) => (
<TodoTable
drizzle={drizzle}
drizzleState={drizzleState}
todos={props.todos}
/>
)}
</DrizzleContext.Consumer>
);
export default withContext;


material-uiでがっつりスタイリングしていますが、中身は表示させてるだけですね。

特に難しくはないかと思います!

こんな感じになります〜。Todo追加して遊んでみてください!

ShowTodo.png


Todoにチェックをつける

このままだといつまでたってもTodoが完了しないので、チェックをつけて完了できるようにしましょう!

まずはチェックボックスとトランザクション状態を表示するようにします。


client/components/TodoTable.jsx


import TableRow from '@material-ui/core/TableRow';
+import Checkbox from '@material-ui/core/Checkbox';
import Paper from '@material-ui/core/Paper';

...

<TableRow key={todo[0]}>
<CustomTableCell>{todo[0]}</CustomTableCell>
<CustomTableCell>{todo[1]}</CustomTableCell>
+ <CustomTableCell>
+ <Checkbox checked={todo[2]} onChange={this.onCheck(todo[0])} />
+ </CustomTableCell>
</TableRow>
));

return (
- <Paper style={{ width: '70%', margin: '0 auto 100px' }}>
- <Table>
- <TableHead>
- <TableRow>
- <CustomTableCell>No</CustomTableCell>
- <CustomTableCell>title</CustomTableCell>
- </TableRow>
- </TableHead>
- <TableBody>{rows}</TableBody>
- </Table>
- </Paper>
+ <>
+ <div>{this.getTxStatus()}</div>
+ <Paper style={{ width: '70%', margin: '0 auto 100px' }}>
+ <Table>
+ <TableHead>
+ <TableRow>
+ <CustomTableCell>No</CustomTableCell>
+ <CustomTableCell>title</CustomTableCell>
+ <CustomTableCell>done</CustomTableCell>
+ </TableRow>
+ </TableHead>
+ <TableBody>{rows}</TableBody>
+ </Table>
+ </Paper>
+ </>
);


次にチェックボックスをクリックした際の処理を実装します。


client/components/TodoTable.jsx


class TodoTable extends Component {
+ state = { stackId: null };
+
+ onCheck = key => (e, checked) => {
+ const { drizzle, drizzleState } = this.props;
+ const contract = drizzle.contracts.TodoStore;
+ const stackId = contract.methods.doneTodo.cacheSend(key, checked, {
+ from: drizzleState.accounts[0]
+ });
+
+ this.setState({ stackId });
+ };

AddTodoのときとおなじですね。cacheSendでコントラクト上のdoneTodoメソッドを呼び出しています。

最後にトランザクションの処理です!こちらもAddTodoのときと同じ。


client/components/TodoTable.jsx


this.setState({ stackId });
};
+
+ getTxStatus = () => {
+ const { transactions, transactionStack } = this.props.drizzleState;
+ const txHash = transactionStack[this.state.stackId];
+
+ if (!txHash) return null;
+ return transactions[txHash].status === 'success'
+ ? 'Todo状態を変更しました!'
+ : 'Todo状態を変更中…';
+ };

render() {
const CustomTableCell = withStyles(theme => ({

これで完成です!

こんな感じになっているはず!!

DoneTodo.png


おわりに

drizzle初チャレンジでしたが、ちゃんと準備すればethereum上の値を追っかけてくれるのはいいですね!

Reduxを使用している方は特にやりやすいんじゃないでしょうか!