@tradle/spender
Version:
Create simple Bitcoin / Testnet transactions for integration testing with the actual network.
203 lines (159 loc) • 4.79 kB
JavaScript
var assert = require('assert')
var dezalgo = require('dezalgo')
var bitcoin = require('@tradle/bitcoinjs-lib')
var toSatoshis = require('./toSatoshis')
var noop = function() {}
// fixed for now
var FEE = 10000 // 100 bits
var MIN_CONFIRMATIONS = 1
function Spender (network) {
if (!(this instanceof Spender)) return new Spender()
this.network = typeof network === 'string' ? bitcoin.networks[network] : network
assert(this.network, 'specify "network"')
this.sends = []
}
module.exports = Spender
Spender.SPLIT_CHANGE = true
Spender.prototype.from = function (key) {
assert(key, 'specify "key"')
this.key = typeof key === 'string' ? bitcoin.ECKey.fromWIF(key) : key
return this
}
Spender.prototype.blockchain = function (chain) {
this.chain = chain
return this
}
Spender.prototype.to = function (toAddress, satoshis) {
assert(toAddress, 'specify "toAddress"')
assert(satoshis, 'specify "satoshis"')
this.sends.push({
to: toAddress,
amount: satoshis
})
return this
}
Spender.prototype.change = function (changeAddress) {
this.changeAddress = typeof changeAddress === 'string' ?
changeAddress :
changeAddress.toString()
return this
}
Spender.prototype.data = function (data) {
this.data = data
return this
}
Spender.prototype.fee = function(satoshis) {
this.feeAmount = satoshis
return this
}
Spender.prototype.build = function(cb) {
var self = this
assert(this.to.length, 'specify "to"')
assert(this.network, 'specify "net"')
assert(this.chain, 'specify "blockchain"')
cb = dezalgo(cb || noop)
if (this._building) {
return cb(new Error('build can only be called once'))
}
this._building = true
var key = this.key
var amount = this.sends.reduce(function(sum, send) {
return sum + send.amount
}, 0)
var fee = this.feeAmount || FEE
var myAddr = key.pub.getAddress(this.network).toString()
var changeAddress = this.changeAddress || myAddr
var sends = this.sends
var data = this.data
var chain = this.chain
this.chain.addresses.unspents(myAddr, function (err, utxos) {
if (err) return cb(err)
utxos = utxos.filter(function (u) {
// otherwise tx will fail
return (u.confirmations || 0) >= MIN_CONFIRMATIONS
})
utxos.forEach(function(u) {
if (typeof u.value === 'string') {
// cb-blockr, i'm looking for you
u.value = toSatoshis(u.value)
}
})
var needed = amount + fee
var collected = 0
utxos = shuffle(utxos)
.filter(function(u) {
if (collected < needed) {
collected += u.value
return true
}
})
if (needed > collected) {
return cb(new Error("Address doesn't contain enough money to send."))
}
var change = collected - needed
var tx = new bitcoin.TransactionBuilder()
sends.forEach(function(send) {
tx.addOutput(send.to, send.amount)
})
if (change > 0) {
// hack for now to split change
if (Spender.SPLIT_CHANGE && changeAddress === myAddr && sends.length === 1 && change > 200000) {
tx.addOutput(changeAddress, change / 2 | 0)
tx.addOutput(changeAddress, change / 2 | 0)
} else {
tx.addOutput(changeAddress, change)
}
}
if (data) {
tx.addOutput(bitcoin.scripts.nullDataOutput(data), 0)
}
utxos.forEach(function (unspent) {
tx.addInput(unspent.txId, unspent.vout)
})
for (var i = 0; i < utxos.length; i++) {
tx.sign(i, key)
}
tx = tx.build()
delete self._building
self.tx = tx
self.usedUnspents = utxos
cb(null, tx, utxos)
})
}
Spender.prototype.execute = function (cb) {
var self = this
cb = dezalgo(cb || noop)
if (this._spending) {
return cb(new Error('spend can only be called once'))
}
this._spending = true
if (this.tx) return this._spend(cb)
if (this._building) throw new Error('still building')
this.build(function(err) {
if (err) return cb(err)
self._spend(cb)
})
}
Spender.prototype._spend = function (cb) {
var self = this
cb = cb || noop
this.chain.transactions.propagate(this.tx.toHex(), function (err) {
cb(err, self.tx, self.usedUnspents)
})
}
// fisher-yates
// http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
function shuffle (array) {
var currentIndex = array.length, temporaryValue, randomIndex
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex)
currentIndex -= 1
// And swap it with the current element.
temporaryValue = array[currentIndex]
array[currentIndex] = array[randomIndex]
array[randomIndex] = temporaryValue
}
return array
}