commit
fc8f53372a
@ -6,8 +6,8 @@ BitcoreNode:
|
|||||||
Reporter: simple # none, simple, matrix
|
Reporter: simple # none, simple, matrix
|
||||||
LevelUp: ./db
|
LevelUp: ./db
|
||||||
RPC:
|
RPC:
|
||||||
username: username
|
user: username
|
||||||
password: password
|
pass: password
|
||||||
protocol: http
|
protocol: http
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: 8332
|
port: 8332
|
||||||
|
|||||||
142
lib/services/address.js
Normal file
142
lib/services/address.js
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Promise = require('bluebird');
|
||||||
|
var bitcore = require('bitcore');
|
||||||
|
var TransactionService = require('./transaction');
|
||||||
|
var _ = bitcore.deps._;
|
||||||
|
|
||||||
|
var NULLTXHASH = bitcore.util.buffer.emptyBuffer(32).toString('hex');
|
||||||
|
var LASTTXHASH = bitcore.util.buffer.fill(bitcore.util.buffer.emptyBuffer(32), -1).toString('hex');
|
||||||
|
var MAXOUTPUT = 4294967295;
|
||||||
|
|
||||||
|
function AddressService(opts) {
|
||||||
|
opts = _.extend({}, opts);
|
||||||
|
this.transactionService = opts.transactionService;
|
||||||
|
this.blockService = opts.blockService;
|
||||||
|
this.database = opts.database || Promise.promisifyAll(new LevelUp(config.get('LevelUp')));
|
||||||
|
this.rpc = opts.rpc || Promise.promisifyAll(new RPC(config.get('RPC')));
|
||||||
|
}
|
||||||
|
|
||||||
|
AddressService.prototype.getSummary = function(address, confirmations) {
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var tip, allOutputs, spent;
|
||||||
|
|
||||||
|
return Promise.try(function() {
|
||||||
|
|
||||||
|
return self.blockService.getLatest();
|
||||||
|
|
||||||
|
}).then(function(latest) {
|
||||||
|
|
||||||
|
tip = latest;
|
||||||
|
return self.getAllOutputs(address);
|
||||||
|
|
||||||
|
}).then(function(outputs) {
|
||||||
|
|
||||||
|
allOutputs = outputs;
|
||||||
|
return self.getSpent(address);
|
||||||
|
|
||||||
|
}).then(function(spent) {
|
||||||
|
|
||||||
|
return self.buildAddressSummary(address, tip, allOutputs, spent, confirmations);
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
AddressService.processOutput = function(data) {
|
||||||
|
var elements = data.key.split('-');
|
||||||
|
var output = _.extend(JSON.parse(data.value), {
|
||||||
|
address: elements[1],
|
||||||
|
txId: elements[2],
|
||||||
|
outputIndex: elements[3]
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
var retrieveOutputs = function(indexFunction, processElement) {
|
||||||
|
return function(address) {
|
||||||
|
var results = [];
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
self.database.createReadStream({
|
||||||
|
gte: indexFunction(address, NULLTXHASH, 0),
|
||||||
|
lte: indexFunction(address, LASTTXHASH, MAXOUTPUT)
|
||||||
|
}).on('data', function(element) {
|
||||||
|
results.push(processElement(element));
|
||||||
|
}).on('error', reject).on('end', function() {
|
||||||
|
return resolve(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
AddressService.prototype.getAllOutputs = retrieveOutputs(
|
||||||
|
TransactionService.Index.getOutputsForAddress,
|
||||||
|
function(e) {
|
||||||
|
return AddressService.processOutput(e);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
AddressService.prototype.getSpent = retrieveOutputs(
|
||||||
|
TransactionService.Index.getSpentOutputsForAddress,
|
||||||
|
function(element) {
|
||||||
|
return JSON.parse(element.value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
AddressService.prototype.buildAddressSummary = function(address, tip, allOutputs, spent, confirmations) {
|
||||||
|
|
||||||
|
var result = {};
|
||||||
|
var transactionsAppended = {};
|
||||||
|
confirmations = confirmations || 6;
|
||||||
|
|
||||||
|
result.address = address.toString();
|
||||||
|
result.transactions = [];
|
||||||
|
|
||||||
|
result.confirmed = {
|
||||||
|
balance: 0,
|
||||||
|
sent: 0,
|
||||||
|
received: 0
|
||||||
|
};
|
||||||
|
result.unconfirmed = {
|
||||||
|
balance: 0,
|
||||||
|
sent: 0,
|
||||||
|
received: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
var outputValues = {};
|
||||||
|
|
||||||
|
_.each(allOutputs, function(output) {
|
||||||
|
var value = output.satoshis;
|
||||||
|
outputValues[output.txId + '-' + output.outputIndex] = value;
|
||||||
|
result.unconfirmed.balance += value;
|
||||||
|
result.unconfirmed.received += value;
|
||||||
|
if (tip.height - output.heightConfirmed + 1 >= confirmations) {
|
||||||
|
result.confirmed.balance += value;
|
||||||
|
result.confirmed.received += value;
|
||||||
|
}
|
||||||
|
if (!transactionsAppended[output.txId]) {
|
||||||
|
transactionsAppended[output.txId] = true;
|
||||||
|
result.transactions.push(output.txId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_.each(spent, function(output) {
|
||||||
|
var value = outputValues[output.spendInput.prevTxId + '-' + output.spendInput.outputIndex];
|
||||||
|
|
||||||
|
if (!transactionsAppended[output.spentTx]) {
|
||||||
|
transactionsAppended[output.spentTx] = true;
|
||||||
|
result.transactions.push(output.spentTx);
|
||||||
|
}
|
||||||
|
result.unconfirmed.balance -= value;
|
||||||
|
result.unconfirmed.sent += value;
|
||||||
|
if (tip.height - output.heightSpent + 1 >= confirmations) {
|
||||||
|
result.confirmed.balance -= value;
|
||||||
|
result.confirmed.sent += value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = AddressService;
|
||||||
@ -63,11 +63,20 @@ function BlockService (opts) {
|
|||||||
|
|
||||||
BlockService.prototype.writeLock = function() {
|
BlockService.prototype.writeLock = function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
return Promise.try(function() {
|
return new Promise(function(resolve, reject) {
|
||||||
// TODO
|
if (self.lock) {
|
||||||
|
return reject();
|
||||||
|
} else {
|
||||||
|
self.lock = true;
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BlockService.prototype.unlock = function() {
|
||||||
|
this.lock = false;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms data as received from an RPC result structure for `getblock`,
|
* Transforms data as received from an RPC result structure for `getblock`,
|
||||||
* plus a list of transactions, and build a block based on thosė.
|
* plus a list of transactions, and build a block based on thosė.
|
||||||
@ -88,23 +97,26 @@ BlockService.blockRPCtoBitcore = function(blockData, transactions) {
|
|||||||
$.checkArgument(_.all(transactions, function(transaction) {
|
$.checkArgument(_.all(transactions, function(transaction) {
|
||||||
return transaction instanceof bitcore.Transaction;
|
return transaction instanceof bitcore.Transaction;
|
||||||
}), 'All transactions must be instances of bitcore.Transaction');
|
}), 'All transactions must be instances of bitcore.Transaction');
|
||||||
return new bitcore.Block({
|
var block = new bitcore.Block({
|
||||||
header: new bitcore.BlockHeader({
|
header: new bitcore.BlockHeader({
|
||||||
version: blockData.version,
|
version: blockData.version,
|
||||||
prevHash: bitcore.util.buffer.reverse(
|
prevHash: blockData.previousblockhash ?
|
||||||
new bitcore.deps.Buffer(blockData.previousblockhash, 'hex')
|
bitcore.util.buffer.reverse(
|
||||||
),
|
new bitcore.deps.Buffer(blockData.previousblockhash, 'hex')
|
||||||
|
) : bitcore.util.buffer.emptyBuffer(32),
|
||||||
time: blockData.time,
|
time: blockData.time,
|
||||||
nonce: blockData.nonce,
|
nonce: blockData.nonce,
|
||||||
bits: new bitcore.deps.bnjs(
|
bits: new bitcore.deps.bnjs(
|
||||||
new bitcore.deps.Buffer(blockData.bits, 'hex')
|
new bitcore.deps.Buffer(blockData.bits, 'hex')
|
||||||
),
|
),
|
||||||
merkleRoot: bitcore.util.buffer.reverse(
|
merkleRoot: bitcore.util.buffer.reverse(
|
||||||
new bitcore.deps.Buffer(blockData.merkleRoot, 'hex')
|
new bitcore.deps.Buffer(blockData.merkleroot, 'hex')
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
transactions: transactions
|
transactions: transactions
|
||||||
});
|
});
|
||||||
|
block.height = blockData.height;
|
||||||
|
return block;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,7 +126,7 @@ BlockService.blockRPCtoBitcore = function(blockData, transactions) {
|
|||||||
* @return {Promise} a promise that will always be rejected
|
* @return {Promise} a promise that will always be rejected
|
||||||
*/
|
*/
|
||||||
var blockNotFound = function(err) {
|
var blockNotFound = function(err) {
|
||||||
console.log(err);
|
console.log(err, err.stack);
|
||||||
return Promise.reject(new BitcoreNode.errors.Blocks.NotFound());
|
return Promise.reject(new BitcoreNode.errors.Blocks.NotFound());
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -168,7 +180,7 @@ BlockService.prototype.getBlockByHeight = function(height) {
|
|||||||
|
|
||||||
return Promise.try(function() {
|
return Promise.try(function() {
|
||||||
|
|
||||||
return this.rpc.getBlockHash(height);
|
return self.rpc.getBlockHash(height);
|
||||||
|
|
||||||
}).then(function(blockHash) {
|
}).then(function(blockHash) {
|
||||||
|
|
||||||
@ -210,11 +222,11 @@ BlockService.prototype._confirmBlock = function(block) {
|
|||||||
|
|
||||||
var ops = [];
|
var ops = [];
|
||||||
|
|
||||||
this.writeLock().then(
|
return this.writeLock().then(function() {
|
||||||
|
|
||||||
self._setNextBlock.bind(self, ops, block.header.prevHash, block)
|
return self._setNextBlock(ops, block.header.prevHash, block);
|
||||||
|
|
||||||
).then(function() {
|
}).then(function() {
|
||||||
|
|
||||||
if (block.header.prevHash.toString('hex') !== NULLBLOCKHASH) {
|
if (block.header.prevHash.toString('hex') !== NULLBLOCKHASH) {
|
||||||
return self.getBlock(block.header.prevHash);
|
return self.getBlock(block.header.prevHash);
|
||||||
@ -226,21 +238,23 @@ BlockService.prototype._confirmBlock = function(block) {
|
|||||||
|
|
||||||
return self._setBlockHeight(ops, block, parent.height + 1);
|
return self._setBlockHeight(ops, block, parent.height + 1);
|
||||||
|
|
||||||
}).then(
|
}).then(function() {
|
||||||
|
|
||||||
self._setBlockByTs.bind(self, ops, block)
|
return self._setBlockByTs(ops, block);
|
||||||
|
|
||||||
).then(function() {
|
}).then(function() {
|
||||||
|
|
||||||
return Promise.all(block.transactions.map(function(transaction) {
|
return Promise.all(block.transactions.map(function(transaction) {
|
||||||
return self.transactionService._confirmTransaction(ops, block, transaction);
|
return self.transactionService._confirmTransaction(ops, block, transaction);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
}).then(
|
}).then(function() {
|
||||||
|
|
||||||
self.database.batchAsync.bind(self, ops)
|
return self.database.batchAsync(ops)
|
||||||
|
|
||||||
).then(this.unlock);
|
}).then(function() {
|
||||||
|
return self.unlock();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockService.prototype._setNextBlock = function(ops, prevBlockHash, block) {
|
BlockService.prototype._setNextBlock = function(ops, prevBlockHash, block) {
|
||||||
|
|||||||
6
lib/services/data/genesistx.js
Normal file
6
lib/services/data/genesistx.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = ''
|
||||||
|
+ '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff'
|
||||||
|
+ '4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72'
|
||||||
|
+ '206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff'
|
||||||
|
+ '0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f'
|
||||||
|
+ '61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000';
|
||||||
@ -25,6 +25,7 @@ var _ = bitcore.deps._;
|
|||||||
var $ = bitcore.util.preconditions;
|
var $ = bitcore.util.preconditions;
|
||||||
|
|
||||||
var NULLTXHASH = bitcore.util.buffer.emptyBuffer(32).toString('hex');
|
var NULLTXHASH = bitcore.util.buffer.emptyBuffer(32).toString('hex');
|
||||||
|
var GENESISTX = '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b'
|
||||||
|
|
||||||
var helper = function(name) {
|
var helper = function(name) {
|
||||||
return function(txId, output) {
|
return function(txId, output) {
|
||||||
@ -80,6 +81,7 @@ function TransactionService (opts) {
|
|||||||
this.database = opts.database || Promise.promisifyAll(new LevelUp(config.get('LevelUp')));
|
this.database = opts.database || Promise.promisifyAll(new LevelUp(config.get('LevelUp')));
|
||||||
this.rpc = opts.rpc || Promise.promisifyAll(new RPC(config.get('RPC')));
|
this.rpc = opts.rpc || Promise.promisifyAll(new RPC(config.get('RPC')));
|
||||||
}
|
}
|
||||||
|
TransactionService.Index = Index;
|
||||||
|
|
||||||
TransactionService.transactionRPCtoBitcore = function(rpcResponse) {
|
TransactionService.transactionRPCtoBitcore = function(rpcResponse) {
|
||||||
if (rpcResponse.error) {
|
if (rpcResponse.error) {
|
||||||
@ -92,6 +94,10 @@ TransactionService.prototype.getTransaction = function(transactionId) {
|
|||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
if (transactionId === GENESISTX) {
|
||||||
|
return new bitcore.Transaction(require('./data/genesistx'));
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.try(function() {
|
return Promise.try(function() {
|
||||||
return self.rpc.getRawTransactionAsync(transactionId);
|
return self.rpc.getRawTransactionAsync(transactionId);
|
||||||
}).then(function(rawTransaction) {
|
}).then(function(rawTransaction) {
|
||||||
@ -104,7 +110,7 @@ TransactionService.prototype._confirmOutput = function(ops, block, transaction)
|
|||||||
ops.push({
|
ops.push({
|
||||||
type: 'put',
|
type: 'put',
|
||||||
key: Index.getOutput(transaction.id, index),
|
key: Index.getOutput(transaction.id, index),
|
||||||
value: output.toObject()
|
value: output.toJSON()
|
||||||
});
|
});
|
||||||
var address;
|
var address;
|
||||||
// TODO: Move this logic to bitcore
|
// TODO: Move this logic to bitcore
|
||||||
@ -118,7 +124,9 @@ TransactionService.prototype._confirmOutput = function(ops, block, transaction)
|
|||||||
ops.push({
|
ops.push({
|
||||||
type: 'put',
|
type: 'put',
|
||||||
key: Index.getOutputsForAddress(address, transaction.id, index),
|
key: Index.getOutputsForAddress(address, transaction.id, index),
|
||||||
value: output.toObject()
|
value: JSON.stringify(_.extend(output.toObject(), {
|
||||||
|
heightConfirmed: block.height
|
||||||
|
}))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -133,26 +141,28 @@ TransactionService.prototype._confirmInput = function(ops, block, transaction) {
|
|||||||
ops.push({
|
ops.push({
|
||||||
type: 'put',
|
type: 'put',
|
||||||
key: Index.getOutput(transaction.id, index),
|
key: Index.getOutput(transaction.id, index),
|
||||||
value: _.extend(input.toObject(), {
|
value: JSON.stringify(_.extend(input.toObject(), {
|
||||||
heightConfirmed: block.height
|
heightConfirmed: block.height
|
||||||
})
|
}))
|
||||||
});
|
});
|
||||||
var script = input.script;
|
var script = input.script;
|
||||||
if (!(script.isPublicKeyHashIn() || script.isPublicKeyIn() || script.isScriptHashIn())) {
|
if (!(script.isPublicKeyHashIn() || script.isPublicKeyIn() || script.isScriptHashIn())) {
|
||||||
return Promise.resolve();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return self._getAddressForInput(input).then(function(address) {
|
return Promise.try(function() {
|
||||||
|
return self._getAddressForInput(input)
|
||||||
|
}).then(function(address) {
|
||||||
if (address) {
|
if (address) {
|
||||||
ops.push({
|
ops.push({
|
||||||
type: 'put',
|
type: 'put',
|
||||||
key: Index.getSpentOutputsForAddress(address, transaction.id, index),
|
key: Index.getSpentOutputsForAddress(address, transaction.id, index),
|
||||||
value: {
|
value: JSON.stringify({
|
||||||
heightSpent: block.height,
|
heightSpent: block.height,
|
||||||
spentTx: transaction.id,
|
spentTx: transaction.id,
|
||||||
spentTxInputIndex: index,
|
spentTxInputIndex: index,
|
||||||
spendInput: input.toObject()
|
spendInput: input.toObject()
|
||||||
}
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -165,40 +175,37 @@ TransactionService.prototype._getAddressForInput = function(input) {
|
|||||||
|
|
||||||
if (script.isPublicKeyHashIn()) {
|
if (script.isPublicKeyHashIn()) {
|
||||||
var hash = bitcore.crypto.Hash.sha256ripemd160(script.chunks[0].buf);
|
var hash = bitcore.crypto.Hash.sha256ripemd160(script.chunks[0].buf);
|
||||||
return Promise.resolve(new bitcore.Address(
|
return new bitcore.Address(
|
||||||
hash, bitcore.Networks.defaultNetwork, bitcore.Address.PayToPublicKeyHash
|
hash, bitcore.Networks.defaultNetwork, bitcore.Address.PayToPublicKeyHash
|
||||||
));
|
);
|
||||||
} else if (script.isPublicKeyIn()) {
|
} else if (script.isPublicKeyIn()) {
|
||||||
return self.getTransaction(input.prevTxId.toString('hex')).then(function(transaction) {
|
return self.getTransaction(input.prevTxId.toString('hex')).then(function(transaction) {
|
||||||
var outputScript = transaction.outputs[input.outputIndex].script;
|
var outputScript = transaction.outputs[input.outputIndex].script;
|
||||||
if (outputScript.isPublicKeyOut()) {
|
if (outputScript.isPublicKeyOut()) {
|
||||||
return Promise.resolve(new bitcore.Address(
|
return new bitcore.Address(
|
||||||
bitcore.crypto.Hash.sha256ripemd160(outputScript.chunks[0].buf),
|
bitcore.crypto.Hash.sha256ripemd160(outputScript.chunks[0].buf),
|
||||||
bitcore.Networks.defaultNetwork, bitcore.Address.PayToPublicKeyHash
|
bitcore.Networks.defaultNetwork, bitcore.Address.PayToPublicKeyHash
|
||||||
));
|
);
|
||||||
} else {
|
|
||||||
return Promise.resolve(undefined);
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve(new bitcore.Script(script.chunks[script.chunks.length - 1]).toAddress());
|
return new bitcore.Script(script.chunks[script.chunks.length - 1]).toAddress();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
TransactionService.prototype._confirmTransaction = function(ops, block, transaction) {
|
TransactionService.prototype._confirmTransaction = function(ops, block, transaction) {
|
||||||
var self = this;
|
var self = this;
|
||||||
return Promise.try(function() {
|
ops.push({
|
||||||
ops.push({
|
type: 'put',
|
||||||
type: 'put',
|
key: Index.getBlockForTransaction(transaction),
|
||||||
key: Index.getBlockForTransaction(transaction),
|
value: block.id
|
||||||
value: block.id
|
|
||||||
});
|
|
||||||
return Promise.all(
|
|
||||||
_.each(transaction.outputs, self._confirmOutput(ops, block, transaction))
|
|
||||||
.concat(
|
|
||||||
_.each(transaction.inputs, self._confirmInput(ops, block, transaction))
|
|
||||||
));
|
|
||||||
});
|
});
|
||||||
|
return Promise.all(
|
||||||
|
_.map(transaction.outputs, self._confirmOutput(ops, block, transaction))
|
||||||
|
.concat(
|
||||||
|
_.map(transaction.inputs, self._confirmInput(ops, block, transaction))
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = TransactionService;
|
module.exports = TransactionService;
|
||||||
|
|||||||
264
test/services/address.js
Normal file
264
test/services/address.js
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var sinon = require('sinon');
|
||||||
|
var should = require('chai').should();
|
||||||
|
var events = require('events');
|
||||||
|
var Promise = require('bluebird');
|
||||||
|
|
||||||
|
var bitcore = require('bitcore');
|
||||||
|
var _ = bitcore.deps._;
|
||||||
|
|
||||||
|
var AddressService = require('../../lib/services/address');
|
||||||
|
|
||||||
|
describe('AddressService', function() {
|
||||||
|
|
||||||
|
var database, rpc, blockService, transactionService, service;
|
||||||
|
|
||||||
|
function initialize() {
|
||||||
|
database = {};
|
||||||
|
rpc = {};
|
||||||
|
blockService = {};
|
||||||
|
transactionService = {};
|
||||||
|
service = new AddressService({
|
||||||
|
database: database,
|
||||||
|
transactionService: transactionService,
|
||||||
|
blockService: blockService,
|
||||||
|
rpc: rpc
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('initializes correctly', function() {
|
||||||
|
initialize();
|
||||||
|
should.exist(service);
|
||||||
|
});
|
||||||
|
|
||||||
|
var thenCaller = {
|
||||||
|
then: function(arg) {
|
||||||
|
return arg();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('getSummary', function() {
|
||||||
|
|
||||||
|
beforeEach(initialize);
|
||||||
|
|
||||||
|
it('calls internal functions as expected', function(done) {
|
||||||
|
service.blockService = { getLatest: sinon.mock() };
|
||||||
|
service.getAllOutputs = sinon.mock();
|
||||||
|
service.getSpent = sinon.mock();
|
||||||
|
service.buildAddressSummary = sinon.mock();
|
||||||
|
|
||||||
|
service.blockService.getLatest.onFirstCall().returns(thenCaller);
|
||||||
|
service.getAllOutputs.onFirstCall().returns(thenCaller);
|
||||||
|
service.getSpent.onFirstCall().returns(thenCaller);
|
||||||
|
service.buildAddressSummary.onFirstCall().returns(thenCaller);
|
||||||
|
|
||||||
|
var address = 'address';
|
||||||
|
var confirmations = 100;
|
||||||
|
var promise = service.getSummary(address, confirmations);
|
||||||
|
promise.then(function() {
|
||||||
|
|
||||||
|
service.blockService.getLatest.calledOnce.should.equal(true);
|
||||||
|
service.getAllOutputs.calledOnce.should.equal(true);
|
||||||
|
service.getSpent.calledOnce.should.equal(true);
|
||||||
|
service.buildAddressSummary.calledOnce.should.equal(true);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('processOutput works as expected', function() {
|
||||||
|
AddressService.processOutput({
|
||||||
|
key: 'txas-A-B-C',
|
||||||
|
value: '{"a": "b"}'
|
||||||
|
}).should.deep.equal({
|
||||||
|
address: 'A',
|
||||||
|
txId: 'B',
|
||||||
|
outputIndex: 'C',
|
||||||
|
a: 'b'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getAllOutputs rejects promise on error', function(done) {
|
||||||
|
var dataCall = new events.EventEmitter();
|
||||||
|
var address = '12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S';
|
||||||
|
service.database.createReadStream = sinon.mock();
|
||||||
|
service.database.createReadStream.onFirstCall().returns(dataCall);
|
||||||
|
service.getAllOutputs(address).catch(done);
|
||||||
|
dataCall.emit('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getSpent rejects promise on error', function(done) {
|
||||||
|
var address = '12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S';
|
||||||
|
var dataCall = new events.EventEmitter();
|
||||||
|
service.database.createReadStream = sinon.mock();
|
||||||
|
service.database.createReadStream.onFirstCall().returns(dataCall);
|
||||||
|
service.getSpent(address).catch(done);
|
||||||
|
dataCall.emit('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getAllOutputs calls the expected functions', function(done) {
|
||||||
|
service.database.createReadStream = sinon.mock();
|
||||||
|
var dataCall = new events.EventEmitter();
|
||||||
|
service.database.createReadStream.onFirstCall().returns(dataCall);
|
||||||
|
|
||||||
|
AddressService.processOutput = sinon.stub(AddressService, 'processOutput');
|
||||||
|
AddressService.processOutput.onFirstCall().returns('processed');
|
||||||
|
|
||||||
|
var element = {key: 'key', value: 'value'};
|
||||||
|
var address = '12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S';
|
||||||
|
service.getAllOutputs(address).then(function(arg) {
|
||||||
|
service.database.createReadStream.firstCall.args[0].should.deep.equal(
|
||||||
|
{
|
||||||
|
gte: 'txa-12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S-'
|
||||||
|
+ '0000000000000000000000000000000000000000000000000000000000000000-0',
|
||||||
|
lte: 'txa-12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S-'
|
||||||
|
+ 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff-4294967295'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
AddressService.processOutput.firstCall.args[0].should.equal(element);
|
||||||
|
AddressService.processOutput.reset();
|
||||||
|
arg[0].should.equal('processed');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
dataCall.emit('data', element);
|
||||||
|
dataCall.emit('end');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getSpent calls the expected functions', function(done) {
|
||||||
|
service.database.createReadStream = sinon.mock();
|
||||||
|
var dataCall = new events.EventEmitter();
|
||||||
|
service.database.createReadStream.onFirstCall().returns(dataCall);
|
||||||
|
|
||||||
|
var element = {key: 'key', value: JSON.stringify({a: 'b'})};
|
||||||
|
var address = '12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S';
|
||||||
|
service.getSpent(address).then(function(arg) {
|
||||||
|
service.database.createReadStream.firstCall.args[0].should.deep.equal(
|
||||||
|
{
|
||||||
|
gte: 'txas-12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S-'
|
||||||
|
+ '0000000000000000000000000000000000000000000000000000000000000000-0',
|
||||||
|
lte: 'txas-12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S-'
|
||||||
|
+ 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff-4294967295'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
arg[0].should.deep.equal({a: 'b'});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
dataCall.emit('data', element);
|
||||||
|
dataCall.emit('end');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildAddressSummary', function() {
|
||||||
|
|
||||||
|
beforeEach(initialize);
|
||||||
|
var address = new bitcore.Address('12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S');
|
||||||
|
var tip = {
|
||||||
|
height: 10
|
||||||
|
};
|
||||||
|
var allOutputs = [
|
||||||
|
{
|
||||||
|
satoshis: 10,
|
||||||
|
txId: 'A',
|
||||||
|
outputIndex: 1,
|
||||||
|
heightConfirmed: 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
it('calculates balance correctly for confirmed balance', function() {
|
||||||
|
var allOutputs = [ { satoshis: 10, txId: 'A', outputIndex: 1, heightConfirmed: 1 } ];
|
||||||
|
var spendOutputs = [];
|
||||||
|
|
||||||
|
service.buildAddressSummary(address, tip, allOutputs, spendOutputs).should.deep.equal({
|
||||||
|
address: address.toString(),
|
||||||
|
transactions: ['A'],
|
||||||
|
confirmed: { balance: 10, sent: 0, received: 10 },
|
||||||
|
unconfirmed: { balance: 10, sent: 0, received: 10 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calculates balance correctly for unconfirmed balance', function() {
|
||||||
|
var allOutputs = [
|
||||||
|
{ satoshis: 20, txId: 'B', outputIndex: 1, heightConfirmed: 10 }
|
||||||
|
];
|
||||||
|
var spendOutputs = [ ];
|
||||||
|
|
||||||
|
service.buildAddressSummary(address, tip, allOutputs, spendOutputs).should.deep.equal({
|
||||||
|
address: address.toString(),
|
||||||
|
transactions: ['B'],
|
||||||
|
confirmed: { balance: 0, sent: 0, received: 0 },
|
||||||
|
unconfirmed: { balance: 20, sent: 0, received: 20 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works with multiple transactions', function() {
|
||||||
|
var allOutputs = [
|
||||||
|
{ satoshis: 10, txId: 'A', outputIndex: 1, heightConfirmed: 1 },
|
||||||
|
{ satoshis: 20, txId: 'B', outputIndex: 1, heightConfirmed: 10 }
|
||||||
|
];
|
||||||
|
var spendOutputs = [
|
||||||
|
{ spendInput: { prevTxId: 'A', outputIndex: 1 }, spentTx: 'A', heightSpent: 10 }
|
||||||
|
];
|
||||||
|
|
||||||
|
service.buildAddressSummary(address, tip, allOutputs, spendOutputs).should.deep.equal({
|
||||||
|
address: address.toString(),
|
||||||
|
transactions: ['A', 'B'],
|
||||||
|
confirmed: { balance: 10, sent: 0, received: 10 },
|
||||||
|
unconfirmed: { balance: 20, sent: 10, received: 30 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works with a medium amount of transactions', function() {
|
||||||
|
var allOutputs = [
|
||||||
|
{ satoshis: 10, txId: 'A', outputIndex: 1, heightConfirmed: 1 },
|
||||||
|
{ satoshis: 20, txId: 'B', outputIndex: 1, heightConfirmed: 5 },
|
||||||
|
{ satoshis: 30, txId: 'C', outputIndex: 1, heightConfirmed: 10 }
|
||||||
|
];
|
||||||
|
var spendOutputs = [
|
||||||
|
{ spendInput: { prevTxId: 'A', outputIndex: 1 }, spentTx: 'D', heightSpent: 10 }
|
||||||
|
];
|
||||||
|
|
||||||
|
service.buildAddressSummary(address, tip, allOutputs, spendOutputs).should.deep.equal({
|
||||||
|
address: address.toString(),
|
||||||
|
transactions: ['A', 'B', 'C', 'D'],
|
||||||
|
confirmed: { balance: 30, sent: 0, received: 30 },
|
||||||
|
unconfirmed: { balance: 50, sent: 10, received: 60 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works with a transaction that includes twice the same address', function() {
|
||||||
|
var allOutputs = [
|
||||||
|
{ satoshis: 10, txId: 'A', outputIndex: 0, heightConfirmed: 1 },
|
||||||
|
{ satoshis: 10, txId: 'A', outputIndex: 1, heightConfirmed: 1 },
|
||||||
|
];
|
||||||
|
var spendOutputs = [];
|
||||||
|
|
||||||
|
service.buildAddressSummary(address, tip, allOutputs, spendOutputs).should.deep.equal({
|
||||||
|
address: address.toString(),
|
||||||
|
transactions: ['A'],
|
||||||
|
confirmed: { balance: 20, sent: 0, received: 20 },
|
||||||
|
unconfirmed: { balance: 20, sent: 0, received: 20 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('confirmed spent transactions change the balance', function() {
|
||||||
|
var allOutputs = [
|
||||||
|
{ satoshis: 10, txId: 'A', outputIndex: 0, heightConfirmed: 1 },
|
||||||
|
];
|
||||||
|
var spendOutputs = [
|
||||||
|
{ spendInput: { prevTxId: 'A', outputIndex: 0 }, spentTx: 'D', heightSpent: 2 }
|
||||||
|
];
|
||||||
|
|
||||||
|
service.buildAddressSummary(address, tip, allOutputs, spendOutputs).should.deep.equal({
|
||||||
|
address: address.toString(),
|
||||||
|
transactions: ['A', 'D'],
|
||||||
|
confirmed: { balance: 0, sent: 10, received: 10 },
|
||||||
|
unconfirmed: { balance: 0, sent: 10, received: 10 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ describe('BlockService', function() {
|
|||||||
size: 215,
|
size: 215,
|
||||||
height: 2,
|
height: 2,
|
||||||
version: 1,
|
version: 1,
|
||||||
merkleRoot: '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5',
|
merkleroot: '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5',
|
||||||
tx: [ '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5' ],
|
tx: [ '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5' ],
|
||||||
time: 1231469744,
|
time: 1231469744,
|
||||||
nonce: 1639830024,
|
nonce: 1639830024,
|
||||||
|
|||||||
@ -71,7 +71,12 @@ describe('TransactionService', function() {
|
|||||||
it('confirms correctly the first transaction on genesis block', function(callback) {
|
it('confirms correctly the first transaction on genesis block', function(callback) {
|
||||||
var ops = [];
|
var ops = [];
|
||||||
service._confirmTransaction(ops, genesisBlock, genesisTx).then(function() {
|
service._confirmTransaction(ops, genesisBlock, genesisTx).then(function() {
|
||||||
ops.should.deep.equal([
|
ops.map(function(k) {
|
||||||
|
if (bitcore.util.js.isValidJSON(k.value)) {
|
||||||
|
k.value = JSON.parse(k.value);
|
||||||
|
}
|
||||||
|
return k;
|
||||||
|
}).should.deep.equal([
|
||||||
{ type: 'put',
|
{ type: 'put',
|
||||||
key: 'btx-4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b',
|
key: 'btx-4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b',
|
||||||
value: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' },
|
value: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' },
|
||||||
@ -84,7 +89,8 @@ describe('TransactionService', function() {
|
|||||||
key: 'txa-1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa-4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b-0',
|
key: 'txa-1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa-4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b-0',
|
||||||
value:
|
value:
|
||||||
{ satoshis: 5000000000,
|
{ satoshis: 5000000000,
|
||||||
script: '65 0x04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f OP_CHECKSIG' } }
|
script: '65 0x04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f OP_CHECKSIG',
|
||||||
|
heightConfirmed: 0} }
|
||||||
]);
|
]);
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
@ -102,7 +108,12 @@ describe('TransactionService', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
service._confirmTransaction(ops, block170, block170.transactions[1]).then(function() {
|
service._confirmTransaction(ops, block170, block170.transactions[1]).then(function() {
|
||||||
ops.should.deep.equal([
|
ops.map(function(k) {
|
||||||
|
if (bitcore.util.js.isValidJSON(k.value)) {
|
||||||
|
k.value = JSON.parse(k.value);
|
||||||
|
}
|
||||||
|
return k;
|
||||||
|
}).should.deep.equal([
|
||||||
{ type: 'put',
|
{ type: 'put',
|
||||||
key: 'btx-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16',
|
key: 'btx-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16',
|
||||||
value: '00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee' },
|
value: '00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee' },
|
||||||
@ -115,7 +126,8 @@ describe('TransactionService', function() {
|
|||||||
key: 'txa-1Q2TWHE3GMdB6BZKafqwxXtWAWgFt5Jvm3-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-0',
|
key: 'txa-1Q2TWHE3GMdB6BZKafqwxXtWAWgFt5Jvm3-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-0',
|
||||||
value:
|
value:
|
||||||
{ satoshis: 1000000000,
|
{ satoshis: 1000000000,
|
||||||
script: '65 0x04ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c OP_CHECKSIG' } },
|
script: '65 0x04ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c OP_CHECKSIG',
|
||||||
|
heightConfirmed: 170 } },
|
||||||
{ type: 'put',
|
{ type: 'put',
|
||||||
key: 'txo-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-1',
|
key: 'txo-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-1',
|
||||||
value:
|
value:
|
||||||
@ -125,7 +137,8 @@ describe('TransactionService', function() {
|
|||||||
key: 'txa-12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-1',
|
key: 'txa-12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-1',
|
||||||
value:
|
value:
|
||||||
{ satoshis: 4000000000,
|
{ satoshis: 4000000000,
|
||||||
script: '65 0x0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3 OP_CHECKSIG' } },
|
script: '65 0x0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3 OP_CHECKSIG',
|
||||||
|
heightConfirmed: 170 } },
|
||||||
{ type: 'put',
|
{ type: 'put',
|
||||||
key: 'txo-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-0',
|
key: 'txo-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-0',
|
||||||
value:
|
value:
|
||||||
@ -133,7 +146,6 @@ describe('TransactionService', function() {
|
|||||||
outputIndex: 0,
|
outputIndex: 0,
|
||||||
sequenceNumber: 4294967295,
|
sequenceNumber: 4294967295,
|
||||||
script: '71 0x304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901',
|
script: '71 0x304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901',
|
||||||
output: undefined,
|
|
||||||
heightConfirmed: 170 } },
|
heightConfirmed: 170 } },
|
||||||
{ type: 'put',
|
{ type: 'put',
|
||||||
key: 'txas-12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-0',
|
key: 'txas-12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-0',
|
||||||
@ -144,8 +156,7 @@ describe('TransactionService', function() {
|
|||||||
spendInput: { prevTxId: '0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9',
|
spendInput: { prevTxId: '0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9',
|
||||||
outputIndex: 0,
|
outputIndex: 0,
|
||||||
sequenceNumber: 4294967295,
|
sequenceNumber: 4294967295,
|
||||||
script: '71 0x304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901',
|
script: '71 0x304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901' }}}]);
|
||||||
output: undefined }}}]);
|
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user