diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 881eac3b..a1523c80 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -41,6 +41,12 @@ function TX(data, block) { // ps = Pending Since this.ps = this.ts === 0 ? +new Date() / 1000 : 0; + + this.m = data.m || 1; + this.n = data.n || 1; + this.change = data.change || null; + this.fee = data.fee || 10000; + this.dust = 5460; } module.exports = TX; @@ -57,6 +63,11 @@ TX.prototype.render = function render() { return bcoin.protocol.framer.tx(this); }; +TX.prototype.input = function input(i, index) { + this._input(i, index); + return this; +}; + TX.prototype._input = function _input(i, index) { if (i instanceof TX) i = { tx: i, index: index }; @@ -88,10 +99,10 @@ TX.prototype._input = function _input(i, index) { var index = this._inputIndex(hash, index); if (index !== -1) { var ex = this.inputs[index]; - - ex.out.tx = input.out.tx || ex.out.tx; - ex.seq = input.seq || ex.seq; - ex.script = input.script.length ? input.script : ex.script; + input.out.tx = input.out.tx || ex.out.tx; + input.seq = input.seq || ex.seq; + input.script = input.script.length ? input.script : ex.script; + this.inputs[index] = input; } else { this.inputs.push(input); index = this.inputs.length - 1; @@ -224,14 +235,10 @@ TX.prototype.scriptSig = function(input, key, pub, type, nsigs) { return input.script; }; -TX.prototype.input = function input(i, index) { - this._input(i, index); - return this; -}; - -TX.prototype.out = function out(output, value) { +TX.prototype.output = function output(output, value) { if (output instanceof bcoin.wallet) output = output.getAddress(); + if (typeof output === 'string') { output = { address: output, @@ -239,16 +246,26 @@ TX.prototype.out = function out(output, value) { }; } - var script = output.script ? output.script.slice() : []; + this.outputs.push({ + value: new bn(output.value), + script: this.scriptOutput(output) + }); +}; - if (Array.isArray(output.keys || output.address)) { +// compat +TX.prototype.out = TX.prototype.output; + +TX.prototype.scriptOutput = function(options) { + var script = []; + + if (Array.isArray(options.keys || options.address)) { // Raw multisig transaction // https://github.com/bitcoin/bips/blob/master/bip-0010.mediawiki // https://github.com/bitcoin/bips/blob/master/bip-0011.mediawiki // https://github.com/bitcoin/bips/blob/master/bip-0019.mediawiki // [required-sigs] [pubkey-hash1] [pubkey-hash2] ... [number-of-keys] checkmultisig - var keys = output.keys || output.address; - if (keys === output.address) { + var keys = options.keys || options.address; + if (keys === options.address) { keys = keys.map(function(address) { return bcoin.wallet.addr2hash(address, 'normal'); }); @@ -260,7 +277,7 @@ TX.prototype.out = function out(output, value) { return key; }); script = [ - [ output.minSignatures || keys.length ] + [ options.minSignatures || keys.length ] ].concat( keys, [ [ keys.length ], 'checkmultisig' ] @@ -268,33 +285,28 @@ TX.prototype.out = function out(output, value) { // outputs: [ [ 2 ], 'key1', 'key2', [ 2 ], 'checkmultisig' ] // in reality: // outputs: [ [ 2 ], [0,1,...], [2,3,...], [ 2 ], 'checkmultisig' ] - } else if (bcoin.wallet.validateAddress(output.address, 'p2sh')) { + } else if (bcoin.wallet.validateAddress(options.address, 'p2sh')) { // p2sh transaction // https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki // hash160 [20-byte-redeemscript-hash] equal script = [ 'hash160', - bcoin.wallet.addr2hash(output.address, 'p2sh'), + bcoin.wallet.addr2hash(options.address, 'p2sh'), 'eq' ]; - } else if (output.address) { + } else if (options.address) { // p2pkh transaction // dup hash160 [pubkey-hash] equalverify checksig script = [ 'dup', 'hash160', - bcoin.wallet.addr2hash(output.address, 'normal'), + bcoin.wallet.addr2hash(options.address, 'normal'), 'eqverify', 'checksig' ]; } - this.outputs.push({ - value: new bn(output.value), - script: script - }); - - return this; + return script; }; TX.prototype.getSubscript = function getSubscript(index) { @@ -347,6 +359,7 @@ TX.prototype.verify = function verify(index, force) { if (!res) return false; + // XXX sighash_all here? return stack.length > 0 && utils.isEqual(stack.pop(), [ 1 ]); }, this); }; @@ -355,10 +368,7 @@ TX.prototype.isCoinbase = function isCoinbase() { return this.inputs.length === 1 && +this.inputs[0].out.hash === 0; }; -TX.prototype.maxSize = function maxSize(m, n) { - m = m || 1; - n = n || 1; - +TX.prototype.maxSize = function maxSize() { // Create copy with 0-script inputs var copy = this.clone(); copy.inputs.forEach(function(input) { @@ -369,7 +379,11 @@ TX.prototype.maxSize = function maxSize(m, n) { // Add size for signatures and public keys copy.inputs.forEach(function(input) { - var s = input.out.tx.outputs[input.out.index].script; + // Get the previous output's script + // var s = input.out.tx.outputs[input.out.index].script; + + // Get the previous output's subscript + var s = input.out.tx.getSubscript(input.out.index); if (bcoin.script.isPubkeyhash(s) || bcoin.script.isSimplePubkeyhash(s)) { // Signature + len @@ -384,18 +398,25 @@ TX.prototype.maxSize = function maxSize(m, n) { // Empty byte size += 1; // Signature + len + var m = s[0][0] || this.m; + assert(m >= 1 && m <= 7); size += 74 * m; return; } - // 1 empty byte - // 1 mcode byte - // loop: - // 1 byte key length - // 65? byte key - // 1 ncode byte - // 1 checkmultisig byte if (bcoin.script.isScripthash(s)) { + var script = this.inputs[i].script; + var redeem, m, n; + if (script) { + redeem = script[script.length - 1]; + m = redeem[0] - constants.opcodes['1'] + 1; + n = redeem[redeem.length - 2] - constants.opcodes['1'] + 1; + } else { + m = this.m; + n = this.n; + } + assert(m >= 1 && m <= n); + assert(n >= 1 && n <= 7); // Multisig // Empty byte size += 1; @@ -412,11 +433,113 @@ TX.prototype.maxSize = function maxSize(m, n) { size += 1; return; } - }); + }, this); return size; }; +// Building a TX: +// 1. Add outputs: +// - this.output({ address: ..., value: ... }); +// - this.output({ address: ..., value: ... }); +// 2. Add inputs with utxos and change output: +// - this.fillUnspent(unspentItems, [changeAddr]); +// 3. Fill input scripts (for each input): +// - this.scriptInput(input, pub, [nsigs]) +// - this.signInput(input, key, [sigHashType]) +TX.prototype.utxos = function utxos(unspent) { + // NOTE: tx should be prefilled with all outputs + var cost = this.funds('out'); + + // Use initial fee for starters + var fee = 1; + + // total = cost + fee + var total = cost.add(new bn(this.fee)); + + var inputs = this.inputs.slice(); + var utxos = []; + + var lastAdded = 0; + function addInput(unspent, i) { + // Add new inputs until TX will have enough funds to cover both + // minimum post cost and fee + var index = this._input(unspent); + utxos.push(this.inputs[index]); + lastAdded++; + return this.funds('in').cmp(total) < 0; + } + + // Transfer `total` funds maximum + // var unspent = wallet.unspent(); + unspent.every(addInput, this); + + // Add dummy output (for `left`) to calculate maximum TX size + this.output({ address: null, value: new bn(0) }); + + // Change fee value if it is more than 1024 bytes + // (10000 satoshi for every 1024 bytes) + do { + // Calculate maximum possible size after signing + var byteSize = this.maxSize(); + + var addFee = Math.ceil(byteSize / 1024) - fee; + total.iadd(new bn(addFee * this.fee)); + fee += addFee; + + // Failed to get enough funds, add more inputs + if (this.funds('in').cmp(total) < 0) + unspent.slice(lastAdded).every(addInput, this); + } while (this.funds('in').cmp(total) < 0 && lastAdded < unspent.length); + + // Still failing to get enough funds + if (this.funds('in').cmp(total) < 0) { + this.inputs = inputs; + this.outputs.pop(); + this.cost = total; + return null; + } + + // How much money is left after sending outputs + var left = this.funds('in').sub(total); + + // Clear the tx of everything we added. + this.inputs = inputs; + this.outputs.pop(); + this.cost = total; + + // Return necessary utxos and change. + return { + utxos: utxos, + change: left, + cost: total + }; +}; + +TX.prototype.fillUnspent = function fillUnspent(unspent, change) { + var result = this.utxos(unspent); + + if (!result) + return result; + + result.utxos.forEach(function(utxo) { + this.input(utxo, null); + }, this); + + // Not enough money, transfer everything to owner + if (result.change.cmpn(this.dust) < 0) { + // NOTE: that this output is either `postCost` or one of the `dust` values + this.outputs[this.outputs.length - 1].value.iadd(result.change); + } else { + this.output({ + address: change || this.change, + value: result.change + }); + } + + return result; +}; + TX.prototype.inputAddrs = function inputAddrs() { return this.inputs.filter(function(input) { return bcoin.script.isPubkeyhashInput(input.script); diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index db20d8d9..2e03b69d 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -450,6 +450,14 @@ Wallet.prototype.fill = function fill(tx, cb) { return tx; }; +Wallet.prototype.fill = function fill(tx, cb) { + cb = utils.asyncify(cb); + tx.fillUnspent(this.unspent(), this.getAddress()); + this.sign(tx); + cb(null, tx); + return tx; +}; + // P2SH Multisig redeem script Wallet.prototype.getRedemption = function() { var sharedKeys = this.sharedKeys.slice().map(utils.toKeyArray);