アルゴリズムとかオーダーとか

仕事で勉強したことなどをまとめてます

ganache-cli@beta+web3.eth.subscribeを試してみた

y-nakajo.hatenablog.com
前回の記事の続きになります。
タイトルの通りweb3.eth.subscribeを試してみました。
が、テストのためだけにGethのノードを準備するのはめんどくさいのでtestRPCあらため、ganache-cliでWebSocketサーバ起動できないのかなぁ?と思い調べてみました。
そしたらこんなIssueを発見!
github.com

ということでganache-cli@betaを使ってweb3.eth.subscribe(= event filter)を利用してみます。


以降はtruffleをインストール済みで、何かしらのEventを発行するContractを作成済みという前提で進めます。

ganache-cli@betaのインストール

まずはWebSocketを実装したganache-cliをインストールします。betaバージョンを指定してインストールすればOKです。
web3@1.0.0も一緒にインストールします。

npm i ganache-cli@beta web3

ganache-cliの起動

アカウントアドレスを固定したかったのでtruffle developで使われているmnemonicを渡して起動します。けど、別に固定する必要なかったので、-mオプションはつけなくてもOKです。
WebSocketを起動する特別なオプションなどは必要ありません。普通に起動すればhttp, wsのエンドポイントが起動します。

node_modules/.bin/ganache-cli -m "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat"

WebSocketは最初のハンドシェイクにhttpを利用して、その後ハンドシェイクで利用したtcpセッションをそのまま使って通信するため、httpとwsのポートは同じになるらしいです。詳しくは以下を参照ください。
今さら聞けないWebSocket~WebSocketとは~ - Qiita
WebSocketについて調べてみた。 - Qiita

truffle.jsの準備

truffleを使ってganache-cliに接続します。ganache-cliへ接続するためのconfigは次の通りです。とはいえここでも特別なものは何も必要ないのでいつも通りの設定でOKです。

module.exports = {
  networks: {
    live: {
      host: "localhost",
      port: 8545, // ポートを指定してganache-cliを起動した場合は適宜修正してください
      network_id: "*"
    }
  }
};

truffleの起動

上記のネットワーク設定を使ってganache-cliに接続します。

node_module/.bin/truffle console --network live

web3@1.0.0をロードして、WebsocketProviderでnodeに接続する

*これ以降のコマンドはすべてtruffle console上で実行します。*

truffle consoleを起動するとすぐにweb3が使えるようになっていますが、これはtruffleにバンドルされているweb3@0.2x.x系なので先ほどインストールしたweb3@1.0.0を有効化します。
その後、WebsocketProviderの作成と設定をしてweb3@1.0.0のインスタンスを作成します。
最後に接続確認のためにaccount addressの取得とbalanceの取得を行っています。

Web31 = require('web3')
wshost = web3.currentProvider.host.replace("http", "ws")
wsProvider = new Web31.providers.WebsocketProvider(wshost)
repl.repl.on("exit", () => wsProvider.connection.close()) // (追記)truffle終了時にwsProviderのconnectionを解放してあげないとganache-cli落とすまで終了しなくなる
web31 = new Web31(wsProvider)
// 以下2つは接続確認
web31.eth.getAccounts((err, res) => account = res[0])
web31.eth.getBalance(account)

web3@1.0.0でcontractのインスタンスを作り直す

migrateを実行してContractをデプロイします。
ここで作成したContractはTruffleContractであってweb3@1.0.0のeth.Contractではないので作り直します。

migrate // まずはデプロイする

web31.eth.getAccounts((err, res) => account = res[0])
options = {from: account, gas: 100000, gasPrice: 100} // contractがtransactionに設定するデフォルト値
MyContract.deployed().then((ins) => contract = new web31.eth.Contract(ins.abi, ins.address, options))

MyContractは適宜自分で作成したContract名に置き換えてください。

これでcontractにweb3@1.0.0のeth.Contractが生成されました。

web3@1.0.0のContractからweb3.eth.subscribe(= event filter)を取得する

ようやく本題です。先ほど生成したweb3@1.0.0のContractからevent filterを取得します。

event = contract.events.MyEvent()
event.on("data", (data) => console.log("data", data))
event.on("changed", (data) => console.log("changed", data))
event.on("error", (error) => console.log("error", error))
event.id

最後のevent.idがnullではなく何かしらのバイト値を表示していればsubscribeが成功しています。
event filterであるSubscriptionはEventEmitterを持っており、data, changed, errorのイベントが発行されるのでそれぞれのイベント発生時に実行するlistenerを登録しています。

Eventを発行してログを確認する

ContractのEventを発行するmethodを実行してeventログが拾えるか試してみます。

Promise.resolve(contract).then((c) => {c.methods.exec(1, [1]).send(); return null})

MyMethod(args)は自分で作成したContractのメソッド名と引数に適宜置き換えてください。

web3@0.2x.x系であればcontractのinstanceに続けてmethod名、event名をつけることで、methodやevent filterが取得できましたが、web3@1.0.0 系ではmethods, eventsをつけてからmethod名やevent名をつなげる必要があります。
さらに、method呼び出しであれば最後に、call()やsend()を繋げないと実際に発行されません。

直接実行するとtransactionReceiptのログが表示されて、event filterがキャッチしたログが流れてしまうので、transactionReceiptが流れないようにPromiseでラップして実行しています。
コンソール上にeventログが表示されたら成功です。

ganache-cliのログをチェック

ganache-cliのログにはこんな感じのRPCログが記録されていると思います。

eth_getTransactionReceipt
eth_subscribe
eth_sendTransaction

  Transaction: 0x2e19a43a01f3b515badb1a4242e44c109b4d6a8a29fdc8ef3d00635a56f85dec
  Gas usage: 56365
  Block Number: 7
  Block Time: Thu Mar 01 2018 10:37:52 GMT+0900 (JST)

eth_getTransactionReceipt

eth_subscribeでevent watcherを登録して、それ以降はサーバ側からevent logがpushされているのがわかります。

web3@0.2x.xのweb3.eth.filter.watchするとganache-cliのログがどうなるか見てみる

web3.eth.filter.watchではeventのwatchingのためになんどもeth_getFilterChangesを発行しています。
それを実際に確認してみます。
TruffleContractからevent watchすればいいので次のコマンドで確認できます。

MyContract.deployed().then((ins) => myContract = ins)
filter = myContract.MyEvent()
filter.watch((err, res) => err ? console.log("error", err) : console.log("event", res))

filter.watchを実行した後に、ganache-cliのコンソールを開くとeth_getFilterChangesのRPCログがズラーッと流れていくのがわかると思います。このようにweb3.eth.filter.watchでは定期的にRPCサーバへ変更を問い合わせるのでEventのwatchingにはDapps、RPCサーバ共に負荷がかかります。
watchingを止めたい場合は

filter.stopWatching()

で止まります。

まとめ

web3@1.0.0のevent filterはPUB/SUBモデルのため、node側がWebSocket対応のRPCサーバを立ててくれないと使えないという制約はありますが、その分低コストで運用できます。
また、event受け取り時の処理をEventEmitterで実装しているため、複数のlistenerを登録したり、途中でlistenerを組み替えたりなどが柔軟にできます。
Infura.ioがすでにWebSocketに対応しています。Infura.ioをバックボーンとして利用しているDappsならAPI発行回数制限の問題もあるので、web3@1.0.0のweb3.eth.subscribe機能へ乗り換えたほうがいいでしょうね。