Ethereum Advent Calendar 2018 の7日目です!
よろしくお願いします!
はじめに
drizzleはethereum上でDAppsを作るためのフロントエンドライブラリです。
Redux上に構築されており、Reduxを通してコントラクトやトランザクションのステータスにアクセスできます。
今回は簡単なTodoアプリを作ってみます!
チュートリアルなのでテストとか権限・セキュリティ関係は割愛です。
公式サイトにもチュートリアルがあり、もっとシンプルな内容を扱っています。
公式の後でこちらをやってみてもらえると、より深く理解できるかと思います。
完成イメージはこちら。
ethereum側の構築
準備
まずtruffleやganacheをインストールしていない方はインストールしましょう。
$ npm install -g truffle
$ npm install -g ganache-cli
次にプロジェクトを新たに作成します。
$ mkdir todo-chain
$ cd todo-chain
$ truffle init
コントラクトの作成
次にコントラクトを作成していきましょう。
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ファイルはこんな感じに。超シンプル。
const TodoStore = artifacts.require("TodoStore");
module.exports = function(deployer) {
deployer.deploy(TodoStore);
};
また、ganache-cli上に作るのでネットワーク設定もしていきます。
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
から。
こんな感じにしましょう!じゃーん!
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
です。
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
は消してください。
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
を返します。
drizzleState
はdrizzle.store.getState()
で取得されるstoreに保持されたstateで、 initialized
はdrizzleState
内のプロパティです。
drizzle
> drizzleState
> initialized
というイメージですね。
drizzle
インスタンスさえあれば全部引っ張り出せますが、よく使うのでContextに乗っけてるんでしょう。
詳細を知りたい方はdrizzle-reactのソースを読んでみるといいかと!中身はとてもシンプルです。
ここまできたら一旦アプリを立ち上げてみましょう!client/
ディレクトリで
$ npm start
「Loading...」という画面が表示されてから、MetaMaskとの接続確認があり、承認後に以下のような画面が出れば、とりあえずOKです!
Todoの追加
Todoの追加機能を作っていきます。まずは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
コンポーネントを作ります。どん!
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)を取得し直す役割もあります。
Todoを見る
次は追加したTodoを確認していきましょう〜
import AddTodo from './components/AddTodo';
+import ShowTodo from './components/ShowTodo';
...
<img src={logo} className='App-logo' alt='logo' />
<AddTodo />
+ <ShowTodo />
</header>
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テーブルはこちら。
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追加して遊んでみてください!
Todoにチェックをつける
このままだといつまでたってもTodoが完了しないので、チェックをつけて完了できるようにしましょう!
まずはチェックボックスとトランザクション状態を表示するようにします。
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>
+ </>
);
次にチェックボックスをクリックした際の処理を実装します。
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
のときと同じ。
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 => ({
おわりに
drizzle初チャレンジでしたが、ちゃんと準備すればethereum上の値を追っかけてくれるのはいいですね!
Reduxを使用している方は特にやりやすいんじゃないでしょうか!