#やりたいこと
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 必要なモジュールをインストールする
今回、追加するモジュールは
の3つです。Glitchプロジェクトフォルダ内のpackage.jsonをクリックし、Add Packagesから追加してください。
#5 LineBotのクラスを定義する
LineBotアプリを構築する中で、Replyメッセージの作成やデータベースの読み書きを頻繁に行うため、一連の処理系をクラスにまとめて使いやすくしておきます。(正確にはES5の疑似クラスです) Replyメッセージの種類はたくさんありますが、よく使うtextとbuttonだけ定義しておきます。少々長いですが、まとめておくと後々楽になります。
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 : ローカルで署名してから送金)