Home
2-to-2 Bitcoin Transaction with Segregated Witness Addresses
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.