Python
JavaScript
Bitcoin
Blockchain
NEM
nemDay 19

仮想通貨NEMでLINEに発言させたりビットコインをマルチシグ送金させたり、いろいろやってみる。

こんにちはXEMBook運営のtakanobuです。

今回、残念ながら多忙のため実際に動くものはお見せできません。
これらのネタを参考に来年のネクストヒロイン/ヒーローが実現してくれることを願っております。

  • LINEにXEM残高を発言させる
  • XEM着金時にビットコインを送金する
  • JavaScriptだけでマルチシグ署名でモザイク送金する(秘密鍵むき出し)

LINEにXEM残高を発言させる

少し前にSlackやSkypeで残高を確認するようなことがコミュニティで流行っており、当時別件でLINEをいじってたこともあったのでちょこっと書き換えて作ったのが上記ツイートの内容になります。
言語はPythonで、サーバはweb.pyをApache上で起動させました。

ライブラリはこちらを使います。
line/line-bot-sdk-python

Line Developer でアカウントを作成し、Messaging APIの登録を行います。
通信に必要なパラメータを指定して、実行するとユーザがメッセージを送信するとhandle_messageが呼び出されるのでメッセージ内容を解析し、必要な情報を発言させるという簡単な内容です。

ユーザIDが取れるはずですので、Addressと紐づけておけば対話プログラムも可能と思います。
Websocket通信は実践していませんが、もしできればtipnem-botと連携させても面白いかもしれませんね。

line.py
# -*- coding: utf-8 -*-

import os
import sys
from datetime import date, datetime, timedelta
import web
import time
import requests

from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage 

line_bot_api = LineBotApi('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
handler = WebhookHandler('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')

urls = (
    '/.*', 'callback'
)

application = web.application(urls, globals()).wsgifunc()

class callback():

    def POST(self):

        signature = web.ctx.env['HTTP_X_LINE_SIGNATURE']
        data = web.data()

        try:
            handler.handle(data, signature)
        except InvalidSignatureError:
            abort(400)

        return 'OK'


@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):

    messages = []
    message = event.message.text.encode('utf-8')
    r = requests.get('http://alice6.nem.ninja:7890/account/get?address=NBZNQL2JDWTGUAW237PXV4SSXSPORY43GUSWGSB7')

    if message == "balance":
        data = r.json()
        balance = data["account"]["balance"]

        messages.append(TextSendMessage(text="残高:" + str(balance)))

    line_bot_api.reply_message(event.reply_token,messages)

XEM着金時にビットコインを送金する

こちらは以前紹介した着金トリガーの応用版です。XEMで指定アドレスに送金すれば、
着金のタイミングで管理されたBTCアドレスから出金が行われるイメージです。
もちろんマルチシグにも対応できるので、BTCにマルチシグ機能を持たせるといった展開が可能かもしれません。
実は私、この実験中にBTCをGOXしました。難しすぎる、ビットコイン。。。

bitcoin.js
var Bitcoin = require('bitcoinjs-lib');
var cheerio = require('cheerio-httpcli');
var request = require('request');
var nem     = require("nem-sdk").default;

const NEM_EPOCH = Date.UTC(2015, 2, 29, 0, 6, 25, 0);
var NODES = Array(
"http://alice2.nem.ninja",
"http://alice3.nem.ninja",
"http://alice4.nem.ninja",
"http://alice5.nem.ninja",
"http://alice6.nem.ninja",
"http://alice7.nem.ninja"
);

var node_url;
function getEndpoint(){

    return NODES[Math.floor(Math.random() * NODES.length)];
}
node_url = getEndpoint();
var endpoint = nem.model.objects.create("endpoint")(node_url, nem.model.nodes.websocketPort);

// Address to subscribe
var address = "NBZNQL2JDWTGUAW237PXV4SSXSPORY43GUSWGSB7";
var connector = nem.com.websockets.connector.create(endpoint, address);


// Try to establish a connection
connect(connector);

