From 66aedc2d8b5a0722ea574abc65b5769413e399c9 Mon Sep 17 00:00:00 2001 From: sanchaz Date: Tue, 14 Aug 2018 03:56:04 +0100 Subject: [PATCH] Add feePerByte for fee calculation options Bitcoin cash allows for 1 sat/byte fees. The current feePerKb forces the user that is paying, to pay too much most of the time. This keeps the default as feePerKb since devs might assume this is the default behaviour as is currently the case. This assumption can be observed in tests as well. --- lib/transaction/transaction.js | 36 +++++++++++++++++- test/transaction/transaction.js | 65 ++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index cc15fcb7..1ab3c108 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -80,6 +80,9 @@ Transaction.NLOCKTIME_MAX_VALUE = 4294967295; // Value used for fee estimation (satoshis per kilobyte) Transaction.FEE_PER_KB = 100000; +// Value used for fee estimation (satoshis per byte) +Transaction.FEE_PER_BYTE = 1; + // Safe upper bound for change address script size in bytes Transaction.CHANGE_OUTPUT_MAX_SIZE = 20 + 4 + 34 + 4; Transaction.MAXIMUM_EXTRA_SIZE = 4 + 9 + 9 + 4; @@ -662,6 +665,7 @@ Transaction.prototype.fee = function(amount) { * Manually set the fee per KB for this transaction. Beware that this resets all the signatures * for inputs (in further versions, SIGHASH_SINGLE or SIGHASH_NONE signatures will not * be reset). + * Takes priority over fee per Byte, for backwards compatibility * * @param {number} amount satoshis per KB to be sent * @return {Transaction} this, for chaining @@ -673,6 +677,22 @@ Transaction.prototype.feePerKb = function(amount) { return this; }; +/** + * Manually set the fee per Byte for this transaction. Beware that this resets all the signatures + * for inputs (in further versions, SIGHASH_SINGLE or SIGHASH_NONE signatures will not + * be reset). + * fee per Byte will be ignored if fee per KB is set + * + * @param {number} amount satoshis per Byte to be sent + * @return {Transaction} this, for chaining + */ +Transaction.prototype.feePerByte = function(amount) { + $.checkArgument(_.isNumber(amount), 'amount must be a number'); + this._feePerByte = amount; + this._updateChangeOutput(); + return this; +}; + /* Output management */ /** @@ -887,7 +907,11 @@ Transaction.prototype.getFee = function() { Transaction.prototype._estimateFee = function() { var estimatedSize = this._estimateSize(); var available = this._getUnspentValue(); - return Transaction._estimateFee(estimatedSize, available, this._feePerKb); + if (this._feePerByte && !this._feePerKb) { + return Transaction._estimateFeePerByte(estimatedSize, available, this._feePerByte); + } else { + return Transaction._estimateFeePerKb(estimatedSize, available, this._feePerKb); + } }; Transaction.prototype._getUnspentValue = function() { @@ -900,7 +924,7 @@ Transaction.prototype._clearSignatures = function() { }); }; -Transaction._estimateFee = function(size, amountAvailable, feePerKb) { +Transaction._estimateFeePerKb = function(size, amountAvailable, feePerKb) { var fee = Math.ceil(size / 1000) * (feePerKb || Transaction.FEE_PER_KB); if (amountAvailable > fee) { size += Transaction.CHANGE_OUTPUT_MAX_SIZE; @@ -908,6 +932,14 @@ Transaction._estimateFee = function(size, amountAvailable, feePerKb) { return Math.ceil(size / 1000) * (feePerKb || Transaction.FEE_PER_KB); }; +Transaction._estimateFeePerByte = function(size, amountAvailable, feePerByte) { + var fee = size * (feePerByte || Transaction.FEE_PER_BYTE); + if (amountAvailable > fee) { + size += Transaction.CHANGE_OUTPUT_MAX_SIZE; + } + return size * (feePerByte || Transaction.FEE_PER_BYTE); +}; + Transaction.prototype._estimateSize = function() { var result = Transaction.MAXIMUM_EXTRA_SIZE; _.each(this.inputs, function(input) { diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index 1601199d..36e16f5e 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -29,7 +29,7 @@ describe('Transaction', function() { }); it('should parse the version as a signed integer', function() { - var transaction = Transaction('ffffffff0000ffffffff') + var transaction = Transaction('ffffffff0000ffffffff'); transaction.version.should.equal(-1); transaction.nLockTime.should.equal(0xffffffff); }); @@ -390,6 +390,69 @@ describe('Transaction', function() { transaction.outputs.length.should.equal(2); transaction.outputs[1].satoshis.should.equal(34000); }); + it('fee per byte (low fee) can be set up manually', function() { + var inputs = _.map(_.range(10), function(i) { + var utxo = _.clone(simpleUtxoWith100000Satoshis); + utxo.outputIndex = i; + return utxo; + }); + var transaction = new Transaction() + .from(inputs) + .to(toAddress, 950000) + .feePerByte(1) + .change(changeAddress) + .sign(privateKey); + transaction._estimateSize().should.be.within(1000, 1999); + transaction.outputs.length.should.equal(2); + transaction.outputs[1].satoshis.should.be.within(48001, 49000); + }); + it('fee per byte (high fee) can be set up manually', function() { + var inputs = _.map(_.range(10), function(i) { + var utxo = _.clone(simpleUtxoWith100000Satoshis); + utxo.outputIndex = i; + return utxo; + }); + var transaction = new Transaction() + .from(inputs) + .to(toAddress, 950000) + .feePerByte(2) + .change(changeAddress) + .sign(privateKey); + transaction._estimateSize().should.be.within(1000, 1999); + transaction.outputs.length.should.equal(2); + transaction.outputs[1].satoshis.should.be.within(46002, 48000); + }); + it('fee per byte can be set up manually', function() { + var inputs = _.map(_.range(10), function(i) { + var utxo = _.clone(simpleUtxoWith100000Satoshis); + utxo.outputIndex = i; + return utxo; + }); + var transaction = new Transaction() + .from(inputs) + .to(toAddress, 950000) + .feePerByte(13) + .change(changeAddress) + .sign(privateKey); + transaction._estimateSize().should.be.within(1000, 1999); + transaction.outputs.length.should.equal(2); + transaction.outputs[1].satoshis.should.be.within(24013, 37000); + }); + it('fee per byte not enough for change', function() { + var inputs = _.map(_.range(10), function(i) { + var utxo = _.clone(simpleUtxoWith100000Satoshis); + utxo.outputIndex = i; + return utxo; + }); + var transaction = new Transaction() + .from(inputs) + .to(toAddress, 999999) + .feePerByte(1) + .change(changeAddress) + .sign(privateKey); + transaction._estimateSize().should.be.within(1000, 1999); + transaction.outputs.length.should.equal(1); + }); it('if satoshis are invalid', function() { var transaction = new Transaction() .from(simpleUtxoWith100000Satoshis)