Merge pull request #53 from pnagurny/feature/get-txs-address
getAddressHistory
This commit is contained in:
commit
47219a745a
@ -304,4 +304,16 @@ describe('Daemon Binding Functionality', function() {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('get transaction with block info', function() {
|
||||||
|
it('should include tx buffer, height and timestamp', function(done) {
|
||||||
|
bitcoind.getTransactionWithBlockInfo(utxo.txid, true, function(err, data) {
|
||||||
|
should.not.exist(err);
|
||||||
|
data.height.should.equal(151);
|
||||||
|
should.exist(data.timestamp);
|
||||||
|
should.exist(data.buffer);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -362,6 +362,10 @@ Daemon.prototype.getTransactionWithBlock = function(txid, blockhash, callback) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Daemon.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) {
|
||||||
|
return bitcoindjs.getTransactionWithBlockInfo(txid, queryMempool, callback);
|
||||||
|
};
|
||||||
|
|
||||||
Daemon.prototype.getMempoolOutputs = function(address) {
|
Daemon.prototype.getMempoolOutputs = function(address) {
|
||||||
return bitcoindjs.getMempoolOutputs(address);
|
return bitcoindjs.getMempoolOutputs(address);
|
||||||
};
|
};
|
||||||
|
|||||||
14
lib/db.js
14
lib/db.js
@ -80,6 +80,20 @@ DB.prototype.getTransaction = function(txid, queryMempool, callback) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DB.prototype.getTransactionWithBlockInfo = function(txid, queryMempool, callback) {
|
||||||
|
this.bitcoind.getTransactionWithBlockInfo(txid, queryMempool, function(err, obj) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tx = Transaction().fromBuffer(obj.buffer);
|
||||||
|
tx.__height = obj.height;
|
||||||
|
tx.__timestamp = obj.timestamp;
|
||||||
|
|
||||||
|
callback(null, tx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
DB.prototype.validateBlockData = function(block, callback) {
|
DB.prototype.validateBlockData = function(block, callback) {
|
||||||
// bitcoind does the validation
|
// bitcoind does the validation
|
||||||
setImmediate(callback);
|
setImmediate(callback);
|
||||||
|
|||||||
@ -5,6 +5,7 @@ var inherits = require('util').inherits;
|
|||||||
var async = require('async');
|
var async = require('async');
|
||||||
var chainlib = require('chainlib');
|
var chainlib = require('chainlib');
|
||||||
var log = chainlib.log;
|
var log = chainlib.log;
|
||||||
|
var levelup = chainlib.deps.levelup;
|
||||||
var errors = chainlib.errors;
|
var errors = chainlib.errors;
|
||||||
var bitcore = require('bitcore');
|
var bitcore = require('bitcore');
|
||||||
var $ = bitcore.util.preconditions;
|
var $ = bitcore.util.preconditions;
|
||||||
@ -23,7 +24,8 @@ var AddressModule = function(options) {
|
|||||||
inherits(AddressModule, BaseModule);
|
inherits(AddressModule, BaseModule);
|
||||||
|
|
||||||
AddressModule.PREFIXES = {
|
AddressModule.PREFIXES = {
|
||||||
OUTPUTS: 'outs'
|
OUTPUTS: 'outs',
|
||||||
|
SPENTS: 'sp'
|
||||||
};
|
};
|
||||||
|
|
||||||
AddressModule.prototype.getAPIMethods = function() {
|
AddressModule.prototype.getAPIMethods = function() {
|
||||||
@ -31,7 +33,8 @@ AddressModule.prototype.getAPIMethods = function() {
|
|||||||
['getBalance', this, this.getBalance, 2],
|
['getBalance', this, this.getBalance, 2],
|
||||||
['getOutputs', this, this.getOutputs, 2],
|
['getOutputs', this, this.getOutputs, 2],
|
||||||
['getUnspentOutputs', this, this.getUnspentOutputs, 2],
|
['getUnspentOutputs', this, this.getUnspentOutputs, 2],
|
||||||
['isSpent', this, this.isSpent, 2]
|
['isSpent', this, this.isSpent, 2],
|
||||||
|
['getAddressHistory', this, this.getAddressHistory, 2]
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -116,6 +119,14 @@ AddressModule.prototype.blockHandler = function(block, addOutput, callback) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for(var j = 0; j < inputs.length; j++) {
|
||||||
|
var input = inputs[j].toObject();
|
||||||
|
operations.push({
|
||||||
|
type: action,
|
||||||
|
key: [AddressModule.PREFIXES.SPENTS, input.prevTxId, input.outputIndex].join('-'),
|
||||||
|
value: [txid, j].join(':')
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setImmediate(function() {
|
setImmediate(function() {
|
||||||
@ -281,4 +292,115 @@ AddressModule.prototype.isSpent = function(output, queryMempool, callback) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = AddressModule;
|
AddressModule.prototype.getSpendInfoForOutput = function(txid, outputIndex, callback) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var key = [AddressModule.PREFIXES.SPENTS, txid, outputIndex].join('-');
|
||||||
|
this.db.store.get(key, function(err, value) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.split(':');
|
||||||
|
|
||||||
|
var info = {
|
||||||
|
txid: value[0],
|
||||||
|
inputIndex: value[1]
|
||||||
|
};
|
||||||
|
|
||||||
|
callback(null, info);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
AddressModule.prototype.getAddressHistory = function(address, queryMempool, callback) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var txinfos = {};
|
||||||
|
|
||||||
|
function getTransactionInfo(txid, callback) {
|
||||||
|
if(txinfos[txid]) {
|
||||||
|
return callback(null, txinfos[txid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.db.getTransactionWithBlockInfo(txid, queryMempool, function(err, transaction) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.populateInputs(self.db, [], function(err) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
txinfos[transaction.hash] = {
|
||||||
|
satoshis: 0,
|
||||||
|
height: transaction.__height,
|
||||||
|
timestamp: transaction.__timestamp,
|
||||||
|
outputIndexes: [],
|
||||||
|
inputIndexes: [],
|
||||||
|
transaction: transaction
|
||||||
|
};
|
||||||
|
|
||||||
|
callback(null, txinfos[transaction.hash]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getOutputs(address, queryMempool, function(err, outputs) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
async.eachSeries(
|
||||||
|
outputs,
|
||||||
|
function(output, next) {
|
||||||
|
getTransactionInfo(output.txid, function(err, txinfo) {
|
||||||
|
if(err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
txinfo.outputIndexes.push(output.outputIndex);
|
||||||
|
txinfo.satoshis += output.satoshis;
|
||||||
|
|
||||||
|
self.getSpendInfoForOutput(output.txid, output.outputIndex, function(err, spendInfo) {
|
||||||
|
if(err instanceof levelup.errors.NotFoundError) {
|
||||||
|
return next();
|
||||||
|
} else if(err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTransactionInfo(spendInfo.txid, function(err, txinfo) {
|
||||||
|
if(err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
txinfo.inputIndexes.push(spendInfo.inputIndex);
|
||||||
|
txinfo.satoshis -= txinfo.transaction.inputs[spendInfo.inputIndex].output.satoshis;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(err) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert to array
|
||||||
|
var history = [];
|
||||||
|
for(var txid in txinfos) {
|
||||||
|
history.push(txinfos[txid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort by height
|
||||||
|
history.sort(function(a, b) {
|
||||||
|
return a.height > b.height;
|
||||||
|
});
|
||||||
|
|
||||||
|
callback(null, history);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = AddressModule;
|
||||||
@ -54,6 +54,10 @@ Transaction.prototype._validateInputs = function(db, poolTransactions, callback)
|
|||||||
Transaction.prototype.populateInputs = function(db, poolTransactions, callback) {
|
Transaction.prototype.populateInputs = function(db, poolTransactions, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
if(this.isCoinbase()) {
|
||||||
|
return setImmediate(callback);
|
||||||
|
}
|
||||||
|
|
||||||
async.each(
|
async.each(
|
||||||
this.inputs,
|
this.inputs,
|
||||||
function(input, next) {
|
function(input, next) {
|
||||||
@ -68,7 +72,7 @@ Transaction.prototype._populateInput = function(db, input, poolTransactions, cal
|
|||||||
return callback(new Error('Input is expected to have prevTxId as a buffer'));
|
return callback(new Error('Input is expected to have prevTxId as a buffer'));
|
||||||
}
|
}
|
||||||
var txid = input.prevTxId.toString('hex');
|
var txid = input.prevTxId.toString('hex');
|
||||||
db.getTransactionFromDB(txid, function(err, prevTx) {
|
db.getTransaction(txid, false, function(err, prevTx) {
|
||||||
if(err instanceof levelup.errors.NotFoundError) {
|
if(err instanceof levelup.errors.NotFoundError) {
|
||||||
// Check the pool for transaction
|
// Check the pool for transaction
|
||||||
for(var i = 0; i < poolTransactions.length; i++) {
|
for(var i = 0; i < poolTransactions.length; i++) {
|
||||||
|
|||||||
@ -74,6 +74,12 @@ async_get_tx(uv_work_t *req);
|
|||||||
static void
|
static void
|
||||||
async_get_tx_after(uv_work_t *req);
|
async_get_tx_after(uv_work_t *req);
|
||||||
|
|
||||||
|
static void
|
||||||
|
async_get_tx_and_info(uv_work_t *req);
|
||||||
|
|
||||||
|
static void
|
||||||
|
async_get_tx_and_info_after(uv_work_t *req);
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
process_messages(CNode* pfrom);
|
process_messages(CNode* pfrom);
|
||||||
|
|
||||||
@ -156,6 +162,8 @@ struct async_tx_data {
|
|||||||
std::string err_msg;
|
std::string err_msg;
|
||||||
std::string txid;
|
std::string txid;
|
||||||
std::string blockhash;
|
std::string blockhash;
|
||||||
|
uint32_t nTime;
|
||||||
|
int64_t height;
|
||||||
bool queryMempool;
|
bool queryMempool;
|
||||||
CTransaction ctx;
|
CTransaction ctx;
|
||||||
Eternal<Function> callback;
|
Eternal<Function> callback;
|
||||||
@ -1018,7 +1026,7 @@ async_get_block_after(uv_work_t *req) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GetTransaction()
|
* GetTransaction()
|
||||||
* bitcoind.getTransaction(txid, callback)
|
* bitcoind.getTransaction(txid, queryMempool, callback)
|
||||||
* Read any transaction from disk asynchronously.
|
* Read any transaction from disk asynchronously.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -1030,7 +1038,7 @@ NAN_METHOD(GetTransaction) {
|
|||||||
|| !args[1]->IsBoolean()
|
|| !args[1]->IsBoolean()
|
||||||
|| !args[2]->IsFunction()) {
|
|| !args[2]->IsFunction()) {
|
||||||
return NanThrowError(
|
return NanThrowError(
|
||||||
"Usage: bitcoindjs.getTransaction(txid, callback)");
|
"Usage: daemon.getTransaction(txid, queryMempool, callback)");
|
||||||
}
|
}
|
||||||
|
|
||||||
String::Utf8Value txid_(args[0]->ToString());
|
String::Utf8Value txid_(args[0]->ToString());
|
||||||
@ -1145,6 +1153,154 @@ async_get_tx_after(uv_work_t *req) {
|
|||||||
delete req;
|
delete req;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetTransactionWithBlockInfo()
|
||||||
|
* bitcoind.getTransactionWithBlockInfo(txid, queryMempool, callback)
|
||||||
|
* Read any transaction from disk asynchronously with block timestamp and height.
|
||||||
|
*/
|
||||||
|
|
||||||
|
NAN_METHOD(GetTransactionWithBlockInfo) {
|
||||||
|
Isolate* isolate = Isolate::GetCurrent();
|
||||||
|
HandleScope scope(isolate);
|
||||||
|
if (args.Length() < 3
|
||||||
|
|| !args[0]->IsString()
|
||||||
|
|| !args[1]->IsBoolean()
|
||||||
|
|| !args[2]->IsFunction()) {
|
||||||
|
return NanThrowError(
|
||||||
|
"Usage: bitcoindjs.getTransactionWithBlockInfo(txid, queryMempool, callback)");
|
||||||
|
}
|
||||||
|
|
||||||
|
String::Utf8Value txid_(args[0]->ToString());
|
||||||
|
bool queryMempool = args[1]->BooleanValue();
|
||||||
|
Local<Function> callback = Local<Function>::Cast(args[2]);
|
||||||
|
|
||||||
|
async_tx_data *data = new async_tx_data();
|
||||||
|
|
||||||
|
data->err_msg = std::string("");
|
||||||
|
data->txid = std::string("");
|
||||||
|
|
||||||
|
std::string txid = std::string(*txid_);
|
||||||
|
|
||||||
|
data->txid = txid;
|
||||||
|
data->queryMempool = queryMempool;
|
||||||
|
Eternal<Function> eternal(isolate, callback);
|
||||||
|
data->callback = eternal;
|
||||||
|
|
||||||
|
uv_work_t *req = new uv_work_t();
|
||||||
|
req->data = data;
|
||||||
|
|
||||||
|
int status = uv_queue_work(uv_default_loop(),
|
||||||
|
req, async_get_tx_and_info,
|
||||||
|
(uv_after_work_cb)async_get_tx_and_info_after);
|
||||||
|
|
||||||
|
assert(status == 0);
|
||||||
|
|
||||||
|
NanReturnValue(Undefined(isolate));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
async_get_tx_and_info(uv_work_t *req) {
|
||||||
|
async_tx_data* data = static_cast<async_tx_data*>(req->data);
|
||||||
|
|
||||||
|
uint256 hash = uint256S(data->txid);
|
||||||
|
uint256 blockHash;
|
||||||
|
CTransaction ctx;
|
||||||
|
|
||||||
|
if (data->queryMempool) {
|
||||||
|
LOCK(mempool.cs);
|
||||||
|
map<uint256, CTxMemPoolEntry>::const_iterator i = mempool.mapTx.find(hash);
|
||||||
|
if (i != mempool.mapTx.end()) {
|
||||||
|
data->ctx = i->second.GetTx();
|
||||||
|
data->nTime = i->second.GetTime();
|
||||||
|
data->height = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CDiskTxPos postx;
|
||||||
|
if (pblocktree->ReadTxIndex(hash, postx)) {
|
||||||
|
|
||||||
|
CAutoFile file(OpenBlockFile(postx, true), SER_DISK, CLIENT_VERSION);
|
||||||
|
|
||||||
|
if (file.IsNull()) {
|
||||||
|
data->err_msg = std::string("%s: OpenBlockFile failed", __func__);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CBlockHeader blockHeader;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read header first to get block timestamp and hash
|
||||||
|
file >> blockHeader;
|
||||||
|
blockHash = blockHeader.GetHash();
|
||||||
|
data->nTime = blockHeader.nTime;
|
||||||
|
fseek(file.Get(), postx.nTxOffset, SEEK_CUR);
|
||||||
|
file >> ctx;
|
||||||
|
data->ctx = ctx;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
data->err_msg = std::string("Deserialize or I/O error - %s", __func__);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get block height
|
||||||
|
CBlockIndex* blockIndex;
|
||||||
|
|
||||||
|
if (mapBlockIndex.count(blockHash) == 0) {
|
||||||
|
data->height = -1;
|
||||||
|
} else {
|
||||||
|
blockIndex = mapBlockIndex[blockHash];
|
||||||
|
data->height = blockIndex->nHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
async_get_tx_and_info_after(uv_work_t *req) {
|
||||||
|
Isolate* isolate = Isolate::GetCurrent();
|
||||||
|
HandleScope scope(isolate);
|
||||||
|
async_tx_data* data = static_cast<async_tx_data*>(req->data);
|
||||||
|
|
||||||
|
CTransaction ctx = data->ctx;
|
||||||
|
Local<Function> cb = data->callback.Get(isolate);
|
||||||
|
Local<Object> obj = NanNew<Object>();
|
||||||
|
|
||||||
|
if (data->err_msg != "") {
|
||||||
|
Local<Value> err = Exception::Error(NanNew<String>(data->err_msg));
|
||||||
|
const unsigned argc = 1;
|
||||||
|
Local<Value> argv[argc] = { err };
|
||||||
|
TryCatch try_catch;
|
||||||
|
cb->Call(isolate->GetCurrentContext()->Global(), argc, argv);
|
||||||
|
if (try_catch.HasCaught()) {
|
||||||
|
node::FatalException(try_catch);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
|
||||||
|
ssTx << ctx;
|
||||||
|
std::string stx = ssTx.str();
|
||||||
|
Local<Value> rawNodeBuffer = node::Buffer::New(isolate, stx.c_str(), stx.size());
|
||||||
|
|
||||||
|
obj->Set(NanNew<String>("height"), NanNew<Number>(data->height));
|
||||||
|
obj->Set(NanNew<String>("timestamp"), NanNew<Number>(data->nTime));
|
||||||
|
obj->Set(NanNew<String>("buffer"), rawNodeBuffer);
|
||||||
|
|
||||||
|
const unsigned argc = 2;
|
||||||
|
Local<Value> argv[argc] = {
|
||||||
|
Local<Value>::New(isolate, NanNull()),
|
||||||
|
obj
|
||||||
|
};
|
||||||
|
TryCatch try_catch;
|
||||||
|
cb->Call(isolate->GetCurrentContext()->Global(), argc, argv);
|
||||||
|
if (try_catch.HasCaught()) {
|
||||||
|
node::FatalException(try_catch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete data;
|
||||||
|
delete req;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IsSpent()
|
* IsSpent()
|
||||||
* bitcoindjs.isSpent()
|
* bitcoindjs.isSpent()
|
||||||
@ -1465,6 +1621,7 @@ init(Handle<Object> target) {
|
|||||||
NODE_SET_METHOD(target, "stopped", IsStopped);
|
NODE_SET_METHOD(target, "stopped", IsStopped);
|
||||||
NODE_SET_METHOD(target, "getBlock", GetBlock);
|
NODE_SET_METHOD(target, "getBlock", GetBlock);
|
||||||
NODE_SET_METHOD(target, "getTransaction", GetTransaction);
|
NODE_SET_METHOD(target, "getTransaction", GetTransaction);
|
||||||
|
NODE_SET_METHOD(target, "getTransactionWithBlockInfo", GetTransactionWithBlockInfo);
|
||||||
NODE_SET_METHOD(target, "getInfo", GetInfo);
|
NODE_SET_METHOD(target, "getInfo", GetInfo);
|
||||||
NODE_SET_METHOD(target, "isSpent", IsSpent);
|
NODE_SET_METHOD(target, "isSpent", IsSpent);
|
||||||
NODE_SET_METHOD(target, "getBlockIndex", GetBlockIndex);
|
NODE_SET_METHOD(target, "getBlockIndex", GetBlockIndex);
|
||||||
|
|||||||
@ -8,6 +8,8 @@ var blockData = require('../data/livenet-345003.json');
|
|||||||
var bitcore = require('bitcore');
|
var bitcore = require('bitcore');
|
||||||
var EventEmitter = require('events').EventEmitter;
|
var EventEmitter = require('events').EventEmitter;
|
||||||
var errors = bitcoindjs.errors;
|
var errors = bitcoindjs.errors;
|
||||||
|
var chainlib = require('chainlib');
|
||||||
|
var levelup = chainlib.deps.levelup;
|
||||||
|
|
||||||
describe('AddressModule', function() {
|
describe('AddressModule', function() {
|
||||||
|
|
||||||
@ -15,7 +17,7 @@ describe('AddressModule', function() {
|
|||||||
it('should return the correct methods', function() {
|
it('should return the correct methods', function() {
|
||||||
var am = new AddressModule({});
|
var am = new AddressModule({});
|
||||||
var methods = am.getAPIMethods();
|
var methods = am.getAPIMethods();
|
||||||
methods.length.should.equal(4);
|
methods.length.should.equal(5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -111,21 +113,35 @@ describe('AddressModule', function() {
|
|||||||
it('should create the correct operations when updating/adding outputs', function(done) {
|
it('should create the correct operations when updating/adding outputs', function(done) {
|
||||||
am.blockHandler({__height: 345003, timestamp: new Date(1424836934000)}, true, function(err, operations) {
|
am.blockHandler({__height: 345003, timestamp: new Date(1424836934000)}, true, function(err, operations) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
operations.length.should.equal(11);
|
operations.length.should.equal(81);
|
||||||
operations[0].type.should.equal('put');
|
operations[0].type.should.equal('put');
|
||||||
var expected0 = ['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].join('-');
|
var expected0 = ['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].join('-');
|
||||||
operations[0].key.should.equal(expected0);
|
operations[0].key.should.equal(expected0);
|
||||||
operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':'));
|
operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':'));
|
||||||
|
operations[3].type.should.equal('put');
|
||||||
|
var expected3 = ['sp', key3.prevTxId, key3.prevOutputIndex].join('-');
|
||||||
|
operations[3].key.should.equal(expected3);
|
||||||
|
operations[3].value.should.equal([value3.txid, value3.inputIndex].join(':'));
|
||||||
|
operations[64].type.should.equal('put');
|
||||||
|
var expected64 = ['outs', key64.address, key64.timestamp, key64.txid, key64.outputIndex].join('-');
|
||||||
|
operations[64].key.should.equal(expected64);
|
||||||
|
operations[64].value.should.equal([value64.satoshis, value64.script, value64.blockHeight].join(':'));
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should create the correct operations when removing outputs', function(done) {
|
it('should create the correct operations when removing outputs', function(done) {
|
||||||
am.blockHandler({__height: 345003, timestamp: new Date(1424836934000)}, false, function(err, operations) {
|
am.blockHandler({__height: 345003, timestamp: new Date(1424836934000)}, false, function(err, operations) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
operations.length.should.equal(11);
|
operations.length.should.equal(81);
|
||||||
operations[0].type.should.equal('del');
|
operations[0].type.should.equal('del');
|
||||||
operations[0].key.should.equal(['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].join('-'));
|
operations[0].key.should.equal(['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].join('-'));
|
||||||
operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':'));
|
operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':'));
|
||||||
|
operations[3].type.should.equal('del');
|
||||||
|
operations[3].key.should.equal(['sp', key3.prevTxId, key3.prevOutputIndex].join('-'));
|
||||||
|
operations[3].value.should.equal([value3.txid, value3.inputIndex].join(':'));
|
||||||
|
operations[64].type.should.equal('del');
|
||||||
|
operations[64].key.should.equal(['outs', key64.address, key64.timestamp, key64.txid, key64.outputIndex].join('-'));
|
||||||
|
operations[64].value.should.equal([value64.satoshis, value64.script, value64.blockHeight].join(':'));
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -481,4 +497,210 @@ describe('AddressModule', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#getSpendInfoForOutput', function() {
|
||||||
|
it('should call store.get the right values', function(done) {
|
||||||
|
var db = {
|
||||||
|
store: {
|
||||||
|
get: sinon.stub().callsArgWith(1, null, 'spendtxid:1')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var am = new AddressModule({db: db});
|
||||||
|
am.getSpendInfoForOutput('txid', 3, function(err, info) {
|
||||||
|
should.not.exist(err);
|
||||||
|
info.txid.should.equal('spendtxid');
|
||||||
|
info.inputIndex.should.equal('1');
|
||||||
|
db.store.get.args[0][0].should.equal('sp-txid-3');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#getAddressHistory', function() {
|
||||||
|
var incoming = [
|
||||||
|
{
|
||||||
|
txid: 'tx1',
|
||||||
|
outputIndex: 0,
|
||||||
|
spentTx: 'tx2',
|
||||||
|
inputIndex: 0,
|
||||||
|
height: 1,
|
||||||
|
timestamp: 1438289011844,
|
||||||
|
satoshis: 5000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
txid: 'tx3',
|
||||||
|
outputIndex: 1,
|
||||||
|
height: 3,
|
||||||
|
timestamp: 1438289031844,
|
||||||
|
satoshis: 2000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
txid: 'tx4',
|
||||||
|
outputIndex: 2,
|
||||||
|
spentTx: 'tx5',
|
||||||
|
inputIndex: 1,
|
||||||
|
height: 4,
|
||||||
|
timestamp: 1438289041844,
|
||||||
|
satoshis: 3000
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
var outgoing = [
|
||||||
|
{
|
||||||
|
txid: 'tx2',
|
||||||
|
height: 2,
|
||||||
|
timestamp: 1438289021844,
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
output: {
|
||||||
|
satoshis: 5000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
txid: 'tx5',
|
||||||
|
height: 5,
|
||||||
|
timestamp: 1438289051844,
|
||||||
|
inputs: [
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
output: {
|
||||||
|
satoshis: 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
var db = {
|
||||||
|
getTransactionWithBlockInfo: function(txid, queryMempool, callback) {
|
||||||
|
var transaction = {
|
||||||
|
populateInputs: sinon.stub().callsArg(2)
|
||||||
|
};
|
||||||
|
for(var i = 0; i < incoming.length; i++) {
|
||||||
|
if(incoming[i].txid === txid) {
|
||||||
|
if(incoming[i].error) {
|
||||||
|
return callback(new Error(incoming[i].error));
|
||||||
|
}
|
||||||
|
transaction.hash = txid;
|
||||||
|
transaction.__height = incoming[i].height;
|
||||||
|
transaction.__timestamp = incoming[i].timestamp;
|
||||||
|
return callback(null, transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(var i = 0; i < outgoing.length; i++) {
|
||||||
|
if(outgoing[i].txid === txid) {
|
||||||
|
if(outgoing[i].error) {
|
||||||
|
return callback(new Error(outgoing[i].error));
|
||||||
|
}
|
||||||
|
transaction.hash = txid;
|
||||||
|
transaction.__height = outgoing[i].height;
|
||||||
|
transaction.__timestamp = outgoing[i].timestamp;
|
||||||
|
transaction.inputs = outgoing[i].inputs;
|
||||||
|
return callback(null, transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback(new Error('tx ' + txid + ' not found'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var am = new AddressModule({db: db});
|
||||||
|
|
||||||
|
am.getOutputs = sinon.stub().callsArgWith(2, null, incoming);
|
||||||
|
am.getSpendInfoForOutput = function(txid, outputIndex, callback) {
|
||||||
|
for(var i = 0; i < incoming.length; i++) {
|
||||||
|
if(incoming[i].txid === txid && incoming[i].outputIndex === outputIndex && incoming[i].spentTx) {
|
||||||
|
if(incoming[i].spendError) {
|
||||||
|
return callback(new Error(incoming[i].spendError));
|
||||||
|
}
|
||||||
|
return callback(null, {
|
||||||
|
txid: incoming[i].spentTx,
|
||||||
|
inputIndex: incoming[i].inputIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(new levelup.errors.NotFoundError());
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should give transaction history for an address', function(done) {
|
||||||
|
am.getAddressHistory('address', true, function(err, history) {
|
||||||
|
should.not.exist(err);
|
||||||
|
history[0].transaction.hash.should.equal('tx1');
|
||||||
|
history[0].satoshis.should.equal(5000);
|
||||||
|
history[0].height.should.equal(1);
|
||||||
|
history[0].timestamp.should.equal(1438289011844);
|
||||||
|
history[1].transaction.hash.should.equal('tx2');
|
||||||
|
history[1].satoshis.should.equal(-5000);
|
||||||
|
history[1].height.should.equal(2);
|
||||||
|
history[1].timestamp.should.equal(1438289021844);
|
||||||
|
history[2].transaction.hash.should.equal('tx3');
|
||||||
|
history[2].satoshis.should.equal(2000);
|
||||||
|
history[2].height.should.equal(3);
|
||||||
|
history[2].timestamp.should.equal(1438289031844);
|
||||||
|
history[3].transaction.hash.should.equal('tx4');
|
||||||
|
history[3].satoshis.should.equal(3000);
|
||||||
|
history[3].height.should.equal(4);
|
||||||
|
history[3].timestamp.should.equal(1438289041844);
|
||||||
|
history[4].transaction.hash.should.equal('tx5');
|
||||||
|
history[4].satoshis.should.equal(-3000);
|
||||||
|
history[4].height.should.equal(5);
|
||||||
|
history[4].timestamp.should.equal(1438289051844);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give an error if the second getTransactionInfo gives an error', function(done) {
|
||||||
|
outgoing[0].error = 'txinfo2err';
|
||||||
|
am.getAddressHistory('address', true, function(err, history) {
|
||||||
|
should.exist(err);
|
||||||
|
err.message.should.equal('txinfo2err');
|
||||||
|
outgoing[0].error = null;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give an error if getSpendInfoForOutput gives an error', function(done) {
|
||||||
|
incoming[0].spendError = 'spenderr';
|
||||||
|
am.getAddressHistory('address', true, function(err, history) {
|
||||||
|
should.exist(err);
|
||||||
|
err.message.should.equal('spenderr');
|
||||||
|
incoming[0].spendError = null;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give an error if the first getTransactionInfo gives an error', function(done) {
|
||||||
|
incoming[1].error = 'txinfo1err';
|
||||||
|
am.getAddressHistory('address', true, function(err, history) {
|
||||||
|
should.exist(err);
|
||||||
|
err.message.should.equal('txinfo1err');
|
||||||
|
incoming[1].error = null;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give an error if populateInputs gives an error', function(done) {
|
||||||
|
var populateStub = sinon.stub().callsArgWith(2, new Error('populateerr'));
|
||||||
|
sinon.stub(db, 'getTransactionWithBlockInfo').callsArgWith(2, null, {
|
||||||
|
populateInputs: populateStub
|
||||||
|
});
|
||||||
|
am.getAddressHistory('address', true, function(err, history) {
|
||||||
|
should.exist(err);
|
||||||
|
err.message.should.equal('populateerr');
|
||||||
|
db.getTransactionWithBlockInfo.restore();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give an error if getOutputs gives an error', function(done) {
|
||||||
|
am.getOutputs = sinon.stub().callsArgWith(2, new Error('getoutputserr'));
|
||||||
|
am.getAddressHistory('address', true, function(err, history) {
|
||||||
|
should.exist(err);
|
||||||
|
err.message.should.equal('getoutputserr');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -106,6 +106,7 @@ describe('Bitcoin Transaction', function() {
|
|||||||
describe('#populateInputs', function() {
|
describe('#populateInputs', function() {
|
||||||
it('will call _populateInput with transactions', function() {
|
it('will call _populateInput with transactions', function() {
|
||||||
var tx = new Transaction();
|
var tx = new Transaction();
|
||||||
|
tx.isCoinbase = sinon.stub().returns(false);
|
||||||
tx._populateInput = sinon.stub().callsArg(3);
|
tx._populateInput = sinon.stub().callsArg(3);
|
||||||
tx.inputs = ['input'];
|
tx.inputs = ['input'];
|
||||||
var transactions = [];
|
var transactions = [];
|
||||||
@ -138,7 +139,7 @@ describe('Bitcoin Transaction', function() {
|
|||||||
it('if an error happened it should pass it along', function(done) {
|
it('if an error happened it should pass it along', function(done) {
|
||||||
var tx = new Transaction();
|
var tx = new Transaction();
|
||||||
var db = {
|
var db = {
|
||||||
getTransactionFromDB: sinon.stub().callsArgWith(1, new Error('error'))
|
getTransaction: sinon.stub().callsArgWith(2, new Error('error'))
|
||||||
};
|
};
|
||||||
tx._populateInput(db, input, [], function(err) {
|
tx._populateInput(db, input, [], function(err) {
|
||||||
should.exist(err);
|
should.exist(err);
|
||||||
@ -149,7 +150,7 @@ describe('Bitcoin Transaction', function() {
|
|||||||
it('should return an error if the transaction for the input does not exist', function(done) {
|
it('should return an error if the transaction for the input does not exist', function(done) {
|
||||||
var tx = new Transaction();
|
var tx = new Transaction();
|
||||||
var db = {
|
var db = {
|
||||||
getTransactionFromDB: sinon.stub().callsArgWith(1, new levelup.errors.NotFoundError())
|
getTransaction: sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError())
|
||||||
};
|
};
|
||||||
tx._populateInput(db, input, [], function(err) {
|
tx._populateInput(db, input, [], function(err) {
|
||||||
should.exist(err);
|
should.exist(err);
|
||||||
@ -160,7 +161,7 @@ describe('Bitcoin Transaction', function() {
|
|||||||
it('should look through poolTransactions if database does not have transaction', function(done) {
|
it('should look through poolTransactions if database does not have transaction', function(done) {
|
||||||
var tx = new Transaction();
|
var tx = new Transaction();
|
||||||
var db = {
|
var db = {
|
||||||
getTransactionFromDB: sinon.stub().callsArgWith(1, new levelup.errors.NotFoundError())
|
getTransaction: sinon.stub().callsArgWith(2, new levelup.errors.NotFoundError())
|
||||||
};
|
};
|
||||||
var transactions = [
|
var transactions = [
|
||||||
{
|
{
|
||||||
@ -179,7 +180,7 @@ describe('Bitcoin Transaction', function() {
|
|||||||
prevTx.outputs = ['output'];
|
prevTx.outputs = ['output'];
|
||||||
var tx = new Transaction();
|
var tx = new Transaction();
|
||||||
var db = {
|
var db = {
|
||||||
getTransactionFromDB: sinon.stub().callsArgWith(1, null, prevTx)
|
getTransaction: sinon.stub().callsArgWith(2, null, prevTx)
|
||||||
};
|
};
|
||||||
tx._populateInput(db, input, [], function(err) {
|
tx._populateInput(db, input, [], function(err) {
|
||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user