LoginSignup
3
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-07-05

やりたいこと

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

イメージ図

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

使用環境

1 Line Messaging APIの設定を行う

 LINE DevelopersのStart using Messaging APIにてプロバイダ、チャンネルの登録を行います。

 詳しい手続きは、前回エントリ Line Messaging API のFlex Messageを触ってみるを参照してください。

2 infura.ioのAPI KEYを取得する

 infura.ioのGET STARTED FOR FREEボタンから、メールアドレスを登録するとAPI KEYを取得できます。

 詳しい手続きは、前回エントリ ERC223トークンを時短でパブリックチェーンに公開する(忙しい人向け)を参照してください。

3 Glitchの設定を行う

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

 今回、Etherbotというプロジェクトを作成しました。

4 必要なモジュールをインストールする

 今回、追加するモジュールは

  • linebot --- LineBotSDKの1つ
  • web3 --- イーサリアム Javascript API
  • nedb --- インストール不要のNoSQL

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

5 LineBotのクラスを定義する

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

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をいちいち書くのは面倒なので、よく使うテキストやボタンだけ定義。
  //ボタンは4つまで設置できるので、可変長引数にしておく。
  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 });
              }
          });
      }
}

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

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('テストだよ');
}


結果

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

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

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

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.');
          }
      });
   }
}

7 メイン処理を書く

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

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

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

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());

上記を実行すると、

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

微妙な点

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

  • アドレスをコピペしなければいけない。特にスマホでの操作を想定しているので、かなり面倒。
  • LINEトーク画面上にjavascriptを埋め込めないので、送金等行う場合は、 アプリケーションサーバーにプライベートキーを送ってから署名等を行うという危険な行為を行うハメになる。

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

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5