// Connect using connector
function connect(connector){
    return connector.connect().then(function() {

        // Subscribe to unconfirmed transactions channel
        nem.com.websockets.subscribe.account.transactions.unconfirmed(connector, function(res) {
            console.log("unconfirmed");
            console.log(res);
        });


        // Subscribe to confirmed transactions channel
        nem.com.websockets.subscribe.account.transactions.confirmed(connector, function(res) {
            var message = JSON.parse(nem.utils.format.hexToUtf8(res.transaction.message.payload));

            var btcaddress = "128mxYbhBUbLQn2qJuU1uQwb4zDLxxxxxxx";
            cheerio.fetch('https://api.blockcypher.com/v1/btc/main/addrs/' + btcaddress, function (err, $, res, body) {

                json = JSON.parse(body);
                var tx_hash = json.txrefs[0].tx_hash;
                var balance = json.balance;

                var tx = new Bitcoin.TransactionBuilder(0x00,5000);
                tx.addInput(tx_hash, 0);
                tx.addOutput(message.address, message.amount);  //送金先
                tx.addOutput(btcaddress, balance - message.amount - 5000);  //送り返し
                var privateKeyWIF = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; //出金元秘密鍵
                var keyPair = Bitcoin.ECPair.fromWIF(privateKeyWIF);
                tx.sign(0, keyPair);

                console.log(tx.build().toHex());

                var pushtx = {
                  tx: tx.build().toHex()
                };


                var headers = {
                  'Content-Type': 'application/json',
                };

                var options = {
                  url: "https://api.blockcypher.com/v1/btc/main/txs/push",
                  method: 'POST',
                  headers: headers,
                  json: pushtx
                };

                request(options
                ,function(error, response, body){
                    if (body) {
                    console.log(body);
                    }
                    if (error) {
                    console.log(error);
                    }           
                });
            });
        });
    }, function(err) {
        console.log(err);
        reconnect();
    });
}

function reconnect() {
    node_url = getEndpoint();
    endpoint = nem.model.objects.create("endpoint")(node_url, nem.model.nodes.websocketPort);
    connector = nem.com.websockets.connector.create(endpoint, address);
    connect(connector);
}

JavaScriptだけでマルチシグ署名でモザイク送金する(秘密鍵むき出し)

最後は少し大作です。でも、たったこれだけでNEMの送金部分のほぼ全てを実装できてしまうことに驚愕してください。マルチシグもモザイクも全部できます。大企業が何億円もかけて保守しているようなセキュリティがこの数十行で記述できてしまいます。大変便利なライブラリも出てきていますが、他人の秘密鍵を預かるようなアプリを検討している人はどういった処理が内部で行われているのかを一度は追ってみるのもよいでしょう。

サンプルはテストネットで動くようにしていいます。
依存ライブラリとして

  • nacl-fast.js
  • KeyPair.js

がありますのでダウンロードしておいてください。私のgithubにもあります。
nem-multisig-transfer

index.htmlが簡単なUIとパラメータの定義です。一応秘密鍵もここに定義しておきます。
実行すると申請ボタンと承認ボタンが出てきます。申請ボタンで、トランザクション作成しアナウンス。承認ボタンでモザイク送信となります。
署名者の数、モザイクの種類は何種類でも送れますが辞書順に並んでいないとエラーが出ますのでご注意ください。

index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>マルチシグ・モザイク送信サンプル</title>
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/core.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/x64-core.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/sha3.js"></script>
<script src="multisig_mosaic_transfer.js"></script>
<script src="nacl-fast.js"></script>
<script src="KeyPair.js"></script>
<script>

var RECIPIENT_ADDRESS = "TAHIYQX57EDTM4HFVRKF46NBDUD57ABYMXHXXXX";                              //受信アドレス

//マルチシグ:Alice
var MULTISIG_ADDRESS = "TCL2IFCXAKFBRYAB54WXMT3KZX4KZR2DHJBXXXX";                               //申請者アドレス
var MULTISIG_PUBLIC_KEY = "99d9631e73cd1f782b5191b04d120b574b04db49a2aa5cadc161ee4658bxxxx";    //申請者公開鍵

//申請者
var SENDER_PUBLIC_KEY1 =  "9db8a3793da623ef1154e07596d5cac80346b77b5487f228a596bf973xxxxxxx";
var SENDER_PRIVATE_KEY1 = "17d5270b3f9153c9267ff0947a2cdfd2d8eba000e7fd0deb862ea0d84xxxxxxx";

//署名者
var SENDER_PUBLIC_KEY2 =  "211f73840b13748136d2ce2b7c3bbc87770b845073f5a819d57bd8e03xxxxxxx";
var SENDER_PRIVATE_KEY2 = "1935ae790611c522b286dd75b6dfc7694e8800b32cdaca9ee2cfed5b3xxxxxxx";

var TRANSACTION_HASH = "";

//送金API URL
var URL_TRANSACTION_ANNOUNCE = "http://50.3.87.123:7890/transaction/announce";

