bitcore node service
This commit is contained in:
parent
038eb401db
commit
e4992a6dfb
213
bitcore-node/addresses.js
Normal file
213
bitcore-node/addresses.js
Normal file
@ -0,0 +1,213 @@
|
||||
'use strict';
|
||||
|
||||
var common = require('./common');
|
||||
var bitcore = require('bitcore');
|
||||
var TxController = require('./transactions');
|
||||
|
||||
function AddressController(node) {
|
||||
this.node = node;
|
||||
this.txController = new TxController(node);
|
||||
}
|
||||
|
||||
AddressController.prototype.show = function(req, res, next) {
|
||||
this.getAddressHistory(req.addr, function(err, data) {
|
||||
if(err) {
|
||||
return common.handleErrors(err, res);
|
||||
}
|
||||
|
||||
res.jsonp(data);
|
||||
});
|
||||
};
|
||||
|
||||
AddressController.prototype.balance = function(req, res, next) {
|
||||
this.addressHistorySubQuery(req, res, next, 'balanceSat');
|
||||
};
|
||||
|
||||
AddressController.prototype.totalReceived = function(req, res, next) {
|
||||
this.addressHistorySubQuery(req, res, next, 'totalReceivedSat');
|
||||
};
|
||||
|
||||
AddressController.prototype.totalSent = function(req, res, next) {
|
||||
this.addressHistorySubQuery(req, res, next, 'totalSentSat');
|
||||
};
|
||||
|
||||
AddressController.prototype.unconfirmedBalance = function(req, res, next) {
|
||||
this.addressHistorySubQuery(req, res, next, 'unconfirmedBalanceSat');
|
||||
};
|
||||
|
||||
AddressController.prototype.addressHistorySubQuery = function(req, res, next, param) {
|
||||
this.getAddressHistory(req.addr, function(err, data) {
|
||||
if(err) {
|
||||
return common.handleErrors(err, res);
|
||||
}
|
||||
|
||||
res.jsonp(data[param]);
|
||||
});
|
||||
};
|
||||
|
||||
AddressController.prototype.getAddressHistory = function(address, callback) {
|
||||
var self = this;
|
||||
|
||||
this.node.getAddressHistory(address, true, function(err, txinfos) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, self.transformAddressHistory(txinfos, address));
|
||||
});
|
||||
};
|
||||
|
||||
AddressController.prototype.checkAddr = function(req, res, next) {
|
||||
req.addr = req.params.addr;
|
||||
this.check(req, res, next, [req.addr]);
|
||||
};
|
||||
|
||||
AddressController.prototype.checkAddrs = function(req, res, next) {
|
||||
if(req.body.addrs) {
|
||||
req.addrs = req.body.addrs.split(',');
|
||||
} else {
|
||||
req.addrs = req.params.addrs.split(',');
|
||||
}
|
||||
|
||||
this.check(req, res, next, req.addrs);
|
||||
}
|
||||
|
||||
AddressController.prototype.check = function(req, res, next, addresses) {
|
||||
if(!addresses.length || !addresses[0]) {
|
||||
return common.handleErrors({
|
||||
message: 'Must include address',
|
||||
code: 1
|
||||
}, res);
|
||||
}
|
||||
|
||||
for(var i = 0; i < addresses.length; i++) {
|
||||
try {
|
||||
var a = new bitcore.Address(addresses[i]);
|
||||
} catch(e) {
|
||||
return common.handleErrors({
|
||||
message: 'Invalid address: ' + e.message,
|
||||
code: 1
|
||||
}, res);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
AddressController.prototype.transformAddressHistory = function(txinfos, address) {
|
||||
var transactions = txinfos.map(function(info) {
|
||||
return info.tx.hash;
|
||||
}).filter(function(value, index, self) {
|
||||
return self.indexOf(value) === index;
|
||||
});
|
||||
|
||||
var balance = 0;
|
||||
var appearances = 0;
|
||||
var totalReceived = 0;
|
||||
var totalSent = 0;
|
||||
var unconfirmedBalance = 0;
|
||||
var unconfirmedAppearances = 0;
|
||||
|
||||
for(var i = 0; i < txinfos.length; i++) {
|
||||
if(txinfos[i].satoshis > 0) {
|
||||
totalReceived += txinfos[i].satoshis;
|
||||
} else {
|
||||
totalSent += -txinfos[i].satoshis;
|
||||
}
|
||||
|
||||
if(txinfos[i].confirmations) {
|
||||
balance += txinfos[i].satoshis;
|
||||
unconfirmedBalance += txinfos[i].satoshis;
|
||||
appearances++;
|
||||
} else {
|
||||
unconfirmedBalance += txinfos[i].satoshis;
|
||||
unconfirmedAppearances++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
addrStr: address,
|
||||
balance: balance / 1e8,
|
||||
balanceSat: balance,
|
||||
totalReceived: totalReceived / 1e8,
|
||||
totalReceivedSat: totalReceived,
|
||||
totalSent: totalSent / 1e8,
|
||||
totalSentSat: totalSent,
|
||||
unconfirmedBalance: unconfirmedBalance / 1e8,
|
||||
unconfirmedBalanceSat: unconfirmedBalance,
|
||||
unconfirmedTxApperances: unconfirmedAppearances, // misspelling - ew
|
||||
txApperances: appearances, // yuck
|
||||
transactions: transactions
|
||||
};
|
||||
};
|
||||
|
||||
AddressController.prototype.utxo = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
this.node.getUnspentOutputs(req.addr, true, function(err, utxos) {
|
||||
if(err && err instanceof self.node.errors.NoOutputs) {
|
||||
return res.jsonp([]);
|
||||
} else if(err) {
|
||||
return common.handleErrors(err, res);
|
||||
}
|
||||
|
||||
res.jsonp(utxos.map(self.transformUtxo.bind(self)));
|
||||
});
|
||||
};
|
||||
|
||||
AddressController.prototype.multiutxo = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
this.node.getUnspentOutputs(req.addrs, true, function(err, utxos) {
|
||||
if(err && err instanceof self.node.errors.NoOutputs) {
|
||||
return res.jsonp([]);
|
||||
} else if(err) {
|
||||
return common.handleErrors(err, res);
|
||||
}
|
||||
|
||||
res.jsonp(utxos.map(self.transformUtxo.bind(self)));
|
||||
});
|
||||
};
|
||||
|
||||
AddressController.prototype.transformUtxo = function(utxo) {
|
||||
return {
|
||||
address: utxo.address,
|
||||
txid: utxo.txid,
|
||||
vout: utxo.outputIndex,
|
||||
ts: utxo.timestamp ? Math.round(utxo.timestamp / 1000) : Math.round(Date.now() / 1000),
|
||||
scriptPubKey: utxo.script,
|
||||
amount: utxo.satoshis / 1e8,
|
||||
confirmations: utxo.confirmations
|
||||
};
|
||||
};
|
||||
|
||||
AddressController.prototype.multitxs = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
this.node.getAddressHistory(req.addrs, true, function(err, txinfos) {
|
||||
if(err) {
|
||||
return common.handleErrors(err, res);
|
||||
}
|
||||
|
||||
res.jsonp(self.transformAddressHistoryForMultiTxs(txinfos));
|
||||
});
|
||||
};
|
||||
|
||||
AddressController.prototype.transformAddressHistoryForMultiTxs = function(txinfos) {
|
||||
var self = this;
|
||||
|
||||
var transformed = {
|
||||
totalItems: txinfos.length,
|
||||
from: 0,
|
||||
to: txinfos.length,
|
||||
items: txinfos.map(function(txinfo) {
|
||||
return self.txController.transformTransaction(txinfo.tx);
|
||||
}).reverse()
|
||||
};
|
||||
|
||||
return transformed;
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports = AddressController;
|
||||
128
bitcore-node/blocks.js
Normal file
128
bitcore-node/blocks.js
Normal file
@ -0,0 +1,128 @@
|
||||
'use strict';
|
||||
|
||||
var common = require('./common');
|
||||
var async = require('async');
|
||||
var bitcore = require('bitcore');
|
||||
var BufferUtil = bitcore.util.buffer;
|
||||
|
||||
function BlockController(node) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
var BLOCK_LIMIT = 200;
|
||||
|
||||
/**
|
||||
* Find block by hash ...
|
||||
*/
|
||||
BlockController.prototype.block = function(req, res, next, hash) {
|
||||
var self = this;
|
||||
|
||||
this.node.getBlock(hash, function(err, block) {
|
||||
if(err && err.message === 'Block not found.') {
|
||||
// TODO libbitcoind should pass an instance of errors.Block.NotFound
|
||||
return common.handleErrors(null, res);
|
||||
} else if(err) {
|
||||
return common.handleErrors(err, res);
|
||||
}
|
||||
|
||||
var info = self.node.services.bitcoind.getBlockIndex(hash);
|
||||
|
||||
req.block = self.transformBlock(block, info);
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
BlockController.prototype.transformBlock = function(block, info) {
|
||||
var blockObj = block.toObject();
|
||||
var transactionIds = blockObj.transactions.map(function(tx) {
|
||||
return tx.hash
|
||||
});
|
||||
return {
|
||||
hash: block.hash,
|
||||
confirmations: this.node.services.db.tip.__height - info.height + 1,
|
||||
size: block.toBuffer().length,
|
||||
height: info.height,
|
||||
version: blockObj.header.version,
|
||||
merkleroot: blockObj.header.merkleRoot,
|
||||
tx: transactionIds,
|
||||
time: blockObj.header.time,
|
||||
nonce: blockObj.header.nonce,
|
||||
bits: blockObj.header.bits.toString(16),
|
||||
difficulty: 0, // placeholder
|
||||
chainwork: info.chainWork,
|
||||
previousblockhash: blockObj.header.prevHash,
|
||||
nextblockhash: null, // placeholder
|
||||
reward: 0, // First output of first transaction gives us the reward + fees. How to isolate just reward?
|
||||
isMainChain: true // placeholder
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Show block
|
||||
*/
|
||||
BlockController.prototype.show = function(req, res) {
|
||||
if (req.block) {
|
||||
res.jsonp(req.block);
|
||||
}
|
||||
};
|
||||
|
||||
BlockController.prototype.blockIndex = function(req, res, next, height) {
|
||||
var info = this.node.services.bitcoind.getBlockIndex(parseInt(height));
|
||||
if(!info) {
|
||||
return common.handleErrors(null, res);
|
||||
}
|
||||
|
||||
res.jsonp({
|
||||
blockHash: info.hash
|
||||
});
|
||||
};
|
||||
|
||||
// List blocks by date
|
||||
BlockController.prototype.list = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
// TODO actually get blocks by date instead of just the last blocks
|
||||
// TODO pagination
|
||||
|
||||
var limit = parseInt(req.query.limit || BLOCK_LIMIT);
|
||||
|
||||
var blocks = [];
|
||||
var lastHash = this.node.services.db.tip.hash;
|
||||
|
||||
async.timesSeries(limit, function(n, next) {
|
||||
self.node.getBlock(lastHash, function(err, block) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var info = self.node.services.bitcoind.getBlockIndex(block.hash);
|
||||
block.__height = info.height;
|
||||
blocks.push(block);
|
||||
lastHash = BufferUtil.reverse(block.header.prevHash).toString('hex');
|
||||
next();
|
||||
});
|
||||
}, function(err) {
|
||||
if(err) {
|
||||
return common.handleErrors(err, res);
|
||||
}
|
||||
|
||||
var data = {
|
||||
blocks: blocks.map(function(block) {
|
||||
return {
|
||||
height: block.__height,
|
||||
size: block.toBuffer().length,
|
||||
hash: block.hash,
|
||||
time: block.header.time,
|
||||
txlength: block.transactions.length,
|
||||
poolInfo: {}
|
||||
};
|
||||
}),
|
||||
length: blocks.length,
|
||||
pagination: {}
|
||||
};
|
||||
|
||||
res.jsonp(data);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = BlockController;
|
||||
19
bitcore-node/common.js
Normal file
19
bitcore-node/common.js
Normal file
@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
exports.notReady = function (err, res, p) {
|
||||
res.status(503).send('Server not yet ready. Sync Percentage:' + p);
|
||||
};
|
||||
|
||||
exports.handleErrors = function (err, res) {
|
||||
if (err) {
|
||||
if (err.code) {
|
||||
res.status(400).send(err.message + '. Code:' + err.code);
|
||||
}
|
||||
else {
|
||||
res.status(503).send(err.message);
|
||||
}
|
||||
}
|
||||
else {
|
||||
res.status(404).send('Not found');
|
||||
}
|
||||
};
|
||||
97
bitcore-node/index.js
Normal file
97
bitcore-node/index.js
Normal file
@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
var BaseService = require('./service');
|
||||
var inherits = require('util').inherits;
|
||||
var BlockController = require('./blocks');
|
||||
var TxController = require('./transactions');
|
||||
var AddressController = require('./addresses');
|
||||
var StatusController = require('./status');
|
||||
|
||||
var InsightAPI = function(options) {
|
||||
BaseService.call(this, options);
|
||||
};
|
||||
|
||||
InsightAPI.dependencies = ['address', 'web'];
|
||||
|
||||
inherits(InsightAPI, BaseService);
|
||||
|
||||
InsightAPI.prototype.setupRoutes = function(app) {
|
||||
var apiPrefix = '/api';
|
||||
|
||||
//Block routes
|
||||
var blocks = new BlockController(this.node);
|
||||
app.get(apiPrefix + '/blocks', blocks.list.bind(blocks));
|
||||
|
||||
|
||||
app.get(apiPrefix + '/block/:blockHash', blocks.show.bind(blocks));
|
||||
app.param('blockHash', blocks.block.bind(blocks));
|
||||
|
||||
app.get(apiPrefix + '/block-index/:height', blocks.blockIndex.bind(blocks));
|
||||
app.param('height', blocks.blockIndex.bind(blocks));
|
||||
|
||||
|
||||
// Transaction routes
|
||||
var transactions = new TxController(this.node);
|
||||
app.get(apiPrefix + '/tx/:txid', transactions.show.bind(transactions));
|
||||
app.param('txid', transactions.transaction.bind(transactions));
|
||||
app.get(apiPrefix + '/txs', transactions.list.bind(transactions));
|
||||
app.post(apiPrefix + '/tx/send', transactions.send.bind(transactions));
|
||||
|
||||
// Raw Routes
|
||||
app.get(apiPrefix + '/rawtx/:txid', transactions.showRaw.bind(transactions));
|
||||
app.param('txid', transactions.rawTransaction.bind(transactions));
|
||||
|
||||
// Address routes
|
||||
var addresses = new AddressController(this.node);
|
||||
app.get(apiPrefix + '/addr/:addr', addresses.checkAddr.bind(addresses), addresses.show.bind(addresses));
|
||||
app.get(apiPrefix + '/addr/:addr/utxo', addresses.checkAddr.bind(addresses), addresses.utxo.bind(addresses));
|
||||
app.get(apiPrefix + '/addrs/:addrs/utxo', addresses.checkAddrs.bind(addresses), addresses.multiutxo.bind(addresses));
|
||||
app.post(apiPrefix + '/addrs/utxo', addresses.checkAddrs.bind(addresses), addresses.multiutxo.bind(addresses));
|
||||
app.get(apiPrefix + '/addrs/:addrs/txs', addresses.checkAddrs.bind(addresses), addresses.multitxs.bind(addresses));
|
||||
app.post(apiPrefix + '/addrs/txs', addresses.checkAddrs.bind(addresses), addresses.multitxs.bind(addresses));
|
||||
|
||||
// Address property routes
|
||||
app.get(apiPrefix + '/addr/:addr/balance', addresses.checkAddr.bind(addresses), addresses.balance.bind(addresses));
|
||||
app.get(apiPrefix + '/addr/:addr/totalReceived', addresses.checkAddr.bind(addresses), addresses.totalReceived.bind(addresses));
|
||||
app.get(apiPrefix + '/addr/:addr/totalSent', addresses.checkAddr.bind(addresses), addresses.totalSent.bind(addresses));
|
||||
app.get(apiPrefix + '/addr/:addr/unconfirmedBalance', addresses.checkAddr.bind(addresses), addresses.unconfirmedBalance.bind(addresses));
|
||||
|
||||
// Status route
|
||||
var status = new StatusController(this.node);
|
||||
app.get(apiPrefix + '/status', status.show.bind(status));
|
||||
app.get(apiPrefix + '/sync', status.sync.bind(status));
|
||||
app.get(apiPrefix + '/peer', status.peer.bind(status));
|
||||
app.get(apiPrefix + '/version', status.version.bind(status));
|
||||
|
||||
|
||||
// Utils route
|
||||
/*var utils = require('../app/controllers/utils');
|
||||
app.get(apiPrefix + '/utils/estimatefee', utils.estimateFee);
|
||||
|
||||
// Currency
|
||||
var currency = require('../app/controllers/currency');
|
||||
app.get(apiPrefix + '/currency', currency.index);
|
||||
|
||||
// Email store plugin
|
||||
if (config.enableEmailstore) {
|
||||
var emailPlugin = require('../plugins/emailstore');
|
||||
app.get(apiPrefix + '/email/retrieve', emailPlugin.retrieve);
|
||||
}
|
||||
|
||||
// Currency rates plugin
|
||||
if (config.enableCurrencyRates) {
|
||||
var currencyRatesPlugin = require('../plugins/currencyrates');
|
||||
app.get(apiPrefix + '/rates/:code', currencyRatesPlugin.getRate);
|
||||
}
|
||||
|
||||
// Address routes
|
||||
var messages = require('../app/controllers/messages');
|
||||
app.get(apiPrefix + '/messages/verify', messages.verify);
|
||||
app.post(apiPrefix + '/messages/verify', messages.verify);
|
||||
|
||||
//Home route
|
||||
var index = require('../app/controllers/index');
|
||||
app.get('*', index.render);*/
|
||||
};
|
||||
|
||||
module.exports = InsightAPI;
|
||||
91
bitcore-node/service.js
Normal file
91
bitcore-node/service.js
Normal file
@ -0,0 +1,91 @@
|
||||
'use strict';
|
||||
|
||||
var util = require('util');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
var Service = function(options) {
|
||||
EventEmitter.call(this);
|
||||
|
||||
this.node = options.node;
|
||||
this.name = options.name;
|
||||
};
|
||||
|
||||
util.inherits(Service, EventEmitter);
|
||||
|
||||
/**
|
||||
* Describes the dependencies that should be loaded before this service.
|
||||
*/
|
||||
Service.dependencies = [];
|
||||
|
||||
/**
|
||||
* blockHandler
|
||||
* @param {Block} block - the block being added or removed from the chain
|
||||
* @param {Boolean} add - whether the block is being added or removed
|
||||
* @param {Function} callback - call with the leveldb database operations to perform
|
||||
*/
|
||||
Service.prototype.blockHandler = function(block, add, callback) {
|
||||
// implement in the child class
|
||||
setImmediate(function() {
|
||||
callback(null, []);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* the bus events available for subscription
|
||||
* @return {Array} an array of event info
|
||||
*/
|
||||
Service.prototype.getPublishEvents = function() {
|
||||
// Example:
|
||||
// return [
|
||||
// ['eventname', this, this.subscribeEvent, this.unsubscribeEvent],
|
||||
// ];
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* the API methods to expose
|
||||
* @return {Array} return array of methods
|
||||
*/
|
||||
Service.prototype.getAPIMethods = function() {
|
||||
// Example:
|
||||
// return [
|
||||
// ['getData', this, this.getData, 1]
|
||||
// ];
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
// Example:
|
||||
// Service.prototype.getData = function(arg1, callback) {
|
||||
//
|
||||
// };
|
||||
|
||||
/**
|
||||
* Function which is called when module is first initialized
|
||||
*/
|
||||
Service.prototype.start = function(done) {
|
||||
setImmediate(done);
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to be called when bitcore-node is stopped
|
||||
*/
|
||||
Service.prototype.stop = function(done) {
|
||||
setImmediate(done);
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup express routes
|
||||
* @param {Express} app
|
||||
*/
|
||||
Service.prototype.setupRoutes = function(app) {
|
||||
// Setup express routes here
|
||||
};
|
||||
|
||||
Service.prototype.getRoutePrefix = function() {
|
||||
return this.name;
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports = Service;
|
||||
75
bitcore-node/status.js
Normal file
75
bitcore-node/status.js
Normal file
@ -0,0 +1,75 @@
|
||||
'use strict';
|
||||
|
||||
function StatusController(node) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
StatusController.prototype.show = function(req, res) {
|
||||
|
||||
var option = req.query.q;
|
||||
|
||||
switch(option) {
|
||||
case 'getDifficulty':
|
||||
res.jsonp(this.getDifficulty());
|
||||
break;
|
||||
case 'getTxOutSetInfo':
|
||||
// TODO
|
||||
// break;
|
||||
case 'getLastBlockHash':
|
||||
// TODO
|
||||
// break;
|
||||
case 'getBestBlockHash':
|
||||
// TODO
|
||||
// break;
|
||||
case 'getInfo':
|
||||
default:
|
||||
res.jsonp(this.getInfo());
|
||||
}
|
||||
};
|
||||
|
||||
StatusController.prototype.getInfo = function() {
|
||||
return this.node.services.bitcoind.getInfo();
|
||||
};
|
||||
|
||||
StatusController.prototype.getDifficulty = function() {
|
||||
var info = this.node.services.bitcoind.getInfo();
|
||||
return {
|
||||
difficulty: info.difficulty
|
||||
};
|
||||
};
|
||||
|
||||
StatusController.prototype.sync = function(req, res) {
|
||||
var status = 'syncing';
|
||||
if(this.node.services.bitcoind.isSynced() && this.node.services.db.tip.__height === this.node.services.bitcoind.height) {
|
||||
status = 'finished';
|
||||
}
|
||||
|
||||
var info = {
|
||||
status: status,
|
||||
blockChainHeight: this.node.services.bitcoind.height,
|
||||
syncPercentage: this.node.services.db.tip.__height / this.node.services.bitcoind.height * 100,
|
||||
height: this.node.services.db.tip.__height,
|
||||
error: null,
|
||||
type: 'bitcore node'
|
||||
};
|
||||
|
||||
res.jsonp(info);
|
||||
};
|
||||
|
||||
// Hard coded to make insight ui happy, but not applicable
|
||||
StatusController.prototype.peer = function(req, res) {
|
||||
res.jsonp({
|
||||
connected: true,
|
||||
host: '127.0.0.1',
|
||||
port: null
|
||||
});
|
||||
};
|
||||
|
||||
StatusController.prototype.version = function(req, res) {
|
||||
var pjson = require('../../../package.json');
|
||||
res.json({
|
||||
version: pjson.version
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = StatusController;
|
||||
251
bitcore-node/transactions.js
Normal file
251
bitcore-node/transactions.js
Normal file
@ -0,0 +1,251 @@
|
||||
'use strict';
|
||||
|
||||
var bitcore = require('bitcore');
|
||||
var common = require('./common');
|
||||
var async = require('async');
|
||||
|
||||
function TxController(node) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
TxController.prototype.show = function(req, res) {
|
||||
if (req.transaction) {
|
||||
res.jsonp(req.transaction);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Find transaction by hash ...
|
||||
*/
|
||||
TxController.prototype.transaction = function(req, res, next, txid) {
|
||||
var self = this;
|
||||
|
||||
this.node.getTransactionWithBlockInfo(txid, true, function(err, transaction) {
|
||||
if (err && err instanceof self.node.errors.Transaction.NotFound) {
|
||||
return common.handleErrors(null, res);
|
||||
} else if(err) {
|
||||
return common.handleErrors(err, res);
|
||||
}
|
||||
|
||||
transaction.populateInputs(self.node.services.db, [], function(err) {
|
||||
if(err) {
|
||||
return res.send({
|
||||
error: err.toString()
|
||||
})
|
||||
}
|
||||
|
||||
req.transaction = self.transformTransaction(transaction);
|
||||
next();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
TxController.prototype.transformTransaction = function(transaction) {
|
||||
var txObj = transaction.toObject();
|
||||
|
||||
var confirmations = 0;
|
||||
if(transaction.__height >= 0) {
|
||||
confirmations = this.node.services.db.tip.__height - transaction.__height + 1;
|
||||
}
|
||||
|
||||
var transformed = {
|
||||
txid: txObj.hash,
|
||||
version: txObj.version,
|
||||
locktime: txObj.nLockTime
|
||||
};
|
||||
|
||||
if(transaction.isCoinbase()) {
|
||||
transformed.vin = [
|
||||
{
|
||||
coinbase: txObj.inputs[0].script,
|
||||
sequence: txObj.inputs[0].sequenceNumber,
|
||||
n: 0
|
||||
}
|
||||
];
|
||||
} else {
|
||||
transformed.vin = txObj.inputs.map(this.transformInput.bind(this));
|
||||
}
|
||||
|
||||
transformed.vout = txObj.outputs.map(this.transformOutput.bind(this));
|
||||
|
||||
transformed.blockhash = transaction.__blockHash;
|
||||
transformed.confirmations = confirmations;
|
||||
transformed.time = transaction.__timestamp ? transaction.__timestamp : Date.now(); // can we get this from bitcoind?
|
||||
transformed.blocktime = transformed.time;
|
||||
|
||||
if(transaction.isCoinbase()) {
|
||||
transformed.isCoinBase = true;
|
||||
}
|
||||
|
||||
transformed.valueOut = transaction.outputAmount / 1e8;
|
||||
transformed.size = transaction.toBuffer().length;
|
||||
if(transaction.hasAllUtxoInfo()) {
|
||||
transformed.valueIn = transaction.inputAmount / 1e8;
|
||||
transformed.fees = transaction.getFee() / 1e8;
|
||||
}
|
||||
|
||||
return transformed;
|
||||
};
|
||||
|
||||
TxController.prototype.transformInput = function(input, index) {
|
||||
var transformed = {
|
||||
txid: input.prevTxId,
|
||||
vout: input.outputIndex,
|
||||
scriptSig: {
|
||||
asm: null, // TODO
|
||||
hex: input.script
|
||||
},
|
||||
sequence: input.sequenceNumber,
|
||||
n: index
|
||||
};
|
||||
|
||||
if(input.output) {
|
||||
transformed.addr = bitcore.Script(input.output.script).toAddress(this.node.network).toString();
|
||||
transformed.valueSat = input.output.satoshis;
|
||||
transformed.value = input.output.satoshis / 1e8;
|
||||
transformed.doubleSpentTxID = null; // TODO
|
||||
transformed.isConfirmed = null; // TODO
|
||||
transformed.confirmations = null; // TODO
|
||||
transformed.unconfirmedInput = null; // TODO
|
||||
}
|
||||
|
||||
return transformed;
|
||||
};
|
||||
|
||||
TxController.prototype.transformOutput = function(output, index) {
|
||||
var transformed = {
|
||||
value: (output.satoshis / 1e8).toFixed(8),
|
||||
n: index,
|
||||
scriptPubKey: {
|
||||
asm: null, // TODO
|
||||
hex: output.script,
|
||||
reqSigs: null, // TODO
|
||||
type: null // TODO
|
||||
},
|
||||
spentTxId: null, // TODO
|
||||
spentIndex: null, // TODO
|
||||
spentTs: null // TODO
|
||||
};
|
||||
|
||||
var address = bitcore.Script(output.script).toAddress(this.node.network).toString();
|
||||
if(address !== 'false') {
|
||||
transformed.scriptPubKey.addresses = [address];
|
||||
}
|
||||
|
||||
return transformed;
|
||||
};
|
||||
|
||||
TxController.prototype.rawTransaction = function(req, res, next, txid) {
|
||||
this.node.getTransaction(txid, true, function(err, transaction) {
|
||||
if (err && err instanceof self.node.errors.Transaction.NotFound) {
|
||||
return common.handleErrors(null, res);
|
||||
} else if(err) {
|
||||
return common.handleErrors(err, res);
|
||||
}
|
||||
|
||||
req.rawTransaction = {
|
||||
'rawtx': transaction.toBuffer().toString('hex')
|
||||
};
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
TxController.prototype.showRaw = function(req, res) {
|
||||
if (req.rawTransaction) {
|
||||
res.jsonp(req.rawTransaction);
|
||||
}
|
||||
};
|
||||
|
||||
TxController.prototype.list = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
var blockHash = req.query.block;
|
||||
var address = req.query.address;
|
||||
var page = req.query.pageNum;
|
||||
var pageLength = 10;
|
||||
var pagesTotal = 1;
|
||||
|
||||
if(blockHash) {
|
||||
self.node.getBlock(blockHash, function(err, block) {
|
||||
if(err && err.message === 'Block not found.') {
|
||||
return common.handleErrors(null, res);
|
||||
} else if(err) {
|
||||
return common.handleErrors(err, res);
|
||||
}
|
||||
|
||||
var blockInfo = self.node.services.bitcoind.getBlockIndex(block.hash);
|
||||
var txs = block.transactions;
|
||||
var totalTxs = txs.length;
|
||||
|
||||
if(page) {
|
||||
txs = txs.splice(page * pageLength, pageLength);
|
||||
pagesTotal = Math.ceil(totalTxs / pageLength);
|
||||
}
|
||||
|
||||
async.mapSeries(txs, function(tx, next) {
|
||||
tx.__blockHash = block.hash;
|
||||
tx.__height = blockInfo.height;
|
||||
tx.__timestamp = block.header.time;
|
||||
|
||||
tx.populateInputs(self.node.services.db, [], function(err) {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next(null, self.transformTransaction(tx));
|
||||
});
|
||||
}, function(err, transformed) {
|
||||
if(err) {
|
||||
return common.handleErrors(err, res);
|
||||
}
|
||||
|
||||
res.jsonp({
|
||||
pagesTotal: pagesTotal,
|
||||
txs: transformed
|
||||
});
|
||||
});
|
||||
});
|
||||
} else if(address) {
|
||||
self.node.getAddressHistory(address, true, function(err, txinfos) {
|
||||
if(err) {
|
||||
return common.handleErrors(err, res);
|
||||
}
|
||||
|
||||
var txs = txinfos.map(function(info) {
|
||||
return info.tx;
|
||||
}).filter(function(value, index, self) {
|
||||
return self.indexOf(value) === index;
|
||||
});
|
||||
|
||||
var totalTxs = txs.length;
|
||||
|
||||
if(page) {
|
||||
txs = txs.splice(page * pageLength, pageLength);
|
||||
pagesTotal = Math.ceil(totalTxs / pageLength);
|
||||
}
|
||||
|
||||
var transformed = txs.map(self.transformTransaction.bind(self));
|
||||
|
||||
res.jsonp({
|
||||
pagesTotal: pagesTotal,
|
||||
txs: transformed
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return common.handleErrors(new Error('Block hash or address expected'), res);
|
||||
}
|
||||
};
|
||||
|
||||
TxController.prototype.send = function(req, res) {
|
||||
this.node.sendTransaction(req.body.rawtx, function(err, txid) {
|
||||
if(err) {
|
||||
// TODO handle specific errors
|
||||
return common.handleErrors(err, res);
|
||||
}
|
||||
|
||||
res.json({'txid': txid});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = TxController;
|
||||
@ -43,6 +43,7 @@
|
||||
"scripts": {
|
||||
"start": "node insight.js"
|
||||
},
|
||||
"bitcoreNode": "bitcore-node",
|
||||
"dependencies": {
|
||||
"async": "*",
|
||||
"base58-native": "0.1.2",
|
||||
|
||||
165
test/bitcore-node/blocks.js
Normal file
165
test/bitcore-node/blocks.js
Normal file
@ -0,0 +1,165 @@
|
||||
'use strict';
|
||||
|
||||
var should = require('should');
|
||||
var sinon = require('sinon');
|
||||
var BlockController = require('../../bitcore-node/blocks');
|
||||
var bitcore = require('bitcore');
|
||||
var _ = require('lodash');
|
||||
|
||||
var blocks = {
|
||||
'0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7': '07000020a491892cca9f143f7f00b8d65bbce0204bb32e17e914325fa5010000000000003e28f0519ecf01f7f01ea8da61084b2e4741a18ce1f3738117b84458353764b06fb9e35567f20c1a78eb626f0301000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2303d6250800feb0aae355fe263600000963676d696e6572343208ae5800000000000000ffffffff01c018824a000000001976a91468bedce8982d25c3b6b03f6238cbad00378b8ead88ac000000000100000002ad5a14ae9d0f3221b790c4fc590fddceea1456e5692d8c4bf1ff7175f2b0c987000000008b4830450221008e5df62719cd92d7b137d00bbd27f153f2909bcad3a300960bc1020ec6d5e961022039df51600ff4fb5da5a794d1648c6b47c1f7d277fd5877fb5e52a730a3595f8c014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307ffffffff9621ac65bc22ea593ca9a61a8d63e461bf3d3f277989df5d3bd33ddfae0aa1d8000000008a4730440220761464d7bab9515d92260762a97af82a9b25d202d8f7197b1aaec81b6fed541f022059f99606de6b06e17b2cd102dceb3807ebdd9e777a5b77c9a0b3672f5eabcb31014104eb1e0ccd9afcac42229348dd776e991c69551ae3474340fada12e787e51758397e1d3afdba360d6374261125ea3b6ea079a5f202c150dfd729e1062d9176a307ffffffff02dc374401000000001976a9144b7b335f978f130269fe661423258ae9642df8a188ac72b3d000000000001976a9146efcf883b4b6f9997be9a0600f6c095fe2bd2d9288ac000000000100000002060d3cb6dfb7ffe85e2908010fea63190c9707e96fc7448128eb895b5e222771030000006b483045022100f67cffc0ae23adb236ff3edb4a9736e277605db30cc7708dfab8cf1e1483bbce022052396aa5d664ec1cb65992c423fd9a17e94dc7af328d2d559e90746dd195ca5901210346134da14907581d8190d3980caaf46d95e4eb9c1ca8e70f1fc6007fefb1909dfeffffff7b2d8a8263cffbdb722e2a5c74166e6f2258634e277c0b08f51b578b667e2fba000000006a473044022077222a91cda23af69179377c62d84a176fb12caff6c5cbf6ae9e5957ff3b1afe0220768edead76819228dcba18cca3c9a5a5d4c32919720f21df21a297ba375bbe5c012103371ea5a4dfe356b3ea4042a537d7ab7ee0faabd43e21b6cc076fda2240629eeefeffffff02209a1d00000000001976a9148e451eec7ca0a1764b4ab119274efdd2727b3c8588ac40420f00000000001976a914d0fce8f064cd1059a6a11501dd66fe42368572b088accb250800',
|
||||
'000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7': '0300000041c49521c6106debe1221077ca9e71850e3c328037e5e98fbd0600000000000082b3a56da828618b1b7b103763e3c21c38c9536ce185ddd4aebdcd627645a878fb96e355ffff001d192907730101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2803bf25081b4d696e656420627920416e74506f6f6c202bde53072055e396fb010300000099050000ffffffff01807c814a000000001976a9144c5220c58749e5474ec4e5bbb0c2cc0342c6f44488ac00000000',
|
||||
'00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441': '030000004705096ec15ee742e9c14aee22f5f30cb568a0817b4a6c1ec4b9010000000000afb4442dbd55dcc8c86c94dc84ca0d130aeade144623fc0e47849343087067204792e35567f20c1a4b773d0f0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2803be25081b4d696e656420627920416e74506f6f6c202bde53072055e392470100000000aba10000ffffffff01807c814a000000001976a9144c5220c58749e5474ec4e5bbb0c2cc0342c6f44488ac00000000'
|
||||
};
|
||||
|
||||
var blockIndexes = {
|
||||
'0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7': {
|
||||
hash: '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7',
|
||||
chainWork: '0000000000000000000000000000000000000000000000054626b1839ade284a',
|
||||
prevHash: '00000000000001a55f3214e9172eb34b20e0bc5bd6b8007f3f149fca2c8991a4',
|
||||
height: 533974
|
||||
},
|
||||
'000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7': {
|
||||
hash: '000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7',
|
||||
chainWork: '00000000000000000000000000000000000000000000000544ea52e1575ca753',
|
||||
prevHash: '00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441',
|
||||
height: 533951
|
||||
},
|
||||
'00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441': {
|
||||
hash: '00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441',
|
||||
chainWork: '00000000000000000000000000000000000000000000000544ea52e0575ba752',
|
||||
prevHash: '000000000001b9c41e6c4a7b81a068b50cf3f522ee4ac1e942e75ec16e090547',
|
||||
height: 533950
|
||||
}
|
||||
};
|
||||
|
||||
describe('Blocks', function() {
|
||||
describe('/blocks/:blockHash route', function() {
|
||||
var insight = {
|
||||
"hash": "0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7",
|
||||
"confirmations": 119,
|
||||
"size": 1011,
|
||||
"height": 533974,
|
||||
"version": 536870919,
|
||||
"merkleroot": "b06437355844b8178173f3e18ca141472e4b0861daa81ef0f701cf9e51f0283e",
|
||||
"tx": [
|
||||
"25a988e54b02e0e5df146a0f8fa7b9db56210533a9f04bdfda5f4ceb6f77aadd",
|
||||
"b85334bf2df35c6dd5b294efe92ffc793a78edff75a2ca666fc296ffb04bbba0",
|
||||
"2e01c7a4a0e335112236b711c4aaddd02e8dc59ba2cda416e8f80ff06dddd7e1"
|
||||
],
|
||||
"time": 1440987503,
|
||||
"nonce": 1868753784,
|
||||
"bits": "1a0cf267",
|
||||
"difficulty": 1295829.93087696,
|
||||
"chainwork": "0000000000000000000000000000000000000000000000054626b1839ade284a",
|
||||
"previousblockhash": "00000000000001a55f3214e9172eb34b20e0bc5bd6b8007f3f149fca2c8991a4",
|
||||
"nextblockhash": "000000000001e866a8057cde0c650796cb8a59e0e6038dc31c69d7ca6649627d",
|
||||
"reward": 12.5,
|
||||
"isMainChain": true
|
||||
};
|
||||
|
||||
var bitcoreBlock = bitcore.Block.fromBuffer(new Buffer(blocks['0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7'], 'hex'));
|
||||
|
||||
var todos = {
|
||||
difficulty: 1295829.93087696,
|
||||
nextblockhash: '000000000001e866a8057cde0c650796cb8a59e0e6038dc31c69d7ca6649627d',
|
||||
reward: 12.5
|
||||
};
|
||||
|
||||
var node = {
|
||||
getBlock: sinon.stub().callsArgWith(1, null, bitcoreBlock),
|
||||
services: {
|
||||
bitcoind: {
|
||||
getBlockIndex: sinon.stub().returns(blockIndexes['0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7'])
|
||||
},
|
||||
db: {
|
||||
tip: {
|
||||
__height: 534092
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
it('block data should be correct', function(done) {
|
||||
var blocks = new BlockController(node);
|
||||
var req = {};
|
||||
var res = {};
|
||||
var next = function() {
|
||||
should.exist(req.block);
|
||||
var block = _.extend(req.block, todos);
|
||||
should(block).eql(insight);
|
||||
done();
|
||||
};
|
||||
|
||||
var hash = '0000000000000afa0c3c0afd450c793a1e300ec84cbe9555166e06132f19a8f7';
|
||||
|
||||
blocks.block(req, res, next, hash);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/blocks route', function() {
|
||||
|
||||
var insight = {
|
||||
"blocks": [
|
||||
{
|
||||
"height": 533951,
|
||||
"size": 206,
|
||||
"hash": "000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7",
|
||||
"time": 1440978683,
|
||||
"txlength": 1,
|
||||
"poolInfo": {}
|
||||
},
|
||||
{
|
||||
"height": 533950,
|
||||
"size": 206,
|
||||
"hash": "00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441",
|
||||
"time": 1440977479,
|
||||
"txlength": 1,
|
||||
"poolInfo": {}
|
||||
}
|
||||
],
|
||||
"length": 2,
|
||||
"pagination": {}
|
||||
};
|
||||
|
||||
var stub = sinon.stub();
|
||||
stub.onFirstCall().callsArgWith(1, null, bitcore.Block.fromBuffer(blocks['000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7'], 'hex'));
|
||||
stub.onSecondCall().callsArgWith(1, null, bitcore.Block.fromBuffer(blocks['00000000000006bd8fe9e53780323c0e85719eca771022e1eb6d10c62195c441'], 'hex'))
|
||||
|
||||
var node = {
|
||||
getBlock: stub,
|
||||
services: {
|
||||
bitcoind: {
|
||||
getBlockIndex: function(hash) {
|
||||
return blockIndexes[hash];
|
||||
}
|
||||
},
|
||||
db: {
|
||||
tip: {
|
||||
hash: '000000000008fbb2e358e382a6f6948b2da24563bba183af447e6e2542e8efc7'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it('should have correct data', function(done) {
|
||||
var blocks = new BlockController(node);
|
||||
|
||||
var req = {
|
||||
query: {
|
||||
limit: 2
|
||||
}
|
||||
};
|
||||
|
||||
var res = {
|
||||
jsonp: function(data) {
|
||||
should(data).eql(insight);
|
||||
done();
|
||||
}
|
||||
};
|
||||
|
||||
blocks.list(req, res);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user