copay/js/models/Insight.js

347 lines
8.6 KiB
JavaScript
Raw Normal View History

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');
var io = require('socket.io-client');
2014-11-02 14:17:55 -08:00
var _ = require('lodash');
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-12-02 06:17:03 -08:00
var bitcore = require('bitcore');
var log = require('../util/log.js');
2014-08-28 10:23:49 -07:00
/*
2015-01-07 06:42:56 -08:00
This class lets interface with the blockchain, making general queries and
subscribing to transactions on addresses and blocks.
2014-08-28 10:23:49 -07:00
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
};
if (opts.transports) {
this.opts['transports'] = opts.transports;
}
2014-09-03 11:05:44 -07:00
this.socket = this.getSocket();
2014-04-14 13:17:56 -07:00
}
Insight.setCompleteUrl = function(uri) {
if (!uri) return uri;
var re = /^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/;
var parts = [
'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'
];
function parseuri(str) {
var m = re.exec(str || ''),
uri = {},
i = 14;
while (i--) {
uri[parts[i]] = m[i] || '';
}
return uri;
};
var opts_host;
var opts_secure;
var opts_port;
var opts_protocol;
if (uri) {
uri = parseuri(uri);
opts_host = uri.host;
opts_protocol = uri.protocol;
opts_secure = uri.protocol == 'https' || uri.protocol == 'wss';
opts_port = uri.port;
}
var this_secure = null != opts_secure ? opts_secure :
('https:' == location.protocol);
var opts_hostname;
if (opts_host) {
var pieces = opts_host.split(':');
opts_hostname = pieces.shift();
if (pieces.length) opts_port = pieces.pop();
}
var this_port = opts_port ||
(this_secure ? 443 : 80);
var newUri = opts_protocol + '://' + opts_host + ':' + this_port;
return newUri;
}
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-10-31 14:14:14 -07:00
log.debug('Insight: Connecting to socket:', this.url);
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() {
var socket = this.getSocket();
this.socket.disconnect();
this.socket.removeAllListeners();
this.socket = null;
this.subscribed = {};
2014-08-28 10:23:49 -07:00
this.status = this.STATUS.DESTROYED;
this.removeAllListeners();
};
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
if (!self.subscribed[address]) return;
2014-09-09 16:55:51 -07:00
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
if (!self.subscribed[address]) {
2014-09-09 16:55:51 -07:00
var handler = handlerFor(self, address);
self.subscribed[address] = handler;
2014-11-29 23:42:39 -08:00
// log.debug('Subscribe to: ', address);
2014-09-09 16:55:51 -07:00
s.emit('subscribe', address);
s.on(address, handler);
}
2014-08-28 10:23:49 -07:00
});
2014-06-26 14:47:27 -07:00
};
Insight.prototype.getSubscriptions = function(addresses) {
2014-09-09 15:05:23 -07:00
return this.subscribed;
}
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) {
if (err || res.statusCode != 200) return cb(err || body);
return cb(null, body ? body.txid : null);
});
};
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
Insight.prototype.getTransactions = function(addresses, from, to, cb) {
2014-08-28 10:23:49 -07:00
preconditions.shouldBeArray(addresses);
preconditions.shouldBeFunction(cb);
2014-05-12 13:41:15 -07:00
var qs = '';
if (_.isNumber(from)) {
qs += '?from=' + from;
if (_.isNumber(to)) {
qs += '&to=' + to;
}
}
this.requestPost('/api/addrs/txs' + qs, {
addrs: addresses.join(',')
}, function(err, res, txs) {
if (err || res.statusCode != 200) return cb(err || res);
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(',')
2014-11-02 14:17:55 -08:00
}, function(err, res, unspentRaw) {
2014-08-28 10:23:49 -07:00
if (err || res.statusCode != 200) return cb(err || res);
2014-11-02 14:17:55 -08:00
// This filter out possible broken unspent, as reported on
// https://github.com/bitpay/copay/issues/1585
// and later gitter conversation.
2014-11-02 14:17:55 -08:00
var unspent = _.filter(unspentRaw, 'scriptPubKey');
cb(null, unspent);
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
this.getTransactions(addresses, null, null, function then(err, txs) {
2014-08-28 10:23:49 -07:00
if (err) return cb(err);
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;
})
);
};
var activityMap = new Array(addresses.length);
var activeAddress = flatArray(txs.map(function(t) {
return getInputs(t).concat(getOutputs(t));
}));
activeAddress.forEach(function(addr) {
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;