diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 0e2a42e0..87207c13 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -42,12 +42,13 @@ function TX(data, block) { // ps = Pending Since this.ps = this.ts === 0 ? +new Date() / 1000 : 0; - this.change = data.change || null; - this.fee = data.fee || 10000; - this.dust = 5460; + this.changeAddress = data.changeAddress || null; } module.exports = TX; +TX.fee = 10000; +TX.dust = 5460; + TX.prototype.clone = function clone() { return new TX(this); }; @@ -155,6 +156,7 @@ TX.prototype.scriptInput = function(input, pub) { // P2PKH and simple tx if (bcoin.script.isPubkeyhash(s) || bcoin.script.isSimplePubkeyhash(s)) { input.script = [ [], pub ]; + this._recalculateFee(); return; } @@ -171,6 +173,7 @@ TX.prototype.scriptInput = function(input, pub) { n = n[0] || 0; for (var i = 0; i < n; i++) input.script[i + 1] = []; + this._recalculateFee(); return; } @@ -187,6 +190,7 @@ TX.prototype.scriptInput = function(input, pub) { input.script[i + 1] = []; // P2SH requires the redeem script after signatures input.script.push(pub); + this._recalculateFee(); return; } @@ -568,7 +572,7 @@ TX.prototype.utxos = function utxos(unspent) { var fee = 1; // total = cost + fee - var total = cost.add(new bn(this.fee)); + var total = cost.add(new bn(TX.fee)); var inputs = this.inputs.slice(); var utxos = []; @@ -587,7 +591,7 @@ TX.prototype.utxos = function utxos(unspent) { // var unspent = wallet.unspent(); unspent.every(addInput, this); - // Add dummy output (for `left`) to calculate maximum TX size + // Add dummy output (for `change`) to calculate maximum TX size this.output({ address: null, value: new bn(0) }); // Change fee value if it is more than 1024 bytes @@ -597,7 +601,7 @@ TX.prototype.utxos = function utxos(unspent) { var byteSize = this.maxSize(); var addFee = Math.ceil(byteSize / 1024) - fee; - total.iadd(new bn(addFee * this.fee)); + total.iadd(new bn(addFee * TX.fee)); fee += addFee; // Failed to get enough funds, add more inputs @@ -609,28 +613,31 @@ TX.prototype.utxos = function utxos(unspent) { if (this.funds('in').cmp(total) < 0) { this.inputs = inputs; this.outputs.pop(); - this.cost = total; + this.total = total; return null; } // How much money is left after sending outputs - var left = this.funds('in').sub(total); + var change = 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 + change: change, + cost: cost, + fee: total.sub(cost), + total: total }; }; -TX.prototype.fillUnspent = function fillUnspent(unspent, change) { - var result = this.utxos(unspent); +TX.prototype.fillUnspent = function fillUnspent(unspent, changeAddress) { + var result = unspent.utxos ? unspent : this.utxos(unspent); + + this.changeAddress = changeAddress || this.changeAddress; if (!result) return result; @@ -639,20 +646,63 @@ TX.prototype.fillUnspent = function fillUnspent(unspent, change) { 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); + if (result.change.cmpn(TX.dust) < 0) { + // Do nothing. Change is added to fee. + assert(this.getFee().cmp(result.fee.add(result.change)) === 0); + // Adding change to outputs. + // this.outputs[this.outputs.length - 1].value.iadd(result.change); + this.changeOutput = null; } else { this.output({ - address: change || this.change, + address: this.changeAddress, value: result.change }); + this.changeOutput = this.outputs[this.outputs.length - 1]; } return result; }; +TX.prototype._recalculateFee = function recalculateFee() { + var output = this.changeOutput; + if (!output) { + this.output({ + address: this.changeAddress, + value: new bn(0) + }); + output = this.outputs[this.outputs.length - 1]; + } + + var byteSize = this.maxSize(); + var newFee = Math.ceil(byteSize / 1024) * TX.fee; + var currentFee = this.getFee().toNumber(); + + if (newFee === currentFee) { + if (!this.changeOutput) + this.outputs.pop(); + return; + } + + if (newFee > currentFee) { + if (output.value.cmpn(newFee - currentFee) < 0) { + this.outputs.pop(); + this.changeOutput = null; + return; + } + output.value.isubn(newFee - currentFee); + } else { + output.value.iaddn(currentFee - newFee); + } + + if (output.value.cmpn(TX.dust) < 0) { + this.outputs.pop(); + this.changeOutput = null; + return; + } + + this.changeOutput = output; +}; + TX.prototype.inputAddrs = function inputAddrs() { return this.inputs.filter(function(input) { return bcoin.script.isPubkeyhashInput(input.script); @@ -663,6 +713,10 @@ TX.prototype.inputAddrs = function inputAddrs() { }); }; +TX.prototype.getFee = function getFee() { + return this.funds('in').sub(this.funds('out')); +}; + TX.prototype.funds = function funds(side) { if (side === 'in') { var inputs = this.inputs.filter(function(input) { diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 77576724..92fe137f 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -406,7 +406,7 @@ Wallet.prototype.fill = function fill(tx, cb) { var result = tx.fillUnspent(this.unspent(), this.getAddress()); if (!result) { var err = new Error('Not enough funds'); - err.minBalance = tx.cost; + err.minBalance = tx.total; cb(err); return null; } diff --git a/test/wallet-test.js b/test/wallet-test.js index 9b95b978..44fc889e 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -177,7 +177,10 @@ describe('Wallet', function() { assert(t2.verify()); assert.equal(t2.funds('in').toString(10), 16380); - assert.equal(t2.funds('out').toString(10), 6380); + // If change < dust and is added to outputs: + // assert.equal(t2.funds('out').toString(10), 6380); + // If change < dust and is added to fee: + assert.equal(t2.funds('out').toString(10), 5460); // Create new transaction var t3 = bcoin.tx().out(w2, 15000);