@sschepis/magazine 0.1.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,273 @@
1
+ /**
2
+ * EventMetadata - Enriches events with additional blockchain metadata
3
+ */
4
+ export default class EventMetadata {
5
+ constructor(networkManager, logger) {
6
+ this.networkManager = networkManager;
7
+ this.logger = logger.child({ context: 'EventMetadata' });
8
+ this.logger.info('EventMetadata initialized');
9
+ }
10
+
11
+ /**
12
+ * Enrich event with metadata
13
+ * @param {Object} event - Event to enrich
14
+ * @param {Object} options - Enrichment options
15
+ * @param {boolean} options.includeBlock - Include block data
16
+ * @param {boolean} options.includeTransaction - Include transaction data
17
+ * @param {boolean} options.includeTimestamp - Include block timestamp
18
+ * @param {boolean} options.includeGasCost - Include gas cost
19
+ * @returns {Promise<Object>} Enriched event
20
+ */
21
+ async enrichEvent(event, options = {}) {
22
+ const {
23
+ includeBlock = true,
24
+ includeTransaction = true,
25
+ includeTimestamp = true,
26
+ includeGasCost = true
27
+ } = options;
28
+
29
+ try {
30
+ const enrichedEvent = { ...event };
31
+ const promises = [];
32
+
33
+ // Fetch block data if needed
34
+ if ((includeBlock || includeTimestamp) && event.blockNumber) {
35
+ promises.push(
36
+ this.networkManager.provider.getBlock(event.blockNumber)
37
+ .then(block => {
38
+ if (block) {
39
+ if (includeBlock) {
40
+ enrichedEvent.block = {
41
+ number: block.number,
42
+ hash: block.hash,
43
+ parentHash: block.parentHash,
44
+ miner: block.miner,
45
+ difficulty: block.difficulty?.toString(),
46
+ gasLimit: block.gasLimit.toString(),
47
+ gasUsed: block.gasUsed.toString()
48
+ };
49
+ }
50
+ if (includeTimestamp) {
51
+ enrichedEvent.timestamp = block.timestamp;
52
+ enrichedEvent.date = new Date(block.timestamp * 1000).toISOString();
53
+ }
54
+ }
55
+ })
56
+ .catch(err => {
57
+ this.logger.warn('Failed to fetch block data', {
58
+ blockNumber: event.blockNumber,
59
+ error: err.message
60
+ });
61
+ })
62
+ );
63
+ }
64
+
65
+ // Fetch transaction data if needed
66
+ if ((includeTransaction || includeGasCost) && event.transactionHash) {
67
+ promises.push(
68
+ Promise.all([
69
+ this.networkManager.provider.getTransaction(event.transactionHash),
70
+ includeGasCost ? this.networkManager.provider.getTransactionReceipt(event.transactionHash) : null
71
+ ]).then(([tx, receipt]) => {
72
+ if (tx) {
73
+ if (includeTransaction) {
74
+ enrichedEvent.transaction = {
75
+ hash: tx.hash,
76
+ from: tx.from,
77
+ to: tx.to,
78
+ value: tx.value.toString(),
79
+ nonce: tx.nonce,
80
+ gasLimit: tx.gasLimit.toString(),
81
+ gasPrice: tx.gasPrice?.toString(),
82
+ maxFeePerGas: tx.maxFeePerGas?.toString(),
83
+ maxPriorityFeePerGas: tx.maxPriorityFeePerGas?.toString(),
84
+ data: tx.data.length > 66 ? tx.data.substring(0, 66) + '...' : tx.data
85
+ };
86
+ }
87
+ if (includeGasCost && receipt) {
88
+ const gasUsed = receipt.gasUsed;
89
+ const gasPrice = tx.gasPrice || tx.maxFeePerGas || 0n;
90
+ const gasCost = gasUsed * gasPrice;
91
+
92
+ enrichedEvent.gas = {
93
+ used: gasUsed.toString(),
94
+ price: gasPrice.toString(),
95
+ cost: gasCost.toString(),
96
+ costInEther: this._formatEther(gasCost)
97
+ };
98
+ }
99
+ }
100
+ }).catch(err => {
101
+ this.logger.warn('Failed to fetch transaction data', {
102
+ transactionHash: event.transactionHash,
103
+ error: err.message
104
+ });
105
+ })
106
+ );
107
+ }
108
+
109
+ // Wait for all enrichment operations
110
+ await Promise.all(promises);
111
+
112
+ // Add metadata about the enrichment
113
+ enrichedEvent._metadata = {
114
+ enrichedAt: new Date().toISOString(),
115
+ enrichmentOptions: options
116
+ };
117
+
118
+ this.logger.debug('Event enriched with metadata', {
119
+ eventName: event.eventName,
120
+ blockNumber: event.blockNumber,
121
+ metadataAdded: Object.keys(enrichedEvent).filter(k => !event.hasOwnProperty(k))
122
+ });
123
+
124
+ return enrichedEvent;
125
+
126
+ } catch (error) {
127
+ this.logger.error('Failed to enrich event', {
128
+ event,
129
+ error: error.message
130
+ });
131
+ // Return original event on error
132
+ return event;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Enrich multiple events in batch
138
+ * @param {Array} events - Events to enrich
139
+ * @param {Object} options - Enrichment options
140
+ * @param {number} options.batchSize - Number of events to process concurrently
141
+ * @returns {Promise<Array>} Enriched events
142
+ */
143
+ async enrichEvents(events, options = {}) {
144
+ const { batchSize = 10, ...enrichOptions } = options;
145
+
146
+ return this.logger.time('enrichEvents', async () => {
147
+ const enrichedEvents = [];
148
+
149
+ // Process in batches to avoid overwhelming the provider
150
+ let failureCount = 0;
151
+ for (let i = 0; i < events.length; i += batchSize) {
152
+ const batch = events.slice(i, i + batchSize);
153
+ const results = await Promise.allSettled(
154
+ batch.map(event => this.enrichEvent(event, enrichOptions))
155
+ );
156
+ for (let j = 0; j < results.length; j++) {
157
+ if (results[j].status === 'fulfilled') {
158
+ enrichedEvents.push(results[j].value);
159
+ } else {
160
+ failureCount++;
161
+ this.logger.warn('Failed to enrich event, using original', {
162
+ error: results[j].reason?.message,
163
+ blockNumber: batch[j]?.blockNumber
164
+ });
165
+ enrichedEvents.push(batch[j]);
166
+ }
167
+ }
168
+
169
+ this.logger.debug('Enriched event batch', {
170
+ batchIndex: Math.floor(i / batchSize),
171
+ batchSize: batch.length,
172
+ failures: failureCount,
173
+ totalProgress: `${enrichedEvents.length}/${events.length}`
174
+ });
175
+ }
176
+
177
+ this.logger.info('Completed event enrichment', {
178
+ totalEvents: events.length,
179
+ enrichedEvents: enrichedEvents.length,
180
+ failures: failureCount
181
+ });
182
+
183
+ return enrichedEvents;
184
+ });
185
+ }
186
+
187
+ /**
188
+ * Calculate statistics for a set of events
189
+ * @param {Array} events - Events to analyze
190
+ * @returns {Object} Event statistics
191
+ */
192
+ calculateStatistics(events) {
193
+ if (!Array.isArray(events) || events.length === 0) {
194
+ return {
195
+ count: 0,
196
+ blockRange: { min: null, max: null },
197
+ timeRange: { min: null, max: null },
198
+ gasStats: { total: '0', average: '0', min: '0', max: '0' }
199
+ };
200
+ }
201
+
202
+ const blockNumbers = events
203
+ .map(e => e.blockNumber)
204
+ .filter(n => n != null)
205
+ .sort((a, b) => a - b);
206
+
207
+ const timestamps = events
208
+ .map(e => e.timestamp)
209
+ .filter(t => t != null)
210
+ .sort((a, b) => a - b);
211
+
212
+ const gasCosts = events
213
+ .map(e => e.gas?.cost)
214
+ .filter(c => c != null)
215
+ .map(c => BigInt(c));
216
+
217
+ const stats = {
218
+ count: events.length,
219
+ blockRange: {
220
+ min: blockNumbers[0] || null,
221
+ max: blockNumbers[blockNumbers.length - 1] || null,
222
+ span: blockNumbers.length > 0 ? blockNumbers[blockNumbers.length - 1] - blockNumbers[0] : 0
223
+ },
224
+ timeRange: {
225
+ min: timestamps[0] || null,
226
+ max: timestamps[timestamps.length - 1] || null,
227
+ span: timestamps.length > 0 ? timestamps[timestamps.length - 1] - timestamps[0] : 0
228
+ }
229
+ };
230
+
231
+ if (gasCosts.length > 0) {
232
+ const totalGas = gasCosts.reduce((sum, cost) => sum + cost, 0n);
233
+ const avgGas = totalGas / BigInt(gasCosts.length);
234
+ const sortedGas = [...gasCosts].sort((a, b) => a < b ? -1 : 1);
235
+
236
+ stats.gasStats = {
237
+ total: totalGas.toString(),
238
+ average: avgGas.toString(),
239
+ min: sortedGas[0].toString(),
240
+ max: sortedGas[sortedGas.length - 1].toString(),
241
+ totalInEther: this._formatEther(totalGas)
242
+ };
243
+ } else {
244
+ stats.gasStats = { total: '0', average: '0', min: '0', max: '0' };
245
+ }
246
+
247
+ // Event type distribution
248
+ const eventTypes = {};
249
+ events.forEach(e => {
250
+ if (e.eventName) {
251
+ eventTypes[e.eventName] = (eventTypes[e.eventName] || 0) + 1;
252
+ }
253
+ });
254
+ stats.eventTypes = eventTypes;
255
+
256
+ this.logger.debug('Calculated event statistics', stats);
257
+ return stats;
258
+ }
259
+
260
+ /**
261
+ * Format wei to ether string
262
+ * @private
263
+ */
264
+ _formatEther(wei) {
265
+ try {
266
+ const weiString = wei.toString();
267
+ const ethValue = Number(weiString) / 1e18;
268
+ return ethValue.toFixed(6) + ' ETH';
269
+ } catch {
270
+ return '0 ETH';
271
+ }
272
+ }
273
+ }