diff --git a/lib/errors/spec.js b/lib/errors/spec.js index 609372498..6448913d6 100644 --- a/lib/errors/spec.js +++ b/lib/errors/spec.js @@ -60,6 +60,9 @@ module.exports = [{ }, { name: 'NeedMoreInfo', message: '{0}' + }, { + name: 'MissingSignatures', + message: 'Some inputs have not been fully signed' }, { name: 'InvalidIndex', message: 'Invalid index: {0} is not between 0, {1}' diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index 13994319d..c5e450036 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -108,14 +108,22 @@ Transaction.prototype._getHash = function() { * Retrieve a hexa string that can be used with bitcoind's CLI interface * (decoderawtransaction, sendrawtransaction) * - * @param {boolean=} unsafe if true, skip testing for fees that are too high + * @param {Object|boolean=} unsafe if true, skip all tests. if it's an object, + * it's expected to contain a set of flags to skip certain tests: + * * @return {string} */ Transaction.prototype.serialize = function(unsafe) { - if (unsafe) { + if (true === unsafe || unsafe && unsafe.disableAll) { return this.uncheckedSerialize(); } else { - return this.checkedSerialize(); + return this.checkedSerialize(unsafe); } }; @@ -123,29 +131,60 @@ Transaction.prototype.uncheckedSerialize = Transaction.prototype.toString = func return this.toBuffer().toString('hex'); }; -Transaction.prototype.checkedSerialize = function() { - var feeError = this._validateFees(); +/** + * Retrieve a hexa string that can be used with bitcoind's CLI interface + * (decoderawtransaction, sendrawtransaction) + * + * @param {Object} opts allows to skip certain tests: + * + * @return {string} + */ +Transaction.prototype.checkedSerialize = function(opts) { + opts = opts || {}; var missingChange = this._missingChange(); - if (feeError && missingChange) { - throw new errors.Transaction.ChangeAddressMissing(); + var feeIsTooLarge = this._isFeeTooLarge(); + var feeIsTooSmall = this._isFeeTooSmall(); + var isFullySigned = this.isFullySigned(); + var hasDustOutputs = this._hasDustOutputs(); + + if (!opts.disableLargeFees && feeIsTooLarge) { + if (missingChange) { + throw new errors.Transaction.ChangeAddressMissing('Fee is too large and no change address was provided'); + } + throw new errors.Transaction.FeeError(feeIsTooLarge); } - if (feeError && !missingChange) { - throw new errors.Transaction.FeeError(feeError); + if (!opts.disableSmallFees && feeIsTooSmall) { + throw new errors.Transaction.FeeError(feeIsTooSmall); } - if (this._hasDustOutputs()) { + if (!opts.disableDustOutputs && this._hasDustOutputs()) { throw new errors.Transaction.DustOutputs(); } + if (!opts.disableIsFullySigned && !isFullySigned) { + throw new errors.Transaction.MissingSignatures(); + } return this.uncheckedSerialize(); }; Transaction.FEE_SECURITY_MARGIN = 15; -Transaction.prototype._validateFees = function() { - if (this._getUnspentValue() > Transaction.FEE_SECURITY_MARGIN * this._estimateFee()) { - return 'Fee is more than ' + Transaction.FEE_SECURITY_MARGIN + ' times the suggested amount'; +Transaction.prototype._isFeeTooLarge = function() { + var fee = this._getUnspentValue(); + var maximumFee = Math.floor(Transaction.FEE_SECURITY_MARGIN * this._estimateFee()); + if (fee > maximumFee) { + return 'Fee is too large: expected less than ' + maximumFee + ' but got ' + fee; } - if (this._getUnspentValue() < this._estimateFee() / Transaction.FEE_SECURITY_MARGIN) { - return 'Fee is less than ' + Transaction.FEE_SECURITY_MARGIN + ' times the suggested amount'; +}; + +Transaction.prototype._isFeeTooSmall = function() { + var fee = this._getUnspentValue(); + var minimumFee = Math.ceil(this._estimateFee() / Transaction.FEE_SECURITY_MARGIN); + if (fee < minimumFee) { + return 'Fee is too small: expected more than ' + minimumFee + ' but got ' + fee; } }; diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index 7557886b1..1f70b7064 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -59,7 +59,8 @@ describe('Transaction', function() { }); it('serialize to Object roundtrip', function() { - new Transaction(testTransaction.toObject()).uncheckedSerialize().should.equal(testTransaction.serialize()); + new Transaction(testTransaction.toObject()).uncheckedSerialize() + .should.equal(testTransaction.uncheckedSerialize()); }); it('constructor returns a shallow copy of another transaction', function() { @@ -338,6 +339,59 @@ describe('Transaction', function() { return transaction.serialize(); }).to.not.throw(errors.Transaction.DustOutputs); }); + describe('skipping checks', function() { + it('can skip the check for too much fee', function() { + var transaction = new Transaction() + .from(simpleUtxoWith1BTC) + .fee(50000000) + .change(changeAddress) + .sign(privateKey); + expect(function() { + return transaction.serialize({disableLargeFees: true}); + }).to.not.throw(); + expect(function() { + return transaction.serialize(); + }).to.throw(); + }); + it('can skip the check for a fee that is too small', function() { + var transaction = new Transaction() + .from(simpleUtxoWith1BTC) + .fee(1) + .change(changeAddress) + .sign(privateKey); + expect(function() { + return transaction.serialize({disableSmallFees: true}); + }).to.not.throw(); + expect(function() { + return transaction.serialize(); + }).to.throw(); + }); + it('can skip the check that prevents dust outputs', function() { + var transaction = new Transaction() + .from(simpleUtxoWith1BTC) + .to(toAddress, 1000) + .change(changeAddress) + .sign(privateKey); + expect(function() { + return transaction.serialize({disableDustOutputs: true}); + }).to.not.throw(); + expect(function() { + return transaction.serialize(); + }).to.throw(); + }); + it('can skip the check that prevents unsigned outputs', function() { + var transaction = new Transaction() + .from(simpleUtxoWith1BTC) + .to(toAddress, 10000) + .change(changeAddress); + expect(function() { + return transaction.serialize({disableIsFullySigned: true}); + }).to.not.throw(); + expect(function() { + return transaction.serialize(); + }).to.throw(); + }); + }); }); describe('to and from JSON', function() {