diff --git a/config/default.yml b/config/default.yml index a5386a21..5b0d42ac 100644 --- a/config/default.yml +++ b/config/default.yml @@ -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 diff --git a/lib/services/address.js b/lib/services/address.js new file mode 100644 index 00000000..47c37896 --- /dev/null +++ b/lib/services/address.js @@ -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; diff --git a/lib/services/block.js b/lib/services/block.js index 48a38946..712e6f37 100644 --- a/lib/services/block.js +++ b/lib/services/block.js @@ -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) { diff --git a/lib/services/data/genesistx.js b/lib/services/data/genesistx.js new file mode 100644 index 00000000..29b5b6fc --- /dev/null +++ b/lib/services/data/genesistx.js @@ -0,0 +1,6 @@ +module.exports = '' + + '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff' + + '4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72' + + '206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff' + + '0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f' + + '61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000'; diff --git a/lib/services/transaction.js b/lib/services/transaction.js index c2fc3e49..375b5d85 100644 --- a/lib/services/transaction.js +++ b/lib/services/transaction.js @@ -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; diff --git a/test/services/address.js b/test/services/address.js new file mode 100644 index 00000000..7f8ad05b --- /dev/null +++ b/test/services/address.js @@ -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 } + }); + }); + + }); +}); + diff --git a/test/services/block.js b/test/services/block.js index 9834a528..f3706012 100644 --- a/test/services/block.js +++ b/test/services/block.js @@ -42,7 +42,7 @@ describe('BlockService', function() { size: 215, height: 2, version: 1, - merkleRoot: '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5', + merkleroot: '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5', tx: [ '9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5' ], time: 1231469744, nonce: 1639830024, diff --git a/test/services/transaction.js b/test/services/transaction.js index bdd0b4ae..07561780 100644 --- a/test/services/transaction.js +++ b/test/services/transaction.js @@ -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(); }); });