2-to-2 Bitcoin Transaction with Segregated Witness Addresses

18 Feb 2018

Recently I wrote some code to transfer 20 USDT worth of Bitcoin from a combination of two segwit (P2WSH) addresses, with the USDT equivalent transfered to a P2PKH address and the change transferred to a P2WSH address. The following node.js code demonstrates how to do this on testnet using BitcoinJS:

/*
 * Transfers 20 USDT and leftover BTC from two UTXOs
 * to two outputs.
 *
 * Dependencies: bitcoinjs, request.
 */

const bitcoin = require('bitcoinjs-lib')
const request = require('request')
const readline = require('readline')
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})

var NETWORK = bitcoin.networks.testnet
var TXN_API_PREFIX = 'https://api.blockcypher.com/v1/btc/test3/'
var USDT_API_PREFIX = 'https://api.coinmarketcap.com/v1/ticker/tether/'

var PRIVKEY1 = 'cTBY6hMdoQHYP5bQaZ1d7cXpvNBKN271STNJD58Q3TYnmWVo2QTA'
var PRIVKEY2 = 'cT9UDdj3UK9i5WzE6s1HH4ATCUX4rCfMGbzpN93QTU5H4erb67Hs'

// Account to send the $20 of Tether USDT to.
var USDT_OUTPUT_ADDR = 'mpZhRhB9ePp8K5KMQoATrf6VY9yLMJs5CS'
// Account to send the leftover BTC (less txn fee) to.
var LEFTOVER_BTC_OUTPUT_ADDR = '2MuZCWcoBejf46bHSAUep6YUgsVqKSuWHXv'

var deriveAccount = function(privkey) {
  var keypair = bitcoin.ECPair.fromWIF(privkey, NETWORK)
  var pubKey = keypair.getPublicKeyBuffer()
  var pubKeyHash = bitcoin.crypto.hash160(pubKey)
  var redeemScript = bitcoin.script.witnessPubKeyHash.output.encode(pubKeyHash)
  var redeemScriptHash = bitcoin.crypto.hash160(redeemScript)
  var scriptPubKey = bitcoin.script.scriptHash.output.encode(redeemScriptHash)
  var address = bitcoin.address.fromOutputScript(scriptPubKey, NETWORK)
  return {'keypair':keypair, 'redeemScript':redeemScript,
          'scriptPubKey':scriptPubKey, 'address':address}
}

// Setup accounts.
acct1 = deriveAccount(PRIVKEY1)
console.log('Account 1 address is ' + acct1.address)
acct2 = deriveAccount(PRIVKEY2)
console.log('Account 2 address is ' + acct2.address)

var txb = new bitcoin.TransactionBuilder(bitcoin.networks.testnet)
var total_input_satoshis = 0
var utxo_seqnum = 0
var inputSigningFuncs = []

var add_utxos_for_address = (acct, continueFn) => {
  request(TXN_API_PREFIX + 'addrs/' + acct.address, (error, response, body) => {
    if (error || response.statusCode != 200) {
      console.log('[ERROR] Unable to query external API for '
               + 'UTXOs for address ' + acct.address)
      process.exit(1)
    }
    console.log('Parsing UTXOs for address ' + acct.address)
    var info = JSON.parse(body)
    if (info.txrefs) {
      for (var txn of info.txrefs) {
        if (txn.spent == false) {
          console.log('Adding UTXO ' + utxo_seqnum + ' for address ' + acct.address
                  + ' with value ' + txn.value)
          txb.addInput(txn.tx_hash, txn.tx_output_n, 0xffffffff, acct.scriptPubKey)
          total_input_satoshis += txn.value
          var utxo_id = utxo_seqnum
          var value = txn.value
          var inputSigningFunc = function() {
            console.log('Signing UTXO ' + utxo_id + ' with value ' + value
                    + ' for address ' + acct.address)
            txb.sign(utxo_id, acct.keypair, acct.redeemScript, null, value)
            console.log('Signed UTXO ' + utxo_id + ' with value ' + value
                    + ' for address ' + acct.address)
          }
          inputSigningFuncs.push(inputSigningFunc)
          utxo_seqnum += 1
        }
      }
    }
    if (inputSigningFuncs.length == 0) {
      console.log("[ERROR] No UTXOs found for address " + acct.address
              + "; check balance and latest transaction"
              + " confirmations before trying again.")
      process.exit(1)
    }
    continueFn()
  })
}

