UNPKG

@owstack/wallet-service

Version:

A service for multisignature HD wallets

381 lines (324 loc) 12.6 kB
const owsCommon = require('@owstack/ows-common'); const async = require('async'); const baseConfig = require('config'); const Constants = owsCommon.Constants; const Context = owsCommon.util.Context; const Lock = require('./lock'); const log = require('npmlog'); const MessageBroker = require('./messagebroker'); const Notification = require('./model/notification'); const Storage = require('./storage'); const WalletService = require('./server'); const lodash = owsCommon.deps.lodash; const $ = require('preconditions').singleton(); log.debug = log.verbose; class BlockchainMonitor { constructor(context, config) { // Context defines the coin network and is set by the implementing service in // order to instance this base service; e.g., btc-service. context.inject(this); // Set some frequently used contant values based on context. this.LIVENET = this.ctx.Networks.livenet; this.TESTNET = this.ctx.Networks.testnet; this.config = config || baseConfig; this.setLog(); } } BlockchainMonitor.prototype.setLog = function () { if (this.config.log) { log.level = (this.config.log.disable == true ? 'silent' : this.config.log.level || 'info'); } else { log.level = 'info'; } }; BlockchainMonitor.prototype.start = function (opts, cb) { const self = this; opts = opts || {}; async.parallel([ function (done) { self.explorers = {}; lodash.forEach([self.LIVENET, self.TESTNET], function (network) { let explorer; if (self.config[network.currency].blockchainExplorers && self.config[network.currency].blockchainExplorers[network.alias]) { explorer = self.config[network.currency].blockchainExplorers[network.alias]; } else { const provider = lodash.get(self.config[network.currency], 'blockchainExplorerOpts.defaultProvider'); if (provider && self.config[network.currency].blockchainExplorerOpts && self.config[network.currency].blockchainExplorerOpts[provider] && self.config[network.currency].blockchainExplorerOpts[provider][network.alias]) { explorer = new self.ctx.BlockchainExplorer({ provider: provider, network: network.alias }, self.config); } } if (explorer) { self._initExplorer(network.alias, explorer); self.explorers[network.alias] = explorer; } }); done(); }, function (done) { if (opts.storage) { self.storage = opts.storage; done(); } else if (self.config.storage) { self.storage = self.config.storage; done(); } else { // Create with empty context (none for this service). self.storage = new Storage(new Context(), self.config.storageOpts, { creator: `BlockchainMonitor (${ self.LIVENET.currency })` }); self.storage.connect(done); } }, function (done) { self.messageBroker = opts.messageBroker || new MessageBroker(self.config.messageBrokerOpts); done(); }, function (done) { self.lock = self.config.lock || new Lock(self.config.lockOpts); done(); }, ], function (err) { if (err) { log.error(err); } return cb(err); }); }; BlockchainMonitor.prototype._initExplorer = function (networkAlias, explorer) { const self = this; const socket = explorer.initSocket(); socket.on('connect', function () { log.info(`Connected to ${ explorer.getConnectionInfo()}`); socket.emit('subscribe', 'inv'); }); socket.on('connect_error', function () { log.error(`Error connecting to ${ explorer.getConnectionInfo()}`); }); socket.on('tx', lodash.bind(self._handleIncomingTx, self)); socket.on('block', lodash.bind(self._handleNewBlock, self, networkAlias)); }; BlockchainMonitor.prototype._handleThirdPartyBroadcasts = function (data, processIt) { const self = this; if (!data || !data.txid) { return; } self.storage.fetchTxByHash(data.txid, function (err, txp) { if (err) { log.error('Could not fetch tx from the db'); return; } if (!txp || txp.status != 'accepted') { return; } const walletId = txp.walletId; if (!processIt) { log.info(`Detected broadcast ${ data.txid } of an accepted txp [${ txp.id }] for wallet ${ walletId } [${ txp.amount } ${ self.ctx.Unit().atomicsAccessor() }]`); return setTimeout(self._handleThirdPartyBroadcasts.bind(self, data, true), 20 * 1000); } log.info(`Processing accepted txp [${ txp.id }] for wallet ${ walletId } [${ txp.amount } ${ self.ctx.Unit().atomicsAccessor() }]`); txp.setBroadcasted(); self.storage.softResetTxHistoryCache(walletId, function () { self.storage.storeTx(self.walletId, txp, function (err) { if (err) { log.error('Could not save TX'); } const args = { txProposalId: txp.id, txid: data.txid, amount: txp.getTotalAmount(), }; const notification = Notification.create({ type: 'NewOutgoingTxByThirdParty', data: args, walletId: walletId, networkName: txp.networkName }); self._storeAndBroadcastNotification(notification); }); }); }); }; BlockchainMonitor.prototype._handleIncomingPayments = function (data) { const self = this; if (!data || !data.vout) { return; } const outs = lodash.compact(lodash.map(data.vout, function (v) { const addr = lodash.keys(v)[0]; return { address: addr, amount: +v[addr] }; })); if (lodash.isEmpty(outs)) { return; } async.each(outs, function (out, nextOut) { let address; let walletId; let wallet; async.series([function (next) { self.storage.fetchAddress(out.address, function (err, addr) { if (err) { log.error('Could not fetch addresses from the db'); return nextOut(err); } if (!addr || addr.isChange) { return nextOut(); } address = addr; walletId = address.walletId; next(); }); }, function (next) { self.storage.fetchWallet(walletId, function (err, w) { if (err) { return next(err); } wallet = w; log.info(`Incoming tx for wallet ${ wallet.id } [${ out.amount } ${ self.ctx.Unit().atomicsAccessor() } -> ${ out.address }]`); next(); }); }, function (next) { const fromTs = Date.now() - 24 * 3600 * 1000; self.storage.fetchNotifications(walletId, null, fromTs, function (err, notifications) { if (err) { return nextOut(err); } const alreadyNotified = lodash.some(notifications, function (n) { return n.type == 'NewIncomingTx' && n.data && n.data.txid == data.txid; }); if (alreadyNotified) { log.info(`The incoming tx ${ data.txid } was already notified`); return nextOut(); } const notification = Notification.create({ type: 'NewIncomingTx', walletId: walletId, networkName: wallet.networkName, data: { txid: data.txid, address: out.address, amount: out.amount, } }); self.storage.softResetTxHistoryCache(walletId, function () { self._updateActiveAddresses(address, function () { self._storeAndBroadcastNotification(notification, nextOut); }); }); }); }]); }, function (err) { return; }); }; BlockchainMonitor.prototype._updateActiveAddresses = function (address, cb) { const self = this; self.storage.storeActiveAddresses(address.walletId, address.address, function (err) { if (err) { log.warn('Could not update wallet cache', err); } return cb(err); }); }; BlockchainMonitor.prototype._handleIncomingTx = function (data) { this._handleThirdPartyBroadcasts(data); this._handleIncomingPayments(data); }; BlockchainMonitor.prototype._notifyNewBlock = function (networkAlias, hash) { const self = this; const network = (networkAlias == Constants.LIVENET ? self.LIVENET : self.TESTNET); log.info(`New ${ networkAlias } block: ${ hash}`); const notification = Notification.create({ type: 'NewBlock', walletId: network.name, // use network name as wallet id for global notifications networkName: network.name, data: { hash: hash } }); self.storage.softResetAllTxHistoryCache(function () { self._storeAndBroadcastNotification(notification, function (err) { return; }); }); }; BlockchainMonitor.prototype._handleTxConfirmations = function (networkAlias, hash) { const self = this; const network = (networkAlias == Constants.LIVENET ? self.LIVENET : self.TESTNET); function processTriggeredSubs(subs, cb) { async.each(subs, function (sub) { log.info(`New tx confirmation ${ sub.txid}`); sub.isActive = false; self.storage.storeTxConfirmationSub(sub, function (err) { if (err) { return cb(err); } const notification = Notification.create({ type: 'TxConfirmation', walletId: sub.walletId, creatorId: sub.copayerId, networkName: network.name, data: { txid: sub.txid // TODO: amount } }); self._storeAndBroadcastNotification(notification, cb); }); }); } const explorer = self.explorers[network]; if (!explorer) { return; } explorer.getTxidsInBlock(hash, function (err, txids) { if (err) { log.error(`Could not fetch txids from block ${ hash}`, err); return; } self.storage.fetchActiveTxConfirmationSubs(null, function (err, subs) { if (err) { return; } if (lodash.isEmpty(subs)) { return; } const indexedSubs = lodash.keyBy(subs, 'txid'); const triggered = []; lodash.each(txids, function (txid) { if (indexedSubs[txid]) { triggered.push(indexedSubs[txid]); } }); processTriggeredSubs(triggered, function (err) { if (err) { log.error('Could not process tx confirmations', err); } return; }); }); }); }; BlockchainMonitor.prototype._handleNewBlock = function (networkAlias, hash) { this._notifyNewBlock(networkAlias, hash); this._handleTxConfirmations(networkAlias, hash); }; BlockchainMonitor.prototype._storeAndBroadcastNotification = function (notification, cb) { const self = this; self.storage.storeNotification(notification.walletId, notification, function () { self.messageBroker.send(notification); if (cb) { return cb(); } }); }; module.exports = BlockchainMonitor;