@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.
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/magazine.cjs.js +5 -0
- package/dist/magazine.cjs.js.map +1 -0
- package/dist/magazine.es.js +4149 -0
- package/dist/magazine.es.js.map +1 -0
- package/dist/magazine.umd.js +5 -0
- package/dist/magazine.umd.js.map +1 -0
- package/package.json +74 -0
- package/src/components/ConfigManager.js +202 -0
- package/src/components/ConfigSchema.js +506 -0
- package/src/components/DataCompressor.js +378 -0
- package/src/components/DataTransformer.js +1172 -0
- package/src/components/DebugUtils.js +657 -0
- package/src/components/EventFilter.js +474 -0
- package/src/components/EventMetadata.js +273 -0
- package/src/components/EventStore.js +443 -0
- package/src/components/EventSyncer.js +353 -0
- package/src/components/Logger.js +299 -0
- package/src/components/NetworkManager.js +461 -0
- package/src/components/Paginator.js +343 -0
- package/src/index.js +571 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import * as ethers from 'ethers';
|
|
2
|
+
import Logger from './Logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* LRUCache - Simple Least Recently Used cache using Map ordering
|
|
6
|
+
*/
|
|
7
|
+
class LRUCache {
|
|
8
|
+
constructor(maxSize = 500) { this.maxSize = maxSize; this.cache = new Map(); }
|
|
9
|
+
get(key) { if (!this.cache.has(key)) return undefined; const v = this.cache.get(key); this.cache.delete(key); this.cache.set(key, v); return v; }
|
|
10
|
+
set(key, value) { if (this.cache.has(key)) this.cache.delete(key); else if (this.cache.size >= this.maxSize) this.cache.delete(this.cache.keys().next().value); this.cache.set(key, value); }
|
|
11
|
+
clear() { this.cache.clear(); }
|
|
12
|
+
get size() { return this.cache.size; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* RateLimiter - Token-bucket rate limiter for throttling RPC calls
|
|
17
|
+
*/
|
|
18
|
+
class RateLimiter {
|
|
19
|
+
constructor(maxPerSecond = 25) {
|
|
20
|
+
this.maxPerSecond = maxPerSecond;
|
|
21
|
+
this.tokens = maxPerSecond;
|
|
22
|
+
this.lastRefill = Date.now();
|
|
23
|
+
this.queue = [];
|
|
24
|
+
}
|
|
25
|
+
async acquire() {
|
|
26
|
+
this._refill();
|
|
27
|
+
if (this.tokens > 0) { this.tokens--; return; }
|
|
28
|
+
return new Promise(resolve => this.queue.push(resolve));
|
|
29
|
+
}
|
|
30
|
+
_refill() {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const elapsed = (now - this.lastRefill) / 1000;
|
|
33
|
+
this.tokens = Math.min(this.maxPerSecond, this.tokens + elapsed * this.maxPerSecond);
|
|
34
|
+
this.lastRefill = now;
|
|
35
|
+
while (this.queue.length > 0 && this.tokens > 0) { this.tokens--; this.queue.shift()(); }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* NetworkManager - Handles provider and network management
|
|
41
|
+
*
|
|
42
|
+
* Supports multi-provider failover, LRU caching for blocks/timestamps,
|
|
43
|
+
* and token-bucket rate limiting for RPC calls.
|
|
44
|
+
*/
|
|
45
|
+
export default class NetworkManager {
|
|
46
|
+
constructor(config, retryOperation) {
|
|
47
|
+
this.config = config;
|
|
48
|
+
this.retryOperation = retryOperation;
|
|
49
|
+
this.logger = new Logger({
|
|
50
|
+
context: 'NetworkManager',
|
|
51
|
+
level: config?.logLevel || 'info'
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.logger.debug('Initializing NetworkManager', {
|
|
55
|
+
provider: config.provider,
|
|
56
|
+
networkId: config.networkId
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Multi-provider failover setup
|
|
60
|
+
this.providers = [];
|
|
61
|
+
this.currentProviderIndex = 0;
|
|
62
|
+
this._initializeProviders(config.provider);
|
|
63
|
+
|
|
64
|
+
// Set the active provider to the first one
|
|
65
|
+
this.provider = this.providers[0];
|
|
66
|
+
|
|
67
|
+
this.interface = new ethers.Interface(config.abi);
|
|
68
|
+
|
|
69
|
+
// Initialize LRU caches for blocks and timestamps
|
|
70
|
+
const cacheSize = config.cacheSize || 500;
|
|
71
|
+
this.blockCache = new LRUCache(cacheSize);
|
|
72
|
+
this.timestampCache = new LRUCache(cacheSize);
|
|
73
|
+
this.logger.debug('LRU caches initialized', { cacheSize });
|
|
74
|
+
|
|
75
|
+
// Initialize rate limiter
|
|
76
|
+
const rateLimit = config.rateLimit || 25;
|
|
77
|
+
this.rateLimiter = new RateLimiter(rateLimit);
|
|
78
|
+
this.logger.debug('Rate limiter initialized', { maxPerSecond: rateLimit });
|
|
79
|
+
|
|
80
|
+
// Initialize event mappings
|
|
81
|
+
this.events = {};
|
|
82
|
+
this.eventsByTopic = {}; // Map topic hash to event name
|
|
83
|
+
|
|
84
|
+
this._initializeEvents();
|
|
85
|
+
|
|
86
|
+
this.logger.info('NetworkManager initialized successfully', {
|
|
87
|
+
eventCount: Object.keys(this.eventsByTopic).length,
|
|
88
|
+
contractAddress: config.address,
|
|
89
|
+
providerCount: this.providers.length
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Initialize providers from config. Accepts a single provider (string or
|
|
95
|
+
* ethers provider object) or an array of providers.
|
|
96
|
+
*/
|
|
97
|
+
_initializeProviders(providerConfig) {
|
|
98
|
+
const providerList = Array.isArray(providerConfig) ? providerConfig : [providerConfig];
|
|
99
|
+
|
|
100
|
+
for (const entry of providerList) {
|
|
101
|
+
if (typeof entry === 'string') {
|
|
102
|
+
const provider = new ethers.JsonRpcProvider(entry);
|
|
103
|
+
this.providers.push(provider);
|
|
104
|
+
this.logger.debug('Created JsonRpcProvider from URL', { url: entry });
|
|
105
|
+
} else if (entry && typeof entry.getNetwork === 'function') {
|
|
106
|
+
this.providers.push(entry);
|
|
107
|
+
this.logger.debug('Using provided ethers provider object', {
|
|
108
|
+
providerType: entry.constructor.name
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
throw new Error('Each provider must be either a string URL or an ethers provider object');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.providers.length === 0) {
|
|
116
|
+
throw new Error('At least one provider must be specified');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.logger.debug('Providers initialized', { count: this.providers.length });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Switch to the next provider in the list on failure.
|
|
124
|
+
* Returns true if a new provider was activated, false if all have been tried.
|
|
125
|
+
*/
|
|
126
|
+
_switchProvider(error) {
|
|
127
|
+
const previousIndex = this.currentProviderIndex;
|
|
128
|
+
this.currentProviderIndex = (this.currentProviderIndex + 1) % this.providers.length;
|
|
129
|
+
|
|
130
|
+
// If we've cycled back to the starting provider, all have been tried
|
|
131
|
+
if (this.currentProviderIndex === previousIndex && this.providers.length === 1) {
|
|
132
|
+
this.logger.error('Single provider failed, no failover available', {
|
|
133
|
+
error: error.message
|
|
134
|
+
});
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.provider = this.providers[this.currentProviderIndex];
|
|
139
|
+
this.logger.warn('Switching provider due to failure', {
|
|
140
|
+
error: error.message,
|
|
141
|
+
previousIndex,
|
|
142
|
+
newIndex: this.currentProviderIndex,
|
|
143
|
+
totalProviders: this.providers.length
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Execute a function through the rate limiter.
|
|
151
|
+
* Acquires a token before executing the call.
|
|
152
|
+
*/
|
|
153
|
+
async _throttledCall(fn) {
|
|
154
|
+
await this.rateLimiter.acquire();
|
|
155
|
+
return fn();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Execute a provider call with failover support.
|
|
160
|
+
* Tries each provider in sequence before giving up.
|
|
161
|
+
*/
|
|
162
|
+
async _callWithFailover(fn, description) {
|
|
163
|
+
const startIndex = this.currentProviderIndex;
|
|
164
|
+
let lastError;
|
|
165
|
+
let attempts = 0;
|
|
166
|
+
|
|
167
|
+
do {
|
|
168
|
+
try {
|
|
169
|
+
return await this._throttledCall(() => fn(this.provider));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
lastError = error;
|
|
172
|
+
attempts++;
|
|
173
|
+
this.logger.warn('Provider call failed', {
|
|
174
|
+
description,
|
|
175
|
+
providerIndex: this.currentProviderIndex,
|
|
176
|
+
attempt: attempts,
|
|
177
|
+
error: error.message
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (this.providers.length > 1) {
|
|
181
|
+
this._switchProvider(error);
|
|
182
|
+
// If we've rotated back to the start, stop
|
|
183
|
+
if (this.currentProviderIndex === startIndex) {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} while (true);
|
|
191
|
+
|
|
192
|
+
throw lastError;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Initialize event mappings from ABI
|
|
196
|
+
_initializeEvents() {
|
|
197
|
+
let eventCount = 0;
|
|
198
|
+
|
|
199
|
+
for (const fragment of this.config.abi) {
|
|
200
|
+
if (fragment.type === 'event') {
|
|
201
|
+
const eventName = fragment.name;
|
|
202
|
+
this.events[eventName] = new ethers.Interface([fragment]);
|
|
203
|
+
|
|
204
|
+
// Create mapping from topic hash to event name
|
|
205
|
+
try {
|
|
206
|
+
const eventFragment = this.interface.getEvent(eventName);
|
|
207
|
+
const topicHash = eventFragment.topicHash;
|
|
208
|
+
this.eventsByTopic[topicHash] = eventName;
|
|
209
|
+
eventCount++;
|
|
210
|
+
|
|
211
|
+
this.logger.debug('Event mapping created', {
|
|
212
|
+
eventName,
|
|
213
|
+
topicHash
|
|
214
|
+
});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
this.logger.warn('Failed to get topic hash for event', {
|
|
217
|
+
eventName,
|
|
218
|
+
error: error.message
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.logger.debug('Event initialization completed', { eventCount });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Get the latest block number from the network
|
|
228
|
+
async getLatestBlockNumber() {
|
|
229
|
+
return this.logger.time('getLatestBlockNumber', async () => {
|
|
230
|
+
const blockNumber = await this.retryOperation(
|
|
231
|
+
() => this._callWithFailover(
|
|
232
|
+
(provider) => provider.getBlockNumber(),
|
|
233
|
+
'getLatestBlockNumber'
|
|
234
|
+
),
|
|
235
|
+
'Getting latest block number'
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
this.logger.logNetworkActivity('getLatestBlockNumber', { blockNumber });
|
|
239
|
+
return blockNumber;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Get logs from the blockchain within a block range
|
|
244
|
+
async getLogs(fromBlock, toBlock) {
|
|
245
|
+
return this.logger.time('getLogs', async () => {
|
|
246
|
+
this.logger.logNetworkActivity('getLogs_request', { fromBlock, toBlock, address: this.config.address });
|
|
247
|
+
|
|
248
|
+
const logs = await this.retryOperation(
|
|
249
|
+
() => this._callWithFailover(
|
|
250
|
+
(provider) => provider.getLogs({
|
|
251
|
+
address: this.config.address,
|
|
252
|
+
fromBlock,
|
|
253
|
+
toBlock
|
|
254
|
+
}),
|
|
255
|
+
'getLogs'
|
|
256
|
+
),
|
|
257
|
+
`Getting logs for blocks ${fromBlock}-${toBlock}`
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
this.logger.logNetworkActivity('getLogs_response', {
|
|
261
|
+
fromBlock,
|
|
262
|
+
toBlock,
|
|
263
|
+
logCount: logs.length
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return logs;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Parse a raw log into structured event data
|
|
271
|
+
parseLog(log) {
|
|
272
|
+
const topicHash = log.topics[0];
|
|
273
|
+
const eventName = this.eventsByTopic[topicHash];
|
|
274
|
+
|
|
275
|
+
if (!eventName) {
|
|
276
|
+
this.logger.trace('Unknown event topic', { topicHash, availableTopics: Object.keys(this.eventsByTopic) });
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const parsedLog = this.interface.parseLog(log);
|
|
282
|
+
const result = {
|
|
283
|
+
eventName,
|
|
284
|
+
parsedLog,
|
|
285
|
+
eventData: {
|
|
286
|
+
...parsedLog.args,
|
|
287
|
+
blockNumber: log.blockNumber,
|
|
288
|
+
transactionHash: log.transactionHash,
|
|
289
|
+
logIndex: log.logIndex,
|
|
290
|
+
timestamp: Date.now()
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
this.logger.debug('Successfully parsed log', {
|
|
295
|
+
eventName,
|
|
296
|
+
blockNumber: log.blockNumber,
|
|
297
|
+
transactionHash: log.transactionHash,
|
|
298
|
+
logIndex: log.logIndex
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return result;
|
|
302
|
+
} catch (error) {
|
|
303
|
+
this.logger.warn('Failed to parse log for event', {
|
|
304
|
+
eventName,
|
|
305
|
+
blockNumber: log.blockNumber,
|
|
306
|
+
transactionHash: log.transactionHash,
|
|
307
|
+
error: error.message
|
|
308
|
+
});
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Create unique event key for deduplication
|
|
314
|
+
createEventKey(log) {
|
|
315
|
+
return `${log.transactionHash}-${log.logIndex}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Get network information
|
|
319
|
+
async getNetworkInfo() {
|
|
320
|
+
return this.retryOperation(async () => {
|
|
321
|
+
const network = await this._callWithFailover(
|
|
322
|
+
(provider) => provider.getNetwork(),
|
|
323
|
+
'getNetwork'
|
|
324
|
+
);
|
|
325
|
+
const blockNumber = await this._callWithFailover(
|
|
326
|
+
(provider) => provider.getBlockNumber(),
|
|
327
|
+
'getBlockNumber'
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
chainId: Number(network.chainId),
|
|
332
|
+
name: network.name,
|
|
333
|
+
currentBlock: blockNumber,
|
|
334
|
+
expectedNetworkId: this.config.networkId
|
|
335
|
+
};
|
|
336
|
+
}, 'Getting network information');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Validate network connection and configuration
|
|
340
|
+
async validateNetwork() {
|
|
341
|
+
const networkInfo = await this.getNetworkInfo();
|
|
342
|
+
|
|
343
|
+
if (networkInfo.chainId !== this.config.networkId) {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`Network mismatch: expected ${this.config.networkId}, got ${networkInfo.chainId}`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return networkInfo;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Get contract code to verify contract exists
|
|
353
|
+
async validateContract() {
|
|
354
|
+
return this.retryOperation(async () => {
|
|
355
|
+
const code = await this._callWithFailover(
|
|
356
|
+
(provider) => provider.getCode(this.config.address),
|
|
357
|
+
'getCode'
|
|
358
|
+
);
|
|
359
|
+
if (code === '0x') {
|
|
360
|
+
throw new Error(`No contract found at address ${this.config.address}`);
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
}, 'Validating contract');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Get a block by number, with LRU caching
|
|
367
|
+
async getBlock(blockNumber) {
|
|
368
|
+
const cached = this.blockCache.get(blockNumber);
|
|
369
|
+
if (cached !== undefined) {
|
|
370
|
+
this.logger.debug('Block cache hit', { blockNumber });
|
|
371
|
+
return cached;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const block = await this.retryOperation(async () => {
|
|
375
|
+
return this._callWithFailover(
|
|
376
|
+
(provider) => provider.getBlock(blockNumber),
|
|
377
|
+
`getBlock(${blockNumber})`
|
|
378
|
+
);
|
|
379
|
+
}, `Getting block ${blockNumber}`);
|
|
380
|
+
|
|
381
|
+
if (block) {
|
|
382
|
+
this.blockCache.set(blockNumber, block);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return block;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Get block timestamp, with LRU caching
|
|
389
|
+
async getBlockTimestamp(blockNumber) {
|
|
390
|
+
const cached = this.timestampCache.get(blockNumber);
|
|
391
|
+
if (cached !== undefined) {
|
|
392
|
+
this.logger.debug('Timestamp cache hit', { blockNumber });
|
|
393
|
+
return cached;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const block = await this.getBlock(blockNumber);
|
|
397
|
+
const timestamp = block ? block.timestamp : null;
|
|
398
|
+
|
|
399
|
+
if (timestamp !== null) {
|
|
400
|
+
this.timestampCache.set(blockNumber, timestamp);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return timestamp;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Get transaction receipt
|
|
407
|
+
async getTransactionReceipt(txHash) {
|
|
408
|
+
return this.retryOperation(
|
|
409
|
+
() => this._callWithFailover(
|
|
410
|
+
(provider) => provider.getTransactionReceipt(txHash),
|
|
411
|
+
'getTransactionReceipt'
|
|
412
|
+
),
|
|
413
|
+
`Getting transaction receipt for ${txHash}`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Get gas price information
|
|
418
|
+
async getGasPrice() {
|
|
419
|
+
return this.retryOperation(
|
|
420
|
+
() => this._callWithFailover(
|
|
421
|
+
(provider) => provider.getFeeData(),
|
|
422
|
+
'getFeeData'
|
|
423
|
+
),
|
|
424
|
+
'Getting gas price information'
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Get provider instance for advanced operations
|
|
429
|
+
getProvider() {
|
|
430
|
+
return this.provider;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Get ethers interface for manual parsing
|
|
434
|
+
getInterface() {
|
|
435
|
+
return this.interface;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Get event mappings
|
|
439
|
+
getEventMappings() {
|
|
440
|
+
return {
|
|
441
|
+
events: { ...this.events },
|
|
442
|
+
eventsByTopic: { ...this.eventsByTopic }
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Clear all caches
|
|
447
|
+
clearCaches() {
|
|
448
|
+
this.blockCache.clear();
|
|
449
|
+
this.timestampCache.clear();
|
|
450
|
+
this.logger.debug('All caches cleared');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Get cache statistics
|
|
454
|
+
getCacheStats() {
|
|
455
|
+
return {
|
|
456
|
+
blockCacheSize: this.blockCache.size,
|
|
457
|
+
timestampCacheSize: this.timestampCache.size,
|
|
458
|
+
maxCacheSize: this.blockCache.maxSize
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|