Web3.jsでイベントハンドリング 前編
お久しぶりです、アメリカ8泊10日の後にぶっ続けで3泊4日北海道に行ったり気温0度の広島の県北で野宿して死にかけたりしてる旅行ガイジです。
(建前上は)ブロックチェーンエンジニアとしてプログラムを書く日々も早1年半、せっかくのアドベントカレンダーということでまだqiitaに無さそうだったWeb3.js@1.xのイベント購読についてまとめようかなと思った次第です。
全部書くと間に合いそうにない割とボリュームが大きくなりそうだったので前後編に分けます。
前編は基本的なEthereumのスマートコントラクト(以下、コントラクト)のイベントハンドリングになります。
前提
EthereumとかSolidityの初歩的なとこは分かっている前提です。
とりあえずコンソールでもなんでもいいからコントラクトをデプロイしてみたことがあれば多分分かります。
各種モジュールのバージョン
node@12.13.1
web3@1.2.1
今回は裏でganache-cliというモジュールを使ってEthereumチェーン(をエミュレートしたもの)を用意しています。
gagache-cliを使えばコマンド一つでHTTPとWebソケットがご用意されます。便利。
そして、その環境に後述のコントラクトがデプロイされているものとします。
コントラクト
今回使用するコントラクトは以下になります。よくチュートリアルとして使われるSimpleStorageコントラクトのデータ型をmappingにしてイベント処理を突っ込んだだけです。
pragma solidity ^0.5.11;
contract MappingStorage {
mapping(uint => uint) private storedValue;
// mappingにセットしたkey, valueを通知するイベント
event SetValue(uint key, uint value);
constructor() public {
storedValue[0] = 1;
emit SetValue(0, 1); // イベントの発火
}
function set(uint key, uint value) public {
storedValue[key] = value;
emit SetValue(key, value); // イベントの発火
}
function get(uint key) public view returns(uint retVal) {
return storedValue[key];
}
}
イベントとはなんぞや
コントラクトにおいて変数などのデータはブロックチェーンのストレージ領域に保存されますが、イベントはその中でもトランザクションレシートのログ領域に書き込まれるものになります。
コントラクトというものは外部から隔離された環境で動作するため、コントラクトから何か外部の機能を発火させるということはできません。
しかし、例えば特定の関数が呼ばれたのをトリガーに処理を行う、といった際にはそれでは不都合な訳です。ひたすら繰り返し値を取得し続けて差分を記録するなんて前時代的なやり方はこのご時世にはちょっといただけません。そこで登場するのがイベントです。
イベントを利用することで外部からコントラクトの動作を動的に知ることができるようになります。
前述のコントラクトではコンストラクタとset関数が呼ばれた際にイベントが発火します。
イベントの購読
ここからが本題になります。コントラクトのイベント購読を実例で見ていきましょう。
まず、イベントを購読するスクリプトが以下になります。
const Web3 = require('web3');
// HttpProviderでなくWebsocketProviderなので注意
const web3 = new Web3(new Web3.providers.WebsocketProvider("http://localhost:8545"));
(async () => {
const contractAddress = "コントラクトアドレス";
const abi = ["コントラクトのABI"];
const MappingStorage = await new web3.eth.Contract(abi, contractAddress);
// {}内に色々入れることで購読するイベントをフィルタリングできます、詳しくは後述のドキュメント参照
MappingStorage.events.SetValue({}, (err, event) => {
console.log(`event called: ${event.event}`);
console.log(JSON.stringify(event, null, " "));
});
})();
MappingStorage.events.SetValue(...)
でイベントの購読を行います。購読するイベントのフィルタリングなんかもできますがここでは省きます。コントラクト内でイベントが発火される(set関数が呼ばれる)と、第2引数にセットしたコールバック関数が呼ばれるようになります。
なお、MappingStorage.events.allEvents(...)
とすることでコントラクト内の全イベントが購読対象となります。(今回はSetValueイベントしかありませんが)
上記スクリプトを実行するとイベントの購読待ちとなります。この状態でset関数が呼ばれると、以下のような出力がされます。
event called: SetValue
{
"logIndex": 0,
"transactionIndex": 0,
"transactionHash": "0xa79f032fb7faf9384da2249cc24e0c4fad6ee582fd5715a39db8f665446866f2",
"blockHash": "0xdabdbac45b7493db64315cde20a1e2635eac54aaa61426d75ec0d0a8fc5d9eee",
"blockNumber": 8,
"address": "0xFcac91F1b345bC6E955fDc317fF827950C014142",
"returnValues": {
"0": "10",
"1": "1",
"key": "10",
"value": "1"
},
"event": "SetValue",
"signature": "0xf2247e57590b0cc543ff61d1604a3fc00440b206ff3ee22e9a2ff5e5bbdf83da",
"raw": {
"data": "0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001",
"topics": [
"0xf2247e57590b0cc543ff61d1604a3fc00440b206ff3ee22e9a2ff5e5bbdf83da"
]
}
}
logIndex
:ブロック内のイベントインデックス
transactionIndex
:ブロック内のトランザクションインデックス
transactionHash
:トランザクションハッシュ
blockHash
:ブロックハッシュ
blockNumber
:ブロック番号
address
:イベントが発火されたコントラクトアドレス
returnValues
:イベントにセットされたパラメータ、数字キーが自動追加され、event.returnValues[0]
のようにアクセスできます
event
:イベント名
signature
:イベント定義のkeccak256ハッシュです。今回の場合はkeccak256("SetValue(uint,uint)")
です
raw.data
:イベントのパラメータが格納されたHEX文字列
raw.topics
:最大4×32ByteのHEX文字列配列。インデックス0にはシグネチャと同内容が入り、1-3にはインデックス化されたイベントパラメータの内容が格納されます(詳細は省略)
おわりに
色々複雑なブロックチェーンですが、最近は補助モジュールやIDEも登場してずいぶん扱いやすいものになっています。
Web3.jsを使えばEthereumの細かな動きが分からずとも動かせてしまうので、JSの環境構築の容易さも相まってなかなかにお手軽だと思います。
後編では、イベントを利用した効率的なデータ管理について書こうと思います。