var usdt_to_btc = (usdt_amt, continueFn) => {
  request(USDT_API_PREFIX, (error, response, body) => {
    if (error || response.statusCode != 200) {
      console.log('[ERROR] Unable to query external API for '
               + 'USDT exchange rate.' + acct.address)
      process.exit(1)
    }
    var info = JSON.parse(body)[0]
    var satoshi_equiv = info.price_btc * 100000000 * usdt_amt
    console.log(usdt_amt + ' USDT == ' + satoshi_equiv + ' satoshis.')
    continueFn(satoshi_equiv)
  })
}

add_utxos_for_address(acct1, () => {
  add_utxos_for_address(acct2, () => {
    usdt_to_btc(20, (twenty_usdt_in_satoshis) => {

      txn_fee = 35000
      change = total_input_satoshis - twenty_usdt_in_satoshis - txn_fee
      if (change < 0) {
        console.log('[ERROR] Txn inputs are insufficient to transfer 20 USDT '
                + 'worth of BTC to the first account + txn fee. Aborting.')
        process.exit(1)
      }

      txb.addOutput(USDT_OUTPUT_ADDR, twenty_usdt_in_satoshis)
      console.log('Added output of ' + twenty_usdt_in_satoshis + ' for '
                        + USDT_OUTPUT_ADDR)
      txb.addOutput(LEFTOVER_BTC_OUTPUT_ADDR, change)
      console.log('Added output of ' + change + ' for '
                        + LEFTOVER_BTC_OUTPUT_ADDR)

      for (var inputSigningFunc of inputSigningFuncs) {
        inputSigningFunc()
      }

      var tx = txb.build()

      // Display recommended fee based on weight of signed txn.
      var weight = tx.weight()
      var satoshiPerByte = 100
      var satoshiPerWeight = satoshiPerByte / 4
      var recommendedFee = parseInt(weight * satoshiPerWeight)
      console.log('Recommended fee is ' + recommendedFee + ', you used ' +
                  txn_fee + '; consider adjusting if difference is large.')

      console.log('\n=== TRANSACTION TO SUBMIT ================================')
      var tx_hex = tx.toHex();
      console.log(tx_hex)

      rl.question('\nSubmit this transaction to the Bitcoin network now [y/n]? ',
              (answer) => {
        if (answer == 'y' || answer == 'Y' || answer == 'yes') {
          console.log('Sending transaction...')
          request({
      uri: TXN_API_PREFIX + 'txs/push',
      method: "POST",
            headers: {
              'Content-Type': 'application/json'
            },
      body: JSON.stringify({tx: tx_hex})
    }, function(error, response, body) {
            if (error || response.statusCode != 201) {
              console.log('[ERROR] External API failed to broadcast txn.')
              console.log('\tError: ' + error)
              console.log('\tResponse: ' + response && response.statusCode)
              process.exit(1)
            }
            console.log('[SUCCESS] Txn broadcasted successfully; external API '
                    + 'replied with: ')
            console.log(body)
    })
        } else {
          console.log('Exiting without sending transaction.')
        }
        rl.close()
      })
    })
  })
})

One thing to note is that Tether is an Omni Layer currency, and thus their protocol can (should) be used to transfer Tether between addresses. But third-party library support for Omni seems incredibly lacking, and my interest lies with the underlying Bitcoin network rather than Omni itself. Therefore this code simply retrieves the latest USDT-BTC exchange rate to determine how many satoshis to transfer. Needless to say, production applications should use some sort of library that delegates this to the Omni protocol itself.