//モザイク定義
var MOSAICS = [
    {"mosaicId":{'name': "tip",'namespaceId': "xembook"},"quantity":10,     "supply":1000000,   "divisibility":1},
    {"mosaicId":{'name': "xem",'namespaceId': "nem"}    ,"quantity":1000000,"supply":8999999999,"divisibility":6}
]; 

var MULTIPLIER = 1;     //送信セット数(固定値)
var SEND_FEE = 300000;
var MSIG_FEE = 150000;

$(function(){
    function sendRequestA(){
        if(confirm("申請します。")){
            mosaicTransferRequest().done(
            function(data){
                console.log(data);
                alert(data["innerTransactionHash"]["data"]);
                TRANSACTION_HASH = data["innerTransactionHash"]["data"];
            });
        }
    }

    function sendRequestB(){

        if(confirm("承認します。")){
            prepareSignature().done(
            function(data){
                console.log(data);
            });
        }
    }

    $("#btnA").click(function(){sendRequestA();return false;});
    $("#btnB").click(function(){sendRequestB();return false;});
});

</script>
</head>
<body>
<h1>マルチシグMOSAIC送信サンプル</h1>
<button id="btnA">申請</button>
<button id="btnB">承認</button>
</body>
</html>
transfer.js
//NEM標準時
var NEM_EPOCH = Date.UTC(2015, 2, 29, 0, 6, 25, 0);
var _hexEncodeArray = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];

function hex2ua_reversed(hexx) {
    var hex = hexx.toString();//force conversion
    var ua = new Uint8Array(hex.length / 2);
    for (var i = 0; i < hex.length; i += 2) {
        ua[ua.length - 1 - (i / 2)] = parseInt(hex.substr(i, 2), 16);
    }
    return ua;
};

function ua2hex(ua) {
    var s = '';
    for (var i = 0; i < ua.length; i++) {
        var code = ua[i];
        s += _hexEncodeArray[code >>> 4];
        s += _hexEncodeArray[code & 0x0F];
    }
    return s;
};

function hex2ua (hexx) {
    var hex = hexx.toString();//force conversion
    var ua = new Uint8Array(hex.length / 2);
    for (var i = 0; i < hex.length; i += 2) {
        ua[i / 2] = parseInt(hex.substr(i, 2), 16);
    }
    return ua;
};

function utf8ToHex(str) {
    var rawString = rstr2utf8(str);
    var hex = "";
    for (var i = 0; i < rawString.length; i++) {
        hex += strlpad(rawString.charCodeAt(i).toString(16), "0", 2);
    }
    return hex;
}

function rstr2utf8(input) {
    var output = "";

    for (var n = 0; n < input.length; n++) {
        var c = input.charCodeAt(n);

        if (c < 128) {
            output += String.fromCharCode(c);
        } else if (c > 127 && c < 2048) {
            output += String.fromCharCode(c >> 6 | 192);
            output += String.fromCharCode(c & 63 | 128);
        } else {
            output += String.fromCharCode(c >> 12 | 224);
            output += String.fromCharCode(c >> 6 & 63 | 128);
            output += String.fromCharCode(c & 63 | 128);
        }
    }

    return output;
}
// Padding helper for above function
function strlpad(str, pad, len) {
    while (str.length < len) {
        str = pad + str;
    }
    return str;
};


function mosaicIdToName(mosaicId) {
    return mosaicId.namespaceId + ":" + mosaicId.name;
}

function _serializeMosaics(entity) {
    var r = new ArrayBuffer(276*10 + 4);
    var d = new Uint32Array(r);
    var b = new Uint8Array(r);

    var i = 0;
    var e = 0;

    d[i++] = entity.length;
    e += 4;

    var temporary = [];
    for (var j=0; j<entity.length; ++j) {
        temporary.push({'entity':entity[j], 'value':mosaicIdToName(entity[j].mosaicId) + " : " + entity[j].quantity})
    }
    temporary.sort(function(a, b) {return a.value < b.value ? -1 : a.value > b.value;});

    for (var j=0; j<temporary.length; ++j) {
        var entity = temporary[j].entity;
        var serializedMosaic = _serializeMosaicAndQuantity(entity);
        for (var k=0; k<serializedMosaic.length; ++k) {
            b[e++] = serializedMosaic[k];
        }
    }

    return new Uint8Array(r, 0, e);
};

