Merge pull request #771 from eordano/feature/change

Add Fee Estimation and Change Address to Transaction
This commit is contained in:
Manuel Aráoz 2014-12-16 22:50:53 -03:00
commit bcae1101fa
6 changed files with 291 additions and 18 deletions

View File

@ -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.

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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';