You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
365 lines
9.9 KiB
365 lines
9.9 KiB
'use strict';
|
|
|
|
var _ = require('lodash');
|
|
var buffer = require('buffer');
|
|
var assert = require('assert');
|
|
|
|
var util = require('../util/js');
|
|
var bufferUtil = require('../util/buffer');
|
|
var BufferReader = require('../encoding/bufferreader');
|
|
var BufferWriter = require('../encoding/bufferwriter');
|
|
var Hash = require('../crypto/hash');
|
|
var Sighash = require('./sighash');
|
|
var Signature = require('../crypto/signature');
|
|
|
|
var errors = require('../errors');
|
|
|
|
var Address = require('../address');
|
|
var Unit = require('../unit');
|
|
var Input = require('./input');
|
|
var Output = require('./output');
|
|
var Script = require('../script');
|
|
var PrivateKey = require('../privatekey');
|
|
|
|
var CURRENT_VERSION = 1;
|
|
var DEFAULT_NLOCKTIME = 0;
|
|
var DEFAULT_SEQNUMBER = 0xFFFFFFFF;
|
|
|
|
function Transaction(serialized) {
|
|
if (!(this instanceof Transaction)) {
|
|
return new Transaction(serialized);
|
|
}
|
|
this.inputs = [];
|
|
this.outputs = [];
|
|
this._outpoints = [];
|
|
this._utxos = {};
|
|
this._inputAmount = 0;
|
|
this._outputAmount = 0;
|
|
this._p2shMap = {};
|
|
this._signatures = {};
|
|
|
|
if (serialized) {
|
|
if (serialized instanceof Transaction) {
|
|
return Transaction.shallowCopy(serialized);
|
|
} else if (util.isHexa(serialized)) {
|
|
this.fromString(serialized);
|
|
} else if (bufferUtil.isBuffer(serialized)) {
|
|
this.fromBuffer(serialized);
|
|
} else if (_.isObject(serialized)) {
|
|
this.fromObject(serialized);
|
|
}
|
|
} else {
|
|
this._newTransaction();
|
|
}
|
|
}
|
|
|
|
/* Constructors and Serialization */
|
|
|
|
Transaction.shallowCopy = function(transaction) {
|
|
var copy = new Transaction(transaction.toBuffer());
|
|
return copy;
|
|
};
|
|
|
|
var hashProperty = {
|
|
configurable: false,
|
|
writeable: false,
|
|
get: function() {
|
|
return new BufferReader(this._getHash()).readReverse().toString('hex');
|
|
}
|
|
};
|
|
Object.defineProperty(Transaction.prototype, 'hash', hashProperty);
|
|
Object.defineProperty(Transaction.prototype, 'id', hashProperty);
|
|
|
|
Transaction.prototype._getHash = function() {
|
|
return Hash.sha256sha256(this.toBuffer());
|
|
};
|
|
|
|
Transaction.prototype.serialize = function() {
|
|
return this.toBuffer().toString('hex');
|
|
};
|
|
|
|
Transaction.prototype.toBuffer = function() {
|
|
var writer = new BufferWriter();
|
|
return this.toBufferWriter(writer).toBuffer();
|
|
};
|
|
|
|
Transaction.prototype.toBufferWriter = function(writer) {
|
|
writer.writeUInt32LE(this.version);
|
|
writer.writeVarintNum(this.inputs.length);
|
|
_.each(this.inputs, function(input) {
|
|
input.toBufferWriter(writer);
|
|
});
|
|
writer.writeVarintNum(this.outputs.length);
|
|
_.each(this.outputs, function(output) {
|
|
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) {
|
|
var i, sizeTxIns, sizeTxOuts;
|
|
|
|
this.version = reader.readUInt32LE();
|
|
sizeTxIns = reader.readVarintNum();
|
|
for (i = 0; i < sizeTxIns; i++) {
|
|
var input = Input.fromBufferReader(reader);
|
|
this.inputs.push(input);
|
|
this._outpoints.push(Transaction._makeOutpoint(input));
|
|
}
|
|
sizeTxOuts = reader.readVarintNum();
|
|
for (i = 0; i < sizeTxOuts; i++) {
|
|
this.outputs.push(Output.fromBufferReader(reader));
|
|
}
|
|
this.nLockTime = reader.readUInt32LE();
|
|
return this;
|
|
};
|
|
|
|
Transaction.prototype.fromJSON = function(json) {
|
|
var self = this;
|
|
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));
|
|
});
|
|
this.version = json.version;
|
|
this.nLockTime = json.nLockTime;
|
|
return this;
|
|
};
|
|
|
|
Transaction.prototype.toJSON = function() {
|
|
var inputs = [];
|
|
this.inputs.forEach(function(input) {
|
|
inputs.push(input.toJSON());
|
|
});
|
|
var outputs = [];
|
|
this.outputs.forEach(function(output) {
|
|
outputs.push(output.toJSON());
|
|
});
|
|
return {
|
|
version: this.version,
|
|
inputs: inputs,
|
|
outputs: outputs,
|
|
nLockTime: this.nLockTime
|
|
};
|
|
};
|
|
|
|
Transaction.prototype.fromString = function(string) {
|
|
this.fromBuffer(new buffer.Buffer(string, 'hex'));
|
|
};
|
|
|
|
Transaction.prototype.fromObject = function(object) {
|
|
};
|
|
|
|
Transaction.prototype._newTransaction = function() {
|
|
this.version = CURRENT_VERSION;
|
|
this.nLockTime = DEFAULT_NLOCKTIME;
|
|
};
|
|
|
|
/* Transaction creation interface */
|
|
|
|
Transaction.prototype.from = function(utxo, pubkeys, threshold) {
|
|
if (pubkeys && threshold) {
|
|
this._fromMultiSigP2SH(utxo, pubkeys, threshold);
|
|
} else {
|
|
this._fromNonP2SH(utxo);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
Transaction.prototype._fromMultiSigP2SH = function(utxo, pubkeys, threshold) {
|
|
throw new errors.NotImplemented('Transaction#_fromMultiSigP2SH');
|
|
};
|
|
|
|
Transaction.prototype._fromNonP2SH = function(utxo) {
|
|
var self = this;
|
|
if (_.isArray(utxo)) {
|
|
_.each(utxo, function(single) {
|
|
self._fromNonP2SH(single);
|
|
});
|
|
return;
|
|
}
|
|
if (Transaction._isNewUtxo(utxo)) {
|
|
this._fromNewUtxo(utxo);
|
|
} else if (Transaction._isOldUtxo(utxo)) {
|
|
this._fromOldUtxo(utxo);
|
|
} else {
|
|
throw new Transaction.Errors.UnrecognizedUtxoFormat(utxo);
|
|
}
|
|
};
|
|
|
|
Transaction._isOldUtxo = function(utxo) {
|
|
var isDefined = function(param) { return !_.isUndefined(param); };
|
|
return _.all(_.map([utxo.txid, utxo.vout, utxo.scriptPubKey, utxo.amount], isDefined));
|
|
};
|
|
|
|
Transaction.prototype._fromOldUtxo = function(utxo) {
|
|
return this._fromNewUtxo({
|
|
address: utxo.address && new Address(utxo.address),
|
|
txId: utxo.txid,
|
|
outputIndex: utxo.vout,
|
|
script: new buffer.Buffer(utxo.scriptPubKey, 'hex'),
|
|
satoshis: Unit.fromBTC(utxo.amount).satoshis
|
|
});
|
|
};
|
|
|
|
Transaction._isNewUtxo = function(utxo) {
|
|
var isDefined = function(param) { return !_.isUndefined(param); };
|
|
return _.all(_.map([utxo.txId, utxo.outputIndex, utxo.satoshis, utxo.script], isDefined));
|
|
};
|
|
|
|
Transaction.prototype._fromNewUtxo = function(utxo) {
|
|
var outpoint = Transaction._makeOutpoint(utxo);
|
|
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._utxos[outpoint] = utxo;
|
|
this._outpoints.push(outpoint);
|
|
this.inputs.push(new Input({
|
|
prevTxId: utxo.txId,
|
|
outputIndex: utxo.outputIndex,
|
|
sequenceNumber: DEFAULT_SEQNUMBER,
|
|
script: Script.empty()
|
|
}));
|
|
this._inputAmount += utxo.satoshis;
|
|
};
|
|
|
|
Transaction._makeOutpoint = function(data) {
|
|
if (!_.isUndefined(data.txId) && !_.isUndefined(data.outputIndex)) {
|
|
return data.txId + ':' + data.outputIndex;
|
|
}
|
|
if (!_.isUndefined(data.prevTxId) && !_.isUndefined(data.outputIndex)) {
|
|
var prevTxId = _.isString(data.prevTxId) ? data.prevTxId : data.prevTxId.toString('hex');
|
|
return prevTxId + ':' + data.outputIndex;
|
|
}
|
|
throw new Transaction.Errors.InvalidOutpointInfo(data);
|
|
};
|
|
|
|
Transaction.prototype.hasAllUtxoInfo = function() {
|
|
var self = this;
|
|
return _.all(this._outpoints.map(function(outpoint) {
|
|
return !!self._utxos[outpoint];
|
|
}));
|
|
};
|
|
|
|
Transaction.prototype.fee = function(amount) {
|
|
this._fee = amount;
|
|
return this;
|
|
};
|
|
|
|
/* Output management */
|
|
|
|
Transaction.prototype.change = function(address) {
|
|
this._change = address;
|
|
return this;
|
|
};
|
|
|
|
Transaction.prototype.to = function() {
|
|
// TODO: Type validation
|
|
var argSize = _.size(arguments);
|
|
if (argSize === 3) {
|
|
Transaction.prototype._payToMultisig.apply(this, arguments);
|
|
} else if (argSize === 2) {
|
|
Transaction.prototype._payToAddress.apply(this, arguments);
|
|
} else {
|
|
// TODO: Error
|
|
throw new Error('');
|
|
}
|
|
return this;
|
|
};
|
|
|
|
Transaction.prototype._payToMultisig = function(pubkeys, threshold, amount) {
|
|
throw new errors.NotImplemented('Transaction#_payToMultisig');
|
|
};
|
|
|
|
Transaction.prototype._payToAddress = function(address, amount) {
|
|
this._addOutput(new Output({
|
|
script: Script.buildPublicKeyHashOut(address),
|
|
satoshis: amount
|
|
}));
|
|
};
|
|
|
|
Transaction.prototype._addOutput = function(output) {
|
|
this.outputs.push(output);
|
|
this._outputAmount += output.satoshis;
|
|
};
|
|
|
|
Transaction.prototype.addData = function(value) {
|
|
this._addOutput(new Output({
|
|
script: Script.buildDataOut(value),
|
|
satoshis: 0
|
|
}));
|
|
return this;
|
|
};
|
|
|
|
/* Signature handling */
|
|
|
|
Transaction.prototype.sign = function(privKey) {
|
|
// TODO: Change for preconditions
|
|
assert(this.hasAllUtxoInfo());
|
|
var self = this;
|
|
if (_.isArray(privKey)) {
|
|
_.each(privKey, function(privKey) {
|
|
self.sign(privKey);
|
|
});
|
|
return this;
|
|
}
|
|
_.each(this.getSignatures(privKey), function(signature) {
|
|
self.applySignature(signature);
|
|
});
|
|
return this;
|
|
};
|
|
|
|
Transaction.prototype._getPrivateKeySignatures = function(privKey) {
|
|
privKey = new PrivateKey(privKey);
|
|
var transaction = this;
|
|
var results = [];
|
|
var hashData = Hash.sha256ripemd160(privKey.publicKey.toBuffer());
|
|
_.each(this._outpoints, function forEachOutput(outpoint, index) {
|
|
var utxo = transaction._utxos[outpoint];
|
|
if ((utxo.address && utxo.address.isPayToPublicKeyHash()) || utxo.script.isPublicKeyHashOut()) {
|
|
var address = new Address(utxo.address || utxo.script);
|
|
if (bufferUtil.equals(hashData, address.hashBuffer)) {
|
|
results.push({
|
|
publicKey: privKey.publicKey,
|
|
prevTxId: utxo.txId,
|
|
outputIndex: utxo.outputIndex,
|
|
inputIndex: index,
|
|
signature: Sighash.sign(transaction, privKey, Signature.SIGHASH_ALL, index, utxo.script),
|
|
sigtype: Signature.SIGHASH_ALL
|
|
});
|
|
} else {
|
|
;
|
|
}
|
|
} else {
|
|
;
|
|
}
|
|
});
|
|
return results;
|
|
};
|
|
|
|
Transaction.prototype.applySignature = function(signature) {
|
|
this.inputs[signature.inputIndex].setScript(
|
|
Script.buildPublicKeyHashIn(
|
|
signature.publicKey,
|
|
signature.signature.toDER(),
|
|
signature.sigtype
|
|
)
|
|
);
|
|
return this;
|
|
};
|
|
|
|
Transaction.prototype.getSignatures = function(privKey) {
|
|
return this._getPrivateKeySignatures(privKey);
|
|
};
|
|
|
|
module.exports = Transaction;
|
|
|