@monygroupcorp/micro-web3 0.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,494 @@
1
+ // src/indexer/SyncEngine.js
2
+
3
+ /**
4
+ * Sync engine for EventIndexer.
5
+ * Handles historical sync, real-time updates, and reorg recovery.
6
+ */
7
+ class SyncEngine {
8
+ constructor(storage, queryEngine, eventBus, config = {}) {
9
+ this.storage = storage;
10
+ this.queryEngine = queryEngine;
11
+ this.eventBus = eventBus;
12
+
13
+ // Configuration with defaults
14
+ this.batchSize = config.batchSize || 2000;
15
+ this.confirmations = config.confirmations || 2;
16
+ this.realTimeEnabled = config.realTimeEnabled !== false;
17
+ this.retryAttempts = config.retryAttempts || 3;
18
+ this.retryDelay = config.retryDelay || 1000;
19
+ this.pollInterval = config.pollInterval || 12000;
20
+ this.reorgTracking = config.reorgTracking || false; // Full reorg tracking (optional)
21
+ this.reorgDepth = config.reorgDepth || 100; // How many blocks to track
22
+
23
+ // State
24
+ this.provider = null;
25
+ this.contract = null;
26
+ this.eventTypes = [];
27
+ this.deployBlock = 0;
28
+ this.chainId = null;
29
+
30
+ this.state = 'initializing'; // initializing | syncing | synced | paused | error
31
+ this.currentBlock = 0;
32
+ this.latestBlock = 0;
33
+ this.eventsIndexed = 0;
34
+ this.lastSyncTime = null;
35
+ this.error = null;
36
+
37
+ // Real-time sync
38
+ this.eventListeners = [];
39
+ this.pollTimer = null;
40
+ this.isPaused = false;
41
+
42
+ // Reorg tracking (optional)
43
+ this.recentBlocks = new Map(); // blockNumber -> blockHash
44
+ }
45
+
46
+ /**
47
+ * Initialize sync engine.
48
+ * @param {Object} params
49
+ * @param {ethers.Provider} params.provider - Ethereum provider
50
+ * @param {ethers.Contract} params.contract - Contract instance
51
+ * @param {Object[]} params.eventTypes - Event types from ABI
52
+ * @param {number} params.deployBlock - Starting block
53
+ * @param {number} params.chainId - Chain ID
54
+ */
55
+ async initialize({ provider, contract, eventTypes, deployBlock, chainId }) {
56
+ this.provider = provider;
57
+ this.contract = contract;
58
+ this.eventTypes = eventTypes;
59
+ this.deployBlock = deployBlock;
60
+ this.chainId = chainId;
61
+
62
+ // Validate chain ID matches stored data
63
+ await this._validateChainId();
64
+
65
+ // Load sync state
66
+ const syncState = await this.storage.getSyncState();
67
+ if (syncState) {
68
+ this.currentBlock = syncState.lastSyncedBlock;
69
+ this.eventsIndexed = Object.values(syncState.eventCounts || {}).reduce((a, b) => a + b, 0);
70
+ this.lastSyncTime = syncState.lastSyncTime;
71
+ } else {
72
+ this.currentBlock = this.deployBlock - 1;
73
+ }
74
+ }
75
+
76
+ async _validateChainId() {
77
+ const syncState = await this.storage.getSyncState();
78
+ if (syncState && syncState.chainId !== this.chainId) {
79
+ console.warn('[SyncEngine] Chain ID mismatch, clearing data');
80
+ await this.storage.clear();
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Start syncing.
86
+ */
87
+ async start() {
88
+ if (this.state === 'synced' || this.state === 'syncing') return;
89
+
90
+ try {
91
+ // Historical sync
92
+ await this._syncHistorical();
93
+
94
+ // Start real-time sync
95
+ if (this.realTimeEnabled && !this.isPaused) {
96
+ await this._startRealTimeSync();
97
+ }
98
+ } catch (error) {
99
+ this.state = 'error';
100
+ this.error = error;
101
+ this.eventBus.emit('indexer:error', {
102
+ code: 'SYNC_ERROR',
103
+ message: error.message,
104
+ cause: error,
105
+ recoverable: true
106
+ });
107
+ }
108
+ }
109
+
110
+ async _syncHistorical() {
111
+ this.state = 'syncing';
112
+ this.latestBlock = await this.provider.getBlockNumber();
113
+
114
+ // Account for confirmations
115
+ const targetBlock = this.latestBlock - this.confirmations;
116
+
117
+ if (this.currentBlock >= targetBlock) {
118
+ this.state = 'synced';
119
+ this.eventBus.emit('indexer:syncComplete', {
120
+ eventsIndexed: this.eventsIndexed,
121
+ duration: 0
122
+ });
123
+ return;
124
+ }
125
+
126
+ const startTime = Date.now();
127
+ const startBlock = this.currentBlock + 1;
128
+
129
+ this.eventBus.emit('indexer:syncStarted', {
130
+ fromBlock: startBlock,
131
+ toBlock: targetBlock
132
+ });
133
+
134
+ // Process in batches
135
+ for (let fromBlock = startBlock; fromBlock <= targetBlock; fromBlock += this.batchSize) {
136
+ if (this.isPaused) break;
137
+
138
+ const toBlock = Math.min(fromBlock + this.batchSize - 1, targetBlock);
139
+
140
+ await this._syncBatch(fromBlock, toBlock);
141
+
142
+ // Emit progress
143
+ const progress = (toBlock - startBlock + 1) / (targetBlock - startBlock + 1);
144
+ this.eventBus.emit('indexer:syncProgress', {
145
+ progress,
146
+ currentBlock: toBlock,
147
+ latestBlock: this.latestBlock,
148
+ eventsIndexed: this.eventsIndexed
149
+ });
150
+ }
151
+
152
+ if (!this.isPaused) {
153
+ this.state = 'synced';
154
+ this.lastSyncTime = Date.now();
155
+
156
+ this.eventBus.emit('indexer:syncComplete', {
157
+ eventsIndexed: this.eventsIndexed,
158
+ duration: Date.now() - startTime
159
+ });
160
+ }
161
+ }
162
+
163
+ async _syncBatch(fromBlock, toBlock) {
164
+ const events = await this._fetchEventsWithRetry(fromBlock, toBlock);
165
+
166
+ if (events.length > 0) {
167
+ // Parse and index events
168
+ const indexedEvents = events.map(e => this._parseEvent(e));
169
+ await this.storage.putEvents(indexedEvents);
170
+ this.eventsIndexed += indexedEvents.length;
171
+
172
+ // Notify query engine
173
+ const eventsByType = this._groupByType(indexedEvents);
174
+ for (const [type, typeEvents] of Object.entries(eventsByType)) {
175
+ this.queryEngine.notifyNewEvents(type, typeEvents);
176
+ }
177
+ }
178
+
179
+ // Update sync state
180
+ this.currentBlock = toBlock;
181
+ await this._saveSyncState();
182
+ }
183
+
184
+ async _fetchEventsWithRetry(fromBlock, toBlock) {
185
+ for (let attempt = 0; attempt < this.retryAttempts; attempt++) {
186
+ try {
187
+ // Query all event types
188
+ const allEvents = [];
189
+ for (const eventType of this.eventTypes) {
190
+ const filter = this.contract.filters[eventType.name]();
191
+ const events = await this.contract.queryFilter(filter, fromBlock, toBlock);
192
+ allEvents.push(...events);
193
+ }
194
+ return allEvents;
195
+ } catch (error) {
196
+ if (attempt === this.retryAttempts - 1) throw error;
197
+
198
+ // Exponential backoff
199
+ const delay = this.retryDelay * Math.pow(2, attempt);
200
+ await this._sleep(delay);
201
+ }
202
+ }
203
+ return [];
204
+ }
205
+
206
+ _parseEvent(rawEvent) {
207
+ // Extract indexed and non-indexed params
208
+ const indexed = {};
209
+ const data = {};
210
+
211
+ if (rawEvent.args) {
212
+ const eventFragment = rawEvent.eventFragment ||
213
+ this.contract.interface.getEvent(rawEvent.event);
214
+
215
+ if (eventFragment) {
216
+ for (let i = 0; i < eventFragment.inputs.length; i++) {
217
+ const input = eventFragment.inputs[i];
218
+ const value = rawEvent.args[i];
219
+ const serialized = this._serializeValue(value);
220
+
221
+ if (input.indexed) {
222
+ indexed[input.name] = serialized;
223
+ }
224
+ data[input.name] = serialized;
225
+ }
226
+ }
227
+ }
228
+
229
+ return {
230
+ id: `${rawEvent.transactionHash}-${rawEvent.logIndex}`,
231
+ type: rawEvent.event,
232
+ blockNumber: rawEvent.blockNumber,
233
+ blockHash: rawEvent.blockHash,
234
+ transactionHash: rawEvent.transactionHash,
235
+ logIndex: rawEvent.logIndex,
236
+ timestamp: 0, // Will be filled by block data if available
237
+ data,
238
+ indexed
239
+ };
240
+ }
241
+
242
+ _serializeValue(value) {
243
+ if (value === null || value === undefined) return null;
244
+ if (typeof value === 'bigint' || value._isBigNumber) {
245
+ return value.toString();
246
+ }
247
+ if (Array.isArray(value)) {
248
+ return value.map(v => this._serializeValue(v));
249
+ }
250
+ return value;
251
+ }
252
+
253
+ _groupByType(events) {
254
+ const grouped = {};
255
+ for (const event of events) {
256
+ if (!grouped[event.type]) {
257
+ grouped[event.type] = [];
258
+ }
259
+ grouped[event.type].push(event);
260
+ }
261
+ return grouped;
262
+ }
263
+
264
+ async _saveSyncState() {
265
+ await this.storage.setSyncState({
266
+ lastSyncedBlock: this.currentBlock,
267
+ lastSyncTime: Date.now(),
268
+ eventCounts: {}, // Could track per-type counts
269
+ chainId: this.chainId
270
+ });
271
+ }
272
+
273
+ async _startRealTimeSync() {
274
+ // Try WebSocket subscription first
275
+ try {
276
+ for (const eventType of this.eventTypes) {
277
+ const filter = this.contract.filters[eventType.name]();
278
+ const listener = async (...args) => {
279
+ const event = args[args.length - 1]; // Last arg is the event object
280
+ await this._handleNewEvent(event);
281
+ };
282
+
283
+ this.contract.on(filter, listener);
284
+ this.eventListeners.push({ filter, listener });
285
+ }
286
+ } catch (error) {
287
+ // Fall back to polling
288
+ console.warn('[SyncEngine] WebSocket subscription failed, falling back to polling');
289
+ this._startPolling();
290
+ }
291
+
292
+ // Also start polling as backup (handles missed events)
293
+ this._startPolling();
294
+ }
295
+
296
+ _startPolling() {
297
+ if (this.pollTimer) return;
298
+
299
+ this.pollTimer = setInterval(async () => {
300
+ if (this.isPaused) return;
301
+
302
+ try {
303
+ const latestBlock = await this.provider.getBlockNumber();
304
+ const targetBlock = latestBlock - this.confirmations;
305
+
306
+ if (targetBlock > this.currentBlock) {
307
+ await this._syncBatch(this.currentBlock + 1, targetBlock);
308
+ }
309
+
310
+ this.latestBlock = latestBlock;
311
+
312
+ // Optional: reorg tracking
313
+ if (this.reorgTracking) {
314
+ await this._checkForReorg(latestBlock);
315
+ }
316
+ } catch (error) {
317
+ console.error('[SyncEngine] Polling error:', error);
318
+ }
319
+ }, this.pollInterval);
320
+ }
321
+
322
+ async _handleNewEvent(rawEvent) {
323
+ // Wait for confirmations
324
+ if (this.confirmations > 0) {
325
+ await this._waitForConfirmations(rawEvent.transactionHash);
326
+ }
327
+
328
+ const indexedEvent = this._parseEvent(rawEvent);
329
+ await this.storage.putEvents([indexedEvent]);
330
+ this.eventsIndexed++;
331
+
332
+ // Update current block if this event is ahead
333
+ if (indexedEvent.blockNumber > this.currentBlock) {
334
+ this.currentBlock = indexedEvent.blockNumber;
335
+ await this._saveSyncState();
336
+ }
337
+
338
+ // Notify
339
+ this.queryEngine.notifyNewEvents(indexedEvent.type, [indexedEvent]);
340
+ this.eventBus.emit('indexer:newEvents', {
341
+ eventType: indexedEvent.type,
342
+ events: [indexedEvent]
343
+ });
344
+ }
345
+
346
+ async _waitForConfirmations(txHash) {
347
+ let confirmations = 0;
348
+ while (confirmations < this.confirmations) {
349
+ const receipt = await this.provider.getTransactionReceipt(txHash);
350
+ if (!receipt) {
351
+ await this._sleep(1000);
352
+ continue;
353
+ }
354
+
355
+ const currentBlock = await this.provider.getBlockNumber();
356
+ confirmations = currentBlock - receipt.blockNumber + 1;
357
+
358
+ if (confirmations < this.confirmations) {
359
+ await this._sleep(1000);
360
+ }
361
+ }
362
+ }
363
+
364
+ async _checkForReorg(currentBlock) {
365
+ const block = await this.provider.getBlock(currentBlock);
366
+ if (!block) return;
367
+
368
+ // Check if parent hash matches what we stored
369
+ const storedParentHash = this.recentBlocks.get(currentBlock - 1);
370
+ if (storedParentHash && storedParentHash !== block.parentHash) {
371
+ // Reorg detected!
372
+ await this._handleReorg(currentBlock - 1);
373
+ }
374
+
375
+ // Track this block
376
+ this.recentBlocks.set(currentBlock, block.hash);
377
+
378
+ // Prune old entries
379
+ if (this.recentBlocks.size > this.reorgDepth) {
380
+ const oldest = Math.min(...this.recentBlocks.keys());
381
+ this.recentBlocks.delete(oldest);
382
+ }
383
+ }
384
+
385
+ async _handleReorg(forkBlock) {
386
+ // Find common ancestor
387
+ let checkBlock = forkBlock;
388
+ while (checkBlock > this.deployBlock) {
389
+ const storedHash = this.recentBlocks.get(checkBlock);
390
+ const chainBlock = await this.provider.getBlock(checkBlock);
391
+
392
+ if (storedHash === chainBlock?.hash) break;
393
+ checkBlock--;
394
+ }
395
+
396
+ // Delete events from orphaned blocks
397
+ await this.storage.deleteEventsAfterBlock(checkBlock);
398
+
399
+ // Update sync state
400
+ this.currentBlock = checkBlock;
401
+ await this._saveSyncState();
402
+
403
+ // Emit reorg event
404
+ this.eventBus.emit('indexer:reorg', {
405
+ forkBlock,
406
+ commonAncestor: checkBlock,
407
+ depth: forkBlock - checkBlock
408
+ });
409
+
410
+ // Re-sync from common ancestor
411
+ await this._syncHistorical();
412
+ }
413
+
414
+ /**
415
+ * Pause real-time sync.
416
+ */
417
+ pause() {
418
+ this.isPaused = true;
419
+ this.state = 'paused';
420
+ this.eventBus.emit('indexer:paused', {});
421
+ }
422
+
423
+ /**
424
+ * Resume real-time sync.
425
+ */
426
+ resume() {
427
+ this.isPaused = false;
428
+ this.start();
429
+ this.eventBus.emit('indexer:resumed', {});
430
+ }
431
+
432
+ /**
433
+ * Force re-sync from block.
434
+ * @param {number} fromBlock - Block to start from (default: deployBlock)
435
+ */
436
+ async resync(fromBlock) {
437
+ this.pause();
438
+
439
+ if (fromBlock !== undefined) {
440
+ this.currentBlock = fromBlock - 1;
441
+ } else {
442
+ this.currentBlock = this.deployBlock - 1;
443
+ }
444
+
445
+ await this.storage.clear();
446
+ await this._saveSyncState();
447
+
448
+ this.eventsIndexed = 0;
449
+ this.resume();
450
+ }
451
+
452
+ /**
453
+ * Get current sync status.
454
+ * @returns {SyncStatus}
455
+ */
456
+ getStatus() {
457
+ return {
458
+ state: this.state,
459
+ currentBlock: this.currentBlock,
460
+ latestBlock: this.latestBlock,
461
+ progress: this.latestBlock > 0
462
+ ? Math.min(1, (this.currentBlock - this.deployBlock) / (this.latestBlock - this.deployBlock))
463
+ : 0,
464
+ eventsIndexed: this.eventsIndexed,
465
+ lastSyncTime: this.lastSyncTime,
466
+ error: this.error
467
+ };
468
+ }
469
+
470
+ /**
471
+ * Stop sync and cleanup.
472
+ */
473
+ async stop() {
474
+ this.isPaused = true;
475
+
476
+ // Remove event listeners
477
+ for (const { filter, listener } of this.eventListeners) {
478
+ this.contract.off(filter, listener);
479
+ }
480
+ this.eventListeners = [];
481
+
482
+ // Stop polling
483
+ if (this.pollTimer) {
484
+ clearInterval(this.pollTimer);
485
+ this.pollTimer = null;
486
+ }
487
+ }
488
+
489
+ _sleep(ms) {
490
+ return new Promise(resolve => setTimeout(resolve, ms));
491
+ }
492
+ }
493
+
494
+ export default SyncEngine;
@@ -0,0 +1,13 @@
1
+ // src/indexer/index.js
2
+
3
+ import QueryEngine from './QueryEngine.js';
4
+ import SyncEngine from './SyncEngine.js';
5
+ import EntityResolver from './EntityResolver.js';
6
+ import Patterns from './Patterns.js';
7
+
8
+ export {
9
+ QueryEngine,
10
+ SyncEngine,
11
+ EntityResolver,
12
+ Patterns
13
+ };
@@ -533,6 +533,36 @@ class BlockchainService {
533
533
  return this.connectionState === 'connected';
534
534
  }
535
535
 
536
+ // NEW: Get provider instance (for EventIndexer)
537
+ getProvider() {
538
+ return this.provider;
539
+ }
540
+
541
+ // NEW: Create contract instance (for EventIndexer)
542
+ getContract(address, abi) {
543
+ if (!this.provider) {
544
+ throw new Error('Provider not initialized');
545
+ }
546
+ return new ethers.Contract(address, abi, this.provider);
547
+ }
548
+
549
+ // NEW: Get current chain ID (for EventIndexer)
550
+ async getChainId() {
551
+ if (!this.provider) {
552
+ throw new Error('Provider not initialized');
553
+ }
554
+ const network = await this.provider.getNetwork();
555
+ return Number(network.chainId);
556
+ }
557
+
558
+ // NEW: Get block by number (for EventIndexer)
559
+ async getBlock(blockNumber) {
560
+ if (!this.provider) {
561
+ throw new Error('Provider not initialized');
562
+ }
563
+ return this.provider.getBlock(blockNumber);
564
+ }
565
+
536
566
  async getCurrentTier() {
537
567
  try {
538
568
  // Check cache first
@@ -1,5 +1,5 @@
1
1
  class ContractCache {
2
- constructor(eventBus) {
2
+ constructor(eventBus, options = {}) {
3
3
  if (!eventBus) {
4
4
  throw new Error('ContractCache requires an eventBus instance.');
5
5
  }
@@ -45,7 +45,12 @@ class ContractCache {
45
45
 
46
46
  // Setup event listeners for cache invalidation
47
47
  this.setupEventListeners();
48
-
48
+
49
+ // NEW: Optional event-driven invalidation from EventIndexer
50
+ if (options.eventInvalidation) {
51
+ this.setupEventInvalidation(options.eventInvalidation);
52
+ }
53
+
49
54
  // Debug mode
50
55
  this.debug = false;
51
56
  }
@@ -87,6 +92,19 @@ class ContractCache {
87
92
  });
88
93
  }
89
94
 
95
+ /**
96
+ * Configure which events invalidate which cache patterns
97
+ */
98
+ setupEventInvalidation(config) {
99
+ // config: { 'Transfer': ['balance'], 'Approval': ['allowance'] }
100
+ this.eventBus.on('indexer:newEvents', ({ eventType }) => {
101
+ const patterns = config[eventType];
102
+ if (patterns && Array.isArray(patterns)) {
103
+ this.invalidateByPattern(...patterns);
104
+ }
105
+ });
106
+ }
107
+
90
108
  /**
91
109
  * Generate cache key from method name and arguments
92
110
  * @param {string} method - Method name