ブラウザでBTC送金トランザクション

ブラウザ上のJavaScriptでビットコイン(BTC)を送金する方法をメモします。

今回ご紹介する方法は、いわゆるブラウザウォレットアプリケーションの作り方の基本になります。通常ブラウザウォレットでは、秘密鍵での署名をブラウザのローカル上だけで行います。そのため、秘密鍵がインターネット上に流れて盗聴されるリスクを低減させることができます。これはウォレットアプリとしてのセキュリティ上とても重要なことです。ただし、秘密鍵の管理を自己責任で行う必要があるため、取引所のWebウォレットのように他社にお任せできる利便性とはトレードオフになります。

目次

前提

今回はbitcoinのテストネット上で alice から bob0.01 BTC 送金します。

  • alice (送信者):
  • bob (受信者):

bitcoinjs-libモジュールをインストール

まずはビットコイン操作のためのnodeモジュールbitcoinjs-libをインストールします。こちらはnodeモジュールですがbrowserifyにも対応しています。

bitcoinjs-libのインストール:

$ npm install --save bitcoinjs-lib

重要: bitcoinjs-libはバージョンによってIFが大幅に変わることがあるので注意してください。ここでは現時点(2019/06/28)で最新の v5.0.5 を前提に話を進めていきます。インストールしたバージョンの確認には以下のコマンドを使います:

$ npm list | grep bitcoinjs-lib@
└─┬ bitcoinjs-lib@5.0.5

NOTE: 正直なところ、bitcoinjs-libよりも競合のbitcore-libの方が直感的で個人的には使いやすいです。しかし、bitcore-lib(v8.3.4)はbrowserifyしてもファイルサイズが700kbぐらいあり、ブラウザアプリにはヘビーなのがデメリット。一方、bitcoinjs-lib(v5.0.5)の場合はbundleサイズがbitcore-libの場合の半分以下なので、ブラウザアプリには比較的優しいです。

BTC送金

aliceのUTXOを確認

最初に、何らかの方法(例えば、Insight APIまたはブロックチェーンエクスプローラなど)でaliceが送金に使う未消費トランザクションアウトプット(UTXO)を調べておく必要があります。

TIP: もしローカルにbitcoindノードが立っていて且つノードが持つウォレット機能を使っているのであれば、RPC APIのlistunspentでウォレット内アドレスのUTXOを取得することもできます:

$ curl -H 'content-type: text/plain;' \
  --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "listunspent", "params": [6, 9999999, "[\"mrt15K57fFTRnADXPsA2sofckiyQ9PKutd\"]"] }' \
  http://rpc:rpc@127.0.0.1:18332

調査の結果、aliceは現在以下のようなUTXOを持っていることが分かりました:

txid: ae4c111a0bca893755f1c83f9cf9427cd550c56f2760258171620644f48a079e 
受取: 0.00100000 BTC

txid: eff3c615f48a1fb442a81d519a9f0e19f5230ae5390c8b42e1d9237a76f01374 
受取: 0.01000000 BTC

txid: fd45499577182fe9f8e1321f47783cdde60ac379164e0c3ea8e8d2393f39bc0c 
受取: 0.00020000 BTC

txid: 2a075df975148e5bc64b44cd93ba84071bfcfd1d8603a283154c05bf1d382611 
受取: 0.06262902 BTC

このトランザクション一覧の中からUTXOとして入力するトランザクションを選びます。 今回は上記トランザクション一覧の中から、一番下にあるトランザクション(txid: 2a075df975148e5bc64b44cd93ba84071bfcfd1d8603a283154c05bf1d382611)のUTXO(残高: 0.06262902 BTC)を送金に使うことにしましょう。トランザクション詳細を確認すると以下のような内訳になっています:

txid: 2a075df975148e5bc64b44cd93ba84071bfcfd1d8603a283154c05bf1d382611

[input]
vin:0 2NEMu9p5Gk94uS2QXa67GW3v6CGcoGs7gM1 546.60207895 BTC

[output]
vout:0 2N7ZGZT6Wu8UGsMsD8XCXXjgevG3F11VGLB 546.53911522 BTC
vout:1 mrt15K57fFTRnADXPsA2sofckiyQ9PKutd 0.06262902 BTC <= 今回はこの vout:1 のUTXO(残高 0.06262902 BTC)を使う!

あるアドレス2NEMu9...からaliceのアドレスmrt15K...宛に 0.06262902 BTC 送られたトランザクションであることが分かりますね。

bitcoinjs-libをロード

bitcoinjs-libをロードし、networkインスタンスも作成しておきます:

const bitcoin = require("bitcoinjs-lib");

