@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,277 @@
1
+ // src/indexer/Patterns.js
2
+
3
+ /**
4
+ * Pre-built patterns for common dApp needs.
5
+ * Zero-config solutions for activity feeds, leaderboards, etc.
6
+ */
7
+ class Patterns {
8
+ constructor(queryEngine, entityResolver) {
9
+ this.queryEngine = queryEngine;
10
+ this.entityResolver = entityResolver;
11
+ }
12
+
13
+ /**
14
+ * User activity feed.
15
+ * @param {string} address - User address
16
+ * @param {Object} options - Options
17
+ * @param {string[]} options.events - Event types to include
18
+ * @param {string[]} options.userFields - Fields to match address against (auto-detected if omitted)
19
+ * @param {number} options.limit - Max results
20
+ * @param {number} options.offset - Skip N results
21
+ * @returns {Promise<ActivityItem[]>}
22
+ */
23
+ async userActivity(address, options = {}) {
24
+ const { events = [], userFields, limit = 50, offset = 0 } = options;
25
+ const normalizedAddress = address.toLowerCase();
26
+
27
+ // Collect activity from all event types
28
+ const allActivity = [];
29
+
30
+ for (const eventType of events) {
31
+ const result = await this.queryEngine.query(eventType, {
32
+ limit: 1000 // Get more to filter
33
+ });
34
+
35
+ // Auto-detect user fields if not specified
36
+ const fields = userFields || this._detectAddressFields(result.events[0]);
37
+
38
+ // Filter events involving this user
39
+ for (const event of result.events) {
40
+ const isUserEvent = fields.some(field => {
41
+ const value = event.indexed[field] || event.data[field];
42
+ return typeof value === 'string' && value.toLowerCase() === normalizedAddress;
43
+ });
44
+
45
+ if (isUserEvent) {
46
+ allActivity.push({
47
+ type: event.type,
48
+ timestamp: event.timestamp || event.blockNumber, // Fall back to block number
49
+ blockNumber: event.blockNumber,
50
+ transactionHash: event.transactionHash,
51
+ data: event.data
52
+ });
53
+ }
54
+ }
55
+ }
56
+
57
+ // Sort by timestamp/block descending
58
+ allActivity.sort((a, b) => (b.timestamp || b.blockNumber) - (a.timestamp || a.blockNumber));
59
+
60
+ // Paginate
61
+ return allActivity.slice(offset, offset + limit);
62
+ }
63
+
64
+ /**
65
+ * Find refundable items (cancelled parent + unclaimed).
66
+ * @param {Object} options - Options
67
+ * @param {string} options.itemEvent - Event creating the item
68
+ * @param {string} options.itemKey - Unique key field
69
+ * @param {string} options.parentKey - Field linking to parent
70
+ * @param {string} options.cancelEvent - Event that cancels parent
71
+ * @param {string} options.claimEvent - Event that claims item
72
+ * @param {string} options.userField - Field containing user address
73
+ * @param {string} options.user - User address to filter
74
+ * @returns {Promise<IndexedEvent[]>}
75
+ */
76
+ async refundable(options) {
77
+ const {
78
+ itemEvent,
79
+ itemKey,
80
+ parentKey,
81
+ cancelEvent,
82
+ claimEvent,
83
+ userField,
84
+ user
85
+ } = options;
86
+
87
+ const normalizedUser = user.toLowerCase();
88
+
89
+ // Get all items for user
90
+ const itemsResult = await this.queryEngine.query(itemEvent, { limit: 10000 });
91
+ const userItems = itemsResult.events.filter(e => {
92
+ const userValue = e.indexed[userField] || e.data[userField];
93
+ return typeof userValue === 'string' && userValue.toLowerCase() === normalizedUser;
94
+ });
95
+
96
+ // Get cancelled parents
97
+ const cancelResult = await this.queryEngine.query(cancelEvent, { limit: 10000 });
98
+ const cancelledParents = new Set(
99
+ cancelResult.events.map(e => e.indexed[parentKey] || e.data[parentKey])
100
+ );
101
+
102
+ // Get claimed items
103
+ const claimResult = await this.queryEngine.query(claimEvent, { limit: 10000 });
104
+ const claimedItems = new Set(
105
+ claimResult.events.map(e => e.indexed[itemKey] || e.data[itemKey])
106
+ );
107
+
108
+ // Filter: parent cancelled AND item not claimed
109
+ return userItems.filter(item => {
110
+ const itemKeyValue = item.indexed[itemKey] || item.data[itemKey];
111
+ const parentKeyValue = item.indexed[parentKey] || item.data[parentKey];
112
+
113
+ return cancelledParents.has(parentKeyValue) && !claimedItems.has(itemKeyValue);
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Leaderboard aggregation.
119
+ * @param {Object} options - Options
120
+ * @param {string} options.event - Event to aggregate
121
+ * @param {string} options.groupBy - Field to group by
122
+ * @param {string} options.aggregate - 'count' or 'sum'
123
+ * @param {string} options.sumField - Field to sum (if aggregate: 'sum')
124
+ * @param {number} options.limit - Max results
125
+ * @param {Object} options.where - Filter conditions
126
+ * @returns {Promise<LeaderboardEntry[]>}
127
+ */
128
+ async leaderboard(options) {
129
+ const { event, groupBy, aggregate, sumField, limit = 10, where } = options;
130
+
131
+ const result = await this.queryEngine.query(event, {
132
+ where,
133
+ limit: 10000
134
+ });
135
+
136
+ // Group by field
137
+ const groups = new Map();
138
+ for (const e of result.events) {
139
+ const key = e.indexed[groupBy] || e.data[groupBy];
140
+ if (key === undefined) continue;
141
+
142
+ const normalizedKey = typeof key === 'string' ? key.toLowerCase() : key;
143
+
144
+ if (!groups.has(normalizedKey)) {
145
+ groups.set(normalizedKey, { key, events: [] });
146
+ }
147
+ groups.get(normalizedKey).events.push(e);
148
+ }
149
+
150
+ // Calculate aggregates
151
+ const entries = [];
152
+ for (const [normalizedKey, group] of groups) {
153
+ let value;
154
+ if (aggregate === 'count') {
155
+ value = group.events.length;
156
+ } else if (aggregate === 'sum') {
157
+ value = group.events.reduce((sum, e) => {
158
+ const fieldValue = e.indexed[sumField] || e.data[sumField];
159
+ const num = typeof fieldValue === 'string' ? parseFloat(fieldValue) : fieldValue;
160
+ return sum + (isNaN(num) ? 0 : num);
161
+ }, 0);
162
+ }
163
+
164
+ entries.push({ key: group.key, value });
165
+ }
166
+
167
+ // Sort by value descending
168
+ entries.sort((a, b) => b.value - a.value);
169
+
170
+ // Add ranks and limit
171
+ return entries.slice(0, limit).map((entry, index) => ({
172
+ ...entry,
173
+ rank: index + 1
174
+ }));
175
+ }
176
+
177
+ /**
178
+ * Time-series aggregation.
179
+ * @param {Object} options - Options
180
+ * @param {string} options.event - Event to aggregate
181
+ * @param {string} options.interval - 'hour' | 'day' | 'week'
182
+ * @param {string} options.aggregate - 'count' or 'sum'
183
+ * @param {string} options.sumField - Field to sum (if aggregate: 'sum')
184
+ * @param {Object} options.where - Filter conditions
185
+ * @param {number} options.periods - Number of periods (default: 30)
186
+ * @returns {Promise<TimeSeriesPoint[]>}
187
+ */
188
+ async timeSeries(options) {
189
+ const { event, interval, aggregate, sumField, where, periods = 30 } = options;
190
+
191
+ const result = await this.queryEngine.query(event, {
192
+ where,
193
+ limit: 100000
194
+ });
195
+
196
+ // Calculate interval in milliseconds
197
+ const intervalMs = {
198
+ hour: 60 * 60 * 1000,
199
+ day: 24 * 60 * 60 * 1000,
200
+ week: 7 * 24 * 60 * 60 * 1000
201
+ }[interval];
202
+
203
+ if (!intervalMs) {
204
+ throw new Error(`Invalid interval: ${interval}`);
205
+ }
206
+
207
+ // Group events by period
208
+ const now = Date.now();
209
+ const periodGroups = new Map();
210
+
211
+ // Initialize periods
212
+ for (let i = 0; i < periods; i++) {
213
+ const periodStart = now - (i + 1) * intervalMs;
214
+ const periodKey = new Date(periodStart).toISOString();
215
+ periodGroups.set(periodKey, []);
216
+ }
217
+
218
+ // Assign events to periods
219
+ for (const e of result.events) {
220
+ const eventTime = e.timestamp ? e.timestamp * 1000 : now; // Assume timestamp is in seconds
221
+ const periodsAgo = Math.floor((now - eventTime) / intervalMs);
222
+
223
+ if (periodsAgo >= 0 && periodsAgo < periods) {
224
+ const periodStart = now - (periodsAgo + 1) * intervalMs;
225
+ const periodKey = new Date(periodStart).toISOString();
226
+
227
+ if (periodGroups.has(periodKey)) {
228
+ periodGroups.get(periodKey).push(e);
229
+ }
230
+ }
231
+ }
232
+
233
+ // Calculate aggregates
234
+ const points = [];
235
+ for (const [period, events] of periodGroups) {
236
+ let value;
237
+ if (aggregate === 'count') {
238
+ value = events.length;
239
+ } else if (aggregate === 'sum') {
240
+ value = events.reduce((sum, e) => {
241
+ const fieldValue = e.indexed[sumField] || e.data[sumField];
242
+ const num = typeof fieldValue === 'string' ? parseFloat(fieldValue) : fieldValue;
243
+ return sum + (isNaN(num) ? 0 : num);
244
+ }, 0);
245
+ }
246
+
247
+ points.push({ period, value });
248
+ }
249
+
250
+ // Sort by period ascending (oldest first)
251
+ points.sort((a, b) => new Date(a.period) - new Date(b.period));
252
+
253
+ return points;
254
+ }
255
+
256
+ /**
257
+ * Detect address fields in an event.
258
+ * @param {IndexedEvent} event - Sample event
259
+ * @returns {string[]} Field names that look like addresses
260
+ */
261
+ _detectAddressFields(event) {
262
+ if (!event) return [];
263
+
264
+ const fields = [];
265
+ const allFields = { ...event.indexed, ...event.data };
266
+
267
+ for (const [key, value] of Object.entries(allFields)) {
268
+ if (typeof value === 'string' && value.match(/^0x[a-fA-F0-9]{40}$/)) {
269
+ fields.push(key);
270
+ }
271
+ }
272
+
273
+ return fields;
274
+ }
275
+ }
276
+
277
+ export default Patterns;
@@ -0,0 +1,149 @@
1
+ // src/indexer/QueryEngine.js
2
+
3
+ /**
4
+ * Query engine for EventIndexer.
5
+ * Provides the Events API for direct event access.
6
+ */
7
+ class QueryEngine {
8
+ constructor(storage, eventBus) {
9
+ this.storage = storage;
10
+ this.eventBus = eventBus;
11
+ this.subscriptions = new Map(); // subscriptionId -> { eventTypes, callback, where }
12
+ this.nextSubscriptionId = 1;
13
+ }
14
+
15
+ /**
16
+ * Query events with filters.
17
+ * @param {string} eventName - Event type to query
18
+ * @param {QueryOptions} options - Query options
19
+ * @returns {Promise<QueryResult>}
20
+ */
21
+ async query(eventName, options = {}) {
22
+ return this.storage.queryEvents(eventName, {
23
+ where: options.where,
24
+ orderBy: options.orderBy || 'blockNumber',
25
+ order: options.order || 'desc',
26
+ limit: options.limit || 100,
27
+ offset: options.offset || 0
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Get single event by ID.
33
+ * @param {string} eventName - Event type
34
+ * @param {string} id - Event ID (txHash-logIndex)
35
+ * @returns {Promise<IndexedEvent|null>}
36
+ */
37
+ async get(eventName, id) {
38
+ return this.storage.getEvent(eventName, id);
39
+ }
40
+
41
+ /**
42
+ * Subscribe to new events.
43
+ * @param {string|string[]} eventNames - Event type(s) to subscribe to
44
+ * @param {Function} callback - Called with new events
45
+ * @returns {Function} Unsubscribe function
46
+ */
47
+ subscribe(eventNames, callback) {
48
+ const types = Array.isArray(eventNames) ? eventNames : [eventNames];
49
+ const subscriptionId = this.nextSubscriptionId++;
50
+
51
+ this.subscriptions.set(subscriptionId, {
52
+ eventTypes: new Set(types),
53
+ callback
54
+ });
55
+
56
+ // Return unsubscribe function
57
+ return () => {
58
+ this.subscriptions.delete(subscriptionId);
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Count events matching filter.
64
+ * @param {string} eventName - Event type
65
+ * @param {Object} where - Filter conditions
66
+ * @returns {Promise<number>}
67
+ */
68
+ async count(eventName, where) {
69
+ return this.storage.countEvents(eventName, where);
70
+ }
71
+
72
+ /**
73
+ * Called by SyncEngine when new events are indexed.
74
+ * Notifies subscribers.
75
+ * @param {string} eventType - Event type
76
+ * @param {IndexedEvent[]} events - New events
77
+ */
78
+ notifyNewEvents(eventType, events) {
79
+ for (const subscription of this.subscriptions.values()) {
80
+ if (subscription.eventTypes.has(eventType) || subscription.eventTypes.has('*')) {
81
+ try {
82
+ subscription.callback(events);
83
+ } catch (error) {
84
+ console.error('[QueryEngine] Subscription callback error:', error);
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Get all events for entity resolution (used by EntityResolver).
92
+ * Returns a lightweight checker interface.
93
+ * @param {Object} relatedWhere - Filter for related events
94
+ * @returns {Promise<EventChecker>}
95
+ */
96
+ async getEventChecker(relatedWhere = {}) {
97
+ // Pre-fetch all potentially related events
98
+ const eventCache = new Map();
99
+
100
+ return {
101
+ has: (eventName, where = {}) => {
102
+ const events = this._getCachedEvents(eventCache, eventName);
103
+ return events.some(e => this._matchesWhere(e, { ...relatedWhere, ...where }));
104
+ },
105
+
106
+ get: (eventName, where = {}) => {
107
+ const events = this._getCachedEvents(eventCache, eventName);
108
+ return events.filter(e => this._matchesWhere(e, { ...relatedWhere, ...where }));
109
+ },
110
+
111
+ count: (eventName, where = {}) => {
112
+ const events = this._getCachedEvents(eventCache, eventName);
113
+ return events.filter(e => this._matchesWhere(e, { ...relatedWhere, ...where })).length;
114
+ },
115
+
116
+ // Async method to prefetch events for a set of keys
117
+ prefetch: async (eventNames) => {
118
+ for (const eventName of eventNames) {
119
+ if (!eventCache.has(eventName)) {
120
+ const result = await this.storage.queryEvents(eventName, { limit: 10000 });
121
+ eventCache.set(eventName, result.events);
122
+ }
123
+ }
124
+ }
125
+ };
126
+ }
127
+
128
+ _getCachedEvents(cache, eventName) {
129
+ return cache.get(eventName) || [];
130
+ }
131
+
132
+ _matchesWhere(event, where) {
133
+ for (const [key, value] of Object.entries(where)) {
134
+ const eventValue = this._getNestedValue(event, key);
135
+ const normalizedEvent = typeof eventValue === 'string' ? eventValue.toLowerCase() : eventValue;
136
+ const normalizedValue = typeof value === 'string' ? value.toLowerCase() : value;
137
+ if (normalizedEvent !== normalizedValue) return false;
138
+ }
139
+ return true;
140
+ }
141
+
142
+ _getNestedValue(obj, path) {
143
+ if (obj.indexed?.[path] !== undefined) return obj.indexed[path];
144
+ if (obj.data?.[path] !== undefined) return obj.data[path];
145
+ return obj[path];
146
+ }
147
+ }
148
+
149
+ export default QueryEngine;