#やりたいこと
前回エントリ LineBotで Ethereum の web3.jsを操作する(その1: バックエンドで残高取得)
ではバックエンドにてLine Messaging APIとweb3.jsを使ってパブリックチェーンのイーサリアム残高を取得しました。しかしながら、送金等その他の処理も全てバックエンドで行うとなると、
- 秘密鍵を外部に送る機会が生じるため、(暗号化されていようといまいと)危険。
- WebStorageが使えない。
など色々不便が多いため、フロントで実装したいと思います。
とは言え、秘密鍵をスマホに保存するのはやはり危険が伴うので、WebStorageに保管するのは、アドレスのみに留めておきます。秘密鍵はペーパーウォレット等のQRコードから読めるようにします。
#1 web3.jsの読み込み
web3.jsはフロント用のライブラリが用意されているので、そのままCDNから読み込みます。
<script type="text/javascript" src="https://cdn.jsdelivr.net/gh/ethereum/web3.js/dist/web3.min.js"></script>
#2 ethereumjs-txとethereumjs-utilの読み込み
ethereumjs-txは秘密鍵を使ってローカルで署名するための機能を持つライブラリです。
秘密鍵からアドレスを導出する機能についてはver1.0-betaのweb3.jsであればweb3.eth.accounts.privateKeyToAccountというメソッドが用意されていますが、stable版のweb3.jsを使う場合は、別途ethereumjs-utilを使用する必要があります。
browserifyを使ってまとめてフロント用に変換します。
require('ethereumjs-tx');
require('ethereumjs-util');
script.onload(require);
//メインプロシージャから呼び出す用の関数。uniqueな関数名でrequireが引数であれば何でも。
browserify src.js -o dist.js
<script type="text/javascript" src="dist.js"></script>
メインプロシージャにrequireを渡してライブラリを読み込みます。(もっとスマートな方法があるとは思いますが・・とりあえず動くのでこうしてます。)
var script = {
onload : function(require){
const ether = new Ether(require);
model.init(ether);
view.init();
controller.init();
}
}
#3 Etherクラスを定義する
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;
})();
#4 Etherクラスを使用する
メインプロシージャのmodelの中で使います。
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);
});
}
};
#5 LineBotからアクセスできるようにする
LinebotクラスはLineBotで Ethereum の web3.jsを操作する(その1: バックエンドで残高取得)で定義したものです。ボタンをクリックすると該当URLにジャンプするだけのBotです。
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等のブラウザにリダイレクトさせるようにします。
<!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>
#6 各機能の実装
##(1) LINE UI
LINE@マネージャーのリッチコンテンツ作成からメニューを作成します。
メニューからアクションを選択すると、クエリ文字列が送られ、リンクが生成されます。
##(2) アカウント情報取得
複数のAddressをWebStorageに保存し、一度に残高等を取得します。
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;
}
},
##(3) 送金
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コードで読めるようにします。
↓
##(4)請求書作成
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コードを生成します。
↓
#まとめ
当初はLineBotだけでEthereum周りのことが色々できたらいいなーとぼんやり思っていましたが、セキュリティや利便性等の事情を考慮して、結局フロントUIを一から作る結果になりました。
それでもやはり、LineBotを使用するメリットとしては、やはり導入の敷居の低さだと思います。
バックエンドの処理が一定以上存在する以上、サーバ負荷を考えた設計にしなければいけないことは勿論ですが、
それを踏まえても、開発側にとっても、使用する側にとっても「とりあえず作ってみる、使ってみる」ができるというメリットは大きいと思います。
ちなみに今回作成したBotのソースは以下です。
##Github
https://github.com/snst-lab/etherbot