// ネットワーク指定:
const networkName = "testnet"; // "testnet":テストネット, "bitcoin":メインネット
const network = bitcoin.networks[networkName];

未署名トランザクションを作成

// aliceの秘密鍵(WIF)でキーペアを作成:
const alice = bitcoin.ECPair.fromWIF('cUU3sQSPnieENaHnQoraMicDW37U6TNz1MQs8NQQTJVHK8qw3Xpz', network);
// トランザクションビルダーを作成:
const txb = new bitcoin.TransactionBuilder(network);
txb.setVersion(1);
// UTXOをセット:
txb.addInput('2a075df975148e5bc64b44cd93ba84071bfcfd1d8603a283154c05bf1d382611', 1); // 第1引数はUTXOのtxid, 第2引数はvoutのindex
/*
 * 送金先をセット:
 *
 * addInputされたUTXOの残高トータル: 0.06262902 BTCを以下の内訳で送金します:
 *
 *  bobへ送金: 0.01 BTC
 *  マイナーへの手数料: 0.01262902 BTC
 *  残りはaliceへ戻す: 0.04 BTC
 */
// addInputされたUTXOの残高トータル(satoshi)
const utxoSat = 6262902; // 0.06262902 BTC
// マイナーへの手数料(satoshi)
const feeSat = 1262902; // 0.01262902 BTC
// 送金額(satoshi)
const sendingSat = 1000000; // 0.01 BTC
// お釣り(satoshi)
const chgSat = utxoSat - (sendingSat + feeSat); // UTXO残高トータルから送金額とマイナーへの手数料を引いた残額がお釣り
// 送金先(bobのアドレス)と送金額(satoshi)をセット:
txb.addOutput('mh2pkXAWRW6Fsdv5TNnZUXf46ruoszo4E3', sendingSat);
// aliceへ戻す送金額をセット: 
txb.addOutput('mrt15K57fFTRnADXPsA2sofckiyQ9PKutd', chgSat);

未署名トランザクションに署名

未署名のトランザクションに署名し、署名済みトランザクションhexを取得します(ついでにtxidも取得しています):

// aliceのキーペア(秘密鍵)で署名:
txb.sign(0, alice); // 第一引数の 0 はaddInputしたUTXOのindex

// 署名済みトランザクションを取得:
const tx = txb.build();
// txidを取得:
const txid = tx.getId();
// txhexを取得:
const txhex = tx.toHex();

console.log("txid:", txid);
console.log("txhex:", txhex);
/*
txid: 96781f350a8cf69994ad7e333316bcf7cf9b9d1ae664d6d95c3460cd81a3bd95
txhex: 01000000011126381dbf054c1583a203861dfdfc1b0784ba93cd444bc65b8e1475f95d072a010000006b483045022100aa8173ef70e45fed02c239820c86b8e5fcb6c60b39a510dde5d355ac584f2c0702200b1aec1cdb757f136a1c25d34767ec2c4ee734f376f693cafd6239cdf75e88fd012102422b6ec8b3eb5c478c74a30619bf89161f76b7c88da2f3491f0de497ee69b1f0ffffffff0240420f00000000001976a914109e4eb7fbfa7f4ed7a7d220c15cede605a03b8788ac00093d00000000001976a9147ca47090df0721b8ea40a824eeaf97123604a73988ac00000000
*/

上記のtxhexが署名済みトランザクションhexです。

署名済みトランザクションを送信

Insight APIやブロックチェーンエクスプローラが提供しているブロードキャストツールなどを使って、先ほど作成した署名済みトランザクションhexをbitcoindノードへ送信します。

Insight API を使って送信する場合:

$ curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"rawtx": "[01000000011126381dbf054c1583a203861dfdfc1b0784ba93cd444bc65b8e1475f95d072a010000006b483045022100aa8173ef70e45fed02c239820c86b8e5fcb6c60b39a510dde5d355ac584f2c0702200b1aec1cdb757f136a1c25d34767ec2c4ee734f376f693cafd6239cdf75e88fd012102422b6ec8b3eb5c478c74a30619bf89161f76b7c88da2f3491f0de497ee69b1f0ffffffff0240420f00000000001976a914109e4eb7fbfa7f4ed7a7d220c15cede605a03b8788ac00093d00000000001976a9147ca47090df0721b8ea40a824eeaf97123604a73988ac00000000]"}' \
  https://test-insight.bitpay.com/api/tx/send

TIP: もしローカルのbitcoindノードや信頼できる公開bitcoindノードがあれば、そちらのRPC APIのsendrawtransactionを使ってトランザクションを送信しても良いでしょう:

