ブラウザでBTC送金トランザクション(segwit対応)の画像
芽萌丸プログラミング部 @programming
投稿日 2019/07/02
更新日 2021/09/16 ✏

ビットコイン

ブラウザでBTC送金トランザクション (segwit対応)

2021/09/16 更新: segwit対応。

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

今回ご紹介する方法は、いわゆるブラウザウォレットアプリケーションの作り方の基本になります。 通常ブラウザウォレットでは、秘密鍵での署名をブラウザのローカル上だけで行います。 そのため、秘密鍵がインターネット上に流れて盗聴されるリスクを低減させることができます。 これはウォレットアプリとしてのセキュリティ上とても重要なことです。

ただし、秘密鍵の管理を自己責任で行う必要があるため、取引所のWebウォレットのように他社にお任せできる利便性とはトレードオフになります。

目次

前提

TIP: bitcoinjs-lib は browserify できる上にbundleサイズも比較的軽量なのでブラウザアプリに便利です。

シナリオ

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

  • alice (送信者):

    既に十分なBTC残高(UTXO)を持っている前提です。また残高がない場合はfaucetでテスト用BTCを入試しましょう。

  • bob (受信者):

    • address: tb1qd7spv5q28348xl4myc8zmh983w5jx32cjhkn97
    • witness: 00146fa016500a3c6a737ebb260e2ddca78ba9234558
    • privateKey_wif: cQFUndrpAyMaE3HAsjMCXiT94MzfsABCREat1x7Qe3Mtq9KihD4V (今回は使いませんが一応メモ)

NOTE: テスト用アカウントの生成についてはこちらをご参考。

BTC送金トランザクションを作成

getSendTx.js

/*
トランザクションを作成

$ node getSendTx.js \
 --address=tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl \
 --wif=cTGhosGriPpuGA586jemcuH9pE9spwUmneMBmYYzrQEbY92DJrbo \
 --recipient=tb1qd7spv5q28348xl4myc8zmh983w5jx32cjhkn97 \
 --sendingSat=10000 \
 --feeSat=2000 \
 --testnet
 */
const bitcoin = require('bitcoinjs-lib');
const Wif = require('wif');
const request = require("superagent");

let {
  address, // 送金元アドレス
  wif, // privateKey(WIF)
  recipient, // 受取先アドレス
  sendingSat, // 送金額(satoshi)
  feeSat, // 手数料(satoshi)
  testnet, // testnet を使うかどうか (無指定ならmainnetを使用)
} = require('minimist')(process.argv.slice(2));
const network = (testnet) ? bitcoin.networks.testnet : bitcoin.networks.bitcoin; // bitcoin or testnet

if (!address) {
  console.log("ERROR: --address オプションで送金元のアドレスを指定して下さい。");
  process.exit(0);
  return;
}
if (!recipient) {
  console.log("ERROR: --recipient オプションで送金先のアドレスを指定して下さい。");
  process.exit(0);
  return;
}
if (!wif) {
  console.log("ERROR: --wif オプションで PrivateKey(WIF) を指定して下さい。");
  process.exit(0);
  return;
}
if (!sendingSat) {
  console.log("ERROR: --sendingSat オプションで 送金額(satoshis) を指定して下さい。");
  process.exit(0);
  return;
}
if (!feeSat) {
  console.log("ERROR: --feeSat オプションで 手数料(satoshis) を指定して下さい。");
  process.exit(0);
  return;
}

// wifで生成されたアドレスと指定された送信元アドレスが同じであることを確認
checkAddressByWif(address, wif, network);

// UTXOを取得
getUtxos({ address, testnet }, (err, utxos) => {
  if (err) throw err;
  if (!utxos || utxos.length <= 0) {
    console.log(`ERROR: 指定されたアドレス ${address} には現在利用可能なUTXOがありませんでした。`);
    process.exit(0);
    return;
  }
  // トランザクションHEXを作成
  const txHex = buildAndSignTx({
    address,
    network,
    recipient,
    sendingSat,
    feeSat,
    utxos,
  });
  console.log("txHex:\n", txHex);
  console.log("\n成功: 上記のトランザクションHEXをブロックチェーンへブロードキャストしてください。");
  console.log("例: https://live.blockcypher.com/btc/pushtx/ (mainnet)");
  console.log("例: https://live.blockcypher.com/btc-testnet/pushtx/ (testnet)");
});

/**
 * トランザクションを作成&署名されたトランザクションHexを返します。
 */
function buildAndSignTx({
  address,
  network,
  recipient,
  sendingSat,
  feeSat,
  utxos,
}) {
  // input元(utxo)のtxを追加:
  // TIP: もし手元に使える node が無い場合は、 https://live.blockcypher.com/ などからutxoを取得。
  const psbt = new bitcoin.Psbt({ network });

  let total = 0;
  for (let len = utxos.length, i = 0; i < len; i++) {
    const { hash, index, value, script } = utxos[i];
    psbt.addInput({
      hash,
      index,
      // sequence: 0xffffffff,
      // IMPORTANT: needs for a tx with witness!
      witnessUtxo: {
        script: Buffer.from(script, 'hex'),
        value,
      },
    });
    total += value;
  }

  // output先(送金先)を追加:
  psbt.addOutput({
    address: recipient,
    value: sendingSat,
  });
  // NOTE: お釣りは送金元に返す:
  const changeSat = total - sendingSat - feeSat;
  if(changeSat<0){
    console.log(`ERROR: 残高が不足しています。残高(UTXOトータル): ${total} satoshi`);
    return process.exit(0);
  }
  psbt.addOutput({
    address: address,
    value: changeSat,
  });

  console.log("\nトランザクション詳細:");
  console.log("送金元:", address);
  console.log("着金先:", recipient);
  console.log("現在の送金元UTXOトータル残高(satoshi):", total);
  console.log("送金額(satoshi):", sendingSat);
  console.log("手数料(satoshi):", feeSat);
  console.log("お釣り(satoshi):", changeSat);
  console.log("");

  // 署名
  const obj = Wif.decode(wif);
  const privKey = bitcoin.ECPair.fromPrivateKey(obj.privateKey);
  for (let len = utxos.length, i = 0; i < len; i++) {
    psbt.signInput(i, privKey);
    psbt.validateSignaturesOfInput(i);
  }
  psbt.finalizeAllInputs();
  // tx hex取得
  const txHex = psbt.extractTransaction().toHex();
  return txHex;
}

