@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,399 @@
|
|
|
1
|
+
// src/services/EventIndexer.js
|
|
2
|
+
|
|
3
|
+
import { selectAdapter } from '../storage/index.js';
|
|
4
|
+
import { QueryEngine, SyncEngine, EntityResolver, Patterns } from '../indexer/index.js';
|
|
5
|
+
import { ethers } from 'ethers';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Error types for EventIndexer.
|
|
9
|
+
*/
|
|
10
|
+
class EventIndexerError extends Error {
|
|
11
|
+
constructor(message, code, cause) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'EventIndexerError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.cause = cause;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ErrorCodes = {
|
|
20
|
+
NOT_INITIALIZED: 'NOT_INITIALIZED',
|
|
21
|
+
STORAGE_ERROR: 'STORAGE_ERROR',
|
|
22
|
+
SYNC_ERROR: 'SYNC_ERROR',
|
|
23
|
+
QUERY_ERROR: 'QUERY_ERROR',
|
|
24
|
+
INVALID_CONFIG: 'INVALID_CONFIG',
|
|
25
|
+
PROVIDER_ERROR: 'PROVIDER_ERROR',
|
|
26
|
+
REORG_DETECTED: 'REORG_DETECTED'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* EventIndexer - Client-side event indexing for dApps.
|
|
31
|
+
*
|
|
32
|
+
* Provides three levels of abstraction:
|
|
33
|
+
* - Events: Raw indexed events with filtering
|
|
34
|
+
* - Entities: Domain objects derived from events
|
|
35
|
+
* - Patterns: Pre-built solutions (activity feeds, leaderboards)
|
|
36
|
+
*/
|
|
37
|
+
class EventIndexer {
|
|
38
|
+
/**
|
|
39
|
+
* Create EventIndexer instance.
|
|
40
|
+
* @param {EventBus} eventBus - microact EventBus instance
|
|
41
|
+
* @param {Object} options - Options
|
|
42
|
+
* @param {BlockchainService} options.blockchainService - For provider access (optional)
|
|
43
|
+
*/
|
|
44
|
+
constructor(eventBus, options = {}) {
|
|
45
|
+
if (!eventBus) {
|
|
46
|
+
throw new EventIndexerError('EventBus is required', ErrorCodes.INVALID_CONFIG);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.eventBus = eventBus;
|
|
50
|
+
this.blockchainService = options.blockchainService;
|
|
51
|
+
|
|
52
|
+
// Internal components (initialized in initialize())
|
|
53
|
+
this.storage = null;
|
|
54
|
+
this.queryEngine = null;
|
|
55
|
+
this.syncEngine = null;
|
|
56
|
+
this.entityResolver = null;
|
|
57
|
+
this.patternsAPI = null;
|
|
58
|
+
this.contract = null;
|
|
59
|
+
|
|
60
|
+
this.initialized = false;
|
|
61
|
+
this.config = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Initialize the EventIndexer.
|
|
66
|
+
* @param {EventIndexerConfig} config - Configuration
|
|
67
|
+
*/
|
|
68
|
+
async initialize(config) {
|
|
69
|
+
this._validateConfig(config);
|
|
70
|
+
this.config = config;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Get provider
|
|
74
|
+
const provider = this._getProvider(config);
|
|
75
|
+
|
|
76
|
+
// Get chain ID
|
|
77
|
+
const network = await provider.getNetwork();
|
|
78
|
+
const chainId = Number(network.chainId);
|
|
79
|
+
|
|
80
|
+
// Extract event types from ABI
|
|
81
|
+
const eventTypes = this._extractEventTypes(config.contract.abi);
|
|
82
|
+
|
|
83
|
+
// Initialize storage
|
|
84
|
+
this.storage = selectAdapter(config.persistence);
|
|
85
|
+
await this.storage.initialize({
|
|
86
|
+
dbName: config.persistence?.dbName || `micro-web3-events-${chainId}`,
|
|
87
|
+
version: config.persistence?.version || 1,
|
|
88
|
+
eventTypes
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Create contract instance
|
|
92
|
+
this.contract = new ethers.Contract(
|
|
93
|
+
config.contract.address,
|
|
94
|
+
config.contract.abi,
|
|
95
|
+
provider
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Detect deploy block if not provided
|
|
99
|
+
const deployBlock = config.contract.deployBlock || await this._detectDeployBlock(provider);
|
|
100
|
+
|
|
101
|
+
// Initialize components
|
|
102
|
+
this.queryEngine = new QueryEngine(this.storage, this.eventBus);
|
|
103
|
+
|
|
104
|
+
this.syncEngine = new SyncEngine(
|
|
105
|
+
this.storage,
|
|
106
|
+
this.queryEngine,
|
|
107
|
+
this.eventBus,
|
|
108
|
+
config.sync || {}
|
|
109
|
+
);
|
|
110
|
+
await this.syncEngine.initialize({
|
|
111
|
+
provider,
|
|
112
|
+
contract: this.contract,
|
|
113
|
+
eventTypes,
|
|
114
|
+
deployBlock,
|
|
115
|
+
chainId
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Initialize entity resolver if entities defined
|
|
119
|
+
if (config.entities) {
|
|
120
|
+
this.entityResolver = new EntityResolver(this.queryEngine, config.entities);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Initialize patterns
|
|
124
|
+
this.patternsAPI = new Patterns(this.queryEngine, this.entityResolver);
|
|
125
|
+
|
|
126
|
+
this.initialized = true;
|
|
127
|
+
this.eventBus.emit('indexer:initialized', {});
|
|
128
|
+
|
|
129
|
+
// Start syncing
|
|
130
|
+
await this.syncEngine.start();
|
|
131
|
+
|
|
132
|
+
} catch (error) {
|
|
133
|
+
this.eventBus.emit('indexer:error', {
|
|
134
|
+
code: ErrorCodes.STORAGE_ERROR,
|
|
135
|
+
message: `Initialization failed: ${error.message}`,
|
|
136
|
+
cause: error,
|
|
137
|
+
recoverable: false
|
|
138
|
+
});
|
|
139
|
+
throw new EventIndexerError(
|
|
140
|
+
`Failed to initialize EventIndexer: ${error.message}`,
|
|
141
|
+
ErrorCodes.STORAGE_ERROR,
|
|
142
|
+
error
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_validateConfig(config) {
|
|
148
|
+
if (!config.contract) {
|
|
149
|
+
throw new EventIndexerError('contract config is required', ErrorCodes.INVALID_CONFIG);
|
|
150
|
+
}
|
|
151
|
+
if (!config.contract.address) {
|
|
152
|
+
throw new EventIndexerError('contract.address is required', ErrorCodes.INVALID_CONFIG);
|
|
153
|
+
}
|
|
154
|
+
if (!config.contract.abi || !Array.isArray(config.contract.abi)) {
|
|
155
|
+
throw new EventIndexerError('contract.abi is required and must be an array', ErrorCodes.INVALID_CONFIG);
|
|
156
|
+
}
|
|
157
|
+
if (!config.provider && !this.blockchainService) {
|
|
158
|
+
throw new EventIndexerError(
|
|
159
|
+
'Either provider in config or blockchainService in constructor is required',
|
|
160
|
+
ErrorCodes.INVALID_CONFIG
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_getProvider(config) {
|
|
166
|
+
if (config.provider) {
|
|
167
|
+
return config.provider;
|
|
168
|
+
}
|
|
169
|
+
if (this.blockchainService?.getProvider) {
|
|
170
|
+
return this.blockchainService.getProvider();
|
|
171
|
+
}
|
|
172
|
+
if (this.blockchainService?.provider) {
|
|
173
|
+
return this.blockchainService.provider;
|
|
174
|
+
}
|
|
175
|
+
throw new EventIndexerError('No provider available', ErrorCodes.PROVIDER_ERROR);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_extractEventTypes(abi) {
|
|
179
|
+
const eventTypes = [];
|
|
180
|
+
|
|
181
|
+
for (const item of abi) {
|
|
182
|
+
if (item.type === 'event') {
|
|
183
|
+
const indexedParams = item.inputs
|
|
184
|
+
.filter(input => input.indexed)
|
|
185
|
+
.map(input => input.name);
|
|
186
|
+
|
|
187
|
+
eventTypes.push({
|
|
188
|
+
name: item.name,
|
|
189
|
+
inputs: item.inputs,
|
|
190
|
+
indexedParams
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return eventTypes;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async _detectDeployBlock(provider) {
|
|
199
|
+
// Default to 0 if we can't detect
|
|
200
|
+
// In production, you'd want to either require this or use a heuristic
|
|
201
|
+
console.warn('[EventIndexer] No deployBlock specified, starting from block 0');
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Events API - Level 1 (Low-level)
|
|
207
|
+
* Direct access to raw indexed events.
|
|
208
|
+
*/
|
|
209
|
+
get events() {
|
|
210
|
+
this._checkInitialized();
|
|
211
|
+
return {
|
|
212
|
+
query: (eventName, options) => this.queryEngine.query(eventName, options),
|
|
213
|
+
get: (eventName, id) => this.queryEngine.get(eventName, id),
|
|
214
|
+
subscribe: (eventNames, callback) => this.queryEngine.subscribe(eventNames, callback),
|
|
215
|
+
count: (eventName, where) => this.queryEngine.count(eventName, where)
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Entities API - Level 2 (Domain-level)
|
|
221
|
+
* Access to domain objects derived from events.
|
|
222
|
+
* Returns proxy for entity.EntityName access pattern.
|
|
223
|
+
*/
|
|
224
|
+
get entities() {
|
|
225
|
+
this._checkInitialized();
|
|
226
|
+
|
|
227
|
+
if (!this.entityResolver) {
|
|
228
|
+
console.warn('[EventIndexer] No entities configured');
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return new Proxy({}, {
|
|
233
|
+
get: (target, prop) => {
|
|
234
|
+
return this.entityResolver.getEntity(prop);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Patterns API - Level 3 (Zero-config)
|
|
241
|
+
* Pre-built solutions for common needs.
|
|
242
|
+
*/
|
|
243
|
+
get patterns() {
|
|
244
|
+
this._checkInitialized();
|
|
245
|
+
return this.patternsAPI;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Sync API - Control sync behavior.
|
|
250
|
+
*/
|
|
251
|
+
get sync() {
|
|
252
|
+
this._checkInitialized();
|
|
253
|
+
return {
|
|
254
|
+
getStatus: () => this.syncEngine.getStatus(),
|
|
255
|
+
resync: (fromBlock) => this.syncEngine.resync(fromBlock),
|
|
256
|
+
clear: () => this.storage.clear(),
|
|
257
|
+
pause: () => this.syncEngine.pause(),
|
|
258
|
+
resume: () => this.syncEngine.resume()
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_checkInitialized() {
|
|
263
|
+
if (!this.initialized) {
|
|
264
|
+
throw new EventIndexerError(
|
|
265
|
+
'EventIndexer not initialized. Call initialize() first.',
|
|
266
|
+
ErrorCodes.NOT_INITIALIZED
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Cleanup and close connections.
|
|
273
|
+
*/
|
|
274
|
+
async destroy() {
|
|
275
|
+
if (this.syncEngine) {
|
|
276
|
+
await this.syncEngine.stop();
|
|
277
|
+
}
|
|
278
|
+
if (this.storage) {
|
|
279
|
+
await this.storage.close();
|
|
280
|
+
}
|
|
281
|
+
this.initialized = false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Install useIndexer hook on Component class.
|
|
286
|
+
* Call this once after importing your Component class.
|
|
287
|
+
*
|
|
288
|
+
* @param {typeof Component} ComponentClass - The Component class to extend
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* import { Component } from '@monygroupcorp/microact';
|
|
292
|
+
* import { EventIndexer } from '@monygroupcorp/micro-web3';
|
|
293
|
+
* EventIndexer.installHook(Component);
|
|
294
|
+
*/
|
|
295
|
+
static installHook(ComponentClass) {
|
|
296
|
+
if (ComponentClass.prototype.useIndexer) {
|
|
297
|
+
console.warn('[EventIndexer] useIndexer hook already installed');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Reactive query hook for EventIndexer.
|
|
303
|
+
* Auto-updates when indexed events change.
|
|
304
|
+
*
|
|
305
|
+
* @param {EntityQueryable|EventsAPI} queryable - What to query (entity or events)
|
|
306
|
+
* @param {Object|Function} where - Filter conditions (can be reactive functions)
|
|
307
|
+
* @param {Object} options - Hook options
|
|
308
|
+
* @param {Function} options.onUpdate - Called when results change
|
|
309
|
+
* @param {Function} options.onError - Called on query error
|
|
310
|
+
* @returns {Proxy} Proxy that returns current results
|
|
311
|
+
*/
|
|
312
|
+
ComponentClass.prototype.useIndexer = function(queryable, where = {}, options = {}) {
|
|
313
|
+
const self = this;
|
|
314
|
+
let result = null;
|
|
315
|
+
let isLoading = true;
|
|
316
|
+
let unsubscribe = null;
|
|
317
|
+
|
|
318
|
+
// Resolve reactive where values
|
|
319
|
+
const resolveWhere = () => {
|
|
320
|
+
const resolved = {};
|
|
321
|
+
for (const [key, value] of Object.entries(where)) {
|
|
322
|
+
resolved[key] = typeof value === 'function' ? value() : value;
|
|
323
|
+
}
|
|
324
|
+
return resolved;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Execute query
|
|
328
|
+
const executeQuery = async () => {
|
|
329
|
+
try {
|
|
330
|
+
const whereValues = resolveWhere();
|
|
331
|
+
|
|
332
|
+
if (queryable.query) {
|
|
333
|
+
// Entity or events queryable
|
|
334
|
+
const queryResult = await queryable.query(whereValues);
|
|
335
|
+
result = Array.isArray(queryResult) ? queryResult : queryResult.events;
|
|
336
|
+
} else {
|
|
337
|
+
result = [];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
isLoading = false;
|
|
341
|
+
|
|
342
|
+
if (options.onUpdate) {
|
|
343
|
+
options.onUpdate(result);
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
isLoading = false;
|
|
347
|
+
console.error('[useIndexer] Query error:', error);
|
|
348
|
+
if (options.onError) {
|
|
349
|
+
options.onError(error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Initial query
|
|
355
|
+
executeQuery();
|
|
356
|
+
|
|
357
|
+
// Subscribe to changes
|
|
358
|
+
if (queryable.subscribe) {
|
|
359
|
+
unsubscribe = queryable.subscribe(resolveWhere(), () => {
|
|
360
|
+
executeQuery();
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Register cleanup
|
|
365
|
+
if (this.registerCleanup) {
|
|
366
|
+
this.registerCleanup(() => {
|
|
367
|
+
if (unsubscribe) unsubscribe();
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Return proxy for array-like access
|
|
372
|
+
return new Proxy([], {
|
|
373
|
+
get(target, prop) {
|
|
374
|
+
if (prop === 'length') return result?.length || 0;
|
|
375
|
+
if (prop === 'isLoading') return isLoading;
|
|
376
|
+
if (prop === Symbol.iterator) {
|
|
377
|
+
return function* () {
|
|
378
|
+
if (result) yield* result;
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (typeof prop === 'string' && !isNaN(prop)) {
|
|
382
|
+
return result?.[parseInt(prop)];
|
|
383
|
+
}
|
|
384
|
+
// Array methods
|
|
385
|
+
if (result && typeof result[prop] === 'function') {
|
|
386
|
+
return result[prop].bind(result);
|
|
387
|
+
}
|
|
388
|
+
return result?.[prop];
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Export error types for external use
|
|
396
|
+
EventIndexer.EventIndexerError = EventIndexerError;
|
|
397
|
+
EventIndexer.ErrorCodes = ErrorCodes;
|
|
398
|
+
|
|
399
|
+
export default EventIndexer;
|