LineBotで Ethereum の web3.jsを操作する（その２ : ローカルで署名してから送金）

#やりたいこと

前回エントリ [LineBotで Ethereum の web3.jsを操作する（その１: バックエンドで残高取得）](https://qiita.com/pragma-curry/items/0cf3171d5f3c44e6c095)
ではバックエンドにてLine Messaging APIとweb3.jsを使ってパブリックチェーンのイーサリアム残高を取得しました。しかしながら、送金等その他の処理も全てバックエンドで行うとなると、
<img src="https://pragma-curry.com/wp/wp-content/uploads/2018/07/9607cde80d31aed3b76ef2b6e65be9ed-2.jpg">
>
> - 秘密鍵を外部に送る機会が生じるため、（暗号化されていようといまいと）危険。
> - WebStorageが使えない。

など色々不便が多いため、フロントで実装したいと思います。

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

とは言え、秘密鍵をスマホに保存するのはやはり危険が伴うので、WebStorageに保管するのは、アドレスのみに留めておきます。秘密鍵はペーパーウォレット等のQRコードから読めるようにします。


#１　web3.jsの読み込み
web3.jsはフロント用のライブラリが用意されているので、そのままCDNから読み込みます。

```html
<script type="text/javascript" src="https://cdn.jsdelivr.net/gh/ethereum/web3.js/dist/web3.min.js"></script>
```


#２　ethereumjs-txとethereumjs-utilの読み込み
ethereumjs-txは秘密鍵を使ってローカルで署名するための機能を持つライブラリです。

秘密鍵からアドレスを導出する機能についてはver1.0-betaのweb3.jsであればweb3.eth.accounts.privateKeyToAccountというメソッドが用意されていますが、stable版のweb3.jsを使う場合は、別途ethereumjs-utilを使用する必要があります。


browserifyを使ってまとめてフロント用に変換します。

```javascript:src.js
require('ethereumjs-tx');
require('ethereumjs-util');
script.onload(require); 
//メインプロシージャから呼び出す用の関数。uniqueな関数名でrequireが引数であれば何でも。
```
```console:console
browserify src.js -o dist.js
```
```html:main.html
<script type="text/javascript" src="dist.js"></script>
```

メインプロシージャにrequireを渡してライブラリを読み込みます。(もっとスマートな方法があるとは思いますが・・とりあえず動くのでこうしてます。)

```javascript:main.js
var script = {
    onload : function(require){
        const ether = new Ether(require);
        model.init(ether);
        view.init();
        controller.init();
    }
}
```

#３　Etherクラスを定義する

```javascript:ether.js

var Ether = (function() {
    /**
    * コンストラクタでethereumjs-txとethtereumjs-utilを読み込み
    */
    var Ether = function(require) {
        if(!(this instanceof Ether)) {
            return new Ether(require);
        }
        this.Util = require('ethereumjs-util');
        this.Tx = require('ethereumjs-tx');
    }

    Ether.prototype.setChain = function(chain){
            switch(chain){
            case 'mainnet':
                return {'node':'https://mainnet.infura.io/[infura.ioのAPI KEY]','api':'https://api.etherscan.io/api','id':1};
                break;
            case 'ropsten':              
                return {'node':'https://ropsten.infura.io/[infura.ioのAPI KEY]','api':'https://api-ropsten.etherscan.io/api','id':3};
                break;
            case 'rinkeby':              
                return {'node':'https://rinkeby.infura.io/[infura.ioのAPI KEY]','api':'https://api-rinkeby.etherscan.io/api','id':4};
                break;
            case 'kovan':              
                return {'node':'https://kovan.infura.io/[infura.ioのAPI KEY]','api':'https://api-kovan.etherscan.io/api','id':42};
                break;
            default:
                return {'node':'https://mainnet.infura.io/[infura.ioのAPI KEY]','api':'https://api.etherscan.io/api','id':1};
                break;
        }
    }
    /**
    * @function validateAddress アドレスのvalidation
    */
    Ether.prototype.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;
    }
    /**
    * @function validateSecret 秘密鍵のvalidation
    */
    Ether.prototype.validateSecret = function(secret){
        var valid = true;
        if(String(secret) == ''){
            valid = false;
        }
        else if(
                String(secret).length != 64
            || !String(secret).match(/^[0-9a-z]/)
        ){
            valid = false;
        }
        return valid;
    }
    /**
    * @function getBalance 残高取得するメソッド（web3.jsのみ使用）
    */
    Ether.prototype.getBalance = function(chain,address){
        const web3 = new Web3(new Web3.providers.HttpProvider(this.setChain(chain).node));
        if(this.validateAddress(address)){
              return web3.fromWei(web3.eth.getBalance(address), "ether").toNumber();
        }
        else{
              return false;
        }
    }
    /**
    * @function getAccountFromSecret 秘密鍵からアドレスや残高を取得するメソッド（web3.jsとehtereumjs-util使用）
    */
    Ether.prototype.getAccountFromSecret = function(chain,secret){
        if(this.validateSecret(secret)){
              const web3 = new Web3(new Web3.providers.HttpProvider(this.setChain(chain).node));
              const privkey = this.Util.toBuffer('0x'+ secret);
              const address = '0x' + this.Util.privateToAddress(privkey).toString('hex');
              if(this.validateAddress(address)){
                    return {'address':address,'balance':web3.fromWei(web3.eth.getBalance(address), "ether").toNumber()};
              }
              else{
                    return false;
              }
        }
        else{
              return false;
        }
    }
    /**
    * @function transfer 送金するメソッド（web3.jsとehtereumjs-tx使用）
    */
    Ether.prototype.transfer = function(chain,secret,sendTo,amount,gas){
        const web3 = new Web3(new Web3.providers.HttpProvider(this.setChain(chain).node));

        return new Promise((resolve, reject) => {
            if(!this.validateSecret(secret)){
                    reject('Invalid secret.')
            }
            if(!this.validateAddress(sendTo)){
                    reject('Destination address is invalid.')
            }
            if(typeof(amount)!=='number' || amount<0){
                    reject('Invalid amount.')
            }
            const account = this.getAccountFromSecret(chain,secret);

            web3.eth.getTransactionCount(account.address, (err,txCount) => {
                const privKey = this.Util.toBuffer('0x'+ secret);
                const rawTx = {
                    nonce: web3.toHex(txCount),
                    gasPrice: web3.toHex(web3.toWei(gas, 'gwei')),
                    gasLimit: web3.toHex(21000),
                    to: sendTo,
                    value: web3.toHex(web3.toWei(amount, 'ether')),  
                    data:null,
                    chainId: this.setChain(chain).id
                }

                const tx = new this.Tx(rawTx);
                tx.sign(privKey);

                const signedTx = tx.serialize();

                web3.eth.sendRawTransaction('0x' + signedTx.toString('hex'),(err, txHash)=>{
                    if(err) {
                        reject(err);
                    } else {
                        resolve(txHash);
                    }
                });
           });
       });
    }
    return Ether;
})();

```


#４　Etherクラスを使用する

メインプロシージャのmodelの中で使います。

```javascript:main.js

var model = {

    ether:{},

    init: function(ether){
        model.ether = ether;
        model.setAddressList();
        model.showTransactionHistory();
    },

    showInfo: function(chain,secret){
        const account  = model.ether.getAccountFromSecret(chain,secret);
        if(account){
            view.showInfo([{'address':account.address,'balance':account.balance}]);
        }
    },

    transfer: function(chain,secret,sendTo,amount,gas){
        model.ether.transfer(chain,secret,sendTo,amount,gas).then(function(txHash){
              alert('Payment completes successfully.');
              view.showList();

        }).catch(function(err){
              alert(err);
        });
    }
};
```

#５　LineBotからアクセスできるようにする

Linebotクラスは[LineBotで Ethereum の web3.jsを操作する（その１: バックエンドで残高取得）](https://qiita.com/pragma-curry/items/0cf3171d5f3c44e6c095)で定義したものです。ボタンをクリックすると該当URLにジャンプするだけのBotです。

```javascript:server.js
var Main = function(app){

    const bot = new Linebot(app);
  
    bot.getAction = function(event,message){
      try{
         if(typeof(message['action']) == 'undefined' || message['action']==''){
             throw 'No Action Detected.';
         }
         switch(message['action']){
            case 'accountInfo':
                event.replyFlex(
                    "Show Account Info",
                    null,
                    "Click the button below to show your account information.",
                    "Show",
                    "https://etherbot.glitch.me/redirect/accountinfo"
                );
                break;
            case 'payment':
                event.replyFlex(
                    "Payment",
                    null,
                    "Click the button below to open URL.",
                    "Open URL",
                    "https://etherbot.glitch.me/redirect/transfer"
                );
                break;
            case 'invoice':
                event.replyFlex(
                    "Manage Invoice",
                    null,
                    "Click the button below to open URL.",
                    "Open URL",
                    "https://etherbot.glitch.me/redirect/invoice"
                );
                break;
            default:
                throw 'No Action Detected.';
                break;
            }

          }catch(e){
               event.replyText(e);
          }
    }

    this.onMessageEvent = function(event){
        switch(event.type){
           case 'message':
              var message = queryParse(event.message.text);      
              break;
           case 'postback':
              var message = queryParse(event.postback.data);
              break;
           default:
              event.replyText('The event type is not supported.');
              break;
        }
        bot.getAction(event,message);
    }
    bot.onMessageEvent(this.onMessageEvent);
}
const express = require('express');
new Main(express());
```

注意点として、WebViewだとQRコードリーダー等のWebRTC系ライブラリが初期状態では使用できないため、Chrome等のブラウザにリダイレクトさせるようにします。

```html:redirect.html
<!DOCTYPE html>
<html>
<head>
    <title>Redirect</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, height=device-height">
    <meta name="apple-mobile-web-app-capable" content="yes" />
</head>
<body>
    <script type="text/javascript">
        window.onload = function(){
            if (navigator.userAgent.match(/(Android)/)) {
                location.href= "intent://"+ location.hostname +"/invoice#Intent;scheme=https;package=com.android.chrome;end";
            }
            else if(navigator.userAgent.match(/(iPhone|iPod|iPad|BlackBerry)/)){
                location.href= "googlechromes://"+ location.hostname +"/transfer";
            }
            else{
                location.href= "https://" + location.hostname + '/transfer';
            }
        }
        </script>
</body>
</html>
```

#６　各機能の実装

##(1) LINE UI 
　LINE@マネージャーのリッチコンテンツ作成からメニューを作成します。
<img src="https://pragma-curry.com/wp/wp-content/uploads/2018/07/a34415.jpg" width="45%">

メニューからアクションを選択すると、クエリ文字列が送られ、リンクが生成されます。
<img src="https://pragma-curry.com/wp/wp-content/uploads/2018/07/a34416.jpg" width="45%">


##(2) アカウント情報取得
複数のAddressをWebStorageに保存し、一度に残高等を取得します。

```javascript
model.showAccountInfo: function(){
   if (typeof localStorage.addressList !== 'undefined') {
          var addressList = JSON.parse(localStorage.getItem("addressList"));
          var data = [];
          var label = [];
          var info = [];

          addressList.forEach((key)=>{
               var balance = model.ether.getBalance(key.chain,key.address);
               data.push(balance);
               label.push(key.chain + ':' + key.name);
               info.push({"name":key.name,"chain":key.chain,"address":key.address,"balance":balance});
          });
          view.drawChart(label,data);
          view.setInfo(info);
    }
    else {
          alert('No showable data exist.')
          return false;
    }
},
```
　　　　　　　　　↓
 <img src="https://pragma-curry.com/wp/wp-content/uploads/2018/07/accountinfo.jpg" width="55%">


##(3) 送金
```javascript
model.transfer: function(chain,secret,sendTo,amount,gas){
    model.ether.transfer(chain,secret,sendTo,amount,gas).then(function(txHash){
          alert('Payment completes successfully.');
     }).catch(function(err){
          alert(err);
     });
}
```
Senderの秘密鍵、Recipientのアドレスまたは請求書情報はQRコードで読めるようにします。
　　　　　　　　　↓
 <img src="https://pragma-curry.com/wp/wp-content/uploads/2018/07/sendether.jpg" width="55%">




##(4)請求書作成
```javascript
view.generateQRCode:function(chain,address,price){
      var string = "chain="+chain+"&address="+address+"&price="+price;
      $('#qrcode').empty();
      $('#qrcode').qrcode({width: 250, height: 250, text:string });
}
```
チェーン(mainnet,ropsten,etc.)、Recipientのアドレス、価格からQRコードを生成します。
　　　　　　　　　↓
 <img src="https://pragma-curry.com/wp/wp-content/uploads/2018/07/invoicemain.jpg" width="55%">


#まとめ
当初はLineBotだけでEthereum周りのことが色々できたらいいなーとぼんやり思っていましたが、セキュリティや利便性等の事情を考慮して、結局フロントUIを一から作る結果になりました。

それでもやはり、LineBotを使用するメリットとしては、やはり導入の敷居の低さだと思います。
バックエンドの処理が一定以上存在する以上、サーバ負荷を考えた設計にしなければいけないことは勿論ですが、

それを踏まえても、開発側にとっても、使用する側にとっても「とりあえず作ってみる、使ってみる」ができるというメリットは大きいと思います。

ちなみに今回作成したBotのソースは以下です。
##Github
https://github.com/snst-lab/etherbot