/**
 * WIFと指定したアドレスの整合性をチェック
 */
function checkAddressByWif(address, wif, network) {
  const keyPair = bitcoin.ECPair.fromWIF(wif, network);
  const obj = bitcoin.payments.p2wpkh({ pubkey: keyPair.publicKey, network });
  if (obj.address !== address) {
    console.error("指定されたアドレスとWIFに整合性がありません。");
    process.exit(0);
    return;
  }
}

/**
 * UTXOを取得
 * @return {Array<{hash, index, value, script}>}
 */
function getUtxos({ address, testnet = false }, cb) {
  const endpoint = `https://api.blockcypher.com/v1/btc/${testnet ? "test3" : "main"}/addrs/${address}?unspentOnly=true&includeScript=true`;
  console.log("Fetching UTXOs from " + endpoint);
  request
    .get(endpoint)
    .end((err, res) => {
      if (err) return cb && cb(err);
      const txrefs = res.body.txrefs;
      if (!txrefs) return cb && cb(null);
      const ret = [];
      for (let len = txrefs.length, i = 0; i < len; i++) {
        const item = txrefs[i];
        const hash = item.tx_hash;
        const index = item.tx_output_n;
        const value = item.value;
        const script = item.script;
        ret.push({
          hash,
          index,
          value,
          script,
        });
      }
      return cb && cb(null, ret);
    });
}

コードレビュー:

UTXO取得処理getUtxos()でBlockCypherさんが提供してくれている https://api.blockcypher.com/v1/btc/test3/addr/{address}/?unspentOnly=true&includeScript=true なエンドポイントを叩いていますが、insight API や bitcoinノードなど他にもUTXO取得に使えるデータソースがあればそちらを利用するコードにカスタムしても良いでしょう。

実行

$ node getSendTx.js \
 --address=tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl \
 --wif=cTGhosGriPpuGA586jemcuH9pE9spwUmneMBmYYzrQEbY92DJrbo \
 --recipient=tb1qd7spv5q28348xl4myc8zmh983w5jx32cjhkn97 \
 --sendingSat=10000 \
 --feeSat=2000 \
 --testnet

Fetching UTXOs from https://api.blockcypher.com/v1/btc/test3/addrs/tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl?unspentOnly=true&includeScript=true

トランザクション詳細:
送金元: tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl
着金先: tb1qd7spv5q28348xl4myc8zmh983w5jx32cjhkn97
現在の送金元UTXOトータル残高(satoshi): 20000
送金額(satoshi): 10000
手数料(satoshi): 2000
お釣り(satoshi): 8000

txHex:
 0200000000010200437da7015df25d0fb9922a9a62b5f47d980030b4f8faf8879f2e57043e37fd0000000000ffffffff80ba78f736ece2a6f89fcc5e528a6724fcc9640b74d70c4c403505ef5215d6780000000000ffffffff0210270000000000001600146fa016500a3c6a737ebb260e2ddca78ba9234558401f000000000000160014d0c4a3ef09e997b6e99e397e518fe3e41a118ca102473044022028b68fe74ca1d5456672f0c701aad7598e58b6ab3d24add8d9a2dabcfb6199e002205a088d781b64a188f0cbc05810ef013d265ac7f64499cb288a5c87f105c28ea5012102e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c319024830450221008d71a06f3b5ea813828bb236f06e78183f0fc20a5c28b209f6201324c428993102204b8023636ea2bf01681a58d77dd4cef5e335b3bd71f477906ab11c3c7d6a0254012102e7ab2537b5d49e970309aae06e9e49f36ce1c9febbd44ec8e0d1cca0b4f9c31900000000

成功: 上記のトランザクションHEXをブロックチェーンへブロードキャストしてください。
例: https://live.blockcypher.com/btc/pushtx/ (mainnet)
例: https://live.blockcypher.com/btc-testnet/pushtx/ (testnet)

作成されたトランザクションHEX(txHexの値)をブロックチェーンへブロードキャストしましょう。 ブロードキャストにも方法はいくつかありますが、https://live.blockcypher.com/btc-testnet/pushtx/ (testnet) を使うとブラウザからブロードキャストできて便利です。

おまけ

ブラウザで使うために

ここまで書いたコード (ここでは 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>

以上です。

このメモがお役に立てましたら、下の投げ銭ボタンから🍺ビールを投げ銭していただければ幸いです(笑)


芽萌丸プログラミング部
芽萌丸プログラミング部 @programming
プログラミング関連アカウント。Web標準技術を中心に書いていきます。フロントエンドからサーバサイドまで JavaScript だけで済ませたい人たちの集いです。記事は主に @TanakaSoftwareLab が担当。