function _serializeMosaicId(mosaicId) {
    var r = new ArrayBuffer(264);
    var serializedNamespaceId = _serializeSafeString(mosaicId.namespaceId);
    var serializedName = _serializeSafeString(mosaicId.name);

    var b = new Uint8Array(r);
    var d = new Uint32Array(r);
    d[0] = serializedNamespaceId.length + serializedName.length;
    var e = 4;
    for (var j=0; j<serializedNamespaceId.length; ++j) {
        b[e++] = serializedNamespaceId[j];
    }
    for (var j=0; j<serializedName.length; ++j) {
        b[e++] = serializedName[j];
    }
    return new Uint8Array(r, 0, e);
}

function _serializeLong(value) {
    var r = new ArrayBuffer(8);
    var d = new Uint32Array(r);
    d[0] = value;
    d[1] = Math.floor((value / 0x100000000));
    return new Uint8Array(r, 0, 8);
}

function _serializeMosaicAndQuantity(mosaic) {
    var r = new ArrayBuffer(4 + 264 + 8);
    var serializedMosaicId = _serializeMosaicId(mosaic.mosaicId);
    var serializedQuantity = _serializeLong(mosaic.quantity);

    //console.log(convert.ua2hex(serializedQuantity), serializedMosaicId, serializedQuantity);

    var b = new Uint8Array(r);
    var d = new Uint32Array(r);
    d[0] = serializedMosaicId.length + serializedQuantity.length;
    var e = 4;
    for (var j=0; j<serializedMosaicId.length; ++j) {
        b[e++] = serializedMosaicId[j];
    }
    for (var j=0; j<serializedQuantity.length; ++j) {
        b[e++] = serializedQuantity[j];
    }
    return new Uint8Array(r, 0, e);
};

function _serializeSafeString(str) {
    var r = new ArrayBuffer(132);
    var d = new Uint32Array(r);
    var b = new Uint8Array(r);

    var e = 4;
    if (str === null) {
        d[0] = 0xffffffff;

    } else {
        d[0] = str.length;
        for (var j = 0; j < str.length; ++j) {
            b[e++] = str.charCodeAt(j);
        }
    }
    return new Uint8Array(r, 0, e);
}

function serializeTransferTransaction (entity) {

    var r = new ArrayBuffer(512 + 2764);
    var d = new Uint32Array(r);
    var b = new Uint8Array(r);
    d[0] = entity['type'];
    d[1] = entity['version'];
    d[2] = entity['timeStamp'];

    var temp = hex2ua(entity['signer']);
    d[3] = temp.length;
    var e = 16;
    for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }

    // Transaction
    var i = e / 4;
    d[i++] = entity['fee'];
    d[i++] = Math.floor((entity['fee'] / 0x100000000));
    d[i++] = entity['deadline'];
    e += 12;

    // TransferTransaction
    if (d[0] === 0x101) {

        d[i++] = entity['recipient'].length;
        e += 4;
        // TODO: check that entity['recipient'].length is always 40 bytes
        for (var j = 0; j < entity['recipient'].length; ++j) {
            b[e++] = entity['recipient'].charCodeAt(j);
        }
        i = e / 4;
        d[i++] = entity['amount'];
        d[i++] = Math.floor((entity['amount'] / 0x100000000));
        e += 8;

        if (entity['message']['type'] === 1 || entity['message']['type'] === 2) {
            var temp = hex2ua(entity['message']['payload']);
            if (temp.length === 0) {
                d[i++] = 0;
                e += 4;
            } else {
                // length of a message object
                d[i++] = 8 + temp.length;
                // object itself
                d[i++] = entity['message']['type'];
                d[i++] = temp.length;
                e += 12;
                for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
            }
        }

        var entityVersion = d[1] & 0xffffff;
        if (entityVersion >= 2) {
            var temp = _serializeMosaics(entity['mosaics']);
            for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
        }

    } else if (d[0] === 0x1002) {
        var temp = hex2ua(entity['otherHash']['data']);
        // length of a hash object....
        d[i++] = 4 + temp.length;
        // object itself
        d[i++] = temp.length;
        e += 8;
        for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
        i = e / 4;

        temp = entity['otherAccount'];
        d[i++] = temp.length;
        e += 4;
        for (var j = 0; j < temp.length; ++j) {
            b[e++] = temp.charCodeAt(j);
        }

    // Multisig wrapped transaction
    } else if (d[0] === 0x1004) {
        var temp = serializeTransferTransaction(entity['otherTrans']);
        d[i++] = temp.length;
        e += 4;
        for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
    }
    return new Uint8Array(r, 0, e);
}

