UNPKG

@bronlabs/intents-sdk

Version:
253 lines 10.1 kB
import { ethers, FetchRequest } from 'ethers'; import WebSocket from 'ws'; import { sleep, log } from './utils.js'; import { EventQueue } from './eventQueue.js'; import { initOrderEngine } from './contracts.js'; export class OrderIndexer { constructor(config) { this.debugTrace = (prefix) => (info) => { if (info.action !== 'receiveRpcResult') { log.debug(`${prefix}: ${info.action} -> ${info.payload?.method}`); } }; this.config = config; const req = new FetchRequest(config.rpcUrl); if (config.rpcAuthToken) { req.setHeader('x-api-key', config.rpcAuthToken); } this.httpProvider = new ethers.JsonRpcProvider(req, undefined, { staticNetwork: true }); void this.httpProvider.on('debug', this.debugTrace('RPC')); this.wsProvider = null; this.wsOrderEngine = null; this.orderEngine = initOrderEngine(config.orderEngineAddress, this.httpProvider); this.eventQueue = new EventQueue(); this.processors = []; this.isRunning = false; this.lastProcessedBlock = 0; this.isProcessingQueue = false; this.reconnectTimeout = null; this.rpcTimeoutMs = 15_000; this.healthCheckInterval = null; } addProcessor(processor) { this.processors.push(processor); } async start() { if (this.isRunning) { log.warn('Indexer is already running'); return; } this.isRunning = true; log.info(`Starting indexer with offset -${this.config.startBlockOffset} blocks`); try { await this.startWebSocketListener(); } catch (error) { log.error('Error starting indexer:', error); this.isRunning = false; } } async stop() { if (!this.isRunning) { log.warn('Indexer is not running'); return; } log.info('Stopping indexer...'); this.isRunning = false; if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; } if (this.wsOrderEngine) { void this.wsOrderEngine.removeAllListeners(); this.wsOrderEngine = null; } if (this.wsProvider) { void this.wsProvider.removeAllListeners(); void this.wsProvider.destroy(); this.wsProvider = null; } const maxWaitTime = 30000; const startTime = Date.now(); while (!this.eventQueue.isEmpty() && (Date.now() - startTime) < maxWaitTime) { log.info(`Waiting for ${this.eventQueue.size()} events to complete before stopping`); await sleep(1000); } if (!this.eventQueue.isEmpty()) { log.warn(`Stopping indexer with ${this.eventQueue.size()} unprocessed events after timeout`); } log.info('Indexer stopped'); } async processHistoricalEvents() { const currentBlock = await this.withTimeout(this.httpProvider.getBlockNumber(), 'httpProvider.getBlockNumber'); if (!this.lastProcessedBlock) { this.lastProcessedBlock = currentBlock - this.config.startBlockOffset; } if (currentBlock > this.lastProcessedBlock) { log.info(`Processing historical events from ${this.lastProcessedBlock + 1} to ${currentBlock}`); await this.processBlockRange(this.lastProcessedBlock + 1, currentBlock); } } async startWebSocketListener() { await this.processHistoricalEvents(); // Process historical events first try { const ws = new WebSocket(this.config.rpcUrl.replace(/^https?:\/\//, 'wss://')); ws.on('error', error => { if (!this.isRunning) return; log.error('WebSocket transport error:', error); this.reconnectWebSocket(); }); ws.on('close', (code, reason) => { if (!this.isRunning) return; log.warn(`WebSocket transport closed: ${code} ${reason?.toString?.() ?? ''}`); this.reconnectWebSocket(); }); this.wsProvider = new ethers.WebSocketProvider(ws, undefined, { staticNetwork: true }); void this.wsProvider.on('debug', this.debugTrace('WS')); this.wsOrderEngine = new ethers.Contract(this.config.orderEngineAddress, this.orderEngine.interface, this.wsProvider); void this.wsOrderEngine.on('OrderStatusChanged', async (orderId, status, event) => { if (!this.isRunning) return; this.eventQueue.add({ type: 'OrderStatusChanged', data: { orderId, status: Number(status) }, event, retries: 0 }); this.lastProcessedBlock = event.blockNumber; if (this.isRunning && !this.eventQueue.isEmpty()) { await this.processEventQueue(); } }); this.monitorWebSocketHealth(); log.info('WebSocket listener started'); } catch (error) { log.error('Error starting WebSocket:', error); this.reconnectWebSocket(); } } monitorWebSocketHealth() { if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval); } this.healthCheckInterval = setInterval(async () => { if (!this.isRunning || !this.wsProvider) { return; } try { await this.withTimeout(this.wsProvider.getBlockNumber(), 'wsProvider.getBlockNumber (health check)'); } catch (error) { log.warn('WebSocket health check failed:', error); this.reconnectWebSocket(); } }, 60_000); } reconnectWebSocket() { if (!this.isRunning || this.reconnectTimeout) return; if (this.wsOrderEngine) { void this.wsOrderEngine.removeAllListeners(); this.wsOrderEngine = null; } if (this.wsProvider) { void this.wsProvider.removeAllListeners(); void this.wsProvider.destroy(); this.wsProvider = null; } log.info('Reconnecting WebSocket in 5 seconds...'); this.reconnectTimeout = setTimeout(async () => { this.reconnectTimeout = null; if (this.isRunning) { try { await this.startWebSocketListener(); } catch (error) { log.error('Error reconnecting WebSocket:', error); this.reconnectWebSocket(); } } }, 5000); } async withTimeout(promise, label) { return await Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error(`RPC timeout in ${label} after ${this.rpcTimeoutMs}ms`)), this.rpcTimeoutMs)) ]); } async processBlockRange(fromBlock, toBlock) { const chunkSize = 2000; for (let from = fromBlock; from <= toBlock; from += chunkSize) { if (!this.isRunning) break; const to = Math.min(from + chunkSize - 1, toBlock); try { const events = await this.withTimeout(this.orderEngine.queryFilter(this.orderEngine.filters.OrderStatusChanged(), from, to), `orderEngine.queryFilter ${from}-${to}`); for (const event of events) { const { args: { orderId, status } } = this.orderEngine.interface.parseLog({ topics: event.topics, data: event.data }); this.eventQueue.add({ type: 'OrderStatusChanged', data: { orderId, status: Number(status) }, event: event, retries: 0 }); } this.lastProcessedBlock = to; if (this.isRunning && !this.eventQueue.isEmpty()) { await this.processEventQueue(); } } catch (error) { log.error(`Error processing block range ${from}-${to}:`, error); throw error; } } } async processEventQueue() { if (this.isProcessingQueue || this.eventQueue.isEmpty() || this.processors.length === 0) { return; } this.isProcessingQueue = true; try { while (!this.eventQueue.isEmpty()) { const event = this.eventQueue.peek(); if (!event) continue; try { for (const processor of this.processors) { await processor(event); } this.eventQueue.remove(); } catch (error) { log.error(`Error processing event:`, error); if (event.retries < this.config.maxRetries) { event.retries++; log.info(`Retrying event processing (${event.retries}/${this.config.maxRetries})`); await sleep(this.config.retryDelay); } else { log.error(`Max retries reached for event, removing from queue:`, event); this.eventQueue.remove(); } } } } finally { this.isProcessingQueue = false; } } } //# sourceMappingURL=orderIndexer.js.map