bitcoind: add getDetailedTransaction method

Adds a new method getDetailedTransaction with a standard JavaScript object
with block information, address, amounts and fees. And removes the
getTransactionWithBlockInfo method since this new method is equivalent,
and will serialize over an API correctly.

Also includes a new method getBlockOverview to get the txids for a block,
that can be combined with getDetailedTransaction for viewing block
transactions with additional information.
This commit is contained in:
Braydon Fuller 2016-05-12 18:07:39 -04:00
parent 950a9d521c
commit 8bddf4f0d6
5 changed files with 409 additions and 245 deletions

View File

@ -113,7 +113,8 @@ Bitcoin.prototype._initCaches = function() {
this.txidsCache = LRU(50000);
this.balanceCache = LRU(50000);
this.summaryCache = LRU(50000);
this.transactionInfoCache = LRU(100000);
this.blockOverviewCache = LRU(144);
this.transactionDetailedCache = LRU(100000);
// caches valid indefinitely
this.transactionCache = LRU(100000);
@ -150,6 +151,7 @@ Bitcoin.prototype.getAPIMethods = function() {
['getBlock', this, this.getBlock, 1],
['getRawBlock', this, this.getRawBlock, 1],
['getBlockHeader', this, this.getBlockHeader, 1],
['getBlockOverview', this, this.getBlockOverview, 1],
['getBlockHashesByTimestamp', this, this.getBlockHashesByTimestamp, 2],
['getBestBlockHash', this, this.getBestBlockHash, 0],
['getSpentInfo', this, this.getSpentInfo, 1],
@ -157,8 +159,8 @@ Bitcoin.prototype.getAPIMethods = function() {
['syncPercentage', this, this.syncPercentage, 0],
['isSynced', this, this.isSynced, 0],
['getRawTransaction', this, this.getRawTransaction, 1],
['getTransaction', this, this.getTransaction, 2],
['getTransactionWithBlockInfo', this, this.getTransactionWithBlockInfo, 2],
['getTransaction', this, this.getTransaction, 1],
['getDetailedTransaction', this, this.getDetailedTransaction, 1],
['sendTransaction', this, this.sendTransaction, 1],
['estimateFee', this, this.estimateFee, 1],
['getAddressTxids', this, this.getAddressTxids, 2],
@ -320,11 +322,12 @@ Bitcoin.prototype._checkConfigIndexes = function(spawnConfig, node) {
};
Bitcoin.prototype._resetCaches = function() {
this.transactionInfoCache.reset();
this.transactionDetailedCache.reset();
this.utxosCache.reset();
this.txidsCache.reset();
this.balanceCache.reset();
this.summaryCache.reset();
this.blockOverviewCache.reset();
};
Bitcoin.prototype._tryAll = function(func, callback) {
@ -1096,8 +1099,8 @@ Bitcoin.prototype.getAddressTxids = function(addressArg, options, callback) {
Bitcoin.prototype._getConfirmationsDetail = function(transaction) {
$.checkState(this.height > 0, 'current height is unknown');
var confirmations = 0;
if (transaction.__height >= 0) {
confirmations = this.height - transaction.__height + 1;
if (transaction.height >= 0) {
confirmations = this.height - transaction.height + 1;
}
if (confirmations < 0) {
log.warn('Negative confirmations calculated for transaction:', transaction.hash);
@ -1106,44 +1109,38 @@ Bitcoin.prototype._getConfirmationsDetail = function(transaction) {
};
Bitcoin.prototype._getAddressDetailsForInput = function(input, inputIndex, result, addressStrings) {
if (!input.script) {
if (!input.address) {
return;
}
var inputAddress = input.script.toAddress(this.node.network);
if (inputAddress) {
var inputAddressString = inputAddress.toString();
if (addressStrings.indexOf(inputAddressString) >= 0) {
if (!result.addresses[inputAddressString]) {
result.addresses[inputAddressString] = {
inputIndexes: [inputIndex],
outputIndexes: []
};
} else {
result.addresses[inputAddressString].inputIndexes.push(inputIndex);
}
result.satoshis -= input.output.satoshis;
var address = input.address;
if (addressStrings.indexOf(address) >= 0) {
if (!result.addresses[address]) {
result.addresses[address] = {
inputIndexes: [inputIndex],
outputIndexes: []
};
} else {
result.addresses[address].inputIndexes.push(inputIndex);
}
result.satoshis -= input.satoshis;
}
};
Bitcoin.prototype._getAddressDetailsForOutput = function(output, outputIndex, result, addressStrings) {
if (!output.script) {
if (!output.address) {
return;
}
var outputAddress = output.script.toAddress(this.node.network);
if (outputAddress) {
var outputAddressString = outputAddress.toString();
if (addressStrings.indexOf(outputAddressString) >= 0) {
if (!result.addresses[outputAddressString]) {
result.addresses[outputAddressString] = {
inputIndexes: [],
outputIndexes: [outputIndex]
};
} else {
result.addresses[outputAddressString].outputIndexes.push(outputIndex);
}
result.satoshis += output.satoshis;
var address = output.address;
if (addressStrings.indexOf(address) >= 0) {
if (!result.addresses[address]) {
result.addresses[address] = {
inputIndexes: [],
outputIndexes: [outputIndex]
};
} else {
result.addresses[address].outputIndexes.push(outputIndex);
}
result.satoshis += output.satoshis;
}
};
@ -1163,6 +1160,8 @@ Bitcoin.prototype._getAddressDetailsForTransaction = function(transaction, addre
this._getAddressDetailsForOutput(output, outputIndex, result, addressStrings);
}
$.checkState(Number.isFinite(result.satoshis));
return result;
};
@ -1171,35 +1170,25 @@ Bitcoin.prototype._getAddressDetailsForTransaction = function(transaction, addre
* @param {Object} txid - A bitcoin transaction id
* @param {Function} callback
*/
Bitcoin.prototype._getDetailedTransaction = function(txid, options, next) {
Bitcoin.prototype._getAddressDetailedTransaction = function(txid, options, next) {
var self = this;
self.getTransactionWithBlockInfo(
self.getDetailedTransaction(
txid,
function(err, transaction) {
if (err) {
return next(err);
}
transaction.populateInputs(self, [], function(err) {
if (err) {
return next(err);
}
var addressDetails = self._getAddressDetailsForTransaction(transaction, options.addressStrings);
var addressDetails = self._getAddressDetailsForTransaction(transaction, options.addressStrings);
var details = {
addresses: addressDetails.addresses,
satoshis: addressDetails.satoshis,
height: transaction.__height,
confirmations: self._getConfirmationsDetail(transaction),
timestamp: transaction.__timestamp,
// TODO bitcore-lib should return null instead of throwing error on coinbase
fees: !transaction.isCoinbase() ? transaction.getFee() : null,
tx: transaction
};
next(null, details);
});
var details = {
addresses: addressDetails.addresses,
satoshis: addressDetails.satoshis,
confirmations: self._getConfirmationsDetail(transaction),
tx: transaction
};
next(null, details);
}
);
};
@ -1270,7 +1259,7 @@ Bitcoin.prototype.getAddressHistory = function(addressArg, options, callback) {
async.mapSeries(
txids,
function(txid, next) {
self._getDetailedTransaction(txid, {
self._getAddressDetailedTransaction(txid, {
queryMempool: queryMempool,
addressStrings: addressStrings
}, next);
@ -1437,6 +1426,69 @@ Bitcoin.prototype.getRawBlock = function(blockArg, callback) {
}
};
/**
* Similar to getBlockHeader but will include a list of txids
* @param {String|Number} block - A block hash or block height number
* @param {Function} callback
*/
Bitcoin.prototype.getBlockOverview = function(blockArg, callback) {
var self = this;
function queryBlock(blockhash) {
var cachedBlock = self.blockOverviewCache.get(blockhash);
if (cachedBlock) {
return setImmediate(function() {
callback(null, cachedBlock);
});
} else {
self._tryAll(function(done) {
self.client.getBlock(blockhash, true, function(err, response) {
if (err) {
return done(self._wrapRPCError(err));
}
var result = response.result;
var blockOverview = {
hash: result.hash,
version: result.version,
confirmations: result.confirmations,
height: result.height,
chainWork: result.chainwork,
prevHash: result.previousblockhash,
nextHash: result.nextblockhash,
merkleRoot: result.merkleroot,
time: result.time,
medianTime: result.mediantime,
nonce: result.nonce,
bits: result.bits,
difficulty: result.difficulty,
txids: result.tx
};
self.blockOverviewCache.set(blockhash, blockOverview);
done(null, blockOverview);
});
}, callback);
}
}
if (_.isNumber(blockArg)) {
self._tryAll(function(done) {
self.client.getBlockHash(blockArg, function(err, response) {
if (err) {
return done(self._wrapRPCError(err));
}
done(null, response.result);
});
}, function(err, blockhash) {
if (err) {
return callback(err);
}
queryBlock(blockhash);
});
} else {
queryBlock(blockArg);
}
};
/**
* Will retrieve a block as a Bitcore object
* @param {String|Number} block - A block hash or block height number
@ -1672,19 +1724,101 @@ Bitcoin.prototype.getTransaction = function(txid, callback) {
};
/**
* Will get a transaction as Bitcore Transaction with additional fields:
* Will get a detailed view of a transaction including addresses, amounts and fees.
*
* Example result:
* {
* __blockHash: '2725743288feae6bdaa976590af7cb12d7b535b5a242787de6d2789c73682ed1',
* __height: 48,
* __timestamp: 1442951110, // in seconds
* }
* @param {String} txid - The transaction hash
* blockHash: '000000000000000002cd0ba6e8fae058747d2344929ed857a18d3484156c9250',
* height: 411462,
* blockTimestamp: 1463070382,
* version: 1,
* hash: 'de184cc227f6d1dc0316c7484aa68b58186a18f89d853bb2428b02040c394479',
* locktime: 411451,
* coinbase: true,
* inputs: [
* {
* prevTxId: '3d003413c13eec3fa8ea1fe8bbff6f40718c66facffe2544d7516c9e2900cac2',
* outputIndex: 0,
* sequence: 123456789,
* script: [hexString],
* scriptAsm: [asmString],
* satoshis: 771146
* }
* ],
* outputs: [
* {
* satoshis: 811146,
* script: '76a914d2955017f4e3d6510c57b427cf45ae29c372c99088ac',
* scriptAsm: 'OP_DUP OP_HASH160 d2955017f4e3d6510c57b427cf45ae29c372c990 OP_EQUALVERIFY OP_CHECKSIG',
* address: '1LCTmj15p7sSXv3jmrPfA6KGs6iuepBiiG',
* spentTxId: '4316b98e7504073acd19308b4b8c9f4eeb5e811455c54c0ebfe276c0b1eb6315',
* spentIndex: 1,
* spentHeight: 100
* }
* ],
* inputSatoshis: 771146,
* outputSatoshis: 811146,
* feeSatoshis: 40000
* };
*
* @param {String} txid - The hex string of the transaction
* @param {Function} callback
*/
Bitcoin.prototype.getTransactionWithBlockInfo = function(txid, callback) {
// TODO give response back as standard js object with bitcore tx
Bitcoin.prototype.getDetailedTransaction = function(txid, callback) {
var self = this;
var tx = self.transactionInfoCache.get(txid);
var tx = self.transactionDetailedCache.get(txid);
function addInputsToTx(tx, result) {
tx.inputs = [];
tx.inputSatoshis = 0;
for(var inputIndex = 0; inputIndex < result.vin.length; inputIndex++) {
var input = result.vin[inputIndex];
if (!tx.coinbase) {
tx.inputSatoshis += input.valueSat;
}
var script;
var scriptAsm;
if (input.scriptSig) {
script = input.scriptSig.hex;
scriptAsm = input.scriptSig.asm;
} else if (input.coinbase) {
script = input.coinbase;
scriptAsm = null;
}
tx.inputs.push({
prevTxId: input.txid || null,
outputIndex: _.isUndefined(input.vout) ? null : input.vout,
script: script,
scriptAsm: scriptAsm || null,
sequence: input.sequence,
address: input.address || null,
satoshis: _.isUndefined(input.valueSat) ? null : input.valueSat
});
}
}
function addOutputsToTx(tx, result) {
tx.outputs = [];
tx.outputSatoshis = 0;
for(var outputIndex = 0; outputIndex < result.vout.length; outputIndex++) {
var out = result.vout[outputIndex];
tx.outputSatoshis += out.valueSat;
var address = null;
if (out.scriptPubKey && out.scriptPubKey.addresses && out.scriptPubKey.addresses.length > 0) {
address = out.scriptPubKey.addresses[0];
}
tx.outputs.push({
satoshis: out.valueSat,
script: out.scriptPubKey.hex,
scriptAsm: out.scriptPubKey.asm,
spentTxId: out.spentTxId,
spentIndex: out.spentIndex,
spentHeight: out.spentHeight,
address: address
});
}
}
if (tx) {
return setImmediate(function() {
callback(null, tx);
@ -1695,18 +1829,32 @@ Bitcoin.prototype.getTransactionWithBlockInfo = function(txid, callback) {
if (err) {
return done(self._wrapRPCError(err));
}
var tx = Transaction();
tx.fromString(response.result.hex);
tx.__blockHash = response.result.blockhash;
tx.__height = response.result.height ? response.result.height : -1;
tx.__timestamp = response.result.time;
var result = response.result;
var tx = {
hex: result.hex,
blockHash: result.blockhash,
height: result.height ? result.height : -1,
blockTimestamp: result.time,
version: result.version,
hash: txid,
locktime: result.locktime,
};
for (var i = 0; i < response.result.vout.length; i++) {
tx.outputs[i].__spentTxId = response.result.vout[i].spentTxId;
tx.outputs[i].__spentIndex = response.result.vout[i].spentIndex;
tx.outputs[i].__spentHeight = response.result.vout[i].spentHeight;
if (result.vin[0] && result.vin[0].coinbase) {
tx.coinbase = true;
}
self.transactionInfoCache.set(txid, tx);
addInputsToTx(tx, result);
addOutputsToTx(tx, result);
if (!tx.coinbase) {
tx.feeSatoshis = tx.inputSatoshis - tx.outputSatoshis;
} else {
tx.feeSatoshis = 0;
}
self.transactionDetailedCache.set(txid, tx);
done(null, tx);
});
}, callback);

View File

@ -424,16 +424,38 @@ describe('Bitcoind Functionality', function() {
});
});
describe('get transaction with block info', function() {
it('should include tx with height and timestamp', function(done) {
bitcoind.getTransactionWithBlockInfo(utxos[0].txid, function(err, tx) {
describe('get detailed transaction', function() {
it('should include details for coinbase tx', function(done) {
bitcoind.getDetailedTransaction(utxos[0].txid, function(err, tx) {
if (err) {
return done(err);
}
should.exist(tx.__height);
tx.__height.should.be.a('number');
should.exist(tx.__timestamp);
should.exist(tx.__blockHash);
should.exist(tx.height);
tx.height.should.be.a('number');
should.exist(tx.blockTimestamp);
should.exist(tx.blockHash);
tx.coinbase.should.equal(true);
tx.version.should.equal(1);
tx.hex.should.be.a('string');
tx.locktime.should.equal(0);
tx.feeSatoshis.should.equal(0);
tx.outputSatoshis.should.equal(50 * 1e8);
tx.inputSatoshis.should.equal(0);
tx.inputs.length.should.equal(1);
tx.outputs.length.should.equal(1);
should.equal(tx.inputs[0].prevTxId, null);
should.equal(tx.inputs[0].outputIndex, null);
tx.inputs[0].script.should.be.a('string');
should.equal(tx.inputs[0].scriptAsm, null);
should.equal(tx.inputs[0].address, null);
should.equal(tx.inputs[0].satoshis, null);
tx.outputs[0].satoshis.should.equal(50 * 1e8);
tx.outputs[0].script.should.be.a('string');
tx.outputs[0].scriptAsm.should.be.a('string');
tx.outputs[0].spentTxId.should.be.a('string');
tx.outputs[0].spentIndex.should.equal(0);
tx.outputs[0].spentHeight.should.be.a('number');
tx.outputs[0].address.should.be.a('string');
done();
});
});

View File

@ -205,9 +205,8 @@ describe('Node Functionality', function() {
info.addresses[address].inputIndexes.should.deep.equal([]);
info.satoshis.should.equal(10 * 1e8);
info.confirmations.should.equal(3);
info.timestamp.should.be.a('number');
info.fees.should.be.within(950, 4000);
info.tx.should.be.an.instanceof(Transaction);
info.tx.blockTimestamp.should.be.a('number');
info.tx.feeSatoshis.should.be.within(950, 4000);
done();
});
});
@ -395,13 +394,13 @@ describe('Node Functionality', function() {
results.totalCount.should.equal(4);
var history = results.items;
history.length.should.equal(4);
history[0].height.should.equal(159);
history[0].tx.height.should.equal(159);
history[0].confirmations.should.equal(1);
history[1].height.should.equal(158);
history[1].tx.height.should.equal(158);
should.exist(history[1].addresses[address4]);
history[2].height.should.equal(157);
history[2].tx.height.should.equal(157);
should.exist(history[2].addresses[address3]);
history[3].height.should.equal(156);
history[3].tx.height.should.equal(156);
should.exist(history[3].addresses[address2]);
history[3].satoshis.should.equal(tx2Amount);
history[3].tx.hash.should.equal(tx2Hash);
@ -429,9 +428,9 @@ describe('Node Functionality', function() {
results.totalCount.should.equal(2);
var history = results.items;
history.length.should.equal(2);
history[0].height.should.equal(158);
history[0].tx.height.should.equal(158);
history[0].confirmations.should.equal(2);
history[1].height.should.equal(157);
history[1].tx.height.should.equal(157);
should.exist(history[1].addresses[address3]);
done();
});
@ -456,8 +455,8 @@ describe('Node Functionality', function() {
results.totalCount.should.equal(2);
var history = results.items;
history.length.should.equal(2);
history[0].height.should.equal(157);
history[1].height.should.equal(156);
history[0].tx.height.should.equal(157);
history[1].tx.height.should.equal(156);
done();
});
});
@ -481,9 +480,9 @@ describe('Node Functionality', function() {
results.totalCount.should.equal(4);
var history = results.items;
history.length.should.equal(3);
history[0].height.should.equal(159);
history[0].tx.height.should.equal(159);
history[0].confirmations.should.equal(1);
history[1].height.should.equal(158);
history[1].tx.height.should.equal(158);
should.exist(history[1].addresses[address4]);
done();
});
@ -501,18 +500,18 @@ describe('Node Functionality', function() {
results.totalCount.should.equal(6);
var history = results.items;
history.length.should.equal(6);
history[0].height.should.equal(159);
history[0].tx.height.should.equal(159);
history[0].addresses[address].inputIndexes.should.deep.equal([0, 1]);
history[0].addresses[address].outputIndexes.should.deep.equal([2]);
history[0].confirmations.should.equal(1);
history[1].height.should.equal(158);
history[2].height.should.equal(157);
history[3].height.should.equal(156);
history[4].height.should.equal(155);
history[1].tx.height.should.equal(158);
history[2].tx.height.should.equal(157);
history[3].tx.height.should.equal(156);
history[4].tx.height.should.equal(155);
history[4].satoshis.should.equal(-10000);
history[4].addresses[address].outputIndexes.should.deep.equal([0, 1, 2, 3, 4]);
history[4].addresses[address].inputIndexes.should.deep.equal([0]);
history[5].height.should.equal(152);
history[5].tx.height.should.equal(152);
history[5].satoshis.should.equal(10 * 1e8);
done();
});
@ -561,7 +560,7 @@ describe('Node Functionality', function() {
}
var history = results.items;
history.length.should.equal(1);
history[0].height.should.equal(159);
history[0].tx.height.should.equal(159);
done();
});
});
@ -576,7 +575,7 @@ describe('Node Functionality', function() {
}
var history = results.items;
history.length.should.equal(1);
history[0].height.should.equal(158);
history[0].tx.height.should.equal(158);
done();
});
});
@ -591,7 +590,7 @@ describe('Node Functionality', function() {
}
var history = results.items;
history.length.should.equal(1);
history[0].height.should.equal(157);
history[0].tx.height.should.equal(157);
done();
});
});
@ -606,7 +605,7 @@ describe('Node Functionality', function() {
}
var history = results.items;
history.length.should.equal(1);
history[0].height.should.equal(156);
history[0].tx.height.should.equal(156);
done();
});
});
@ -621,7 +620,7 @@ describe('Node Functionality', function() {
}
var history = results.items;
history.length.should.equal(1);
history[0].height.should.equal(155);
history[0].tx.height.should.equal(155);
history[0].satoshis.should.equal(-10000);
history[0].addresses[address].outputIndexes.should.deep.equal([0, 1, 2, 3, 4]);
history[0].addresses[address].inputIndexes.should.deep.equal([0]);
@ -639,7 +638,7 @@ describe('Node Functionality', function() {
}
var history = results.items;
history.length.should.equal(1);
history[0].height.should.equal(152);
history[0].tx.height.should.equal(152);
history[0].satoshis.should.equal(10 * 1e8);
done();
});
@ -744,13 +743,13 @@ describe('Node Functionality', function() {
it('will not show confirmation count for orphaned transaction', function(done) {
// This test verifies that in the situation that the transaction is not in the mempool and
// is included in an orphaned block transaction index that the confirmation count will be unconfirmed.
node.getTransactionWithBlockInfo(orphanedTransaction, function(err, data) {
node.getDetailedTransaction(orphanedTransaction, function(err, data) {
if (err) {
return done(err);
}
should.exist(data);
should.exist(data.__height);
data.__height.should.equal(-1);
should.exist(data.height);
data.height.should.equal(-1);
done();
});
});

View File

@ -6,8 +6,8 @@ root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
platform=`uname -a | awk '{print tolower($1)}'`
arch=`uname -m`
version="0.12.0"
url="https://github.com/bitpay/bitcoin/releases/download"
tag="v0.12-bitcore-rc1"
url="https://github.com/braydonf/bitcoin/releases/download"
tag="v0.12-bitcore-rc2-spent"
if [ "${platform}" == "linux" ]; then
if [ "${arch}" == "x86_64" ]; then

View File

@ -5,6 +5,7 @@ var EventEmitter = require('events').EventEmitter;
var should = require('chai').should();
var crypto = require('crypto');
var bitcore = require('bitcore-lib');
var _ = bitcore.deps._;
var sinon = require('sinon');
var proxyquire = require('proxyquire');
var fs = require('fs');
@ -51,7 +52,7 @@ describe('Bitcoin Service', function() {
should.exist(bitcoind.txidsCache);
should.exist(bitcoind.balanceCache);
should.exist(bitcoind.summaryCache);
should.exist(bitcoind.transactionInfoCache);
should.exist(bitcoind.transactionDetailedCache);
should.exist(bitcoind.transactionCache);
should.exist(bitcoind.rawTransactionCache);
@ -90,7 +91,7 @@ describe('Bitcoin Service', function() {
var bitcoind = new BitcoinService(baseConfig);
var methods = bitcoind.getAPIMethods();
should.exist(methods);
methods.length.should.equal(20);
methods.length.should.equal(21);
});
});
@ -295,14 +296,14 @@ describe('Bitcoin Service', function() {
var keys = [];
for (var i = 0; i < 10; i++) {
keys.push(crypto.randomBytes(32));
bitcoind.transactionInfoCache.set(keys[i], {});
bitcoind.transactionDetailedCache.set(keys[i], {});
bitcoind.utxosCache.set(keys[i], {});
bitcoind.txidsCache.set(keys[i], {});
bitcoind.balanceCache.set(keys[i], {});
bitcoind.summaryCache.set(keys[i], {});
}
bitcoind._resetCaches();
should.equal(bitcoind.transactionInfoCache.get(keys[0]), undefined);
should.equal(bitcoind.transactionDetailedCache.get(keys[0]), undefined);
should.equal(bitcoind.utxosCache.get(keys[0]), undefined);
should.equal(bitcoind.txidsCache.get(keys[0]), undefined);
should.equal(bitcoind.balanceCache.get(keys[0]), undefined);
@ -1919,7 +1920,7 @@ describe('Bitcoin Service', function() {
});
it('should get 1 confirmation', function() {
var tx = new Transaction(txhex);
tx.__height = 10;
tx.height = 10;
var bitcoind = new BitcoinService(baseConfig);
bitcoind.height = 10;
var confirmations = bitcoind._getConfirmationsDetail(tx);
@ -1929,7 +1930,7 @@ describe('Bitcoin Service', function() {
var bitcoind = new BitcoinService(baseConfig);
var tx = new Transaction(txhex);
bitcoind.height = 11;
tx.__height = 10;
tx.height = 10;
var confirmations = bitcoind._getConfirmationsDetail(tx);
confirmations.should.equal(2);
});
@ -1937,7 +1938,7 @@ describe('Bitcoin Service', function() {
var bitcoind = new BitcoinService(baseConfig);
var tx = new Transaction(txhex);
bitcoind.height = 3;
tx.__height = 10;
tx.height = 10;
var confirmations = bitcoind._getConfirmationsDetail(tx);
log.warn.callCount.should.equal(1);
confirmations.should.equal(0);
@ -1946,7 +1947,7 @@ describe('Bitcoin Service', function() {
var bitcoind = new BitcoinService(baseConfig);
var tx = new Transaction(txhex);
bitcoind.height = 1000;
tx.__height = 1;
tx.height = 1;
var confirmations = bitcoind._getConfirmationsDetail(tx);
confirmations.should.equal(1000);
});
@ -1955,46 +1956,37 @@ describe('Bitcoin Service', function() {
describe('#_getAddressDetailsForTransaction', function() {
it('will calculate details for the transaction', function(done) {
/* jshint sub:true */
var tx = bitcore.Transaction({
'hash': 'b12b3ae8489c5a566b629a3c62ce4c51c3870af550fb5dc77d715b669a91343c',
'version': 1,
'inputs': [
var tx = {
inputs: [
{
'prevTxId': 'a2b7ea824a92f4a4944686e67ec1001bc8785348b8c111c226f782084077b543',
'outputIndex': 0,
'sequenceNumber': 4294967295,
'script': '47304402201b81c933297241960a57ae1b2952863b965ac8c9ec7466ff0b715712d27548d50220576e115b63864f003889443525f47c7cf0bc1e2b5108398da085b221f267ba2301210229766f1afa25ca499a51f8e01c292b0255a21a41bb6685564a1607a811ffe924',
'scriptString': '71 0x304402201b81c933297241960a57ae1b2952863b965ac8c9ec7466ff0b715712d27548d50220576e115b63864f003889443525f47c7cf0bc1e2b5108398da085b221f267ba2301 33 0x0229766f1afa25ca499a51f8e01c292b0255a21a41bb6685564a1607a811ffe924',
'output': {
'satoshis': 1000000000,
'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac'
}
satoshis: 1000000000,
address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'
}
],
'outputs': [
outputs: [
{
'satoshis': 100000000,
'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac'
satoshis: 100000000,
address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'
},
{
'satoshis': 200000000,
'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac'
satoshis: 200000000,
address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'
},
{
'satoshis': 50000000,
'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac'
satoshis: 50000000,
address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'
},
{
'satoshis': 300000000,
'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac'
satoshis: 300000000,
address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'
},
{
'satoshis': 349990000,
'script': '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac'
satoshis: 349990000,
address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'
}
],
'nLockTime': 0
});
locktime: 0
};
var bitcoind = new BitcoinService(baseConfig);
var addresses = ['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW'];
var details = bitcoind._getAddressDetailsForTransaction(tx, addresses);
@ -2008,105 +2000,40 @@ describe('Bitcoin Service', function() {
});
});
describe('#_getDetailedTransaction', function() {
describe('#_getAddressDetailedTransaction', function() {
it('will get detailed transaction info', function(done) {
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
var tx = {
populateInputs: sinon.stub().callsArg(2),
__height: 20,
__timestamp: 1453134151,
isCoinbase: sinon.stub().returns(false),
getFee: sinon.stub().returns(1000)
height: 20,
};
var bitcoind = new BitcoinService(baseConfig);
bitcoind.getTransactionWithBlockInfo = sinon.stub().callsArgWith(1, null, tx);
bitcoind.getDetailedTransaction = sinon.stub().callsArgWith(1, null, tx);
bitcoind.height = 300;
var addresses = {};
bitcoind._getAddressDetailsForTransaction = sinon.stub().returns({
addresses: {},
addresses: addresses,
satoshis: 1000,
});
bitcoind._getDetailedTransaction(txid, {}, function(err) {
bitcoind._getAddressDetailedTransaction(txid, {}, function(err, details) {
if (err) {
return done(err);
}
details.addresses.should.equal(addresses);
details.satoshis.should.equal(1000);
details.confirmations.should.equal(281);
details.tx.should.equal(tx);
done();
});
});
it('give error from getTransactionWithBlockInfo', function(done) {
it('give error from getDetailedTransaction', function(done) {
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
var bitcoind = new BitcoinService(baseConfig);
bitcoind.getTransactionWithBlockInfo = sinon.stub().callsArgWith(1, new Error('test'));
bitcoind._getDetailedTransaction(txid, {}, function(err) {
bitcoind.getDetailedTransaction = sinon.stub().callsArgWith(1, new Error('test'));
bitcoind._getAddressDetailedTransaction(txid, {}, function(err) {
err.should.be.instanceof(Error);
done();
});
});
it('give error from populateInputs', function(done) {
var txid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
var tx = {
populateInputs: sinon.stub().callsArgWith(2, new Error('test')),
};
var bitcoind = new BitcoinService(baseConfig);
bitcoind.getTransactionWithBlockInfo = sinon.stub().callsArgWith(1, null, tx);
bitcoind._getDetailedTransaction(txid, {}, function(err) {
err.should.be.instanceof(Error);
done();
});
});
it('will correct detailed info', function(done) {
// block #314159
// txid 30169e8bf78bc27c4014a7aba3862c60e2e3cce19e52f1909c8255e4b7b3174e
// outputIndex 1
var txAddress = '1Cj4UZWnGWAJH1CweTMgPLQMn26WRMfXmo';
var txString = '0100000001a08ee59fcd5d86fa170abb6d925d62d5c5c476359681b70877c04f270c4ef246000000008a47304402203fb9b476bb0c37c9b9ed5784ebd67ae589492be11d4ae1612be29887e3e4ce750220741ef83781d1b3a5df8c66fa1957ad0398c733005310d7d9b1d8c2310ef4f74c0141046516ad02713e51ecf23ac9378f1069f9ae98e7de2f2edbf46b7836096e5dce95a05455cc87eaa1db64f39b0c63c0a23a3b8df1453dbd1c8317f967c65223cdf8ffffffff02b0a75fac000000001976a91484b45b9bf3add8f7a0f3daad305fdaf6b73441ea88ac20badc02000000001976a914809dc14496f99b6deb722cf46d89d22f4beb8efd88ac00000000';
var previousTxString = '010000000155532fad2869bb951b0bd646a546887f6ee668c4c0ee13bf3f1c4bce6d6e3ed9000000008c4930460221008540795f4ef79b1d2549c400c61155ca5abbf3089c84ad280e1ba6db2a31abce022100d7d162175483d51174d40bba722e721542c924202a0c2970b07e680b51f3a0670141046516ad02713e51ecf23ac9378f1069f9ae98e7de2f2edbf46b7836096e5dce95a05455cc87eaa1db64f39b0c63c0a23a3b8df1453dbd1c8317f967c65223cdf8ffffffff02f0af3caf000000001976a91484b45b9bf3add8f7a0f3daad305fdaf6b73441ea88ac80969800000000001976a91421277e65777760d1f3c7c982ba14ed8f934f005888ac00000000';
var transaction = new Transaction();
var previousTransaction = new Transaction();
previousTransaction.fromString(previousTxString);
var previousTransactionTxid = '46f24e0c274fc07708b781963576c4c5d5625d926dbb0a17fa865dcd9fe58ea0';
transaction.fromString(txString);
var txid = transaction.hash;
transaction.__blockHash = '00000000000000001bb82a7f5973618cfd3185ba1ded04dd852a653f92a27c45';
transaction.__height = 314159;
transaction.__timestamp = 1407292005;
var bitcoind = new BitcoinService(baseConfig);
bitcoind.height = 314159;
bitcoind.getTransactionWithBlockInfo = sinon.stub().callsArgWith(1, null, transaction);
bitcoind.getTransaction = function(prevTxid, callback) {
prevTxid.should.equal(previousTransactionTxid);
setImmediate(function() {
callback(null, previousTransaction);
});
};
var transactionInfo = {
addresses: {},
txid: txid,
timestamp: 1407292005,
satoshis: 48020000,
address: txAddress
};
transactionInfo.addresses[txAddress] = {};
transactionInfo.addresses[txAddress].outputIndexes = [1];
transactionInfo.addresses[txAddress].inputIndexes = [];
bitcoind._getAddressDetailsForTransaction = sinon.stub().returns(transactionInfo);
bitcoind._getDetailedTransaction(txid, {}, function(err, info) {
if (err) {
return done(err);
}
info.addresses[txAddress].should.deep.equal({
outputIndexes: [1],
inputIndexes: []
});
info.satoshis.should.equal(48020000);
info.height.should.equal(314159);
info.confirmations.should.equal(1);
info.timestamp.should.equal(1407292005);
info.fees.should.equal(20000);
info.tx.should.equal(transaction);
done();
});
});
});
describe('#_getAddressStrings', function() {
@ -2221,7 +2148,7 @@ describe('Bitcoin Service', function() {
});
it('will paginate', function(done) {
var bitcoind = new BitcoinService(baseConfig);
bitcoind._getDetailedTransaction = function(txid, options, callback) {
bitcoind._getAddressDetailedTransaction = function(txid, options, callback) {
callback(null, txid);
};
var txids = ['one', 'two', 'three', 'four'];
@ -3075,7 +3002,7 @@ describe('Bitcoin Service', function() {
});
});
describe('#getTransactionWithBlockInfo', function() {
describe('#getDetailedTransaction', function() {
var txBuffer = new Buffer('01000000016f95980911e01c2c664b3e78299527a47933aac61a515930a8fe0213d1ac9abe01000000da0047304402200e71cda1f71e087c018759ba3427eb968a9ea0b1decd24147f91544629b17b4f0220555ee111ed0fc0f751ffebf097bdf40da0154466eb044e72b6b3dcd5f06807fa01483045022100c86d6c8b417bff6cc3bbf4854c16bba0aaca957e8f73e19f37216e2b06bb7bf802205a37be2f57a83a1b5a8cc511dc61466c11e9ba053c363302e7b99674be6a49fc0147522102632178d046673c9729d828cfee388e121f497707f810c131e0d3fc0fe0bd66d62103a0951ec7d3a9da9de171617026442fcd30f34d66100fab539853b43f508787d452aeffffffff0240420f000000000017a9148a31d53a448c18996e81ce67811e5fb7da21e4468738c9d6f90000000017a9148ce5408cfeaddb7ccb2545ded41ef478109454848700000000', 'hex');
var info = {
blockHash: '00000000000ec715852ea2ecae4dc8563f62d603c820f81ac284cd5be0a944d6',
@ -3083,7 +3010,40 @@ describe('Bitcoin Service', function() {
timestamp: 1439559434000,
buffer: txBuffer
};
var rpcRawTransaction = {
hex: txBuffer.toString('hex'),
blockhash: info.blockHash,
height: info.height,
version: 1,
locktime: 411451,
time: info.timestamp,
vin: [
{
valueSat: 110,
address: 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW',
txid: '3d003413c13eec3fa8ea1fe8bbff6f40718c66facffe2544d7516c9e2900cac2',
sequence: 0xFFFFFFFF,
vout: 0,
scriptSig: {
hex: 'scriptSigHex',
asm: 'scriptSigAsm'
}
}
],
vout: [
{
spentTxId: '4316b98e7504073acd19308b4b8c9f4eeb5e811455c54c0ebfe276c0b1eb6315',
spentIndex: 2,
spentHeight: 100,
valueSat: 100,
scriptPubKey: {
hex: '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac',
asm: 'OP_DUP OP_HASH160 0b2f0a0c31bfe0406b0ccc1381fdbe311946dadc OP_EQUALVERIFY OP_CHECKSIG',
addresses: ['mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW']
}
}
]
};
it('should give a transaction with height and timestamp', function(done) {
var bitcoind = new BitcoinService(baseConfig);
bitcoind.nodes.push({
@ -3092,39 +3052,74 @@ describe('Bitcoin Service', function() {
}
});
var txid = '2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f';
bitcoind.getTransactionWithBlockInfo(txid, function(err) {
bitcoind.getDetailedTransaction(txid, function(err) {
should.exist(err);
err.should.be.instanceof(errors.RPCError);
done();
});
});
it('should give a transaction with height and timestamp', function(done) {
it('should give a transaction with all properties', function(done) {
var bitcoind = new BitcoinService(baseConfig);
bitcoind.nodes.push({
client: {
getRawTransaction: sinon.stub().callsArgWith(2, null, {
result: {
hex: txBuffer.toString('hex'),
blockhash: info.blockHash,
height: info.height,
time: info.timestamp,
vout: [
{
spentTxId: 'txid',
spentIndex: 2,
spentHeight: 100
}
]
}
result: rpcRawTransaction
})
}
});
var txid = '2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f';
bitcoind.getTransactionWithBlockInfo(txid, function(err, tx) {
should.equal(tx.__blockHash, '00000000000ec715852ea2ecae4dc8563f62d603c820f81ac284cd5be0a944d6');
should.equal(tx.__height, 530482);
should.equal(tx.__timestamp, 1439559434000);
bitcoind.getDetailedTransaction(txid, function(err, tx) {
should.exist(tx);
should.not.exist(tx.coinbase);
should.equal(tx.hex, txBuffer.toString('hex'));
should.equal(tx.blockHash, '00000000000ec715852ea2ecae4dc8563f62d603c820f81ac284cd5be0a944d6');
should.equal(tx.height, 530482);
should.equal(tx.blockTimestamp, 1439559434000);
should.equal(tx.version, 1);
should.equal(tx.locktime, 411451);
should.equal(tx.feeSatoshis, 10);
should.equal(tx.inputSatoshis, 110);
should.equal(tx.outputSatoshis, 100);
should.equal(tx.hash, txid);
var input = tx.inputs[0];
should.equal(input.prevTxId, '3d003413c13eec3fa8ea1fe8bbff6f40718c66facffe2544d7516c9e2900cac2');
should.equal(input.outputIndex, 0);
should.equal(input.satoshis, 110);
should.equal(input.sequence, 0xFFFFFFFF);
should.equal(input.script, 'scriptSigHex');
should.equal(input.scriptAsm, 'scriptSigAsm');
should.equal(input.address, 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW');
var output = tx.outputs[0];
should.equal(output.satoshis, 100);
should.equal(output.script, '76a9140b2f0a0c31bfe0406b0ccc1381fdbe311946dadc88ac');
should.equal(output.scriptAsm, 'OP_DUP OP_HASH160 0b2f0a0c31bfe0406b0ccc1381fdbe311946dadc OP_EQUALVERIFY OP_CHECKSIG');
should.equal(output.address, 'mgY65WSfEmsyYaYPQaXhmXMeBhwp4EcsQW');
should.equal(output.spentTxId, '4316b98e7504073acd19308b4b8c9f4eeb5e811455c54c0ebfe276c0b1eb6315');
should.equal(output.spentIndex, 2);
should.equal(output.spentHeight, 100);
done();
});
});
it('should set coinbase to true', function(done) {
var bitcoind = new BitcoinService(baseConfig);
var rawTransaction = _.clone(rpcRawTransaction);
delete rawTransaction.vin[0];
rawTransaction.vin = [
{
coinbase: 'abcdef'
}
];
bitcoind.nodes.push({
client: {
getRawTransaction: sinon.stub().callsArgWith(2, null, {
result: rawTransaction
})
}
});
var txid = '2d950d00494caf6bfc5fff2a3f839f0eb50f663ae85ce092bc5f9d45296ae91f';
bitcoind.getDetailedTransaction(txid, function(err, tx) {
should.exist(tx);
should.equal(tx.coinbase, true);
done();
});
});