function calcXemEquivalent(multiplier, q, sup, divisibility) {
    if (sup === 0) {
        return 0;
    }
    // TODO: can this go out of JS (2^54) bounds? (possible BUG)
    return 8999999999 * q * multiplier / sup / Math.pow(10, divisibility + 6);
}


function CALC_MIN_FEE(numNem) {
    return Math.ceil(Math.max(10 - numNem, 2, Math.floor(Math.atan(numNem / 150000.0) * 3 * 33)));
}

var CURRENT_NETWORK_ID = -104; //テストネット
var CURRENT_NETWORK_VERSION = function(val) {

    if (CURRENT_NETWORK_ID === 104) {
        return 0x68000000 | val;
    } else if (CURRENT_NETWORK_ID === -104) {
        return 0x98000000 | val;
    }
    return 0x60000000 | val;
};

function fixPrivateKey(privatekey) {
    return ("0000000000000000000000000000000000000000000000000000000000000000" + privatekey.replace(/^00/, '')).slice(-64);
}

//承認処理
function prepareSignature() {
    var kp = KeyPair.create(fixPrivateKey(SENDER_PRIVATE_KEY2));
    var actualSender = kp.publicKey.toString();
    var due = 60;
    var otherHash = null;

    var timeStamp = Math.floor((Date.now() / 1000) - (NEM_EPOCH / 1000));
    var version = CURRENT_NETWORK_VERSION(1);

    var data ={
        'type': 0x1002,
        'version': CURRENT_NETWORK_VERSION(1),
        'signer': SENDER_PUBLIC_KEY2,
        'timeStamp': timeStamp,
        'deadline': timeStamp + due * 60
    };

    var custom = {
        'otherHash': { 'data': TRANSACTION_HASH },
        'otherAccount': MULTISIG_ADDRESS,
        'fee': MSIG_FEE,
    };
    var entity = $.extend(data, custom);

    var result = serializeTransferTransaction(entity);
    var signature = kp.sign(result);
    var obj = {'data':ua2hex(result), 'signature':signature.toString()};
    console.log(entity);

    return $.ajax({
        url: URL_TRANSACTION_ANNOUNCE  ,
        type: 'POST',
        contentType:'application/json',
        data: JSON.stringify(obj)  ,
        error: function(XMLHttpRequest) {
            console.log( $.parseJSON(XMLHttpRequest.responseText));
        }
    });
}

//申請処理
function mosaicTransferRequest(){

    var amount = parseInt(MULTIPLIER * 1000000, 10);
    var due = 60;
    var timeStamp = Math.floor((Date.now() / 1000) - (NEM_EPOCH / 1000));

    var message = {payload:utf8ToHex(""),type:1};

    var data ={
        'type': 0x101,
        'version': CURRENT_NETWORK_VERSION(2),
        'signer': MULTISIG_PUBLIC_KEY,
        'timeStamp': timeStamp,
        'deadline': timeStamp + due * 60
    };
    var custom = {
        'recipient': RECIPIENT_ADDRESS,
        'amount': amount,
        'fee': SEND_FEE,
        'message': message,
        'mosaics': MOSAICS
    };
    var entity = $.extend(data, custom);

    var data2 = {
        'type': 0x1004,
        'version': CURRENT_NETWORK_VERSION(1),
        'signer': SENDER_PUBLIC_KEY1,
        'timeStamp': timeStamp,
        'deadline': timeStamp + due * 60
    }
    var custom2 = {
        'fee': MSIG_FEE,
        'otherTrans': entity

    }
    var entity2 = $.extend(data2, custom2);

    var result = serializeTransferTransaction(entity2);
    var kp = KeyPair.create(fixPrivateKey(SENDER_PRIVATE_KEY1));  
    var signature = kp.sign(result);
    var obj = {'data':ua2hex(result), 'signature':signature.toString()};
    console.log(entity2);
    console.log(result);
    console.log(obj);

    return $.ajax({
        url: URL_TRANSACTION_ANNOUNCE  ,
        type: 'POST',
        contentType:'application/json',
        data: JSON.stringify(obj)  ,
        error: function(XMLHttpRequest) {
            console.log( $.parseJSON(XMLHttpRequest.responseText));
        }
    });
}

今回は以上です。ほとんど解説が無くてごめんなさい。わからないところがあれば、そこを重点的に解説していきますのでご指摘いただければと思います。

それではみなさん、よいクリスマスを!