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