bitcore-lib-cash
Version:
A pure and powerful JavaScript Bitcoin Cash library.
1,541 lines (1,381 loc) • 48.1 kB
JavaScript
'use strict';
var _ = require('lodash');
var $ = require('../util/preconditions');
var buffer = require('buffer');
var compare = Buffer.compare || require('buffer-compare');
var errors = require('../errors');
var BufferUtil = require('../util/buffer');
var JSUtil = require('../util/js');
var BufferReader = require('../encoding/bufferreader');
var BufferWriter = require('../encoding/bufferwriter');
var Hash = require('../crypto/hash');
var Signature = require('../crypto/signature');
var Sighash = require('./sighash');
var Address = require('../address');
var UnspentOutput = require('./unspentoutput');
var Input = require('./input');
var PublicKeyHashInput = Input.PublicKeyHash;
var PublicKeyInput = Input.PublicKey;
var MultiSigScriptHashInput = Input.MultiSigScriptHash;
var MultiSigInput = Input.MultiSig;
var EscrowInput = Input.Escrow;
var Output = require('./output');
var Script = require('../script');
var PrivateKey = require('../privatekey');
var PublicKey = require('../publickey');
var BN = require('../crypto/bn');
/**
* Represents a transaction, a set of inputs and outputs to change ownership of tokens
*
* @param {*} serialized
* @constructor
*/
function Transaction(serialized) {
if (!(this instanceof Transaction)) {
return new Transaction(serialized);
}
this.inputs = [];
this.outputs = [];
this._inputAmount = undefined;
this._outputAmount = undefined;
if (serialized) {
if (serialized instanceof Transaction) {
return Transaction.shallowCopy(serialized);
} else if (JSUtil.isHexa(serialized)) {
this.fromString(serialized);
} else if (BufferUtil.isBuffer(serialized)) {
this.fromBuffer(serialized);
} else if (_.isObject(serialized)) {
this.fromObject(serialized);
} else {
throw new errors.InvalidArgument('Must provide an object or string to deserialize a transaction');
}
} else {
this._newTransaction();
}
}
var CURRENT_VERSION = 2;
var DEFAULT_NLOCKTIME = 0;
var MAX_BLOCK_SIZE = 1000000;
// Minimum amount for an output for it not to be considered a dust output
Transaction.DUST_AMOUNT = 546;
// Margin of error to allow fees in the vecinity of the expected value but doesn't allow a big difference
Transaction.FEE_SECURITY_MARGIN = 150;
// max amount of satoshis in circulation
Transaction.MAX_MONEY = 21000000 * 1e8;
// nlocktime limit to be considered block height rather than a timestamp
Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT = 5e8;
// Max value for an unsigned 32 bit value
Transaction.NLOCKTIME_MAX_VALUE = 4294967295;
// Value used for fee estimation (satoshis per kilobyte)
Transaction.FEE_PER_KB = 100000;
// Safe upper bound for change address script size in bytes
Transaction.CHANGE_OUTPUT_MAX_SIZE = 20 + 4 + 34 + 4;
Transaction.MAXIMUM_EXTRA_SIZE = 4 + 9 + 9 + 4;
/* Constructors and Serialization */
/**
* Create a 'shallow' copy of the transaction, by serializing and deserializing
* it dropping any additional information that inputs and outputs may have hold
*
* @param {Transaction} transaction
* @return {Transaction}
*/
Transaction.shallowCopy = function(transaction) {
var copy = new Transaction(transaction.toBuffer());
return copy;
};
var hashProperty = {
configurable: false,
enumerable: true,
get: function() {
this._hash = new BufferReader(this._getHash()).readReverse().toString('hex');
return this._hash;
}
};
Object.defineProperty(Transaction.prototype, 'hash', hashProperty);
Object.defineProperty(Transaction.prototype, 'id', hashProperty);
var ioProperty = {
configurable: false,
enumerable: true,
get: function() {
return this._getInputAmount();
}
};
Object.defineProperty(Transaction.prototype, 'inputAmount', ioProperty);
ioProperty.get = function() {
return this._getOutputAmount();
};
Object.defineProperty(Transaction.prototype, 'outputAmount', ioProperty);
Object.defineProperty(Transaction.prototype, 'size', {
configurable: false,
enumerable: false,
get: function() {
return this._calculateSize();
}
});
/**
* Retrieve the little endian hash of the transaction (used for serialization)
* @return {Buffer}
*/
Transaction.prototype._getHash = function() {
return Hash.sha256sha256(this.toBuffer());
};
/**
* Retrieve a hexa string that can be used with bitcoind's CLI interface
* (decoderawtransaction, sendrawtransaction)
*
* @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
* * `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
* * `disableMoreOutputThanInput`: disable checking if the transaction spends more bitcoins than the sum of the input amounts
* @return {string}
*/
Transaction.prototype.serialize = function(unsafe) {
if (true === unsafe || unsafe && unsafe.disableAll) {
return this.uncheckedSerialize();
} else {
return this.checkedSerialize(unsafe);
}
};
Transaction.prototype.uncheckedSerialize = Transaction.prototype.toString = function() {
return this.toBuffer().toString('hex');
};
/**
* Retrieve a hexa string that can be used with bitcoind's CLI interface
* (decoderawtransaction, sendrawtransaction)
*
* @param {Object} opts allows to skip certain tests. {@see Transaction#serialize}
* @return {string}
*/
Transaction.prototype.checkedSerialize = function(opts) {
var serializationError = this.getSerializationError(opts);
if (serializationError) {
serializationError.message += ' - For more information please see: ' +
'https://bitcore.io/api/lib/transaction#serialization-checks';
throw serializationError;
}
return this.uncheckedSerialize();
};
Transaction.prototype.invalidSatoshis = function() {
var invalid = false;
for (var i = 0; i < this.outputs.length; i++) {
if (this.outputs[i].invalidSatoshis()) {
invalid = true;
}
}
return invalid;
};
/**
* Retrieve a possible error that could appear when trying to serialize and
* broadcast this transaction.
*
* @param {Object} opts allows to skip certain tests. {@see Transaction#serialize}
* @return {bitcore.Error}
*/
Transaction.prototype.getSerializationError = function(opts) {
opts = opts || {};
if (this.invalidSatoshis()) {
return new errors.Transaction.InvalidSatoshis();
}
var unspent = this._getUnspentValue();
var unspentError;
if (unspent < 0) {
if (!opts.disableMoreOutputThanInput) {
unspentError = new errors.Transaction.InvalidOutputAmountSum();
}
} else {
unspentError = this._hasFeeError(opts, unspent);
}
return unspentError ||
this._hasDustOutputs(opts) ||
this._isMissingSignatures(opts);
};
Transaction.prototype._hasFeeError = function(opts, unspent) {
if (this._fee != null && this._fee !== unspent) {
return new errors.Transaction.FeeError.Different(
'Unspent value is ' + unspent + ' but specified fee is ' + this._fee
);
}
if (!opts.disableLargeFees) {
var maximumFee = Math.floor(Transaction.FEE_SECURITY_MARGIN * this._estimateFee());
if (unspent > maximumFee) {
if (this._missingChange()) {
return new errors.Transaction.ChangeAddressMissing(
'Fee is too large and no change address was provided'
);
}
return new errors.Transaction.FeeError.TooLarge(
'expected less than ' + maximumFee + ' but got ' + unspent
);
}
}
if (!opts.disableSmallFees) {
var minimumFee = Math.ceil(this._estimateFee() / Transaction.FEE_SECURITY_MARGIN);
if (unspent < minimumFee) {
return new errors.Transaction.FeeError.TooSmall(
'expected more than ' + minimumFee + ' but got ' + unspent
);
}
}
};
Transaction.prototype._missingChange = function() {
return !this._changeScript;
};
Transaction.prototype._hasDustOutputs = function(opts) {
if (opts.disableDustOutputs) {
return;
}
var index, output;
for (index in this.outputs) {
output = this.outputs[index];
if (output.satoshis < Transaction.DUST_AMOUNT && !output.script.isDataOut()) {
return new errors.Transaction.DustOutputs();
}
}
};
Transaction.prototype._isMissingSignatures = function(opts) {
if (opts.disableIsFullySigned) {
return;
}
if (!this.isFullySigned()) {
return new errors.Transaction.MissingSignatures();
}
};
Transaction.prototype.inspect = function() {
return '<Transaction: ' + this.uncheckedSerialize() + '>';
};
Transaction.prototype.toBuffer = function() {
var writer = new BufferWriter();
return this.toBufferWriter(writer).toBuffer();
};
Transaction.prototype.toBufferWriter = function(writer) {
writer.writeInt32LE(this.version);
writer.writeVarintNum(this.inputs ? this.inputs.length : 0);
for (const input of this.inputs || []) {
input.toBufferWriter(writer);
}
writer.writeVarintNum(this.outputs ? this.outputs.length : 0);
for (const output of this.outputs || []) {
output.toBufferWriter(writer);
}
writer.writeUInt32LE(this.nLockTime);
return writer;
};
Transaction.prototype.fromBuffer = function(buffer) {
var reader = new BufferReader(buffer);
return this.fromBufferReader(reader);
};
Transaction.prototype.fromBufferReader = function(reader) {
$.checkArgument(!reader.finished(), 'No transaction data received');
var i, sizeTxIns, sizeTxOuts;
this.version = reader.readInt32LE();
sizeTxIns = reader.readVarintNum();
for (i = 0; i < sizeTxIns; i++) {
var input = Input.fromBufferReader(reader);
this.inputs.push(input);
}
sizeTxOuts = reader.readVarintNum();
for (i = 0; i < sizeTxOuts; i++) {
this.outputs.push(Output.fromBufferReader(reader));
}
this.nLockTime = reader.readUInt32LE();
return this;
};
Transaction.prototype.toObject = Transaction.prototype.toJSON = function toObject() {
var inputs = [];
this.inputs.forEach(function(input) {
inputs.push(input.toObject());
});
var outputs = [];
this.outputs.forEach(function(output) {
outputs.push(output.toObject());
});
var obj = {
hash: this.hash,
version: this.version,
inputs: inputs,
outputs: outputs,
nLockTime: this.nLockTime
};
if (this._changeScript) {
obj.changeScript = this._changeScript.toString();
}
if (this._changeIndex != null) {
obj.changeIndex = this._changeIndex;
}
if (this._fee != null) {
obj.fee = this._fee;
}
return obj;
};
Transaction.prototype.fromObject = function fromObject(arg) {
/* jshint maxstatements: 20 */
$.checkArgument(_.isObject(arg) || arg instanceof Transaction);
var transaction;
if (arg instanceof Transaction) {
transaction = arg.toObject();
} else {
transaction = arg;
}
for (const input of transaction.inputs || []) {
if (!input.output || !input.output.script) {
this.uncheckedAddInput(new Input(input));
continue;
}
var script = new Script(input.output.script);
var txin;
if (script.isPublicKeyHashOut()) {
txin = new Input.PublicKeyHash(input);
} else if (script.isScriptHashOut() && input.publicKeys && input.threshold) {
txin = new Input.MultiSigScriptHash(
input, input.publicKeys, input.threshold, input.signatures
);
} else if (script.isPublicKeyOut()) {
txin = new Input.PublicKey(input);
} else {
throw new errors.Transaction.Input.UnsupportedScript(input.output.script);
}
this.addInput(txin);
}
for (const output of transaction.outputs || []) {
this.addOutput(new Output(output));
}
if (transaction.changeIndex) {
this._changeIndex = transaction.changeIndex;
}
if (transaction.changeScript) {
this._changeScript = new Script(transaction.changeScript);
}
if (transaction.fee) {
this._fee = transaction.fee;
}
this.nLockTime = transaction.nLockTime;
this.version = transaction.version;
this._checkConsistency(arg);
return this;
};
Transaction.prototype._checkConsistency = function(arg) {
if (this._changeIndex != null) {
$.checkState(this._changeScript, 'Change script is expected.');
$.checkState(this.outputs[this._changeIndex], 'Change index points to undefined output.');
$.checkState(this.outputs[this._changeIndex].script.toString() ===
this._changeScript.toString(), 'Change output has an unexpected script.');
}
if (arg && arg.hash) {
$.checkState(arg.hash === this.hash, 'Hash in object does not match transaction hash.');
}
};
/**
* Sets nLockTime so that transaction is not valid until the desired date(a
* timestamp in seconds since UNIX epoch is also accepted)
*
* @param {Date | Number} time
* @return {Transaction} this
*/
Transaction.prototype.lockUntilDate = function(time) {
$.checkArgument(time);
if (!isNaN(time) && time < Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) {
throw new errors.Transaction.LockTimeTooEarly();
}
if (_.isDate(time)) {
time = time.getTime() / 1000;
}
for (var i = 0; i < this.inputs.length; i++) {
if (this.inputs[i].sequenceNumber === Input.DEFAULT_SEQNUMBER){
this.inputs[i].sequenceNumber = Input.DEFAULT_LOCKTIME_SEQNUMBER;
}
}
this.nLockTime = time;
return this;
};
/**
* Sets nLockTime so that transaction is not valid until the desired block
* height.
*
* @param {Number} height
* @return {Transaction} this
*/
Transaction.prototype.lockUntilBlockHeight = function(height) {
$.checkArgument(!isNaN(height));
if (height >= Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) {
throw new errors.Transaction.BlockHeightTooHigh();
}
if (height < 0) {
throw new errors.Transaction.NLockTimeOutOfRange();
}
for (var i = 0; i < this.inputs.length; i++) {
if (this.inputs[i].sequenceNumber === Input.DEFAULT_SEQNUMBER){
this.inputs[i].sequenceNumber = Input.DEFAULT_LOCKTIME_SEQNUMBER;
}
}
this.nLockTime = height;
return this;
};
/**
* Returns a semantic version of the transaction's nLockTime.
* @return {Number|Date}
* If nLockTime is 0, it returns null,
* if it is < 500000000, it returns a block height (number)
* else it returns a Date object.
*/
Transaction.prototype.getLockTime = function() {
if (!this.nLockTime) {
return null;
}
if (this.nLockTime < Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) {
return this.nLockTime;
}
return new Date(1000 * this.nLockTime);
};
Transaction.prototype.fromString = function(string) {
this.fromBuffer(buffer.Buffer.from(string, 'hex'));
};
Transaction.prototype._newTransaction = function() {
this.version = CURRENT_VERSION;
this.nLockTime = DEFAULT_NLOCKTIME;
};
/* Transaction creation interface */
/**
* @typedef {Object} Transaction~fromObject
* @property {string} prevTxId
* @property {number} outputIndex
* @property {(Buffer|string|Script)} script
* @property {number} satoshis
*/
/**
* 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
* ```javascript
* 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 {(Array.<Transaction~fromObject>|Transaction~fromObject)} utxo
* @param {Array=} pubkeys
* @param {number=} threshold
* @param {Object=} opts - Several options:
* - noSorting: defaults to false, if true and is multisig, don't
* sort the given public keys before creating the script
*/
Transaction.prototype.from = function(utxo, pubkeys, threshold, opts) {
if (Array.isArray(utxo)) {
for (const u of utxo) {
this.from(u, pubkeys, threshold, opts);
}
return this;
}
var exists = this.inputs.some(function(input) {
// TODO: Maybe prevTxId should be a string? Or defined as read only property?
return input.prevTxId.toString('hex') === utxo.txId && input.outputIndex === utxo.outputIndex;
});
if (exists) {
return this;
}
if (pubkeys && threshold) {
this._fromMultisigUtxo(utxo, pubkeys, threshold, opts);
} else if (utxo.publicKeys && utxo.publicKeys.length > 1) {
this._fromEscrowUtxo(utxo, utxo.publicKeys);
} else {
this._fromNonP2SH(utxo);
}
return this;
};
/**
* associateInputs - Update inputs with utxos, allowing you to specify value, and pubkey.
* Populating these inputs allows for them to be signed with .sign(privKeys)
*
* @param {Array<Object>} utxos
* @param {Array<string | PublicKey>} pubkeys
* @param {number} threshold
* @param {Object} opts
* @returns {Array<number>}
*/
Transaction.prototype.associateInputs = function(utxos, pubkeys, threshold, opts) {
let indexes = [];
for(let utxo of utxos) {
const index = this.inputs.findIndex(i => i.prevTxId.toString('hex') === utxo.txId && i.outputIndex === utxo.outputIndex);
indexes.push(index);
if(index >= 0) {
this.inputs[index] = this._getInputFrom(utxo, pubkeys, threshold, opts);
}
}
return indexes;
};
Transaction.prototype._selectInputType = function(utxo, pubkeys, threshold) {
var clazz;
utxo = new UnspentOutput(utxo);
if(pubkeys && threshold) {
if (utxo.script.isMultisigOut()) {
clazz = MultiSigInput;
} else if (utxo.script.isScriptHashOut() || utxo.script.isWitnessScriptHashOut()) {
clazz = MultiSigScriptHashInput;
}
} else if (utxo.script.isPublicKeyHashOut() || utxo.script.isWitnessPublicKeyHashOut() || utxo.script.isScriptHashOut()) {
clazz = PublicKeyHashInput;
} else if (utxo.script.isPublicKeyOut()) {
clazz = PublicKeyInput;
} else {
clazz = Input;
}
return clazz;
};
Transaction.prototype._getInputFrom = function(utxo, pubkeys, threshold, opts) {
utxo = new UnspentOutput(utxo);
const InputClass = this._selectInputType(utxo, pubkeys, threshold);
const input = {
output: new Output({
script: utxo.script,
satoshis: utxo.satoshis
}),
prevTxId: utxo.txId,
outputIndex: utxo.outputIndex,
sequenceNumber: utxo.sequenceNumber,
script: Script.empty()
};
let args = pubkeys && threshold ? [pubkeys, threshold, false, opts] : []
return new InputClass(input, ...args);
}
Transaction.prototype._fromEscrowUtxo = function(utxo, pubkeys) {
const publicKeys = pubkeys.map(pubkey => new PublicKey(pubkey));
const inputPublicKeys = publicKeys.slice(1);
const reclaimPublicKey = publicKeys[0];
utxo = new UnspentOutput(utxo);
this.addInput(
new EscrowInput(
{
output: new Output({
script: utxo.script,
satoshis: utxo.satoshis
}),
prevTxId: utxo.txId,
outputIndex: utxo.outputIndex,
script: Script.empty()
},
inputPublicKeys,
reclaimPublicKey
)
);
};
Transaction.prototype._fromNonP2SH = function(utxo) {
var clazz;
utxo = new UnspentOutput(utxo);
if (utxo.script.isPublicKeyHashOut()) {
clazz = PublicKeyHashInput;
} else if (utxo.script.isPublicKeyOut()) {
clazz = PublicKeyInput;
} else {
clazz = Input;
}
this.addInput(new clazz({
output: new Output({
script: utxo.script,
satoshis: utxo.satoshis
}),
prevTxId: utxo.txId,
outputIndex: utxo.outputIndex,
script: Script.empty()
}));
};
Transaction.prototype._fromMultisigUtxo = function(utxo, pubkeys, threshold, opts) {
$.checkArgument(threshold <= pubkeys.length,
'Number of required signatures must be greater than the number of public keys');
var clazz;
utxo = new UnspentOutput(utxo);
if (utxo.script.isMultisigOut()) {
clazz = MultiSigInput;
} else if (utxo.script.isScriptHashOut()) {
clazz = MultiSigScriptHashInput;
} else {
throw new Error("@TODO");
}
this.addInput(new clazz({
output: new Output({
script: utxo.script,
satoshis: utxo.satoshis
}),
prevTxId: utxo.txId,
outputIndex: utxo.outputIndex,
script: Script.empty()
}, pubkeys, threshold, undefined, opts));
};
/**
* Add an input to this transaction. The input must be an instance of the `Input` class.
* It should have information about the Output that it's spending, but if it's not already
* set, two additional parameters, `outputScript` and `satoshis` can be provided.
*
* @param {Input} input
* @param {String|Script} outputScript
* @param {number} satoshis
* @return Transaction this, for chaining
*/
Transaction.prototype.addInput = function(input, outputScript, satoshis) {
$.checkArgumentType(input, Input, 'input');
if (!input.output && (outputScript == null || satoshis == null)) {
throw new errors.Transaction.NeedMoreInfo('Need information about the UTXO script and satoshis');
}
if (!input.output && outputScript && satoshis != null) {
outputScript = outputScript instanceof Script ? outputScript : new Script(outputScript);
$.checkArgumentType(satoshis, 'number', 'satoshis');
input.output = new Output({
script: outputScript,
satoshis: satoshis
});
}
return this.uncheckedAddInput(input);
};
/**
* Add an input to this transaction, without checking that the input has information about
* the output that it's spending.
*
* @param {Input} input
* @return Transaction this, for chaining
*/
Transaction.prototype.uncheckedAddInput = function(input) {
$.checkArgumentType(input, Input, 'input');
this.inputs.push(input);
this._inputAmount = undefined;
this._updateChangeOutput();
return this;
};
/**
* Returns true if the transaction has enough info on all inputs to be correctly validated
*
* @return {boolean}
*/
Transaction.prototype.hasAllUtxoInfo = function() {
return this.inputs.every(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) {
$.checkArgument(!isNaN(amount), 'amount must be a number');
this._fee = amount;
this._updateChangeOutput();
return this;
};
/**
* Manually set the fee per KB 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 per KB to be sent
* @return {Transaction} this, for chaining
*/
Transaction.prototype.feePerKb = function(amount) {
$.checkArgument(!isNaN(amount), 'amount must be a number');
this._feePerKb = amount;
this._updateChangeOutput();
return this;
};
/**
* Manually set the fee per Byte 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).
* fee per Byte will be ignored if fee per KB is set
*
* @param {number} amount satoshis per Byte to be sent
* @return {Transaction} this, for chaining
*/
Transaction.prototype.feePerByte = function(amount) {
$.checkArgument(!isNaN(amount), 'amount must be a number');
this._feePerByte = amount;
this._updateChangeOutput();
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 {Address} address An address for change to be sent to.
* @return {Transaction} this, for chaining
*/
Transaction.prototype.change = function(address) {
$.checkArgument(address, 'address is required');
this._changeScript = Script.fromAddress(address);
this._updateChangeOutput();
return this;
};
/**
* Set the Zero-Confirmation Escrow (ZCE) address for this transaction
*
* @param {Address} address The Zero-Confirmation Escrow (ZCE) address for this payment
* @param {number} amount The amount in satoshis to send to the ZCE address
* @return {Transaction} this, for chaining
*/
Transaction.prototype.escrow = function(address, amount) {
$.checkArgument(this.inputs.length > 0, 'inputs must have already been set when setting escrow');
$.checkArgument(this.outputs.length > 0, 'non-change outputs must have already been set when setting escrow');
$.checkArgument(!this.getChangeOutput(), 'change must still be unset when setting escrow');
$.checkArgument(address, 'address is required');
$.checkArgument(amount, 'amount is required');
const totalSendAmountWithoutChange = this._getOutputAmount() + this.getFee() + amount;
const hasChange = this._getInputAmount() - totalSendAmountWithoutChange > Transaction.DUST_AMOUNT;
this.to(address, amount);
if(!hasChange) {
this._fee = undefined;
}
return this;
};
/**
* @return {Output} change output, if it exists
*/
Transaction.prototype.getChangeOutput = function() {
if (this._changeIndex != null) {
return this.outputs[this._changeIndex];
}
return null;
};
/**
* @typedef {Object} Transaction~toObject
* @property {(string|Address)} address
* @property {number} satoshis
*/
/**
* 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|Array.<Transaction~toObject>)} address
* @param {number} amount in satoshis
* @return {Transaction} this, for chaining
*/
Transaction.prototype.to = function(address, amount) {
if (Array.isArray(address)) {
var self = this;
for (const to of address) {
this.to(to.address, to.satoshis);
}
return this;
}
$.checkArgument(
JSUtil.isNaturalNumber(amount),
'Amount is expected to be a positive integer'
);
this.addOutput(new Output({
script: Script(new Address(address)),
satoshis: amount
}));
return this;
};
/**
* 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),
satoshis: 0
}));
return this;
};
/**
* Add an output to the transaction.
*
* @param {Output} output the output to add.
* @return {Transaction} this, for chaining
*/
Transaction.prototype.addOutput = function(output) {
$.checkArgumentType(output, Output, 'output');
this._addOutput(output);
this._updateChangeOutput();
return this;
};
/**
* Remove all outputs from the transaction.
*
* @return {Transaction} this, for chaining
*/
Transaction.prototype.clearOutputs = function() {
this.outputs = [];
this._clearSignatures();
this._outputAmount = undefined;
this._changeIndex = undefined;
this._updateChangeOutput();
return this;
};
Transaction.prototype._addOutput = function(output) {
this.outputs.push(output);
this._outputAmount = undefined;
};
/**
* Calculates or gets the total output amount in satoshis
*
* @return {Number} the transaction total output amount
*/
Transaction.prototype._getOutputAmount = function() {
if (this._outputAmount == null) {
this._outputAmount = 0;
for (const output of this.outputs) {
this._outputAmount += output.satoshis;
}
}
return this._outputAmount;
};
/**
* Calculates or gets the total input amount in satoshis
*
* @return {Number} the transaction total input amount
*/
Transaction.prototype._getInputAmount = function() {
if (this._inputAmount == null) {
this._inputAmount = _.sumBy(this.inputs, function(input) {
if (input.output == null) {
throw new errors.Transaction.Input.MissingPreviousOutput();
}
return input.output.satoshis;
});
}
return this._inputAmount;
};
Transaction.prototype._updateChangeOutput = function() {
if (!this._changeScript) {
return;
}
this._clearSignatures();
if (this._changeIndex != null) {
this._removeOutput(this._changeIndex);
}
var available = this._getUnspentValue();
var fee = this.getFee();
var changeAmount = available - fee;
if (changeAmount >= Transaction.DUST_AMOUNT) {
this._changeIndex = this.outputs.length;
this._addOutput(new Output({
script: this._changeScript,
satoshis: changeAmount
}));
} else {
this._changeIndex = undefined;
}
};
/**
* Calculates the fee of the transaction.
*
* If there's a fixed fee set, return that.
*
* If there is no change output set, the fee is the
* total value of the outputs minus inputs. Note that
* a serialized transaction only specifies the value
* of its outputs. (The value of inputs are recorded
* in the previous transaction outputs being spent.)
* This method therefore raises a "MissingPreviousOutput"
* error when called on a serialized transaction.
*
* If there's no fee set and no change address,
* estimate the fee based on size.
*
* @return {Number} fee of this transaction in satoshis
*/
Transaction.prototype.getFee = function() {
if (this.isCoinbase()) {
return 0;
}
if (this._fee != null) {
return this._fee;
}
// if no change output is set, fees should equal all the unspent amount
if (!this._changeScript) {
return this._getUnspentValue();
}
return this._estimateFee();
};
/**
* Estimates fee from serialized transaction size in bytes.
*/
Transaction.prototype._estimateFee = function () {
const estimatedSize = this._estimateSize();
const available = this._getUnspentValue();
const feeRate = this._feePerByte || (this._feePerKb || Transaction.FEE_PER_KB) / 1000;
function getFee(size) {
return size * feeRate;
}
const fee = Math.ceil(getFee(estimatedSize));
const feeWithChange = Math.ceil(getFee(estimatedSize) + getFee(this._estimateSizeOfChangeOutput()));
if (!this._changeScript || available <= feeWithChange) {
return fee;
}
return feeWithChange;
};
Transaction.prototype._estimateSizeOfChangeOutput = function () {
if (!this._changeScript) {
return 0;
}
const scriptLen = this._changeScript.toBuffer().length;
// 8 bytes for satoshis + script size + actual script size
return 8 + BufferWriter.varintBufNum(scriptLen).length + scriptLen;
};
Transaction.prototype._getUnspentValue = function() {
return this._getInputAmount() - this._getOutputAmount();
};
Transaction.prototype._clearSignatures = function() {
for (const input of this.inputs) {
input.clearSignatures();
}
};
/**
* Estimate the tx size before input signatures are added.
*/
Transaction.prototype._estimateSize = function() {
let result = 4 // version
result += BufferWriter.varintBufNum(this.inputs.length).length;
for (const input of this.inputs) {
result += input._estimateSize();
}
result += BufferWriter.varintBufNum(this.outputs.length).length;
for (const output of this.outputs) {
result += output.calculateSize();
}
result += 4; // nLockTime
return result;
};
Transaction.prototype._calculateSize = function() {
return this.toBuffer().length;
};
Transaction.prototype._removeOutput = function(index) {
var output = this.outputs[index];
this.outputs = _.without(this.outputs, output);
this._outputAmount = undefined;
};
Transaction.prototype.removeOutput = function(index) {
this._removeOutput(index);
this._updateChangeOutput();
};
/**
* Sort a transaction's inputs and outputs according to BIP69
*
* @see {https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki}
* @return {Transaction} this
*/
Transaction.prototype.sort = function() {
this.sortInputs(function(inputs) {
var copy = Array.prototype.concat.apply([], inputs);
let i = 0;
copy.forEach((x) => { x.i = i++});
copy.sort(function(first, second) {
return compare(first.prevTxId, second.prevTxId)
|| first.outputIndex - second.outputIndex
|| first.i - second.i; // to ensure stable sort
});
return copy;
});
this.sortOutputs(function(outputs) {
var copy = Array.prototype.concat.apply([], outputs);
let i = 0;
copy.forEach((x) => { x.i = i++});
copy.sort(function(first, second) {
return first.satoshis - second.satoshis
|| compare(first.script.toBuffer(), second.script.toBuffer())
|| first.i - second.i; // to ensure stable sort
});
return copy;
});
return this;
};
/**
* Randomize this transaction's outputs ordering. The shuffling algorithm is a
* version of the Fisher-Yates shuffle, provided by lodash's _.shuffle().
*
* @return {Transaction} this
*/
Transaction.prototype.shuffleOutputs = function() {
return this.sortOutputs(_.shuffle);
};
/**
* Sort this transaction's outputs, according to a given sorting function that
* takes an array as argument and returns a new array, with the same elements
* but with a different order. The argument function MUST NOT modify the order
* of the original array
*
* @param {Function} sortingFunction
* @return {Transaction} this
*/
Transaction.prototype.sortOutputs = function(sortingFunction) {
var outs = sortingFunction(this.outputs);
return this._newOutputOrder(outs);
};
/**
* Sort this transaction's inputs, according to a given sorting function that
* takes an array as argument and returns a new array, with the same elements
* but with a different order.
*
* @param {Function} sortingFunction
* @return {Transaction} this
*/
Transaction.prototype.sortInputs = function(sortingFunction) {
this.inputs = sortingFunction(this.inputs);
this._clearSignatures();
return this;
};
Transaction.prototype._newOutputOrder = function(newOutputs) {
var isInvalidSorting = (this.outputs.length !== newOutputs.length ||
_.difference(this.outputs, newOutputs).length !== 0);
if (isInvalidSorting) {
throw new errors.Transaction.InvalidSorting();
}
if (this._changeIndex != null) {
var changeOutput = this.outputs[this._changeIndex];
this._changeIndex = newOutputs.indexOf(changeOutput);
}
this.outputs = newOutputs;
return this;
};
Transaction.prototype.removeInput = function(txId, outputIndex) {
var index;
if (!outputIndex && !isNaN(txId)) {
index = txId;
} else {
index = this.inputs.findIndex(function(input) {
return input.prevTxId.toString('hex') === txId && input.outputIndex === outputIndex;
});
}
if (index < 0 || index >= this.inputs.length) {
throw new errors.Transaction.InvalidIndex(index, this.inputs.length);
}
var input = this.inputs[index];
this.inputs = _.without(this.inputs, input);
this._inputAmount = undefined;
this._updateChangeOutput();
};
/* Signature handling */
/**
* 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, signingMethod) {
signingMethod = signingMethod || "ecdsa"
$.checkState(this.hasAllUtxoInfo(), 'Not all utxo information is available to sign the transaction.');
if (Array.isArray(privateKey)) {
for (const pk of privateKey) {
this.sign(pk, sigtype, signingMethod);
}
return this;
}
for (const signature of this.getSignatures(privateKey, sigtype, signingMethod)) {
this.applySignature(signature, signingMethod);
}
return this;
};
Transaction.prototype.getSignatures = function(privKey, sigtype, signingMethod) {
privKey = new PrivateKey(privKey);
// By default, signs using ALL|FORKID
sigtype = sigtype || (Signature.SIGHASH_ALL | Signature.SIGHASH_FORKID);
var transaction = this;
var results = [];
var hashData = Hash.sha256ripemd160(privKey.publicKey.toBuffer());
for (let index = 0; index < this.inputs.length; index++) {
var input = this.inputs[index];
var signatures = input.getSignatures(transaction, privKey, index, sigtype, hashData, signingMethod);
for (let signature of signatures) {
results.push(signature);
}
}
return results;
};
/**
* Add a signature to the transaction
*
* @param {Object} signature
* @param {number} signature.inputIndex
* @param {number} signature.sigtype
* @param {PublicKey} signature.publicKey
* @param {Signature} signature.signature
* @param {String} signingMethod "ecdsa" or "schnorr"
* @return {Transaction} this, for chaining
*/
Transaction.prototype.applySignature = function(signature, signingMethod) {
this.inputs[signature.inputIndex].addSignature(this, signature, signingMethod);
return this;
};
Transaction.prototype.isFullySigned = function() {
for (const input of this.inputs) {
if (input.isFullySigned === Input.prototype.isFullySigned) {
throw new errors.Transaction.UnableToVerifySignature(
'Unrecognized script kind, or not enough information to execute script.' +
'This usually happens when creating a transaction from a serialized transaction'
);
}
}
return this.inputs.every(function(input) {
return input.isFullySigned();
});
};
Transaction.prototype.isValidSignature = function(signature) {
if (this.inputs[signature.inputIndex].isValidSignature === Input.prototype.isValidSignature) {
throw new errors.Transaction.UnableToVerifySignature(
'Unrecognized script kind, or not enough information to execute script.' +
'This usually happens when creating a transaction from a serialized transaction'
);
}
return this.inputs[signature.inputIndex].isValidSignature(this, signature);
};
/**
* @returns {bool} whether the signature is valid for this transaction input
*/
Transaction.prototype.verifySignature = function(sig, pubkey, nin, subscript, satoshisBN, flags, signingMethod) {
return Sighash.verify(this, sig, pubkey, nin, subscript, satoshisBN, flags, signingMethod);
};
/**
* @returns {bool} whether the token validation algorithm is satisifed by this transaction
*/
Transaction.prototype.validateTokens = function() {
const tokenInputs = this.inputs.filter(input => input.output.tokenData);
const tokenOutputs = this.outputs.filter(output => output.tokenData);
const outputsGroupedByCategory = _.groupBy(tokenOutputs, (output) => output.tokenData.category);
Object.values(outputsGroupedByCategory).forEach(categoryOutputs => {
const category = categoryOutputs[0].tokenData.category;
let unusedCategoryInputs = tokenInputs.filter(input => input.output.tokenData.category === category);
const inputFungibleAmount = unusedCategoryInputs.reduce((sum, input) => {
const tokenAmount = input.output.tokenData.amount;
return tokenAmount ? sum.add(tokenAmount) : sum;
}, new BN(0));
let mintedAmount = new BN(0);
let sentAmount = new BN(0);
categoryOutputs.forEach(output => {
const mintingUtxo = this.inputs.find(input => input.prevTxId.toString('hex') === output.tokenData.category);
const tokenAmount = output.tokenData.amount;
if (mintingUtxo) {
if (mintingUtxo.outputIndex !== 0) {
throw new Error('the transaction creates an immutable token for a category without a matching minting token or sufficient mutable tokens.');
}
mintedAmount = mintedAmount.add(tokenAmount);
} else {
if (output.tokenData.nft) {
const parentUtxo = unusedCategoryInputs.filter(input => input.output.tokenData.nft).find(input => {
if (output.tokenData.nft.capability === 'none') {
if (input.output.tokenData.nft.commitment === output.tokenData.nft.commitment) {
return true;
}
return input.output.tokenData.nft.capability !== 'none';
}
return input.output.tokenData.nft.capability !== 'none';
});
if (!parentUtxo) {
throw new Error('the transaction creates an immutable token for a category without a matching minting token or sufficient mutable tokens.');
}
if (parentUtxo.output.tokenData.nft.capability !== 'minting') {
unusedCategoryInputs = unusedCategoryInputs.filter(input => !(input.prevTxId === parentUtxo.prevTxId && input.outputIndex === parentUtxo.outputIndex));
}
}
sentAmount = sentAmount.add(tokenAmount);
}
});
if (mintedAmount.gt(new BN('9223372036854775807'))) {
throw new Error('the transaction outputs include a sum of fungible tokens for a category exceeding the maximum supply (9223372036854775807)');
}
if (sentAmount.gt(inputFungibleAmount)) {
throw new Error("the sum of fungible tokens in the transaction's outputs exceed that of the transactions inputs for a category");
}
});
return true;
};
/**
* Check that a transaction passes basic sanity tests. If not, return a string
* describing the error. This function contains the same logic as
* CheckTransaction in bitcoin core.
*/
Transaction.prototype.verify = function() {
// Basic checks that don't depend on any context
if (this.inputs.length === 0) {
return 'transaction txins empty';
}
if (this.outputs.length === 0) {
return 'transaction txouts empty';
}
// Check for negative or overflow output values
var valueoutbn = new BN(0);
for (var i = 0; i < this.outputs.length; i++) {
var txout = this.outputs[i];
if (txout.invalidSatoshis()) {
return 'transaction txout ' + i + ' satoshis is invalid';
}
if (txout._satoshisBN.gt(new BN(Transaction.MAX_MONEY, 10))) {
return 'transaction txout ' + i + ' greater than MAX_MONEY';
}
valueoutbn = valueoutbn.add(txout._satoshisBN);
if (valueoutbn.gt(new BN(Transaction.MAX_MONEY))) {
return 'transaction txout ' + i + ' total output greater than MAX_MONEY';
}
}
// Size limits
if (this.toBuffer().length > MAX_BLOCK_SIZE) {
return 'transaction over the maximum block size';
}
// Check for duplicate inputs
var txinmap = {};
for (i = 0; i < this.inputs.length; i++) {
var txin = this.inputs[i];
var inputid = txin.prevTxId + ':' + txin.outputIndex;
if (txinmap[inputid] != null) {
return 'transaction input ' + i + ' duplicate input';
}
txinmap[inputid] = true;
}
var isCoinbase = this.isCoinbase();
if (isCoinbase) {
var buf = this.inputs[0]._scriptBuffer;
if (buf.length < 2 || buf.length > 100) {
return 'coinbase transaction script size invalid';
}
} else {
for (i = 0; i < this.inputs.length; i++) {
if (this.inputs[i].isNull()) {
return 'transaction input ' + i + ' has null input';
}
}
}
return true;
};
Transaction.prototype.isZceSecured = function(escrowReclaimTx, instantAcceptanceEscrow, requiredFeeRate) {
// ZCE-secured transactions must not contain more than 2^16 inputs (65,536)
// https://github.com/bitjson/bch-zce#zce-root-hash
if(this.inputs.length > 65536) {
return false;
}
const allInputsAreP2pkh = this.inputs.every(input => input.script.isPublicKeyHashIn());
if (!allInputsAreP2pkh) {
return false;
}
const escrowInputIndex = 0;
let reclaimTx;
try {
reclaimTx = new Transaction(escrowReclaimTx);
} catch (e) {
return false;
}
const escrowInput = reclaimTx.inputs[escrowInputIndex];
if (escrowInput.prevTxId.toString('hex') !== this.id) {
return false;
}
const escrowUtxo = this.outputs[escrowInput.outputIndex];
if (!escrowUtxo) {
return false;
}
// The escrow address must contain the instantAcceptanceEscrow satoshis specified
// by the merchant plus the minimum required miner fee on the ZCE-secured payment.
// Rationale: https://github.com/bitjson/bch-zce#zce-extension-to-json-payment-protocol
const zceRawTx = this.uncheckedSerialize();
const zceTxSize = zceRawTx.length / 2;
const minFee = zceTxSize * requiredFeeRate;
if (escrowUtxo.toObject().satoshis < instantAcceptanceEscrow + minFee) {
return false;
}
escrowInput.output = escrowUtxo;
const reclaimTxSize = escrowReclaimTx.length / 2;
const reclaimTxFeeRate = reclaimTx.getFee() / reclaimTxSize;
if (reclaimTxFeeRate < requiredFeeRate) {
return false;
}
const escrowUnlockingScriptParts = escrowInput.script.toASM().split(' ');
if (escrowUnlockingScriptParts.length !== 3) {
return false;
}
const [reclaimSignatureString, reclaimPublicKeyString, redeemScriptString] = escrowUnlockingScriptParts;
const reclaimPublicKey = new PublicKey(reclaimPublicKeyString);
const inputPublicKeys = this.inputs.map(input => new PublicKey(input.script.getPublicKey()));
const inputSignatureStrings = this.inputs.map(input => input.script.toASM().split(' ')[0]);
const sighashAll = Signature.SIGHASH_ALL | Signature.SIGHASH_FORKID;
const allSignaturesSighashAll = [reclaimSignatureString, ...inputSignatureStrings].every(signatureString =>
signatureString.endsWith(sighashAll.toString(16))
);
if (!allSignaturesSighashAll) {
return false;
}
const correctEscrowRedeemScript = Script.buildEscrowOut(inputPublicKeys, reclaimPublicKey);
const correctEscrowRedeemScriptHash = Hash.sha256ripemd160(correctEscrowRedeemScript.toBuffer());
const escrowUtxoRedeemScriptHash = escrowUtxo.script.getData();
const escrowInputRedeemScript = new Script(redeemScriptString);
const escrowInputRedeemScriptHash = Hash.sha256ripemd160(escrowInputRedeemScript.toBuffer());
const allRedeemScriptHashes = [
correctEscrowRedeemScriptHash,
escrowInputRedeemScriptHash,
escrowUtxoRedeemScriptHash
].map(hash => hash.toString('hex'));
if (!allRedeemScriptHashes.every(hash => hash === allRedeemScriptHashes[0])) {
return false;
}
const reclaimSignature = Signature.fromTxString(reclaimSignatureString);
reclaimSignature.nhashtype = sighashAll;
const reclaimSigValid = reclaimTx.verifySignature(
reclaimSignature,
reclaimPublicKey,
escrowInputIndex,
escrowInputRedeemScript,
escrowUtxo.satoshisBN,
undefined,
reclaimSignature.isSchnorr ? 'schnorr' : 'ecdsa'
);
if (!reclaimSigValid) {
return false;
}
return true;
};
/**
* Analogous to bitcoind's IsCoinBase function in transaction.h
*/
Transaction.prototype.isCoinbase = function() {
return (this.inputs.length === 1 && this.inputs[0].isNull());
};
Transaction.prototype.setVersion = function(version) {
$.checkArgument(
JSUtil.isNaturalNumber(version) && version <= CURRENT_VERSION,
'Wrong version number');
this.version = version;
return this;
};
module.exports = Transaction;