UNPKG

bitcore-node

Version:

A blockchain indexing node with extended capabilities using bitcore

319 lines 14.9 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.XrpP2pWorker = void 0; const events_1 = require("events"); const stream_1 = require("stream"); const Loggify_1 = require("../../../decorators/Loggify"); const logger_1 = __importDefault(require("../../../logger")); const logger_2 = require("../../../logger"); const cache_1 = require("../../../models/cache"); const state_1 = require("../../../models/state"); const walletAddress_1 = require("../../../models/walletAddress"); const p2p_1 = require("../../../services/p2p"); const utils_1 = require("../../../utils"); const csp_1 = require("../api/csp"); const block_1 = require("../models/block"); const transaction_1 = require("../models/transaction"); let XrpP2pWorker = class XrpP2pWorker extends p2p_1.BaseP2PWorker { constructor({ chain, network, chainConfig, blockModel = block_1.XrpBlockStorage }) { super({ chain, network, chainConfig, blockModel }); this.blockModel = blockModel; this.chain = chain || 'XRP'; this.network = network; this.chainConfig = chainConfig; this.syncing = false; this.initialSyncComplete = false; this.provider = new csp_1.RippleStateProvider(this.chain); this.events = new events_1.EventEmitter(); this.invCache = {}; this.invCacheLimits = { TX: 100000 }; this.disconnecting = false; } cacheInv(type, hash) { if (!this.invCache[type]) { this.invCache[type] = []; } if (this.invCache[type].length > this.invCacheLimits[type]) { this.invCache[type].shift(); } this.invCache[type].push(hash); } isCachedInv(type, hash) { if (!this.invCache[type]) { this.invCache[type] = []; } return this.invCache[type].includes(hash); } async setupListeners() { const { host, port } = this.chainConfig.provider; this.events.on('disconnected', async () => { logger_1.default.warn(`${(0, logger_2.timestamp)()} | Not connected to peer: ${host}:${port || ''} | Chain: ${this.chain} | Network: ${this.network}`); }); this.events.on('connected', async () => { this.client.rpc.on('ledgerClosed', async () => { this.sync(); }); this.client.asyncRequest('subscribe', { streams: ['ledger'] }); }); } async teardownListeners() { this.client.rpc.removeAllListeners('ledgerClosed'); await this.client.asyncRequest('unsubscribe', { streams: ['ledger'] }); } async disconnect() { this.disconnecting = true; await this.teardownListeners(); } async handleReconnects() { this.disconnecting = false; let firstConnect = true; let connected = false; let disconnected = false; const { host, port } = this.chainConfig.provider; while (!this.disconnecting && !this.stopping) { try { try { this.client = await this.provider.getClient(this.network); connected = this.client.rpc.isConnected(); } catch (e) { connected = false; } if (connected) { if (disconnected || firstConnect) { this.events.emit('connected'); } } else { this.client = await this.provider.getClient(this.network); this.events.emit('disconnected'); } if (disconnected && connected && !firstConnect) { logger_1.default.warn(`${(0, logger_2.timestamp)()} | Reconnected to peer: ${host}:${port || ''} | Chain: ${this.chain} | Network: ${this.network}`); } if (connected && firstConnect) { firstConnect = false; logger_1.default.info(`${(0, logger_2.timestamp)()} | Connected to peer: ${host}:${port || ''} | Chain: ${this.chain} | Network: ${this.network}`); } disconnected = !connected; } catch (e) { } await (0, utils_1.wait)(5000); } } async connect() { this.handleReconnects(); } async getBlock(height) { return this.provider.getBlock({ chain: this.chain, network: this.network, blockId: height.toString() }); } async syncWallets() { return new Promise(async (resolve) => { try { const { chain, network } = this; // After wallet syncs, start block sync from the current height const client = await this.provider.getClient(this.network); let { ledger: chainBestBlock } = await client.getBlock({ index: 'latest' }); this.chainConfig.startHeight = chainBestBlock; const count = await walletAddress_1.WalletAddressStorage.collection.countDocuments({ chain, network }); let done = 0; logger_1.default.info(`Syncing ${count} ${chain} ${network} wallets`); let lastLog = Date.now(); const addressStream = walletAddress_1.WalletAddressStorage.collection.find({ chain, network }).stream(); addressStream .pipe(new stream_1.Transform({ objectMode: true, transform: async (data, _, cb) => { if (Date.now() - 5000 > lastLog) { logger_1.default.info(`Syncing ${count - done} ${chain} ${network} wallets`); } const walletAddress = data; const [lastTx] = await transaction_1.XrpTransactionStorage.collection .find({ wallets: walletAddress.wallet, 'wallets.0': { $exists: true } }) .sort({ blockTimeNormalized: -1 }) .limit(1) .toArray(); const synced = await cache_1.CacheStorage.getForWallet(walletAddress.wallet, `sync-${walletAddress.address}`); if (synced) { // if this is happening, it means initial sync wasn't completed the first time, likely due to a crash return cb(); } const txs = await this.provider.getAddressTransactions({ chain: this.chain, network: this.network, address: walletAddress.address, args: { ...(lastTx && !this.chainConfig.freshSync && { startTx: lastTx.txid }) } }); if (txs.length) { logger_1.default.info(`Saving ${txs.length} transactions`); } const blockTxs = new Array(); const blockCoins = new Array(); for (const tx of txs) { const bitcoreTx = this.provider.transformAccountTx(tx, network); const bitcoreCoins = this.provider.transformToCoins(tx, network); const { transaction, coins } = await this.provider.tag(chain, network, bitcoreTx, bitcoreCoins); blockTxs.push(transaction); blockCoins.push(...coins); } await transaction_1.XrpTransactionStorage.batchImport({ txs: blockTxs, coins: blockCoins, chain, network, initialSyncComplete: false }); await cache_1.CacheStorage.setForWallet(walletAddress.wallet, `sync-${walletAddress.address}`, true, cache_1.CacheStorage.Times.Hour / 2); done++; cb(); } })) .on('finish', async () => { logger_1.default.info(`FINISHED Syncing ${count} ${chain} ${network} wallets`); this.initialSyncComplete = true; await state_1.StateStorage.collection.findOneAndUpdate({}, { $addToSet: { initialSyncComplete: `${chain}:${network}` } }, { upsert: true }); resolve(); }); } catch (e) { logger_1.default.error('%o', e); } }) .finally(() => { if (this.disconnecting) { this.client.rpc.disconnect(); } }); } async syncBlocks() { const { chain, network } = this; const client = await this.provider.getClient(this.network); let ourBestBlock = await this.provider.getLocalTip({ chain, network }); const serverInfo = await client.getServerInfo(); const [earliestLedgerIdx, latestLedgerIdx] = serverInfo.complete_ledgers.split('-'); let chainBestBlock = Number(latestLedgerIdx); const startTime = Date.now(); let lastLog = Date.now(); const configuredStart = Math.max(this.chainConfig.startHeight || 0, ourBestBlock?.height || 0, earliestLedgerIdx || 0); logger_1.default.info(`Starting XRP Sync @ ${configuredStart}`); ourBestBlock = { height: configuredStart }; const startHeight = ourBestBlock.height; let currentHeight = startHeight; while (ourBestBlock.height < chainBestBlock && !this.disconnecting) { currentHeight = ourBestBlock.height + 1; const { ledger: block } = await client.getBlock({ index: currentHeight }); if (!block) { await (0, utils_1.wait)(2000); continue; } const transformedBlock = this.provider.transformLedger(block, network); const coinsAndTxs = (block.transactions || []) .map((tx) => ({ tx: this.provider.transform(tx, network, transformedBlock), coins: this.provider.transformToCoins(tx, network, transformedBlock) })) .filter(data => { return !!data.tx.txid; }); const blockTxs = new Array(); const blockCoins = new Array(); for (const coinAndTx of coinsAndTxs) { const { transaction, coins } = await this.provider.tag(chain, network, coinAndTx.tx, coinAndTx.coins); if (this.chainConfig.walletOnlySync && !transaction.wallets.length) { continue; } blockTxs.push(transaction); blockCoins.push(...coins); } await this.blockModel.processBlock({ chain, network, block: transformedBlock, transactions: blockTxs, coins: blockCoins, initialSyncComplete: true }); this.maybeLog(chain, network, startHeight, currentHeight, startTime, lastLog); lastLog = Date.now(); ourBestBlock = await this.provider.getLocalTip({ chain, network }); if (ourBestBlock.height === chainBestBlock) { ({ ledger: chainBestBlock } = await client.getBlock({ index: 'latest' })); } } if (this.disconnecting) { this.client.rpc.disconnect(); } } async sync() { if (this.syncing) { return false; } const { chain, network } = this; this.syncing = true; const state = await state_1.StateStorage.collection.findOne({}); this.initialSyncComplete = state && state.initialSyncComplete && state.initialSyncComplete.includes(`${chain}:${network}`); try { if (this.chainConfig.walletOnlySync && !this.initialSyncComplete) { await this.syncWallets(); } else { await this.syncBlocks(); } logger_1.default.info(`${chain}:${network} up to date.`); this.syncing = false; state_1.StateStorage.collection.findOneAndUpdate({}, { $addToSet: { initialSyncComplete: `${chain}:${network}` } }, { upsert: true }); this.events.emit('SYNCDONE'); return true; } catch (e) { logger_1.default.error('%o', e); this.syncing = false; await (0, utils_1.wait)(2000); return this.sync(); } } maybeLog(chain, network, startHeight, currentHeight, startTime, lastLog) { const oneSecond = 1000; const now = Date.now(); if (now - lastLog > oneSecond || startHeight === currentHeight) { const blocksProcessed = currentHeight - startHeight; const elapsedMinutes = (now - startTime) / (60 * oneSecond); logger_1.default.info(`${(0, logger_2.timestamp)()} | Syncing... | Chain: ${chain} | Network: ${network} |${(blocksProcessed / elapsedMinutes) .toFixed(2) .padStart(8)} blocks/min | Height: ${currentHeight.toString().padStart(7)}`); } } async syncDone() { return new Promise(resolve => this.events.once('SYNCDONE', resolve)); } async stop() { this.stopping = true; logger_1.default.debug(`Stopping worker for chain ${this.chain} ${this.network}`); await this.disconnect(); } async start() { logger_1.default.debug(`Started worker for chain ${this.chain} ${this.network}`); this.connect(); await this.setupListeners(); this.sync(); } }; exports.XrpP2pWorker = XrpP2pWorker; exports.XrpP2pWorker = XrpP2pWorker = __decorate([ Loggify_1.LoggifyClass ], XrpP2pWorker); //# sourceMappingURL=index.js.map