diff --git a/lib/errors/spec.js b/lib/errors/spec.js
index 6093724..6448913 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 1399431..3f8c48d 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:
+ *
+ * - disableAll: disable all checks
+ * - disableSmallFees: disable checking for fees that are too small
+ * - disableLargeFees: disable checking for fees that are too large
+ * - disableNotFullySigned: disable checking if all inputs are fully signed
+ * - disableDustOutputs: disable checking if there are no outputs that are dust amounts
+ *
* @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} skipOptions allows to skip certain tests:
+ *
+ * - disableSmallFees: disable checking for fees that are too small
+ * - disableLargeFees: disable checking for fees that are too large
+ * - disableIsFullySigned: disable checking if all inputs are fully signed
+ * - disableDustOutputs: disable checking if there are no outputs that are dust amounts
+ *
+ * @return {string}
+ */
+Transaction.prototype.checkedSerialize = function(skipOptions) {
+ skipOptions = skipOptions || {};
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 (!skipOptions.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 (!skipOptions.disableSmallFees && feeIsTooSmall) {
+ throw new errors.Transaction.FeeError(feeIsTooSmall);
}
- if (this._hasDustOutputs()) {
+ if (!skipOptions.disableDustOutputs && this._hasDustOutputs()) {
throw new errors.Transaction.DustOutputs();
}
+ if (!skipOptions.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 7557886..1f70b70 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() {