@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.
- package/CLAUDE.md +6 -0
- package/dist/{micro-web3.cjs.js → micro-web3.cjs} +3 -3
- package/dist/micro-web3.cjs.map +1 -0
- package/dist/micro-web3.esm.js +2 -2
- package/dist/micro-web3.esm.js.map +1 -1
- package/dist/micro-web3.umd.js +2 -2
- package/dist/micro-web3.umd.js.map +1 -1
- package/docs/plans/2026-01-22-event-indexer.md +3642 -0
- package/monygroupcorp-micro-web3-0.1.3.tgz +0 -0
- package/package.json +2 -2
- package/rollup.config.cjs +1 -1
- package/src/components/FloatingWalletButton/FloatingWalletButton.js +26 -5
- package/src/components/SettingsModal/SettingsModal.js +371 -0
- package/src/components/SyncProgressBar/SyncProgressBar.js +238 -0
- package/src/components/WalletButton/WalletButton.js +213 -0
- package/src/index.js +18 -2
- package/src/indexer/EntityResolver.js +218 -0
- package/src/indexer/Patterns.js +277 -0
- package/src/indexer/QueryEngine.js +149 -0
- package/src/indexer/SyncEngine.js +494 -0
- package/src/indexer/index.js +13 -0
- package/src/services/BlockchainService.js +30 -0
- package/src/services/ContractCache.js +20 -2
- package/src/services/EventIndexer.js +399 -0
- package/src/storage/IndexedDBAdapter.js +423 -0
- package/src/storage/IndexerSettings.js +88 -0
- package/src/storage/MemoryAdapter.js +194 -0
- package/src/storage/StorageAdapter.js +129 -0
- package/src/storage/index.js +41 -0
- package/dist/micro-web3.cjs.js.map +0 -1
|
@@ -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
|