$ curl -XPOST \
  -H 'content-type: text/plain;' \
  -d '{"jsonrpc": "1.0", "id": "curltest", "method": "sendrawtransaction", "params": ["01000000011126381dbf054c1583a203861dfdfc1b0784ba93cd444bc65b8e1475f95d072a010000006b483045022100aa8173ef70e45fed02c239820c86b8e5fcb6c60b39a510dde5d355ac584f2c0702200b1aec1cdb757f136a1c25d34767ec2c4ee734f376f693cafd6239cdf75e88fd012102422b6ec8b3eb5c478c74a30619bf89161f76b7c88da2f3491f0de497ee69b1f0ffffffff0240420f00000000001976a914109e4eb7fbfa7f4ed7a7d220c15cede605a03b8788ac00093d00000000001976a9147ca47090df0721b8ea40a824eeaf97123604a73988ac00000000"] }' \
  http://rpc:rpc@127.0.0.1:18332

トランザクション確認状況をチェック

bitcoindノードへの送信が成功すると、txidを元にトランザクションの確認(confirmations)状況などを確認できます:

txid: 96781f350a8cf69994ad7e333316bcf7cf9b9d1ae664d6d95c3460cd81a3bd95

おまけ

アドレス生成

アドレスと秘密鍵を生成します。

新規生成する場合

// キーペア生成:
const keyPair = bitcoin.ECPair.makeRandom({
  network,
});
// アドレス取得:
const { address } = bitcoin.payments.p2pkh({
  pubkey: keyPair.publicKey,
  network,
});
// 秘密鍵(WIF)取得:
const pkwif = keyPair.toWIF();

console.log("ネットワーク名:", networkName);
console.log("アドレス:", address);
console.log("秘密鍵(WIF):", pkwif);

秘密鍵(WIF)からインポートする場合

// 秘密鍵(WIF)をセット:
const pkwif = "cUU3sQSPnieENaHnQoraMicDW37U6TNz1MQs8NQQTJVHK8qw3Xpz";
// キーペア生成:
const keyPair = bitcoin.ECPair.fromWIF(pkwif, network)
// アドレス取得:
const { address } = bitcoin.payments.p2pkh({
  pubkey: keyPair.publicKey,
  network,
});

console.log("ネットワーク名:", networkName);
console.log("アドレス:", address);
console.log("秘密鍵(WIF):", pkwif);

ブラウザで使うために

ここまで書いたコード (ここでは app.js とします) をまとめてbundleし、ブラウザで使えるようにしましょう。

jsファイルをbrowserifyし、bundle.jsファイルを生成します:

$ browserify app.js > bundle.js

uglifyjsと併用してファイルサイズをもっとコンパクトにすることもできます:

$ browserify app.js | uglifyjs --compress warnings=false --mangle > bundle.js

生成されたbundle.jsファイルはHTML内の</body>の直線あたりにロードします:

  ...
  <script type="text/javascript" src="bundle.js"></script>
</body>
</html>

このHTMLをブラウザで開くと、jsが実行されtxhexなどがコンソールに出力されます。

bitcoinjs-libだけをブラウザ対応させたい場合

全てのjsを一つにbundleしたい訳ではなく、ただ単にbitcoinjs-libをブラウザ対応させたいだけという場合もあると思いますが、その場合は以下の手順を行います。

まず、以下の1行だけを書いたファイル (名前は何でも良いですが、ここでは bitcoinjs-import.js とします) を用意します:

bitcoinjs-import.js

global.bitcoin = require("bitcoinjs-lib");

それをbrowserifyし、bundleファイル(ここでは bitcoinjs-browser.js とします)を生成します:

$ browserify bitcoinjs-import.js > bitcoinjs-browser.js
## または:
$ browserify bitcoinjs-import.js | uglifyjs --compress warnings=false --mangle > bitcoinjs-browser.js

生成した bitcoinjs-browser.js をHTML内で<script>を使ってロードすれば、bitcoinjsを使った処理を書くことができます:

  ...
  <script type="text/javascript" src="bitcoinjs-browser.js"></script>
  <script type="text/javascript">
  console.log("bitcoin:", bitcoin); // グローバルにbitcoinjsインスタンス!
  // IMPLEMENT: ここに送金のためのロジックを書きます
  </script>
</body>
</html>

以上です。

このメモがお役に立てましたら、🍺ビールを一杯おごってやってください笑

PR:時事ネタチェックの時間節約!
芽萌丸プログラミング部 @programming
プログラミング関連アカウント。Web標準技術を中心に書いていきます。フロントエンドからサーバサイドまで JavaScript だけで済ませたい人たちの集いです。