flocore-node/lib/services/transaction.js
2015-07-07 15:14:33 -04:00

203 lines
6.5 KiB
JavaScript

/**
* @file service/transaction.js
*
* This implementation stores a set of indexes so quick queries are possible.
* An "index" for the purposes of this explanation is a structure for a set
* of keys to the LevelDB key/value store so that both the key and values can be
* sequentially accesed, which is a fast operation on LevelDB.
*
* Map of transaction to related addresses:
* * address-<address>-<ts>-<transaction>-<outputIndex> -> true (unspent)
* -> <spendTxId:inputIndex>
* * output-<transaction>-<outputIndex> -> { script, amount, spendTxId, spendIndex }
* * input-<transaction>-<inputIndex> -> { script, amount, prevTxId, outputIndex, output }
*
*/
'use strict';
var RPC = require('bitcoind-rpc');
var LevelUp = require('levelup');
var Promise = require('bluebird');
var bitcore = require('bitcore');
var config = require('config');
var errors = require('../errors');
var _ = bitcore.deps._;
var $ = bitcore.util.preconditions;
var GENESISTX = '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b';
var helper = function(name) {
return function(txId, output) {
if (txId instanceof bitcore.Transaction) {
txId = txId.hash;
}
$.checkArgument(_.isString(txId), 'txId must be a string');
$.checkArgument(_.isNumber(output), 'output must be a number');
return name + txId + '-' + output;
};
};
var helperAddress = function(index) {
return function(address, txid, number) {
if (_.isString(address)) {
address = new bitcore.Address(address);
}
$.checkArgument(address instanceof bitcore.Address, 'address must be a string or bitcore.Address');
$.checkArgument(bitcore.util.js.isHexa(txid), 'TXID must be an hexa string');
$.checkArgument(_.isNumber(number), 'Input number must be a number');
return index + address.toString() + '-' + txid + '-' + number;
};
};
var Index = {
output: 'txo-', // txo-<txid>-<n> -> serialized Output
spent: 'txs-', // txo-<txid>-<n>-<spend txid>-<m> -> block height of confirmation for spend
address: 'txa-', // txa-<address>-<txid>-<n> -> Output
addressSpent: 'txas-',
// txa-<address>-<txid>-<n> -> {
// heightSpent: number, (may be -1 for unconfirmed tx)
// spentTx: string, spentTxInputIndex: number, spendInput: Input
// }
transaction: 'btx-' // btx-<txid> -> block in main chain that confirmed the tx
};
_.extend(Index, {
getOutput: helper(Index.output),
getSpentHeight: helper(Index.spent),
getOutputsForAddress: helperAddress(Index.address),
getSpentOutputsForAddress: helperAddress(Index.addressSpent),
getBlockForTransaction: function(transaction) {
if (_.isString(transaction)) {
return Index.transaction + transaction;
} else if (transaction instanceof bitcore.Transaction) {
return Index.transaction + transaction.id;
} else {
throw new bitcore.errors.InvalidArgument(transaction + ' is not a transaction');
}
}
});
function TransactionService(opts) {
opts = _.extend({}, opts);
this.database = opts.database || Promise.promisifyAll(new LevelUp(config.get('LevelUp')));
this.rpc = opts.rpc || Promise.promisifyAll(new RPC(config.get('RPC')));
}
TransactionService.Index = Index;
var txNotFound = function(error) {
if (error.message === 'No information available about transaction') {
throw new errors.Transactions.NotFound();
}
throw error;
};
TransactionService.transactionRPCtoBitcore = function(rpcResponse) {
return new bitcore.Transaction(rpcResponse.result);
};
TransactionService.prototype.getTransaction = function(transactionId) {
var self = this;
if (transactionId === GENESISTX) {
return new bitcore.Transaction(require('./data/genesistx'));
}
return Promise.try(function() {
return self.rpc.getRawTransactionAsync(transactionId);
})
.catch(txNotFound)
.then(function(rawTransaction) {
return TransactionService.transactionRPCtoBitcore(rawTransaction);
});
};
TransactionService.prototype._confirmOutput = function(ops, block, transaction) {
var txid = transaction.id;
return function(output, index) {
ops.push({
type: 'put',
key: Index.getOutput(txid, index),
value: output.toJSON()
});
var script = output.script;
if (!script || !(script.isPublicKeyHashOut() || script.isScriptHashOut())) {
return;
}
var address = output.script.toAddress();
//console.log('o', address.type, address.toString());
var obj = output.toObject();
obj.heightConfirmed = block.height;
ops.push({
type: 'put',
key: Index.getOutputsForAddress(address, txid, index),
value: JSON.stringify(obj)
});
};
};
TransactionService.prototype._confirmInput = function(ops, block, transaction) {
var txid = transaction.id;
return function(input, index) {
if (input.isNull()) {
return Promise.resolve();
}
ops.push({
type: 'put',
key: Index.getOutput(txid, index),
value: JSON.stringify(_.extend(input.toObject(), {
heightConfirmed: block.height
}))
});
var script = input.script;
if (!script || !(script.isPublicKeyHashIn() || script.isScriptHashIn())) {
return Promise.resolve();
}
return Promise.try(function() {
var address = input.script.toAddress();
//console.log('i', address.type, address.toString());
ops.push({
type: 'put',
key: Index.getSpentOutputsForAddress(address, txid, index),
value: JSON.stringify({
heightSpent: block.height,
spentTx: txid,
spentTxInputIndex: index,
spendInput: input.toObject()
})
});
});
};
};
TransactionService.prototype._confirmTransaction = function(ops, block, transaction) {
var self = this;
ops.push({
type: 'put',
key: Index.getBlockForTransaction(transaction),
value: block.id
});
var confirmFunctions =
_.map(transaction.outputs, self._confirmOutput(ops, block, transaction))
.concat(
_.map(transaction.inputs, self._confirmInput(ops, block, transaction))
);
return Promise.all(confirmFunctions);
};
TransactionService.prototype._unconfirmTransaction = function(ops, block, transaction) {
var self = this;
ops.push({
type: 'del',
key: Index.getBlockForTransaction(transaction),
value: block.id
});
return Promise.all(
_.map(transaction.outputs, self._unconfirmOutput(ops, block, transaction))
.concat(
_.map(transaction.inputs, self._unconfirmInput(ops, block, transaction))
));
};
module.exports = TransactionService;