2014-04-14 13:17:56 -07:00
|
|
|
'use strict';
|
|
|
|
|
2014-08-28 10:23:49 -07:00
|
|
|
var util = require('util');
|
|
|
|
var async = require('async');
|
|
|
|
var request = require('request');
|
2014-05-12 13:41:15 -07:00
|
|
|
var bitcore = require('bitcore');
|
2014-08-28 10:23:49 -07:00
|
|
|
var io = require('socket.io-client');
|
2014-09-23 11:24:57 -07:00
|
|
|
var log = require('../log');
|
2014-08-28 10:23:49 -07:00
|
|
|
|
|
|
|
var EventEmitter = require('events').EventEmitter;
|
2014-07-07 14:12:58 -07:00
|
|
|
var preconditions = require('preconditions').singleton();
|
2014-04-14 13:17:56 -07:00
|
|
|
|
2014-08-28 10:23:49 -07:00
|
|
|
/*
|
|
|
|
This class lets interfaces with the blockchain, making general queries and
|
|
|
|
subscribing to transactions on adressess and blocks.
|
|
|
|
|
|
|
|
Opts:
|
2014-09-10 12:36:33 -07:00
|
|
|
- url
|
2014-08-28 10:23:49 -07:00
|
|
|
- reconnection (optional)
|
|
|
|
- reconnectionDelay (optional)
|
|
|
|
|
|
|
|
Events:
|
|
|
|
- tx: activity on subscribed address.
|
|
|
|
- block: a new block that includes a subscribed address.
|
|
|
|
- connect: the connection with the blockchain is ready.
|
|
|
|
- disconnect: the connection with the blochckain is unavailable.
|
|
|
|
*/
|
|
|
|
|
2014-09-09 09:45:50 -07:00
|
|
|
var Insight = function(opts) {
|
2014-09-05 10:28:21 -07:00
|
|
|
preconditions.checkArgument(opts)
|
|
|
|
.shouldBeObject(opts)
|
2014-09-10 12:36:33 -07:00
|
|
|
.checkArgument(opts.url)
|
2014-08-28 10:23:49 -07:00
|
|
|
|
2014-09-03 11:09:06 -07:00
|
|
|
this.status = this.STATUS.DISCONNECTED;
|
|
|
|
this.subscribed = {};
|
|
|
|
this.listeningBlocks = false;
|
|
|
|
|
2014-09-10 12:36:33 -07:00
|
|
|
this.url = opts.url;
|
2014-08-28 10:23:49 -07:00
|
|
|
this.opts = {
|
|
|
|
'reconnection': opts.reconnection || true,
|
|
|
|
'reconnectionDelay': opts.reconnectionDelay || 1000,
|
2014-09-11 09:29:11 -07:00
|
|
|
'secure': opts.url.indexOf('https') === 0
|
2014-08-28 10:23:49 -07:00
|
|
|
};
|
|
|
|
|
2014-09-03 11:05:44 -07:00
|
|
|
this.socket = this.getSocket();
|
2014-04-14 13:17:56 -07:00
|
|
|
}
|
|
|
|
|
2014-08-28 10:23:49 -07:00
|
|
|
util.inherits(Insight, EventEmitter);
|
|
|
|
|
|
|
|
Insight.prototype.STATUS = {
|
|
|
|
CONNECTED: 'connected',
|
|
|
|
DISCONNECTED: 'disconnected',
|
|
|
|
DESTROYED: 'destroyed'
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @private */
|
2014-08-28 11:18:05 -07:00
|
|
|
Insight.prototype.subscribeToBlocks = function() {
|
2014-09-09 09:45:50 -07:00
|
|
|
var socket = this.getSocket();
|
2014-09-09 16:55:51 -07:00
|
|
|
if (this.listeningBlocks || !socket.connected) return;
|
2014-08-28 10:23:49 -07:00
|
|
|
|
|
|
|
var self = this;
|
2014-09-09 09:45:50 -07:00
|
|
|
socket.on('block', function(blockHash) {
|
2014-08-28 10:23:49 -07:00
|
|
|
self.emit('block', blockHash);
|
|
|
|
});
|
|
|
|
this.listeningBlocks = true;
|
|
|
|
}
|
|
|
|
|
2014-09-09 09:45:50 -07:00
|
|
|
/** @private */
|
|
|
|
Insight.prototype._getSocketIO = function(url, opts) {
|
2014-09-09 16:55:51 -07:00
|
|
|
return io(this.url, this.opts);
|
2014-09-09 09:45:50 -07:00
|
|
|
};
|
|
|
|
|
2014-09-09 16:55:51 -07:00
|
|
|
|
|
|
|
Insight.prototype._setMainHandlers = function(url, opts) {
|
|
|
|
// Emmit connection events
|
|
|
|
var self = this;
|
|
|
|
this.socket.on('connect', function() {
|
|
|
|
self.status = self.STATUS.CONNECTED;
|
|
|
|
self.subscribeToBlocks();
|
|
|
|
self.emit('connect', 0);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.socket.on('connect_error', function() {
|
|
|
|
if (self.status != self.STATUS.CONNECTED) return;
|
|
|
|
self.status = self.STATUS.DISCONNECTED;
|
|
|
|
self.emit('disconnect');
|
|
|
|
});
|
|
|
|
|
|
|
|
this.socket.on('connect_timeout', function() {
|
|
|
|
if (self.status != self.STATUS.CONNECTED) return;
|
|
|
|
self.status = self.STATUS.DISCONNECTED;
|
|
|
|
self.emit('disconnect');
|
|
|
|
});
|
|
|
|
|
|
|
|
this.socket.on('reconnect', function(attempt) {
|
|
|
|
if (self.status != self.STATUS.DISCONNECTED) return;
|
|
|
|
self.emit('reconnect', attempt);
|
|
|
|
self.reSubscribe();
|
|
|
|
self.status = self.STATUS.CONNECTED;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2014-08-28 10:23:49 -07:00
|
|
|
/** @private */
|
2014-09-03 11:05:44 -07:00
|
|
|
Insight.prototype.getSocket = function() {
|
2014-09-09 09:45:50 -07:00
|
|
|
|
|
|
|
if (!this.socket) {
|
|
|
|
this.socket = this._getSocketIO(this.url, this.opts);
|
2014-09-09 16:55:51 -07:00
|
|
|
this._setMainHandlers();
|
2014-09-09 09:45:50 -07:00
|
|
|
}
|
|
|
|
return this.socket;
|
2014-08-28 10:23:49 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @private */
|
|
|
|
Insight.prototype.request = function(path, cb) {
|
2014-08-29 06:50:52 -07:00
|
|
|
preconditions.checkArgument(path).shouldBeFunction(cb);
|
2014-08-28 10:23:49 -07:00
|
|
|
request(this.url + path, cb);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @private */
|
|
|
|
Insight.prototype.requestPost = function(path, data, cb) {
|
2014-08-29 06:50:52 -07:00
|
|
|
preconditions.checkArgument(path).checkArgument(data).shouldBeFunction(cb);
|
2014-09-09 09:45:50 -07:00
|
|
|
request({
|
|
|
|
method: "POST",
|
|
|
|
url: this.url + path,
|
|
|
|
json: data
|
|
|
|
}, cb);
|
2014-08-28 10:23:49 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
Insight.prototype.destroy = function() {
|
2014-09-09 11:30:21 -07:00
|
|
|
var socket = this.getSocket();
|
|
|
|
this.socket.disconnect();
|
|
|
|
this.socket.removeAllListeners();
|
|
|
|
this.socket = null;
|
2014-08-29 11:01:07 -07:00
|
|
|
this.subscribed = {};
|
2014-08-28 10:23:49 -07:00
|
|
|
this.status = this.STATUS.DESTROYED;
|
|
|
|
this.removeAllListeners();
|
2014-04-22 23:37:17 -07:00
|
|
|
};
|
|
|
|
|
2014-08-28 10:23:49 -07:00
|
|
|
Insight.prototype.subscribe = function(addresses) {
|
|
|
|
addresses = Array.isArray(addresses) ? addresses : [addresses];
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
function handlerFor(self, address) {
|
2014-09-09 09:45:50 -07:00
|
|
|
return function(txid) {
|
2014-08-28 10:23:49 -07:00
|
|
|
// verify the address is still subscribed
|
2014-08-29 11:01:07 -07:00
|
|
|
if (!self.subscribed[address]) return;
|
2014-09-09 16:55:51 -07:00
|
|
|
log.debug('insight tx event');
|
|
|
|
|
2014-09-09 09:45:50 -07:00
|
|
|
self.emit('tx', {
|
|
|
|
address: address,
|
|
|
|
txid: txid
|
|
|
|
});
|
2014-06-26 14:47:27 -07:00
|
|
|
}
|
2014-08-28 10:23:49 -07:00
|
|
|
}
|
|
|
|
|
2014-09-09 16:55:51 -07:00
|
|
|
var s = self.getSocket();
|
2014-08-28 10:23:49 -07:00
|
|
|
addresses.forEach(function(address) {
|
|
|
|
preconditions.checkArgument(new bitcore.Address(address).isValid());
|
|
|
|
|
2014-08-28 11:18:05 -07:00
|
|
|
// skip already subscibed
|
2014-08-29 11:01:07 -07:00
|
|
|
if (!self.subscribed[address]) {
|
2014-09-09 16:55:51 -07:00
|
|
|
var handler = handlerFor(self, address);
|
|
|
|
self.subscribed[address] = handler;
|
|
|
|
log.debug('Subcribe to: ', address);
|
|
|
|
|
|
|
|
s.emit('subscribe', address);
|
|
|
|
s.on(address, handler);
|
2014-08-28 11:18:05 -07:00
|
|
|
}
|
2014-08-28 10:23:49 -07:00
|
|
|
});
|
2014-06-26 14:47:27 -07:00
|
|
|
};
|
|
|
|
|
2014-08-29 11:01:07 -07:00
|
|
|
Insight.prototype.getSubscriptions = function(addresses) {
|
2014-09-09 15:05:23 -07:00
|
|
|
return this.subscribed;
|
2014-08-29 11:01:07 -07:00
|
|
|
}
|
|
|
|
|
2014-08-05 12:25:02 -07:00
|
|
|
|
2014-09-09 16:55:51 -07:00
|
|
|
Insight.prototype.reSubscribe = function() {
|
|
|
|
log.debug('insight reSubscribe');
|
|
|
|
var allAddresses = Object.keys(this.subscribed);
|
|
|
|
this.subscribed = {};
|
|
|
|
var s = this.socket;
|
|
|
|
if (s) {
|
|
|
|
s.removeAllListeners();
|
|
|
|
this._setMainHandlers();
|
|
|
|
this.subscribe(allAddresses);
|
|
|
|
this.subscribeToBlocks();
|
|
|
|
}
|
2014-08-05 12:25:02 -07:00
|
|
|
};
|
|
|
|
|
2014-08-28 10:23:49 -07:00
|
|
|
|
|
|
|
Insight.prototype.broadcast = function(rawtx, cb) {
|
|
|
|
preconditions.checkArgument(rawtx);
|
2014-07-07 14:12:58 -07:00
|
|
|
preconditions.shouldBeFunction(cb);
|
2014-05-12 13:41:15 -07:00
|
|
|
|
2014-09-09 09:45:50 -07:00
|
|
|
this.requestPost('/api/tx/send', {
|
|
|
|
rawtx: rawtx
|
|
|
|
}, function(err, res, body) {
|
2014-10-01 07:08:56 -07:00
|
|
|
if (err || res.status != 200) cb(err || res);
|
2014-10-07 11:46:05 -07:00
|
|
|
cb(null, body ? body.txid : null);
|
2014-04-22 21:55:00 -07:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2014-08-28 10:23:49 -07:00
|
|
|
Insight.prototype.getTransaction = function(txid, cb) {
|
|
|
|
preconditions.shouldBeFunction(cb);
|
|
|
|
this.request('/api/tx/' + txid, function(err, res, body) {
|
|
|
|
if (err || res.statusCode != 200 || !body) return cb(err || res);
|
|
|
|
cb(null, JSON.parse(body));
|
|
|
|
});
|
|
|
|
};
|
2014-04-14 13:17:56 -07:00
|
|
|
|
2014-08-28 10:23:49 -07:00
|
|
|
Insight.prototype.getTransactions = function(addresses, cb) {
|
|
|
|
preconditions.shouldBeArray(addresses);
|
|
|
|
preconditions.shouldBeFunction(cb);
|
2014-05-12 13:41:15 -07:00
|
|
|
|
2014-06-05 08:18:54 -07:00
|
|
|
var self = this;
|
2014-08-28 10:23:49 -07:00
|
|
|
if (!addresses.length) return cb(null, []);
|
2014-05-21 14:03:11 -07:00
|
|
|
|
2014-08-28 10:23:49 -07:00
|
|
|
// Iterator: get a list of transaction ids for an address
|
|
|
|
function getTransactionIds(address, next) {
|
|
|
|
self.request('/api/addr/' + address, function(err, res, body) {
|
|
|
|
if (err || res.statusCode != 200 || !body) return next(err || res);
|
|
|
|
next(null, JSON.parse(body).transactions);
|
|
|
|
});
|
|
|
|
}
|
2014-05-21 14:03:11 -07:00
|
|
|
|
2014-08-28 10:23:49 -07:00
|
|
|
async.map(addresses, getTransactionIds, function then(err, txids) {
|
|
|
|
if (err) return cb(err);
|
|
|
|
|
|
|
|
// txids it's a list of list, let's fix that:
|
|
|
|
var txidsList = txids.reduce(function(a, r) {
|
|
|
|
return r.concat(a);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Remove duplicated txids
|
|
|
|
txidsList = txidsList.filter(function(elem, pos, self) {
|
|
|
|
return self.indexOf(elem) == pos;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Now get the transactions for that list of txIds
|
|
|
|
async.map(txidsList, self.getTransaction.bind(self), function then(err, txs) {
|
|
|
|
if (err) return cb(err);
|
|
|
|
cb(null, txs);
|
|
|
|
});
|
2014-04-14 13:17:56 -07:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2014-08-28 10:23:49 -07:00
|
|
|
Insight.prototype.getUnspent = function(addresses, cb) {
|
|
|
|
preconditions.shouldBeArray(addresses);
|
|
|
|
preconditions.shouldBeFunction(cb);
|
2014-04-20 16:24:24 -07:00
|
|
|
|
2014-09-09 09:45:50 -07:00
|
|
|
this.requestPost('/api/addrs/utxo', {
|
|
|
|
addrs: addresses.join(',')
|
|
|
|
}, function(err, res, body) {
|
2014-08-28 10:23:49 -07:00
|
|
|
if (err || res.statusCode != 200) return cb(err || res);
|
2014-08-29 06:50:52 -07:00
|
|
|
cb(null, body);
|
2014-04-14 13:17:56 -07:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2014-08-28 10:23:49 -07:00
|
|
|
Insight.prototype.getActivity = function(addresses, cb) {
|
|
|
|
preconditions.shouldBeArray(addresses);
|
2014-06-19 12:34:37 -07:00
|
|
|
|
2014-08-28 10:23:49 -07:00
|
|
|
this.getTransactions(addresses, function then(err, txs) {
|
|
|
|
if (err) return cb(err);
|
2014-06-18 06:58:34 -07:00
|
|
|
|
2014-06-24 08:36:32 -07:00
|
|
|
var flatArray = function(xss) {
|
|
|
|
return xss.reduce(function(r, xs) {
|
|
|
|
return r.concat(xs);
|
|
|
|
}, []);
|
|
|
|
};
|
|
|
|
var getInputs = function(t) {
|
|
|
|
return t.vin.map(function(vin) {
|
|
|
|
return vin.addr
|
|
|
|
});
|
|
|
|
};
|
|
|
|
var getOutputs = function(t) {
|
|
|
|
return flatArray(
|
|
|
|
t.vout.map(function(vout) {
|
2014-09-09 09:45:50 -07:00
|
|
|
return vout.scriptPubKey.addresses;
|
|
|
|
})
|
2014-06-24 08:36:32 -07:00
|
|
|
);
|
|
|
|
};
|
2014-06-18 06:58:34 -07:00
|
|
|
|
|
|
|
var activityMap = new Array(addresses.length);
|
2014-06-24 08:36:32 -07:00
|
|
|
var activeAddress = flatArray(txs.map(function(t) {
|
|
|
|
return getInputs(t).concat(getOutputs(t));
|
|
|
|
}));
|
|
|
|
activeAddress.forEach(function(addr) {
|
2014-06-18 06:58:34 -07:00
|
|
|
var index = addresses.indexOf(addr);
|
|
|
|
if (index != -1) activityMap[index] = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
cb(null, activityMap);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2014-08-14 11:39:48 -07:00
|
|
|
module.exports = Insight;
|