Merge pull request #771 from eordano/feature/change
Add Fee Estimation and Change Address to Transaction
This commit is contained in:
commit
bcae1101fa
|
@ -24,6 +24,12 @@ Now, this could just be serialized to hexadecimal ASCII values (`transaction.ser
|
|||
bitcoin-cli sendrawtransaction <serialized transaction>
|
||||
```
|
||||
|
||||
You can also override the fee estimation with another amount, specified in satoshis:
|
||||
```javascript
|
||||
var transaction = new Transaction().fee(5430); // Minimum non-dust amount
|
||||
var transaction = new Transaction().fee(1e8); // Generous fee of 1 BTC
|
||||
```
|
||||
|
||||
## Transaction API
|
||||
|
||||
You can take a look at the javadocs for the [Transaction class here](link missing).
|
||||
|
@ -71,8 +77,6 @@ There are a number of data structures being stored internally in a `Transaction`
|
|||
* `outputs`: This is the ordered set of output scripts
|
||||
* `_inputAmount`: sum of the amount for all the inputs
|
||||
* `_outputAmount`: sum of the amount for all the outputs
|
||||
|
||||
TO BE IMPLEMENTED YET:
|
||||
* `_fee`: if user specified a non-standard fee, the amount (in satoshis) will be stored in this variable so the change amount can be calculated.
|
||||
* `_change`: stores the value provided by calling the `change` method.
|
||||
|
||||
|
|
|
@ -132,6 +132,10 @@ Input.prototype.addSignature = function() {
|
|||
throw new errors.AbstractMethodInvoked('Input#addSignature');
|
||||
};
|
||||
|
||||
Input.prototype.clearSignatures = function() {
|
||||
throw new errors.AbstractMethodInvoked('Input#clearSignatures');
|
||||
};
|
||||
|
||||
Input.prototype.isValidSignature = function(transaction, signature) {
|
||||
// FIXME: Refactor signature so this is not necessary
|
||||
signature.signature.nhashtype = signature.sigtype;
|
||||
|
|
|
@ -124,4 +124,11 @@ MultiSigScriptHashInput.prototype.isValidSignature = function(transaction, signa
|
|||
);
|
||||
};
|
||||
|
||||
MultiSigScriptHashInput.OPCODES_SIZE = 10;
|
||||
MultiSigScriptHashInput.SIGNATURE_SIZE = 36;
|
||||
|
||||
MultiSigScriptHashInput.prototype._estimateSize = function() {
|
||||
return MultiSigScriptHashInput.OPCODES_SIZE + this.threshold * MultiSigScriptHashInput.SIGNATURE_SIZE;
|
||||
};
|
||||
|
||||
module.exports = MultiSigScriptHashInput;
|
||||
|
|
|
@ -72,7 +72,7 @@ PublicKeyHashInput.prototype.addSignature = function(transaction, signature) {
|
|||
* Clear the input's signature
|
||||
* @return {PublicKeyHashInput} this, for chaining
|
||||
*/
|
||||
PublicKeyHashInput.prototype.clearSignature = function() {
|
||||
PublicKeyHashInput.prototype.clearSignatures = function() {
|
||||
this.setScript(Script.empty());
|
||||
return this;
|
||||
};
|
||||
|
@ -85,4 +85,10 @@ PublicKeyHashInput.prototype.isFullySigned = function() {
|
|||
return this.script.isPublicKeyHashIn();
|
||||
};
|
||||
|
||||
PublicKeyHashInput.FIXED_SIZE = 32 + 4 + 2;
|
||||
PublicKeyHashInput.SCRIPT_MAX_SIZE = 34 + 20;
|
||||
PublicKeyHashInput.prototype._estimateSize = function() {
|
||||
return PublicKeyHashInput.FIXED_SIZE + PublicKeyHashInput.SCRIPT_MAX_SIZE;
|
||||
};
|
||||
|
||||
module.exports = PublicKeyHashInput;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
var _ = require('lodash');
|
||||
var $ = require('../util/preconditions');
|
||||
var buffer = require('buffer');
|
||||
var assert = require('assert');
|
||||
|
||||
|
@ -27,8 +28,7 @@ var DEFAULT_NLOCKTIME = 0;
|
|||
var DEFAULT_SEQNUMBER = 0xFFFFFFFF;
|
||||
|
||||
/**
|
||||
* Represents a transaction, a set of inputs and outputs to change
|
||||
* ownership of tokens
|
||||
* Represents a transaction, a set of inputs and outputs to change ownership of tokens
|
||||
*
|
||||
* @param {*} serialized
|
||||
* @constructor
|
||||
|
@ -196,6 +196,45 @@ Transaction.prototype._newTransaction = function() {
|
|||
|
||||
/* Transaction creation interface */
|
||||
|
||||
/**
|
||||
* Add an input to this transaction. This is a high level interface
|
||||
* to add an input, for more control, use @{link Transaction#addInput}.
|
||||
*
|
||||
* Can receive, as output information, the output of bitcoind's `listunspent` command,
|
||||
* and a slightly fancier format recognized by bitcore:
|
||||
*
|
||||
* ```
|
||||
* {
|
||||
* address: 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1',
|
||||
* txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458',
|
||||
* outputIndex: 0,
|
||||
* script: Script.empty(),
|
||||
* satoshis: 1020000
|
||||
* }
|
||||
* ```
|
||||
* Where `address` can be either a string or a bitcore Address object. The
|
||||
* same is true for `script`, which can be a string or a bitcore Script.
|
||||
*
|
||||
* Beware that this resets all the signatures for inputs (in further versions,
|
||||
* SIGHASH_SINGLE or SIGHASH_NONE signatures will not be reset).
|
||||
*
|
||||
* @example
|
||||
* var transaction = new Transaction();
|
||||
*
|
||||
* // From a pay to public key hash output from bitcoind's listunspent
|
||||
* transaction.from({'txid': '0000...', vout: 0, amount: 0.1, scriptPubKey: 'OP_DUP ...'});
|
||||
*
|
||||
* // From a pay to public key hash output
|
||||
* transaction.from({'txId': '0000...', outputIndex: 0, satoshis: 1000, script: 'OP_DUP ...'});
|
||||
*
|
||||
* // From a multisig P2SH output
|
||||
* transaction.from({'txId': '0000...', inputIndex: 0, satoshis: 1000, script: '... OP_HASH'},
|
||||
* ['03000...', '02000...'], 2);
|
||||
*
|
||||
* @param {Object} utxo
|
||||
* @param {Array=} pubkeys
|
||||
* @param {number=} threshold
|
||||
*/
|
||||
Transaction.prototype.from = function(utxo, pubkeys, threshold) {
|
||||
if (pubkeys && threshold) {
|
||||
this._fromMultiSigP2SH(utxo, pubkeys, threshold);
|
||||
|
@ -283,6 +322,7 @@ Transaction.prototype._fromMultisigOldUtxo = function(utxo, pubkeys, threshold)
|
|||
};
|
||||
|
||||
Transaction.prototype._fromMultisigNewUtxo = function(utxo, pubkeys, threshold) {
|
||||
this._changeSetup = false;
|
||||
utxo.address = utxo.address && new Address(utxo.address);
|
||||
utxo.script = new Script(util.isHexa(utxo.script) ? new buffer.Buffer(utxo.script, 'hex') : utxo.script);
|
||||
this.inputs.push(new MultiSigScriptHashInput({
|
||||
|
@ -298,24 +338,58 @@ Transaction.prototype._fromMultisigNewUtxo = function(utxo, pubkeys, threshold)
|
|||
this._inputAmount += utxo.satoshis;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the transaction has enough info on all inputs to be correctly validated
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
Transaction.prototype.hasAllUtxoInfo = function() {
|
||||
return _.all(this.inputs.map(function(input) {
|
||||
return !!input.output;
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Manually set the fee 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).
|
||||
*
|
||||
* @param {number} amount satoshis to be sent
|
||||
* @return {Transaction} this, for chaining
|
||||
*/
|
||||
Transaction.prototype.fee = function(amount) {
|
||||
this._fee = amount;
|
||||
this._changeSetup = false;
|
||||
return this;
|
||||
};
|
||||
|
||||
/* Output management */
|
||||
|
||||
/**
|
||||
* Set the change address 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).
|
||||
*
|
||||
* @param {number} amount satoshis to be sent
|
||||
* @return {Transaction} this, for chaining
|
||||
*/
|
||||
Transaction.prototype.change = function(address) {
|
||||
this._change = address;
|
||||
this._change = new Address(address);
|
||||
this._changeSetup = false;
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an output to the transaction.
|
||||
*
|
||||
* Beware that this resets all the signatures for inputs (in further versions,
|
||||
* SIGHASH_SINGLE or SIGHASH_NONE signatures will not be reset).
|
||||
*
|
||||
* @param {string|Address} address
|
||||
* @param {number} amount in satoshis
|
||||
* @return {Transaction} this, for chaining
|
||||
*/
|
||||
Transaction.prototype.to = function(address, amount) {
|
||||
this._addOutput(new Output({
|
||||
script: Script(new Address(address)),
|
||||
|
@ -324,11 +398,16 @@ Transaction.prototype.to = function(address, amount) {
|
|||
return this;
|
||||
};
|
||||
|
||||
Transaction.prototype._addOutput = function(output) {
|
||||
this.outputs.push(output);
|
||||
this._outputAmount += output.satoshis;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an OP_RETURN output to the transaction.
|
||||
*
|
||||
* Beware that this resets all the signatures for inputs (in further versions,
|
||||
* SIGHASH_SINGLE or SIGHASH_NONE signatures will not be reset).
|
||||
*
|
||||
* @param {Buffer|string} value the data to be stored in the OP_RETURN output.
|
||||
* In case of a string, the UTF-8 representation will be stored
|
||||
* @return {Transaction} this, for chaining
|
||||
*/
|
||||
Transaction.prototype.addData = function(value) {
|
||||
this._addOutput(new Output({
|
||||
script: Script.buildDataOut(value),
|
||||
|
@ -337,19 +416,100 @@ Transaction.prototype.addData = function(value) {
|
|||
return this;
|
||||
};
|
||||
|
||||
Transaction.prototype._addOutput = function(output) {
|
||||
this.outputs.push(output);
|
||||
this._changeSetup = false;
|
||||
this._outputAmount += output.satoshis;
|
||||
};
|
||||
|
||||
Transaction.prototype._updateChangeOutput = function() {
|
||||
if (!this._change) {
|
||||
return;
|
||||
}
|
||||
if (this._changeSetup) {
|
||||
return;
|
||||
}
|
||||
if (!_.isUndefined(this._changeSetup)) {
|
||||
this._clearSignatures();
|
||||
}
|
||||
if (!_.isUndefined(this._changeOutput)) {
|
||||
this.removeOutput(this._changeOutput);
|
||||
}
|
||||
var estimatedSize = this._estimateSize();
|
||||
var available = this._inputAmount - this._outputAmount;
|
||||
var fee = this._fee || Transaction._estimateFee(estimatedSize, available);
|
||||
if (available - fee > 0) {
|
||||
this._changeOutput = this.outputs.length;
|
||||
this._addOutput(new Output({
|
||||
script: Script.fromAddress(this._change),
|
||||
satoshis: available - fee
|
||||
}));
|
||||
} else {
|
||||
this._changeOutput = undefined;
|
||||
}
|
||||
this._changeSetup = true;
|
||||
};
|
||||
|
||||
Transaction.prototype._clearSignatures = function() {
|
||||
_.each(this.inputs, function(input) {
|
||||
input.clearSignatures();
|
||||
});
|
||||
};
|
||||
|
||||
Transaction.FEE_PER_KB = 10000;
|
||||
Transaction.CHANGE_OUTPUT_MAX_SIZE = 20 + 4 + 34 + 4;
|
||||
|
||||
Transaction._estimateFee = function(size, amountAvailable) {
|
||||
var fee = Math.ceil(size / Transaction.FEE_PER_KB);
|
||||
if (amountAvailable > fee) {
|
||||
// Safe upper bound for change address script
|
||||
size += Transaction.CHANGE_OUTPUT_MAX_SIZE;
|
||||
}
|
||||
return Math.ceil(size / 1000 / Transaction.FEE_PER_KB) * 1000;
|
||||
};
|
||||
|
||||
Transaction.MAXIMUM_EXTRA_SIZE = 4 + 9 + 9 + 4;
|
||||
|
||||
Transaction.prototype._estimateSize = function() {
|
||||
var result = Transaction.MAXIMUM_EXTRA_SIZE;
|
||||
_.each(this.inputs, function(input) {
|
||||
result += input._estimateSize();
|
||||
});
|
||||
_.each(this.outputs, function(output) {
|
||||
result += output.script.toBuffer().length + 9;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
Transaction.prototype.removeOutput = function(index) {
|
||||
var output = this.outputs[index];
|
||||
this._outputAmount -= output.satoshis;
|
||||
this.outputs = _.without(this.outputs, this.outputs[this._changeOutput]);
|
||||
};
|
||||
|
||||
/* Signature handling */
|
||||
|
||||
Transaction.prototype.sign = function(privKey, sigtype) {
|
||||
// TODO: Change for preconditions
|
||||
assert(this.hasAllUtxoInfo());
|
||||
/**
|
||||
* Sign the transaction using one or more private keys.
|
||||
*
|
||||
* It tries to sign each input, verifying that the signature will be valid
|
||||
* (matches a public key).
|
||||
*
|
||||
* @param {Array|String|PrivateKey} privateKey
|
||||
* @param {number} sigtype
|
||||
* @return {Transaction} this, for chaining
|
||||
*/
|
||||
Transaction.prototype.sign = function(privateKey, sigtype) {
|
||||
$.checkState(this.hasAllUtxoInfo());
|
||||
this._updateChangeOutput();
|
||||
var self = this;
|
||||
if (_.isArray(privKey)) {
|
||||
_.each(privKey, function(privKey) {
|
||||
self.sign(privKey);
|
||||
if (_.isArray(privateKey)) {
|
||||
_.each(privateKey, function(privateKey) {
|
||||
self.sign(privateKey);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
_.each(this.getSignatures(privKey, sigtype), function(signature) {
|
||||
_.each(this.getSignatures(privateKey, sigtype), function(signature) {
|
||||
self.applySignature(signature);
|
||||
});
|
||||
return this;
|
||||
|
@ -369,6 +529,16 @@ Transaction.prototype._getPrivateKeySignatures = function(privKey, sigtype) {
|
|||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a signature to the transaction
|
||||
*
|
||||
* @param {Object} signature
|
||||
* @param {number} signature.inputIndex
|
||||
* @param {number} signature.sighash
|
||||
* @param {PublicKey} signature.publicKey
|
||||
* @param {Signature} signature.signature
|
||||
* @return {Transaction} this, for chaining
|
||||
*/
|
||||
Transaction.prototype.applySignature = function(signature) {
|
||||
this.inputs[signature.inputIndex].addSignature(this, signature);
|
||||
return this;
|
||||
|
|
|
@ -119,6 +119,88 @@ describe('Transaction', function() {
|
|||
}).should.equal(true);
|
||||
});
|
||||
});
|
||||
describe('change address', function() {
|
||||
var fromAddress = 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1';
|
||||
var simpleUtxoWith100000Satoshis = {
|
||||
address: fromAddress,
|
||||
txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458',
|
||||
outputIndex: 0,
|
||||
script: Script.buildPublicKeyHashOut(fromAddress).toString(),
|
||||
satoshis: 100000
|
||||
};
|
||||
var toAddress = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc';
|
||||
var changeAddress = 'mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up';
|
||||
var changeAddressP2SH = '2N7T3TAetJrSCruQ39aNrJvYLhG1LJosujf';
|
||||
var privateKey = 'cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY';
|
||||
|
||||
it('can calculate simply the output amount', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(simpleUtxoWith100000Satoshis)
|
||||
.to(toAddress, 50000)
|
||||
.change(changeAddress)
|
||||
.sign(privateKey);
|
||||
transaction.outputs.length.should.equal(2);
|
||||
transaction.outputs[1].satoshis.should.equal(49000);
|
||||
transaction.outputs[1].script.toString()
|
||||
.should.equal(Script.fromAddress(changeAddress).toString());
|
||||
});
|
||||
it('accepts a P2SH address for change', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(simpleUtxoWith100000Satoshis)
|
||||
.to(toAddress, 50000)
|
||||
.change(changeAddressP2SH)
|
||||
.sign(privateKey);
|
||||
transaction.outputs.length.should.equal(2);
|
||||
transaction.outputs[1].script.isScriptHashOut().should.equal(true);
|
||||
});
|
||||
it('can recalculate the change amount', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(simpleUtxoWith100000Satoshis)
|
||||
.to(toAddress, 50000)
|
||||
.change(changeAddress)
|
||||
.sign(privateKey)
|
||||
.to(toAddress, 20000)
|
||||
.sign(privateKey);
|
||||
transaction.outputs.length.should.equal(3);
|
||||
transaction.outputs[2].satoshis.should.equal(29000);
|
||||
transaction.outputs[2].script.toString()
|
||||
.should.equal(Script.fromAddress(changeAddress).toString());
|
||||
});
|
||||
it('adds no fee if no change is available', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(simpleUtxoWith100000Satoshis)
|
||||
.to(toAddress, 99000)
|
||||
.sign(privateKey);
|
||||
transaction.outputs.length.should.equal(1);
|
||||
});
|
||||
it('adds no fee if no money is available', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(simpleUtxoWith100000Satoshis)
|
||||
.to(toAddress, 100000)
|
||||
.change(changeAddress)
|
||||
.sign(privateKey);
|
||||
transaction.outputs.length.should.equal(1);
|
||||
});
|
||||
it('fee can be set up manually', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(simpleUtxoWith100000Satoshis)
|
||||
.to(toAddress, 80000)
|
||||
.fee(10000)
|
||||
.change(changeAddress)
|
||||
.sign(privateKey);
|
||||
transaction.outputs.length.should.equal(2);
|
||||
transaction.outputs[1].satoshis.should.equal(10000);
|
||||
});
|
||||
it('coverage: on second call to sign, change is not recalculated', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(simpleUtxoWith100000Satoshis)
|
||||
.to(toAddress, 100000)
|
||||
.change(changeAddress)
|
||||
.sign(privateKey)
|
||||
.sign(privateKey);
|
||||
transaction.outputs.length.should.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var tx_empty_hex = '01000000000000000000';
|
||||
|
|
Loading…
Reference in New Issue