LineBotで Ethereum の web3.jsを操作する（その１: バックエンドで残高取得）



#やりたいこと
　Line Messaging APIを使って、Lineのトーク画面から、Ethereumの残高取得したり、送金したり。


#イメージ図
<img src="https://pragma-curry.com/wp/wp-content/uploads/2018/07/9608cde80d31aed3b76ef2b6e65be9ed-1.jpg" style="width:100%">

　Lineのトーク画面から直接ブロックチェーンへはアクセスできないため、Application Serverを経由しブロックチェーンからアカウントやトランザクションの情報を取得し、Line Messaging APIにリクエストを送るという流れ。
　ただし、このモデルを用いる場合、残高やトランザクションの取得程度なら問題ありませんが、送金等、プライベートキーを扱う際に問題が生じます（後述）
　とりあえず今回は残高取得のテストのみなので、このモデルで行います。



#使用環境
> - Ethereum Node - [infura.io](https://infura.io/)
> - Application Server - [Glitch](https://glitch.com/) 


#１　Line Messaging APIの設定を行う
　LINE Developersの[Start using Messaging API](https://developers.line.me/console/register/messaging-api/provider/)にてプロバイダ、チャンネルの登録を行います。

　詳しい手続きは、前回エントリ [Line Messaging API のFlex Messageを触ってみる](https://qiita.com/pragma-curry/items/e528d28d6a9707c730d)を参照してください。


#２　infura.ioのAPI KEYを取得する
　[infura.io](https://infura.io/)のGET STARTED FOR FREEボタンから、メールアドレスを登録するとAPI KEYを取得できます。

　詳しい手続きは、前回エントリ [ERC223トークンを時短でパブリックチェーンに公開する（忙しい人向け）](https://qiita.com/pragma-curry/items/158615bc490c43e3da13)を参照してください。


#３　Glitchの設定を行う
　GlitchはNode.jsが使えるPaasです。サインアップはGithubのアカウントで行うこともできます。
ログインしたら、New Projectから新しいプロジェクトを作成します。

　今回、**[Etherbot](https://glitch.com/~etherbot)**というプロジェクトを作成しました。
<img src="https://pragma-curry.com/wp/wp-content/uploads/2018/07/Etherbot.png" width="60%">


#４　必要なモジュールをインストールする
　今回、追加するモジュールは
> - [linebot](https://www.npmjs.com/package/linebot)  ---　LineBotSDKの１つ
> - [web3](https://www.npmjs.com/package/web3)  --- イーサリアム Javascript API
> - [nedb](https://www.npmjs.com/package/nedb)  --- インストール不要のNoSQL

の３つです。Glitchプロジェクトフォルダ内のpackage.jsonをクリックし、Add Packagesから追加してください。

#５　LineBotのクラスを定義する
 LineBotアプリを構築する中で、Replyメッセージの作成やデータベースの読み書きを頻繁に行うため、一連の処理系をクラスにまとめて使いやすくしておきます。(正確にはES5の疑似クラスです)　Replyメッセージの種類はたくさんありますが、よく使うtextとbuttonだけ定義しておきます。少々長いですが、まとめておくと後々楽になります。

```javascript:Linebot.js
const Linebot = function(app) {
   const linebot = require('linebot'); //linebot sdkの読み込み
   const Database = require('nedb'); //nedbの読み込み
   //各ユーザー端末のLineBotの状態を格納するためのデータベースファイルを指定
   const db={};
   db.botstatus = new Database({
      filename: '.database/botstatus', 
      autoload: true
   });

  //LineBotのチャンネルID等をコンストラクタに渡しthis.botに代入
   this.bot = linebot({
      channelId: process.env.CHANNEL_ID,
      channelSecret: process.env.CHANNEL_SECRET,
      channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
      verify: true
   });
   app.post('/', this.bot.parser());
  
   //Lineからのメッセージやポストバックのイベントを受取り、コールバック関数にイベントを渡す
   this.onMessageEvent = function (callback){
      this.bot.on('message',event => {
            this.setMessageTemplate(event);
            callback(event);
      });
      this.bot.on('postback',event => {
            this.setMessageTemplate(event);
            callback(event);
      });
  }
  
  //ReplyのたびにJSONをいちいち書くのは面倒なので、よく使うテキストやボタンだけ定義。
  //ボタンは４つまで設置できるので、可変長引数にしておく。
  this.setMessageTemplate = function(event){
    
      event.replyText = function(message){
           event.reply([{
              "type": "text",
              "text": message
           }]).then(data => {
                console.log('Success', data);
           }).catch(error => {
                console.log('Failed', error);
           });
      }
      
      event.replyButton = function(/*title,message,button,postback,...*/){
           var obj = {
              "type": "template",
              "altText": arguments[0],
              "template": {
                  "type": "buttons",
                  "thumbnailImageUrl": null,                     
                  "title": arguments[0],
                  "text": arguments[1],
                  "actions": [
                      {
                        "type": "postback",
                        "label": arguments[2],
                        "data": arguments[3],
                      }
                  ]
              }
           }
           for (var i = 0; i < 3; i++) {
             if(arguments.length > 2*i+4){
                obj.template.actions[i+1]={
                  "type":  "postback",
                  "label": arguments[2*i+4],
                  "data":  arguments[2*i+5] 
                };
             }
           }
          event.reply([obj]).then(data => {
                console.log('Success', data);
           }).catch(error => {
                console.log('Failed', error);
           });
      }

      //ユーザー端末のLineBotの状態をデータベースから読み込む
      this.readDatabase = function(lineID){
          return new Promise((resolve, reject) =>  {
              db.botstatus.findOne({ lineid: lineID }, (err, obj) =>{
                   if(err == null && obj!=null){
                       resolve(obj.status);
                   }
                   else if(err == null && obj==null){
                       resolve(null);
                   }
                   else{
                       reject(err);
                   }
              });
          });
      }

      //ユーザー端末のLineBotの状態をデータベースに書き込む
      this.writeDatabase = function(lineID,status){
          db.botstatus.findOne({ lineid: lineID }, (err, obj) => {
              if(obj==null){
                  db.botstatus.insert({'lineid':lineID,'status':status});  
              }
              else{
                  db.botstatus.update({ 'lineid': lineID }, { $set: { status: status } }, { multi: true });
              }
          });
      }
}
```

上のようなクラスを用意しておくと、

```javascript
const express = require('express');
const app = express();
const bot = new Linebot(app);
bot.onMessageEvent(someFunction);

function someFunction(event){
   event.replyButton(
      '選んでください','どれが好き？',
      'ビーフカレー','answer=beef',
      'ポークカレー','answer=pork',
      'チキンカレー','answer=chicken'
   );
   event.replyText('テストだよ');
}
```
 ↓
結果
<img src="https://pragma-curry.com/wp/wp-content/uploads/2018/07/34150.jpg" width="40%">

と、すこぶる快適になります。


#６　Ethereumのweb3.jsを扱うクラスを定義する

　残高取得のみなので、チェーンの選択、イーサリアムアドレスのvalidationがあれば十分かと思います。

```javascript
const Ether = function(){
   const Web3 = require('web3');  //web3.jsの読み込み

   //mainnet,ropsten,rinkeby,kovanのどれかを指定してnodeとchain idを返す
   //infura.ioで取得したAPIアクセスキーを環境変数に入れて呼び出している。
   this.setChain = function(chain){
        switch(chain){
          case 'mainnet':
             return {'node':'https://mainnet.infura.io/'+ process.env.INFURA_KEY, 'id':1};
             break;
          case 'ropsten':              
             return {'node':'https://ropsten.infura.io/'+ process.env.INFURA_KEY, 'id':3};
             break;
          case 'rinkeby':              
             return {'node':'https://rinkeby.infura.io/'+ process.env.INFURA_KEY, 'id':4};
             break;
          case 'kovan':              
             return {'node':'https://kovan.infura.io/'+ process.env.INFURA_KEY, 'id':42};
             break;
          default:
             return {'node':'https://mainnet.infura.io/'+ process.env.INFURA_KEY, 'id':1};
             break;
       }
   }

   //正しいイーサリアムアドレスの形式になっているか確認する
   this.validateAddress = function(address){
      var valid = true;
      if(String(address) == ''){
          valid = false;
      }
      else if(
              String(address).length != 42
          || !String(address).match(/^[0-9a-zA-Z]/)
          || String(address).slice(0,2)!='0x'
      ){
          valid = false;
      }
      return valid;
   }
  
   //単位Etherで残高取得する
   this.getBalance = function(chain,address){
      const web3 = new Web3(new Web3.providers.HttpProvider(this.setChain(chain).node));
      return new Promise((resolve, reject) => {
          if(this.validateAddress(address)){
                resolve(web3.fromWei(web3.eth.getBalance(address), "ether").toNumber());
          }
          else{
                reject('Invalid address.');
          }
      });
   }
}
```

#７　メイン処理を書く

上記5,6で作成したメソッド群を使用して、

　1　ユーザーがLINEメッセージを送る
　2　nedbにユーザーの状態とメッセージが保存される
　3　Line Messaging APIがReplyを返す（質問）
　4　ユーザーがLINEメッセージを送る
　5　２で保存されたメッセージと４で送られたメッセージに基づいてイーサリアムから情報を取得する。
　6　ユーザーの状態が初期化される
　7　Line Messaging APIがReplyを返す（ユーザーがほしい情報)

という基本的な流れを実装します。

```javascript
var Main = function(app){
    const bot = new Linebot(app);
    const ether = new Ether();
  
    //botインスタンスにgetActionメソッドを追加。
    //DBにユーザーの状態が保存されていない場合（初期状態）では、LINEで送られたクエリ形式のアクション
    // (action=showBalance 等)を解析して次のアクションを行う。

    bot.getAction = function(event,message){
        try{
            if(typeof(message['action']) == 'undefined' || message['action']==''){
                throw 'No Action Detected.';
            }
            switch(message['action']){
            case 'showBalance':
                bot.writeDatabase(event.source.userId,'action=showbalance&listen=chain');
                event.replyButton(
                    'Select Chain','Select Ethereum chain.',
                    'Mainnet','chain=mainnet',
                    'Ropsten','chain=ropsten',
                    'Rinkeby','chain=rinkeby',
                    'Kovan','chain=kovan'
                );
                break;
            default:
                throw 'No Action Detected.';
                break;
            }

          }catch(e){
               event.replyText(e);
          }
    }
    
    //メッセージかポストバックイベントを受け取ったら、まずどちらのイベントか判断する
    this.onMessageEvent = function(event){
        switch(event.type){
           case 'message':
              var message = functions.queryParse(event.message.text);      
              break;
           case 'postback':
              var message = functions.queryParse(event.postback.data);
              break;
           default:
              event.replyText('The event type is not supported.');
              break;
        }
      
        //ユーザーのLINE IDをキーにDBファイルからクエリ形式で状態を取得
        bot.readDatabase(event.source.userId).then(function(statusQuery) {
             
              //クエリ文字列をオブジェクトに変換する関数queryParseを定義しておく
              //action=shobalance等のクエリ形式を解析
              var status = queryParse(statusQuery); 
          
              if(status['action']=='showbalance' && status['listen']=='chain'){
                      
                    if(typeof message['chain'] !== 'undefined'){
                        bot.writeDatabase(event.source.userId, 'action=showbalance&listen=address&chain=' + message['chain']); 
                        //chainの選択が終わったら、次はアドレスを取得するよう、ユーザー状態を変化させる。
                        event.replyText('Send '+ message['chain']+ ' Address.');
                    }
                    else{
                        bot.writeDatabase(event.source.userId, null); //ユーザー状態を初期化
                        event.replyText('Failed to select chain.');
                    }
              }
              else if(status['action']=='showbalance' && status['listen']=='address' && typeof status['chain'] !== 'undefined'){
                    ether.getBalance(status['chain'] , event.message.text).then(function(data){
                        event.replyText(data);
                    })
                    .catch(function(err){
                        event.replyText(err);
                    });
                    bot.writeDatabase(event.source.userId, null); //ユーザー状態を初期化
              }
              else{
                  bot.getAction(event,message);
              }
          
        }).catch(function (err) {
            event.replyText(err);
        });
    }
    bot.onMessageEvent(this.onMessageEvent);
}
const express = require('express');
new Main(express());
```
上記を実行すると、

<img src="https://pragma-curry.com/wp/wp-content/uploads/2018/07/34170.jpg" width="60%">

とLINEトーク画面からRopstenのEther残高を取得できました。


#微妙な点
　と、ここまで書いたものの、勉強用としてはまだしも、実用的には結構微妙な感じがします。なぜなら、

- アドレスをコピペしなければいけない。特にスマホでの操作を想定しているので、かなり面倒。
- LINEトーク画面上にjavascriptを埋め込めないので、送金等行う場合は、
アプリケーションサーバーにプライベートキーを送ってから署名等を行うという危険な行為を行うハメになる。
<img src="https://pragma-curry.com/wp/wp-content/uploads/2018/07/9607cde80d31aed3b76ef2b6e65be9ed-2.jpg" width="100%">


　LINEトーク画面にこだわらなければ選択の幅は広がります。というわけで余力があれば次回はWebViewでフロントで動かしてみようと思います。
　　↓
2018/7/14追記
  [LineBotで Ethereum の web3.jsを操作する（その２ : ローカルで署名してから送金）](https://qiita.com/pragma-curry/items/695d7feeb997f04ba631)