UNPKG

bitcore-wallet-service

Version:
404 lines 16.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BlockchainMonitor = void 0; const async = __importStar(require("async")); const lodash_1 = __importDefault(require("lodash")); require("source-map-support/register"); const blockchainexplorer_1 = require("./blockchainexplorer"); const index_1 = require("./chain/index"); const common_1 = require("./common"); const utils_1 = require("./common/utils"); const lock_1 = require("./lock"); const logger_1 = __importDefault(require("./logger")); const messagebroker_1 = require("./messagebroker"); const model_1 = require("./model"); const server_1 = require("./server"); const storage_1 = require("./storage"); const $ = require('preconditions').singleton(); const Constants = common_1.Common.Constants; const throttle = (fn) => { let lastCalled = 0; return (...args) => { const bcmContext = args[0], chain = args[1], network = args[2], hash = args[3]; let msDelay = bcmContext.getChainThrottleSetting(chain, network) * 1000; let now = new Date().getTime(); if (now - lastCalled < msDelay) { return; } lastCalled = now; return fn(bcmContext, chain, network, hash); }; }; const throttledNewBlocks = throttle((bcmContext, chain, network, hash) => { bcmContext._notifyNewBlock(chain, network, hash); bcmContext._handleTxConfirmations(chain, network, hash); }); class BlockchainMonitor { constructor() { this.blockThrottleSettings = Constants.CHAIN_NEW_BLOCK_THROTTLE_TIME_SECONDS; } start(opts, cb) { opts = opts || {}; this.N = opts.N || 100; this.Ni = this.Nix = 0; this.last = this.lastTx = []; async.parallel([ done => { this.explorers = { btc: {}, bch: {}, eth: {}, matic: {}, xrp: {}, doge: {}, ltc: {}, arb: {}, base: {}, op: {}, sol: {}, }; const chainNetworkPairs = []; lodash_1.default.each(lodash_1.default.values(Constants.CHAINS), chain => { lodash_1.default.each(lodash_1.default.values(Constants.NETWORKS[chain]), network => { chainNetworkPairs.push({ chain, network }); }); }); lodash_1.default.each(chainNetworkPairs, pair => { let explorer; if (opts.blockchainExplorers && opts.blockchainExplorers[pair.chain] && opts.blockchainExplorers[pair.chain][pair.network]) { explorer = opts.blockchainExplorers[pair.chain][pair.network]; } else { let config = {}; if (opts.blockchainExplorerOpts && opts.blockchainExplorerOpts[pair.chain] && opts.blockchainExplorerOpts[pair.chain][pair.network]) { config = opts.blockchainExplorerOpts[pair.chain][pair.network]; } else { return; } const bcNetwork = utils_1.Utils.getNetworkType(pair.network) === 'testnet' && config.regtestEnabled ? 'regtest' : pair.network; explorer = (0, blockchainexplorer_1.BlockChainExplorer)({ provider: config.provider, chain: pair.chain, network: bcNetwork, url: config.url, userAgent: server_1.WalletService.getServiceVersion() }); } $.checkState(explorer, 'Failed State: explorer undefined at <start()>'); this._initExplorer(pair.chain, pair.network, explorer); this.explorers[pair.chain][pair.network] = explorer; }); done(); }, done => { if (opts.storage) { this.storage = opts.storage; done(); } else { this.storage = new storage_1.Storage(); this.storage.connect({ ...opts.storageOpts, secondaryPreferred: true }, done); } }, done => { this.messageBroker = opts.messageBroker || new messagebroker_1.MessageBroker(opts.messageBrokerOpts); done(); }, done => { this.lock = opts.lock || new lock_1.Lock(this.storage); done(); } ], err => { if (err) { logger_1.default.error('%o', err); } return cb(err); }); } _initExplorer(chain, network, explorer) { explorer.initSocket({ onBlock: lodash_1.default.bind(this._handleNewBlock, this, chain, network), onIncomingPayments: lodash_1.default.bind(this._handleIncomingPayments, this, chain, network) }); } _handleThirdPartyBroadcasts(chain, network, data, processIt) { if (!data || !data.txid) return; if (!processIt) { if (this.lastTx.indexOf(data.txid) >= 0) { return; } this.lastTx[this.Nix++] = data.txid; if (this.Nix >= this.N) this.Nix = 0; logger_1.default.debug(`\tChecking ${chain}/${network} txid: ${data.txid}`); } this.storage.fetchTxByHash(data.txid, (err, txp) => { if (err) { logger_1.default.error('Could not fetch tx from the db'); return; } if (!txp || txp.status != 'accepted') return; const walletId = txp.walletId; if (!processIt) { logger_1.default.debug('Detected broadcast ' + data.txid + ' of an accepted txp [' + txp.id + '] for wallet ' + walletId + ' [' + txp.amount + 'sat ]'); return setTimeout(this._handleThirdPartyBroadcasts.bind(this, chain, network, data, true), 20 * 1000); } logger_1.default.debug('Processing accepted txp [' + txp.id + '] for wallet ' + walletId + ' [' + txp.amount + 'sat ]'); txp.setBroadcasted(); this.storage.storeTx(this.walletId, txp, err => { if (err) logger_1.default.error('Could not save TX for wallet %o, %o', this.walletId, err); const args = { txProposalId: txp.id, txid: data.txid, amount: txp.getTotalAmount() }; const notification = model_1.Notification.create({ type: 'NewOutgoingTxByThirdParty', data: args, walletId }); this._storeAndBroadcastNotification(notification); }); }); } _handleIncomingPayments(chain, network, data) { if (!data) return; let out = data.out; if (!out || !out.address || out.address.length < 10) return; if (!Constants.EVM_CHAINS[chain.toUpperCase()]) { if (!(out.amount > 0)) return; if (this.last.indexOf(out.address) >= 0) { logger_1.default.debug('The incoming tx"s out ' + out.address + ' was already processed'); return; } this.last[this.Ni++] = out.address; if (this.Ni >= this.N) this.Ni = 0; } else { if (this.lastTx.indexOf(data.txid) >= 0) { logger_1.default.debug('The incoming tx ' + data.txid + ' was already processed'); return; } this.lastTx[this.Nix++] = data.txid; if (this.Nix >= this.N) this.Nix = 0; } logger_1.default.debug(`Checking ${chain}:${network}:${out.address} ${out.amount}`); this.storage.fetchAddressByChain(chain, out.address, (err, address) => { if (err) { logger_1.default.error('Could not fetch addresses from the db %o', err); return; } if (!address || address.isChange) { return this._handleThirdPartyBroadcasts(chain, network, data, null); } const walletId = address.walletId; const fromTs = Date.now() - 24 * 3600 * 1000; this.storage.fetchNotifications(walletId, null, fromTs, (err, notifications) => { if (err) return; const alreadyNotified = lodash_1.default.some(notifications, n => { return n.type == 'NewIncomingTx' && n.data && n.data.txid == data.txid; }); if (alreadyNotified) { logger_1.default.debug('The incoming tx ' + data.txid + ' was already notified'); return; } logger_1.default.debug('Incoming tx for wallet ' + walletId + ' [' + out.amount + 'amount -> ' + out.address + ']'); const notification = model_1.Notification.create({ type: 'NewIncomingTx', data: { txid: data.txid, address: out.address, amount: out.amount, tokenAddress: out.tokenAddress, multisigContractAddress: out.multisigContractAddress, network }, walletId }); if (utils_1.Utils.getNetworkType(network) !== 'testnet') { this.storage.fetchWallet(walletId, (err, wallet) => { if (err) return; async.each(wallet.copayers, (c, next) => { const sub = model_1.TxConfirmationSub.create({ copayerId: c.id, walletId, txid: data.txid, amount: out.amount, isCreator: false }); this.storage.storeTxConfirmationSub(sub, next); }, err => { if (err) logger_1.default.error('%o', err); }); }); } this._storeAndBroadcastNotification(notification, () => { return; }); }); }); } _notifyNewBlock(chain, network, hash) { logger_1.default.debug(` ** NOTIFY New ${chain}/${network} block ${hash}`); const notification = model_1.Notification.create({ type: 'NewBlock', walletId: `${chain}:${network}`, data: { hash, chain, network } }); this._storeAndBroadcastNotification(notification, () => { }); } _handleTxConfirmations(chain, network, hash) { if (!index_1.ChainService.notifyConfirmations(chain, network)) return; const processTriggeredSub = (sub, cb) => { logger_1.default.debug('New tx confirmation ' + sub.txid); sub.isActive = false; async.waterfall([ next => { this.storage.storeTxConfirmationSub(sub, err => { if (err) return cb(err); const notification = model_1.Notification.create({ type: 'TxConfirmation', walletId: sub.walletId, creatorId: sub.copayerId, isCreator: sub.isCreator, data: { txid: sub.txid, chain, network, amount: sub.amount } }); next(null, notification); }); }, (notification, next) => { this._storeAndBroadcastNotification(notification, next); } ], cb); }; const explorer = this.explorers[chain][network]; if (!explorer) return; explorer.getTxidsInBlock(hash, async (err, txids) => { if (err) { logger_1.default.error('Could not fetch txids from block %o %o', hash, err); return; } const stream = this.storage.streamActiveTxConfirmationSubs(null, txids); let txSub = await stream.next(); while (txSub != null) { processTriggeredSub(txSub, err => { if (err) { logger_1.default.error('Could not process tx confirmation %o', err); } return; }); txSub = await stream.next(); } }); } _handleNewBlock(chain, network, hash) { const cacheKey = storage_1.Storage.BCHEIGHT_KEY + ':' + chain + ':' + network; this.storage.clearGlobalCache(cacheKey, () => { }); if (chain == 'xrp') { return; } if (this.getChainThrottleSetting(chain, network) > 0) { throttledNewBlocks(this, chain, network, hash); } else { this._notifyNewBlock(chain, network, hash); this._handleTxConfirmations(chain, network, hash); } } _storeAndBroadcastNotification(notification, cb) { this.storage.storeNotification(notification.walletId, notification, () => { this.messageBroker.send(notification); if (cb) return cb(); }); } getChainThrottleSetting(chain, network) { const config = this.blockThrottleSettings; if (typeof config[chain] === 'object') { if (typeof config[chain][network] === 'number') { return config[chain][network]; } } return 0; } } exports.BlockchainMonitor = BlockchainMonitor; //# sourceMappingURL=blockchainmonitor.js.map