commit
fc8f53372a
@ -6,8 +6,8 @@ BitcoreNode:
|
||||
Reporter: simple # none, simple, matrix
|
||||
LevelUp: ./db
|
||||
RPC:
|
||||
username: username
|
||||
password: password
|
||||
user: username
|
||||
pass: password
|
||||
protocol: http
|
||||
host: 127.0.0.1
|
||||
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() {
|
||||
var self = this;
|
||||
return Promise.try(function() {
|
||||
// TODO
|
||||
return new Promise(function(resolve, reject) {
|
||||
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`,
|
||||
* 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) {
|
||||
return transaction instanceof bitcore.Transaction;
|
||||
}), 'All transactions must be instances of bitcore.Transaction');
|
||||
return new bitcore.Block({
|
||||
var block = new bitcore.Block({
|
||||
header: new bitcore.BlockHeader({
|
||||
version: blockData.version,
|
||||
prevHash: bitcore.util.buffer.reverse(
|
||||
new bitcore.deps.Buffer(blockData.previousblockhash, 'hex')
|
||||
),
|
||||
prevHash: blockData.previousblockhash ?
|
||||
bitcore.util.buffer.reverse(
|
||||
new bitcore.deps.Buffer(blockData.previousblockhash, 'hex')
|
||||
) : bitcore.util.buffer.emptyBuffer(32),
|
||||
time: blockData.time,
|
||||
nonce: blockData.nonce,
|
||||
bits: new bitcore.deps.bnjs(
|
||||
new bitcore.deps.Buffer(blockData.bits, 'hex')
|
||||
),
|
||||
merkleRoot: bitcore.util.buffer.reverse(
|
||||
new bitcore.deps.Buffer(blockData.merkleRoot, 'hex')
|
||||
new bitcore.deps.Buffer(blockData.merkleroot, 'hex')
|
||||
)
|
||||
}),
|
||||
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
|
||||
*/
|
||||
var blockNotFound = function(err) {
|
||||
console.log(err);
|
||||
console.log(err, err.stack);
|
||||
return Promise.reject(new BitcoreNode.errors.Blocks.NotFound());
|
||||
};
|
||||
|
||||
@ -168,7 +180,7 @@ BlockService.prototype.getBlockByHeight = function(height) {
|
||||
|
||||
return Promise.try(function() {
|
||||
|
||||
return this.rpc.getBlockHash(height);
|
||||
return self.rpc.getBlockHash(height);
|
||||
|
||||
}).then(function(blockHash) {
|
||||
|
||||
@ -210,11 +222,11 @@ BlockService.prototype._confirmBlock = function(block) {
|
||||
|
||||
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) {
|
||||
return self.getBlock(block.header.prevHash);
|
||||
@ -226,21 +238,23 @@ BlockService.prototype._confirmBlock = function(block) {
|
||||
|
||||
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 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) {
|
||||
|
||||
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 NULLTXHASH = bitcore.util.buffer.emptyBuffer(32).toString('hex');
|
||||
var GENESISTX = '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b'
|
||||
|
||||
var helper = function(name) {
|
||||
return function(txId, output) {
|
||||
@ -80,6 +81,7 @@ function TransactionService (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;
|
||||
|
||||
TransactionService.transactionRPCtoBitcore = function(rpcResponse) {
|
||||
if (rpcResponse.error) {
|
||||
@ -92,6 +94,10 @@ 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);
|
||||
}).then(function(rawTransaction) {
|
||||
@ -104,7 +110,7 @@ TransactionService.prototype._confirmOutput = function(ops, block, transaction)
|
||||
ops.push({
|
||||
type: 'put',
|
||||
key: Index.getOutput(transaction.id, index),
|
||||
value: output.toObject()
|
||||
value: output.toJSON()
|
||||
});
|
||||
var address;
|
||||
// TODO: Move this logic to bitcore
|
||||
@ -118,7 +124,9 @@ TransactionService.prototype._confirmOutput = function(ops, block, transaction)
|
||||
ops.push({
|
||||
type: 'put',
|
||||
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({
|
||||
type: 'put',
|
||||
key: Index.getOutput(transaction.id, index),
|
||||
value: _.extend(input.toObject(), {
|
||||
value: JSON.stringify(_.extend(input.toObject(), {
|
||||
heightConfirmed: block.height
|
||||
})
|
||||
}))
|
||||
});
|
||||
var script = input.script;
|
||||
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) {
|
||||
ops.push({
|
||||
type: 'put',
|
||||
key: Index.getSpentOutputsForAddress(address, transaction.id, index),
|
||||
value: {
|
||||
value: JSON.stringify({
|
||||
heightSpent: block.height,
|
||||
spentTx: transaction.id,
|
||||
spentTxInputIndex: index,
|
||||
spendInput: input.toObject()
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -165,40 +175,37 @@ TransactionService.prototype._getAddressForInput = function(input) {
|
||||
|
||||
if (script.isPublicKeyHashIn()) {
|
||||
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
|
||||
));
|
||||
);
|
||||
} else if (script.isPublicKeyIn()) {
|
||||
return self.getTransaction(input.prevTxId.toString('hex')).then(function(transaction) {
|
||||
var outputScript = transaction.outputs[input.outputIndex].script;
|
||||
if (outputScript.isPublicKeyOut()) {
|
||||
return Promise.resolve(new bitcore.Address(
|
||||
return new bitcore.Address(
|
||||
bitcore.crypto.Hash.sha256ripemd160(outputScript.chunks[0].buf),
|
||||
bitcore.Networks.defaultNetwork, bitcore.Address.PayToPublicKeyHash
|
||||
));
|
||||
} else {
|
||||
return Promise.resolve(undefined);
|
||||
);
|
||||
}
|
||||
return;
|
||||
});
|
||||
} 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) {
|
||||
var self = this;
|
||||
return Promise.try(function() {
|
||||
ops.push({
|
||||
type: 'put',
|
||||
key: Index.getBlockForTransaction(transaction),
|
||||
value: block.id
|
||||
});
|
||||
return Promise.all(
|
||||
_.each(transaction.outputs, self._confirmOutput(ops, block, transaction))
|
||||
.concat(
|
||||
_.each(transaction.inputs, self._confirmInput(ops, block, transaction))
|
||||
));
|
||||
ops.push({
|
||||
type: 'put',
|
||||
key: Index.getBlockForTransaction(transaction),
|
||||
value: block.id
|
||||
});
|
||||
return Promise.all(
|
||||
_.map(transaction.outputs, self._confirmOutput(ops, block, transaction))
|
||||
.concat(
|
||||
_.map(transaction.inputs, self._confirmInput(ops, block, transaction))
|
||||
));
|
||||
};
|
||||
|
||||
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,
|
||||
height: 2,
|
||||
version: 1,
|
||||
merkleRoot: '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5',
|
||||
merkleroot: '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5',
|
||||
tx: [ '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5' ],
|
||||
time: 1231469744,
|
||||
nonce: 1639830024,
|
||||
|
||||
@ -71,7 +71,12 @@ describe('TransactionService', function() {
|
||||
it('confirms correctly the first transaction on genesis block', function(callback) {
|
||||
var ops = [];
|
||||
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',
|
||||
key: 'btx-4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b',
|
||||
value: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' },
|
||||
@ -84,7 +89,8 @@ describe('TransactionService', function() {
|
||||
key: 'txa-1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa-4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b-0',
|
||||
value:
|
||||
{ satoshis: 5000000000,
|
||||
script: '65 0x04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f OP_CHECKSIG' } }
|
||||
script: '65 0x04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f OP_CHECKSIG',
|
||||
heightConfirmed: 0} }
|
||||
]);
|
||||
callback();
|
||||
});
|
||||
@ -102,7 +108,12 @@ describe('TransactionService', 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',
|
||||
key: 'btx-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16',
|
||||
value: '00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee' },
|
||||
@ -115,7 +126,8 @@ describe('TransactionService', function() {
|
||||
key: 'txa-1Q2TWHE3GMdB6BZKafqwxXtWAWgFt5Jvm3-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-0',
|
||||
value:
|
||||
{ satoshis: 1000000000,
|
||||
script: '65 0x04ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c OP_CHECKSIG' } },
|
||||
script: '65 0x04ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84c OP_CHECKSIG',
|
||||
heightConfirmed: 170 } },
|
||||
{ type: 'put',
|
||||
key: 'txo-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-1',
|
||||
value:
|
||||
@ -125,7 +137,8 @@ describe('TransactionService', function() {
|
||||
key: 'txa-12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-1',
|
||||
value:
|
||||
{ satoshis: 4000000000,
|
||||
script: '65 0x0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3 OP_CHECKSIG' } },
|
||||
script: '65 0x0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3 OP_CHECKSIG',
|
||||
heightConfirmed: 170 } },
|
||||
{ type: 'put',
|
||||
key: 'txo-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-0',
|
||||
value:
|
||||
@ -133,7 +146,6 @@ describe('TransactionService', function() {
|
||||
outputIndex: 0,
|
||||
sequenceNumber: 4294967295,
|
||||
script: '71 0x304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901',
|
||||
output: undefined,
|
||||
heightConfirmed: 170 } },
|
||||
{ type: 'put',
|
||||
key: 'txas-12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S-f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16-0',
|
||||
@ -144,8 +156,7 @@ describe('TransactionService', function() {
|
||||
spendInput: { prevTxId: '0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9',
|
||||
outputIndex: 0,
|
||||
sequenceNumber: 4294967295,
|
||||
script: '71 0x304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901',
|
||||
output: undefined }}}]);
|
||||
script: '71 0x304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901' }}}]);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user