fix bug in change calculation
This commit is contained in:
parent
986264e181
commit
aa1158097d
|
@ -112,11 +112,11 @@ When outputs' value don't sum up to the same amount that inputs, the difference
|
||||||
|
|
||||||
For this reason, some methods in the Transaction class are provided:
|
For this reason, some methods in the Transaction class are provided:
|
||||||
|
|
||||||
* `change(address)`: Set up the change address. This will set an internal `_change` property that will store the change address.
|
* `change(address)`: Set up the change address. This will set an internal `_changeScript` property that will store the change script associated with that address.
|
||||||
* `fee(amount)`: Sets up the exact amount of fee to pay. If no change address is provided, this will raise an exception.
|
* `fee(amount)`: Sets up the exact amount of fee to pay. If no change address is provided, this will raise an exception.
|
||||||
* `getFee()`: returns the estimated fee amount to be paid, based on the size of the transaction, but disregarding the priority of the outputs.
|
* `getFee()`: returns the estimated fee amount to be paid, based on the size of the transaction, but disregarding the priority of the outputs.
|
||||||
|
|
||||||
Internally, a `_changeOutput` property stores the index of the change output (so it can get updated when a new input or output is added).
|
Internally, a `_changeIndex` property stores the index of the change output (so it can get updated when a new input or output is added).
|
||||||
|
|
||||||
## Multisig Transactions
|
## Multisig Transactions
|
||||||
|
|
||||||
|
|
|
@ -36,15 +36,14 @@ Input.prototype._fromObject = function(params) {
|
||||||
params.prevTxId = new buffer.Buffer(params.prevTxId, 'hex');
|
params.prevTxId = new buffer.Buffer(params.prevTxId, 'hex');
|
||||||
}
|
}
|
||||||
this.output = params.output ?
|
this.output = params.output ?
|
||||||
(params.output instanceof Output ? params.output : new Output(params.output)) : undefined;
|
(params.output instanceof Output ? params.output : new Output(params.output)) : undefined;
|
||||||
this.prevTxId = params.prevTxId;
|
this.prevTxId = params.prevTxId;
|
||||||
this.outputIndex = params.outputIndex;
|
this.outputIndex = params.outputIndex;
|
||||||
this.sequenceNumber = params.sequenceNumber;
|
this.sequenceNumber = params.sequenceNumber;
|
||||||
if (!_.isUndefined(params.script) || !_.isUndefined(params.scriptBuffer)) {
|
if (_.isUndefined(params.script) && _.isUndefined(params.scriptBuffer)) {
|
||||||
this.setScript(_.isUndefined(params.script) ? params.scriptBuffer : params.script);
|
|
||||||
} else {
|
|
||||||
throw new errors.Transaction.Input.MissingScript();
|
throw new errors.Transaction.Input.MissingScript();
|
||||||
}
|
}
|
||||||
|
this.setScript(params.scriptBuffer || params.script);
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -126,13 +126,12 @@ Transaction.prototype.uncheckedSerialize = function() {
|
||||||
|
|
||||||
Transaction.prototype.checkedSerialize = Transaction.prototype.toString = function() {
|
Transaction.prototype.checkedSerialize = Transaction.prototype.toString = function() {
|
||||||
var feeError = this._validateFees();
|
var feeError = this._validateFees();
|
||||||
if (feeError) {
|
var missingChange = this._missingChange();
|
||||||
var changeError = this._validateChange();
|
if (feeError && missingChange) {
|
||||||
if (changeError) {
|
throw new errors.Transaction.ChangeAddressMissing();
|
||||||
throw new errors.Transaction.ChangeAddressMissing();
|
}
|
||||||
} else {
|
if (feeError && !missingChange) {
|
||||||
throw new errors.Transaction.FeeError(feeError);
|
throw new errors.Transaction.FeeError(feeError);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (this._hasDustOutputs()) {
|
if (this._hasDustOutputs()) {
|
||||||
throw new errors.Transaction.DustOutputs();
|
throw new errors.Transaction.DustOutputs();
|
||||||
|
@ -148,10 +147,8 @@ Transaction.prototype._validateFees = function() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Transaction.prototype._validateChange = function() {
|
Transaction.prototype._missingChange = function() {
|
||||||
if (!this._change) {
|
return !this._changeScript;
|
||||||
return 'Missing change address';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Transaction.DUST_AMOUNT = 5460;
|
Transaction.DUST_AMOUNT = 5460;
|
||||||
|
@ -217,23 +214,7 @@ Transaction.prototype.fromJSON = function(json) {
|
||||||
if (JSUtil.isValidJSON(json)) {
|
if (JSUtil.isValidJSON(json)) {
|
||||||
json = JSON.parse(json);
|
json = JSON.parse(json);
|
||||||
}
|
}
|
||||||
var self = this;
|
return this.fromObject(json);
|
||||||
this.inputs = [];
|
|
||||||
var inputs = json.inputs || json.txins;
|
|
||||||
inputs.forEach(function(input) {
|
|
||||||
self.inputs.push(Input.fromJSON(input));
|
|
||||||
});
|
|
||||||
this.outputs = [];
|
|
||||||
var outputs = json.outputs || json.txouts;
|
|
||||||
outputs.forEach(function(output) {
|
|
||||||
self.outputs.push(Output.fromJSON(output));
|
|
||||||
});
|
|
||||||
if (json.change) {
|
|
||||||
this.change(json.change);
|
|
||||||
}
|
|
||||||
this.version = json.version;
|
|
||||||
this.nLockTime = json.nLockTime;
|
|
||||||
return this;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Transaction.prototype.toObject = function toObject() {
|
Transaction.prototype.toObject = function toObject() {
|
||||||
|
@ -246,7 +227,8 @@ Transaction.prototype.toObject = function toObject() {
|
||||||
outputs.push(output.toObject());
|
outputs.push(output.toObject());
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
change: this._change ? this._change.toString() : undefined,
|
changeScript: this._changeScript ? this._changeScript.toString() : undefined,
|
||||||
|
changeIndex: !_.isUndefined(this._changeIndex) ? this._changeIndex : undefined,
|
||||||
fee: this._fee ? this._fee : undefined,
|
fee: this._fee ? this._fee : undefined,
|
||||||
version: this.version,
|
version: this.version,
|
||||||
inputs: inputs,
|
inputs: inputs,
|
||||||
|
@ -275,21 +257,36 @@ Transaction.prototype.fromObject = function(transaction) {
|
||||||
_.each(transaction.outputs, function(output) {
|
_.each(transaction.outputs, function(output) {
|
||||||
self.addOutput(new Output(output));
|
self.addOutput(new Output(output));
|
||||||
});
|
});
|
||||||
if (transaction.change) {
|
if (transaction.changeIndex) {
|
||||||
this.change(transaction.change);
|
this._changeIndex = transaction.changeIndex;
|
||||||
|
}
|
||||||
|
if (transaction.changeScript) {
|
||||||
|
this._changeScript = new Script(transaction.changeScript);
|
||||||
}
|
}
|
||||||
if (transaction.fee) {
|
if (transaction.fee) {
|
||||||
this.fee(transaction.fee);
|
this.fee(transaction.fee);
|
||||||
}
|
}
|
||||||
this.nLockTime = transaction.nLockTime;
|
this.nLockTime = transaction.nLockTime;
|
||||||
this.version = transaction.version;
|
this.version = transaction.version;
|
||||||
|
this._checkConsistency();
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
Transaction.prototype._checkConsistency = function() {
|
||||||
|
if (!_.isUndefined(this._changeIndex)) {
|
||||||
|
$.checkState(this._changeScript);
|
||||||
|
$.checkState(this.outputs[this._changeIndex]);
|
||||||
|
$.checkState(this.outputs[this._changeIndex].script.toString() ===
|
||||||
|
this._changeScript.toString());
|
||||||
|
}
|
||||||
|
// TODO: add other checks
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets nLockTime so that transaction is not valid until the desired date(a
|
* Sets nLockTime so that transaction is not valid until the desired date(a
|
||||||
* timestamp in seconds since UNIX epoch is also accepted)
|
* timestamp in seconds since UNIX epoch is also accepted)
|
||||||
*
|
*
|
||||||
* @param {Date | Number} time
|
* @param {Date | Number} time
|
||||||
* @return {Transaction} this
|
* @return {Transaction} this
|
||||||
*/
|
*/
|
||||||
Transaction.prototype.lockUntilDate = function(time) {
|
Transaction.prototype.lockUntilDate = function(time) {
|
||||||
|
@ -337,7 +334,7 @@ Transaction.prototype.getLockTime = function() {
|
||||||
if (this.nLockTime < Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) {
|
if (this.nLockTime < Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) {
|
||||||
return this.nLockTime;
|
return this.nLockTime;
|
||||||
}
|
}
|
||||||
return new Date(1000*this.nLockTime);
|
return new Date(1000 * this.nLockTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
Transaction.prototype.toJSON = function toJSON() {
|
Transaction.prototype.toJSON = function toJSON() {
|
||||||
|
@ -533,11 +530,22 @@ Transaction.prototype.fee = function(amount) {
|
||||||
* @return {Transaction} this, for chaining
|
* @return {Transaction} this, for chaining
|
||||||
*/
|
*/
|
||||||
Transaction.prototype.change = function(address) {
|
Transaction.prototype.change = function(address) {
|
||||||
this._change = new Address(address);
|
this._changeScript = Script.fromAddress(address);
|
||||||
this._updateChangeOutput();
|
this._updateChangeOutput();
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Output} change output, if it exists
|
||||||
|
*/
|
||||||
|
Transaction.prototype.getChangeOutput = function() {
|
||||||
|
if (!_.isUndefined(this._changeIndex)) {
|
||||||
|
return this.outputs[this._changeIndex];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an output to the transaction.
|
* Add an output to the transaction.
|
||||||
*
|
*
|
||||||
|
@ -586,33 +594,46 @@ Transaction.prototype._addOutput = function(output) {
|
||||||
};
|
};
|
||||||
|
|
||||||
Transaction.prototype._updateChangeOutput = function() {
|
Transaction.prototype._updateChangeOutput = function() {
|
||||||
if (!this._change) {
|
if (!this._changeScript) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._clearSignatures();
|
this._clearSignatures();
|
||||||
if (!_.isUndefined(this._changeOutput)) {
|
if (!_.isUndefined(this._changeIndex)) {
|
||||||
this._removeOutput(this._changeOutput);
|
this._removeOutput(this._changeIndex);
|
||||||
}
|
}
|
||||||
var available = this._getUnspentValue();
|
var available = this._getUnspentValue();
|
||||||
var fee = this.getFee();
|
var fee = this.getFee();
|
||||||
if (available - fee > 0) {
|
var changeAmount = available - fee;
|
||||||
this._changeOutput = this.outputs.length;
|
if (changeAmount > 0) {
|
||||||
|
this._changeIndex = this.outputs.length;
|
||||||
this._addOutput(new Output({
|
this._addOutput(new Output({
|
||||||
script: Script.fromAddress(this._change),
|
script: this._changeScript,
|
||||||
satoshis: available - fee
|
satoshis: changeAmount
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
this._changeOutput = undefined;
|
this._changeIndex = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Calculates the fees for the transaction.
|
||||||
|
*
|
||||||
|
* If there is no change output set, the fee will be the
|
||||||
|
* output amount minus the input amount.
|
||||||
|
* If there's a fixed fee set, return that
|
||||||
|
* If there's no fee set, estimate it based on size
|
||||||
|
* @return {Number} miner fee for this transaction in satoshis
|
||||||
|
*/
|
||||||
Transaction.prototype.getFee = function() {
|
Transaction.prototype.getFee = function() {
|
||||||
if (!this._change) {
|
// if no change output is set, fees should equal all the unspent amount
|
||||||
|
if (!this._changeScript) {
|
||||||
return this._getUnspentValue();
|
return this._getUnspentValue();
|
||||||
}
|
}
|
||||||
return this._fee || this._estimateFee();
|
return this._fee || this._estimateFee();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimates fee from serialized transaction size in bytes.
|
||||||
|
*/
|
||||||
Transaction.prototype._estimateFee = function() {
|
Transaction.prototype._estimateFee = function() {
|
||||||
var estimatedSize = this._estimateSize();
|
var estimatedSize = this._estimateSize();
|
||||||
var available = this._getUnspentValue();
|
var available = this._getUnspentValue();
|
||||||
|
@ -630,12 +651,12 @@ Transaction.prototype._clearSignatures = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
Transaction.FEE_PER_KB = 10000;
|
Transaction.FEE_PER_KB = 10000;
|
||||||
|
// Safe upper bound for change address script
|
||||||
Transaction.CHANGE_OUTPUT_MAX_SIZE = 20 + 4 + 34 + 4;
|
Transaction.CHANGE_OUTPUT_MAX_SIZE = 20 + 4 + 34 + 4;
|
||||||
|
|
||||||
Transaction._estimateFee = function(size, amountAvailable) {
|
Transaction._estimateFee = function(size, amountAvailable) {
|
||||||
var fee = Math.ceil(size / Transaction.FEE_PER_KB);
|
var fee = Math.ceil(size / Transaction.FEE_PER_KB);
|
||||||
if (amountAvailable > fee) {
|
if (amountAvailable > fee) {
|
||||||
// Safe upper bound for change address script
|
|
||||||
size += Transaction.CHANGE_OUTPUT_MAX_SIZE;
|
size += Transaction.CHANGE_OUTPUT_MAX_SIZE;
|
||||||
}
|
}
|
||||||
return Math.ceil(size / 1000) * Transaction.FEE_PER_KB;
|
return Math.ceil(size / 1000) * Transaction.FEE_PER_KB;
|
||||||
|
|
|
@ -142,7 +142,16 @@ describe('Transaction', function() {
|
||||||
tx.inputs.length.should.equal(1);
|
tx.inputs.length.should.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('not enough information errors', function() {
|
describe('isFullySigned', function() {
|
||||||
|
it('works for normal p2pkh', function() {
|
||||||
|
var transaction = new Transaction()
|
||||||
|
.from(simpleUtxoWith100000Satoshis)
|
||||||
|
.to(toAddress, 50000)
|
||||||
|
.change(changeAddress)
|
||||||
|
.sign(privateKey);
|
||||||
|
transaction.isFullySigned().should.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('fails when Inputs are not subclassed and isFullySigned is called', function() {
|
it('fails when Inputs are not subclassed and isFullySigned is called', function() {
|
||||||
var tx = new Transaction(tx_1_hex);
|
var tx = new Transaction(tx_1_hex);
|
||||||
expect(function() {
|
expect(function() {
|
||||||
|
@ -172,6 +181,7 @@ describe('Transaction', function() {
|
||||||
transaction.outputs[1].satoshis.should.equal(40000);
|
transaction.outputs[1].satoshis.should.equal(40000);
|
||||||
transaction.outputs[1].script.toString()
|
transaction.outputs[1].script.toString()
|
||||||
.should.equal(Script.fromAddress(changeAddress).toString());
|
.should.equal(Script.fromAddress(changeAddress).toString());
|
||||||
|
transaction.getChangeOutput().script.should.deep.equal(Script.fromAddress(changeAddress));
|
||||||
});
|
});
|
||||||
it('accepts a P2SH address for change', function() {
|
it('accepts a P2SH address for change', function() {
|
||||||
var transaction = new Transaction()
|
var transaction = new Transaction()
|
||||||
|
@ -243,7 +253,8 @@ describe('Transaction', function() {
|
||||||
.change(changeAddress)
|
.change(changeAddress)
|
||||||
.toObject();
|
.toObject();
|
||||||
var deserialized = new Transaction(serialized);
|
var deserialized = new Transaction(serialized);
|
||||||
expect(deserialized._change.toString()).to.equal(changeAddress);
|
expect(deserialized._changeScript.toString()).to.equal(Script.fromAddress(changeAddress).toString());
|
||||||
|
expect(deserialized.getChangeOutput()).to.equal(null);
|
||||||
});
|
});
|
||||||
it('can avoid checked serialize', function() {
|
it('can avoid checked serialize', function() {
|
||||||
var transaction = new Transaction()
|
var transaction = new Transaction()
|
||||||
|
@ -309,13 +320,23 @@ describe('Transaction', function() {
|
||||||
|
|
||||||
describe('to and from JSON', function() {
|
describe('to and from JSON', function() {
|
||||||
it('takes a string that is a valid JSON and deserializes from it', function() {
|
it('takes a string that is a valid JSON and deserializes from it', function() {
|
||||||
var transaction = new Transaction();
|
var simple = new Transaction();
|
||||||
expect(new Transaction(transaction.toJSON()).serialize()).to.equal(transaction.serialize());
|
expect(new Transaction(simple.toJSON()).serialize()).to.equal(simple.serialize());
|
||||||
|
var complex = new Transaction()
|
||||||
|
.from(simpleUtxoWith100000Satoshis)
|
||||||
|
.to(toAddress, 50000)
|
||||||
|
.change(changeAddress)
|
||||||
|
.sign(privateKey);
|
||||||
|
var cj = complex.toJSON();
|
||||||
|
var ctx = new Transaction(cj);
|
||||||
|
expect(ctx.serialize()).to.equal(complex.serialize());
|
||||||
|
|
||||||
});
|
});
|
||||||
it('serializes the `change` information', function() {
|
it('serializes the `change` information', function() {
|
||||||
var transaction = new Transaction();
|
var transaction = new Transaction();
|
||||||
transaction.change(changeAddress);
|
transaction.change(changeAddress);
|
||||||
expect(JSON.parse(transaction.toJSON()).change).to.equal(changeAddress.toString());
|
expect(JSON.parse(transaction.toJSON()).changeScript).to.equal(Script.fromAddress(changeAddress).toString());
|
||||||
|
expect(new Transaction(transaction.toJSON()).serialize()).to.equal(transaction.serialize());
|
||||||
});
|
});
|
||||||
it('serializes correctly p2sh multisig signed tx', function() {
|
it('serializes correctly p2sh multisig signed tx', function() {
|
||||||
var t = new Transaction(tx2hex);
|
var t = new Transaction(tx2hex);
|
||||||
|
@ -413,11 +434,15 @@ describe('Transaction', function() {
|
||||||
var timestamp = 1423504946;
|
var timestamp = 1423504946;
|
||||||
var blockHeight = 342734;
|
var blockHeight = 342734;
|
||||||
var date = new Date(timestamp * MILLIS_IN_SECOND);
|
var date = new Date(timestamp * MILLIS_IN_SECOND);
|
||||||
|
it('handles a null locktime', function() {
|
||||||
|
var transaction = new Transaction();
|
||||||
|
expect(transaction.getLockTime()).to.equal(null);
|
||||||
|
});
|
||||||
it('handles a simple example', function() {
|
it('handles a simple example', function() {
|
||||||
var future = new Date(2025,10,30); // Sun Nov 30 2025
|
var future = new Date(2025, 10, 30); // Sun Nov 30 2025
|
||||||
var transaction = new Transaction()
|
var transaction = new Transaction()
|
||||||
.lockUntilDate(future);
|
.lockUntilDate(future);
|
||||||
transaction.nLockTime.should.equal(future.getTime()/1000);
|
transaction.nLockTime.should.equal(future.getTime() / 1000);
|
||||||
transaction.getLockTime().should.deep.equal(future);
|
transaction.getLockTime().should.deep.equal(future);
|
||||||
});
|
});
|
||||||
it('accepts a date instance', function() {
|
it('accepts a date instance', function() {
|
||||||
|
@ -430,7 +455,7 @@ describe('Transaction', function() {
|
||||||
var transaction = new Transaction()
|
var transaction = new Transaction()
|
||||||
.lockUntilDate(timestamp);
|
.lockUntilDate(timestamp);
|
||||||
transaction.nLockTime.should.equal(timestamp);
|
transaction.nLockTime.should.equal(timestamp);
|
||||||
transaction.getLockTime().should.deep.equal(new Date(timestamp*1000));
|
transaction.getLockTime().should.deep.equal(new Date(timestamp * 1000));
|
||||||
});
|
});
|
||||||
it('accepts a block height', function() {
|
it('accepts a block height', function() {
|
||||||
var transaction = new Transaction()
|
var transaction = new Transaction()
|
||||||
|
|
Loading…
Reference in New Issue