@explorins/pers-sdk 2.1.39 → 2.1.42
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/dist/chunks/{pers-sdk-Cwlrl8jb.cjs → pers-sdk-DeFxjuRB.cjs} +5 -4
- package/dist/chunks/pers-sdk-DeFxjuRB.cjs.map +1 -0
- package/dist/chunks/{pers-sdk-CNIfzWYX.js → pers-sdk-DxYmXQcW.js} +6 -5
- package/dist/chunks/pers-sdk-DxYmXQcW.js.map +1 -0
- package/dist/chunks/{web3-manager-Dvcq4xmn.js → web3-manager-B-IsluxI.js} +770 -34
- package/dist/chunks/web3-manager-B-IsluxI.js.map +1 -0
- package/dist/chunks/{web3-manager-C-JflQ86.cjs → web3-manager-NJaeBrci.cjs} +770 -32
- package/dist/chunks/web3-manager-NJaeBrci.cjs.map +1 -0
- package/dist/core.cjs +1 -1
- package/dist/core.js +1 -1
- package/dist/index.cjs +140 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +82 -2
- package/dist/index.js.map +1 -1
- package/dist/managers/token-manager.d.ts +3 -1
- package/dist/managers/token-manager.d.ts.map +1 -1
- package/dist/managers/web3-manager.d.ts +69 -1
- package/dist/managers/web3-manager.d.ts.map +1 -1
- package/dist/node.cjs +1 -1
- package/dist/node.js +1 -1
- package/dist/package.json +3 -3
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/utils/image-url-utils.d.ts +76 -0
- package/dist/shared/utils/image-url-utils.d.ts.map +1 -0
- package/dist/web3/domain/services/balance-manager.d.ts +160 -0
- package/dist/web3/domain/services/balance-manager.d.ts.map +1 -0
- package/dist/web3/domain/services/index.d.ts +2 -0
- package/dist/web3/domain/services/index.d.ts.map +1 -1
- package/dist/web3/domain/services/token-collection-manager.d.ts +155 -0
- package/dist/web3/domain/services/token-collection-manager.d.ts.map +1 -0
- package/dist/web3/domain/services/token-domain.service.d.ts.map +1 -1
- package/dist/web3/index.d.ts +1 -0
- package/dist/web3/index.d.ts.map +1 -1
- package/dist/web3/infrastructure/api/web3-api.d.ts +16 -0
- package/dist/web3/infrastructure/api/web3-api.d.ts.map +1 -1
- package/dist/web3-manager.cjs +1 -1
- package/dist/web3-manager.js +1 -1
- package/dist/web3.cjs +4 -2
- package/dist/web3.cjs.map +1 -1
- package/dist/web3.js +2 -2
- package/package.json +3 -3
- package/dist/chunks/pers-sdk-CNIfzWYX.js.map +0 -1
- package/dist/chunks/pers-sdk-Cwlrl8jb.cjs.map +0 -1
- package/dist/chunks/web3-manager-C-JflQ86.cjs.map +0 -1
- package/dist/chunks/web3-manager-Dvcq4xmn.js.map +0 -1
|
@@ -76,7 +76,10 @@ class TokenDomainService {
|
|
|
76
76
|
try {
|
|
77
77
|
const abi = ethers.convertAbiToInterface(params.abi);
|
|
78
78
|
const analysis = this.contractService.analyzeContract(abi);
|
|
79
|
-
|
|
79
|
+
// ERC-1155 balanceOfBatch can handle large batches efficiently (1 RPC call)
|
|
80
|
+
// Default to 100 for ERC-1155, 20 for ERC-721 (which has no batch function)
|
|
81
|
+
const defaultBatchSize = analysis.isERC1155 ? 100 : 20;
|
|
82
|
+
const batchSize = Math.min(params.batchSize || defaultBatchSize, analysis.isERC1155 ? 200 : 50);
|
|
80
83
|
// ERC-1155: Requires specific token IDs
|
|
81
84
|
if (analysis.isERC1155) {
|
|
82
85
|
if (!params.tokenIds?.length) {
|
|
@@ -130,31 +133,44 @@ class TokenDomainService {
|
|
|
130
133
|
async processTokenBatch(params, tokenIds, batchSize, tokenStandard) {
|
|
131
134
|
const tokens = [];
|
|
132
135
|
const errors = [];
|
|
136
|
+
const abi = ethers.convertAbiToInterface(params.abi);
|
|
133
137
|
// Process in batches
|
|
134
138
|
for (let i = 0; i < tokenIds.length; i += batchSize) {
|
|
135
139
|
const batch = tokenIds.slice(i, i + batchSize);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
140
|
+
let validTokens;
|
|
141
|
+
if (tokenStandard === 'ERC-721') {
|
|
142
|
+
// For ERC-721: Skip balance check, we know ownership from enumeration or explicit tokenIds
|
|
143
|
+
validTokens = batch.map(tokenId => ({
|
|
144
|
+
tokenId,
|
|
145
|
+
balance: 1,
|
|
146
|
+
hasBalance: true,
|
|
147
|
+
metadata: null
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// For ERC-1155: Use native balanceOfBatch (1 RPC call for entire batch)
|
|
152
|
+
const batchResults = await this.web3Api.getBatchTokenBalances({
|
|
153
|
+
accountAddress: params.accountAddress,
|
|
154
|
+
contractAddress: params.contractAddress,
|
|
155
|
+
abi,
|
|
156
|
+
tokenIds: batch,
|
|
157
|
+
chainId: params.chainId,
|
|
158
|
+
isERC1155: true
|
|
159
|
+
});
|
|
160
|
+
// Convert batch results to TokenBalance format
|
|
161
|
+
validTokens = batchResults
|
|
162
|
+
.filter(r => r.success && r.balance > 0n)
|
|
163
|
+
.map(r => ({
|
|
164
|
+
tokenId: r.tokenId,
|
|
165
|
+
balance: Number(r.balance),
|
|
166
|
+
hasBalance: true,
|
|
167
|
+
metadata: null
|
|
168
|
+
}));
|
|
169
|
+
// Track errors
|
|
170
|
+
batchResults.filter(r => !r.success).forEach(r => {
|
|
171
|
+
errors.push(`${r.tokenId}: ${r.error}`);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
158
174
|
// Step 2: Get metadata for tokens that have balance
|
|
159
175
|
if (validTokens.length > 0) {
|
|
160
176
|
const metadataPromises = validTokens.map(token => this.getTokenMetadata({
|
|
@@ -278,6 +294,627 @@ class ContractDomainService {
|
|
|
278
294
|
}
|
|
279
295
|
}
|
|
280
296
|
|
|
297
|
+
/**
|
|
298
|
+
* TokenCollectionManager - Auto-refresh token collections on wallet events
|
|
299
|
+
*
|
|
300
|
+
* This manager provides:
|
|
301
|
+
* 1. Cached token collection storage with automatic refresh
|
|
302
|
+
* 2. Auto-subscription to wallet events (transfers, mints, burns)
|
|
303
|
+
* 3. Targeted invalidation based on contract address
|
|
304
|
+
* 4. Subscriber pattern for reactive updates
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* ```typescript
|
|
308
|
+
* // Get from SDK
|
|
309
|
+
* const manager = sdk.tokens.collectionManager;
|
|
310
|
+
*
|
|
311
|
+
* // Subscribe to collection updates
|
|
312
|
+
* const unsubscribe = manager.subscribe((collection, contractAddress) => {
|
|
313
|
+
* console.log(`Collection updated for ${contractAddress}`);
|
|
314
|
+
* updateUI(collection);
|
|
315
|
+
* });
|
|
316
|
+
*
|
|
317
|
+
* // Fetch collection (auto-cached, auto-refreshed on wallet events)
|
|
318
|
+
* const collection = await manager.getCollection({
|
|
319
|
+
* accountAddress: '0x...',
|
|
320
|
+
* contractAddress: '0x...',
|
|
321
|
+
* abi: contractAbi,
|
|
322
|
+
* chainId: 39123,
|
|
323
|
+
* tokenIds: ['1', '2', '3']
|
|
324
|
+
* });
|
|
325
|
+
*
|
|
326
|
+
* // Cleanup
|
|
327
|
+
* unsubscribe();
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
/**
|
|
331
|
+
* Manages token collections with automatic refresh on wallet events
|
|
332
|
+
*/
|
|
333
|
+
class TokenCollectionManager {
|
|
334
|
+
constructor(tokenService, eventEmitter, config) {
|
|
335
|
+
this.tokenService = tokenService;
|
|
336
|
+
this.eventEmitter = eventEmitter;
|
|
337
|
+
this.collections = new Map();
|
|
338
|
+
this.handlers = new Set();
|
|
339
|
+
this.pendingRefresh = new Map();
|
|
340
|
+
this.config = {
|
|
341
|
+
autoRefreshOnWalletEvents: config?.autoRefreshOnWalletEvents ?? true,
|
|
342
|
+
eventDebounceMs: config?.eventDebounceMs ?? 500,
|
|
343
|
+
debug: config?.debug ?? false
|
|
344
|
+
};
|
|
345
|
+
if (this.config.autoRefreshOnWalletEvents && eventEmitter) {
|
|
346
|
+
this.setupEventSubscription();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Subscribe to wallet events for auto-refresh
|
|
351
|
+
*/
|
|
352
|
+
setupEventSubscription() {
|
|
353
|
+
if (!this.eventEmitter) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
this.eventUnsubscribe = this.eventEmitter.subscribe((event) => {
|
|
357
|
+
const contractAddress = event.details?.contractAddress;
|
|
358
|
+
this.handleWalletEvent(contractAddress, event.type);
|
|
359
|
+
}, { domains: ['wallet'] });
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Get a token collection (cached, auto-refreshed on wallet events)
|
|
363
|
+
*
|
|
364
|
+
* @param params - Collection parameters
|
|
365
|
+
* @returns Promise resolving to the token collection
|
|
366
|
+
*/
|
|
367
|
+
async getCollection(params) {
|
|
368
|
+
const key = this.getCacheKey(params);
|
|
369
|
+
const cached = this.collections.get(key);
|
|
370
|
+
// Return cached if available and not stale
|
|
371
|
+
if (cached?.lastCollection) {
|
|
372
|
+
this.log(`Cache HIT for ${key}`);
|
|
373
|
+
return cached.lastCollection;
|
|
374
|
+
}
|
|
375
|
+
this.log(`Cache MISS for ${key}, fetching...`);
|
|
376
|
+
return this.fetchAndCache(params);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Force refresh a specific collection
|
|
380
|
+
*
|
|
381
|
+
* @param contractAddress - Contract address to refresh
|
|
382
|
+
* @param accountAddress - Account address to refresh
|
|
383
|
+
* @returns Promise that resolves when refresh is complete
|
|
384
|
+
*/
|
|
385
|
+
async refreshCollection(contractAddress, accountAddress) {
|
|
386
|
+
const toRefresh = [];
|
|
387
|
+
for (const [key, entry] of this.collections) {
|
|
388
|
+
if (entry.params.contractAddress === contractAddress) {
|
|
389
|
+
if (!accountAddress || entry.params.accountAddress === accountAddress) {
|
|
390
|
+
toRefresh.push(key);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
await Promise.all(toRefresh.map(key => {
|
|
395
|
+
const entry = this.collections.get(key);
|
|
396
|
+
if (entry) {
|
|
397
|
+
return this.refreshEntry(key, entry, 'refresh');
|
|
398
|
+
}
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Subscribe to collection change events
|
|
403
|
+
*
|
|
404
|
+
* @param handler - Callback when any tracked collection changes
|
|
405
|
+
* @returns Unsubscribe function
|
|
406
|
+
*/
|
|
407
|
+
subscribe(handler) {
|
|
408
|
+
this.handlers.add(handler);
|
|
409
|
+
return () => this.handlers.delete(handler);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Get all currently tracked collections
|
|
413
|
+
*/
|
|
414
|
+
getTrackedCollections() {
|
|
415
|
+
return Array.from(this.collections.entries()).map(([_, entry]) => ({
|
|
416
|
+
contractAddress: entry.params.contractAddress,
|
|
417
|
+
accountAddress: entry.params.accountAddress,
|
|
418
|
+
collection: entry.lastCollection
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Check if a collection is currently being tracked
|
|
423
|
+
*/
|
|
424
|
+
isTracking(contractAddress, accountAddress) {
|
|
425
|
+
const key = `${contractAddress}:${accountAddress}`;
|
|
426
|
+
return this.collections.has(key);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Clear tracking for a specific collection
|
|
430
|
+
*/
|
|
431
|
+
untrack(contractAddress, accountAddress) {
|
|
432
|
+
if (accountAddress) {
|
|
433
|
+
const key = `${contractAddress}:${accountAddress}`;
|
|
434
|
+
this.collections.delete(key);
|
|
435
|
+
this.log(`Untracked ${key}`);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
// Untrack all for this contract
|
|
439
|
+
for (const key of this.collections.keys()) {
|
|
440
|
+
if (key.startsWith(`${contractAddress}:`)) {
|
|
441
|
+
this.collections.delete(key);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
this.log(`Untracked all collections for ${contractAddress}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Clear all tracked collections
|
|
449
|
+
*/
|
|
450
|
+
clearAll() {
|
|
451
|
+
this.collections.clear();
|
|
452
|
+
this.log('Cleared all tracked collections');
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Handle wallet events (with debouncing)
|
|
456
|
+
*/
|
|
457
|
+
handleWalletEvent(contractAddress, eventType) {
|
|
458
|
+
// Determine which collections to refresh
|
|
459
|
+
const keysToRefresh = [];
|
|
460
|
+
for (const [key, entry] of this.collections) {
|
|
461
|
+
// If no specific contract, refresh all; otherwise match contract
|
|
462
|
+
if (!contractAddress || entry.params.contractAddress.toLowerCase() === contractAddress.toLowerCase()) {
|
|
463
|
+
keysToRefresh.push(key);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (keysToRefresh.length === 0) {
|
|
467
|
+
this.log(`No tracked collections affected by event for ${contractAddress || 'all contracts'}`);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
// Debounce refresh per key
|
|
471
|
+
for (const key of keysToRefresh) {
|
|
472
|
+
const existing = this.pendingRefresh.get(key);
|
|
473
|
+
if (existing) {
|
|
474
|
+
clearTimeout(existing);
|
|
475
|
+
}
|
|
476
|
+
const timeout = setTimeout(async () => {
|
|
477
|
+
this.pendingRefresh.delete(key);
|
|
478
|
+
const entry = this.collections.get(key);
|
|
479
|
+
if (entry) {
|
|
480
|
+
this.log(`Refreshing ${key} due to wallet event: ${eventType}`);
|
|
481
|
+
await this.refreshEntry(key, entry, eventType);
|
|
482
|
+
}
|
|
483
|
+
}, this.config.eventDebounceMs);
|
|
484
|
+
this.pendingRefresh.set(key, timeout);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Fetch and cache a collection
|
|
489
|
+
*/
|
|
490
|
+
async fetchAndCache(params) {
|
|
491
|
+
const key = this.getCacheKey(params);
|
|
492
|
+
// Mark as refreshing to prevent duplicate fetches
|
|
493
|
+
const existing = this.collections.get(key);
|
|
494
|
+
if (existing?.refreshing) {
|
|
495
|
+
// Wait for existing refresh to complete
|
|
496
|
+
return existing.lastCollection || this.emptyCollection(params);
|
|
497
|
+
}
|
|
498
|
+
this.collections.set(key, {
|
|
499
|
+
params,
|
|
500
|
+
lastCollection: null,
|
|
501
|
+
timestamp: Date.now(),
|
|
502
|
+
refreshing: true
|
|
503
|
+
});
|
|
504
|
+
try {
|
|
505
|
+
const collection = await this.tokenService.getTokenCollection(params);
|
|
506
|
+
this.collections.set(key, {
|
|
507
|
+
params,
|
|
508
|
+
lastCollection: collection,
|
|
509
|
+
timestamp: Date.now(),
|
|
510
|
+
refreshing: false
|
|
511
|
+
});
|
|
512
|
+
this.log(`Cached collection for ${key}: ${collection.tokensRetrieved} tokens`);
|
|
513
|
+
return collection;
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
// Remove failed entry
|
|
517
|
+
this.collections.delete(key);
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Refresh a cached entry and notify handlers
|
|
523
|
+
*/
|
|
524
|
+
async refreshEntry(key, entry, source) {
|
|
525
|
+
if (entry.refreshing) {
|
|
526
|
+
this.log(`Skipping refresh for ${key} - already refreshing`);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
entry.refreshing = true;
|
|
530
|
+
try {
|
|
531
|
+
const newCollection = await this.tokenService.getTokenCollection(entry.params);
|
|
532
|
+
entry.lastCollection = newCollection;
|
|
533
|
+
entry.timestamp = Date.now();
|
|
534
|
+
entry.refreshing = false;
|
|
535
|
+
this.log(`Refreshed ${key}: ${newCollection.tokensRetrieved} tokens`);
|
|
536
|
+
// Notify subscribers
|
|
537
|
+
for (const handler of this.handlers) {
|
|
538
|
+
try {
|
|
539
|
+
handler(newCollection, entry.params.contractAddress, {
|
|
540
|
+
type: source === 'refresh' ? 'refresh' : 'wallet_event',
|
|
541
|
+
source
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
catch (handlerError) {
|
|
545
|
+
console.error('[TokenCollectionManager] Handler error:', handlerError);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
entry.refreshing = false;
|
|
551
|
+
console.error(`[TokenCollectionManager] Refresh failed for ${key}:`, error);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Generate cache key from params
|
|
556
|
+
*/
|
|
557
|
+
getCacheKey(params) {
|
|
558
|
+
return `${params.contractAddress}:${params.accountAddress}`;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Create empty collection placeholder
|
|
562
|
+
*/
|
|
563
|
+
emptyCollection(params) {
|
|
564
|
+
return {
|
|
565
|
+
accountAddress: params.accountAddress,
|
|
566
|
+
contractAddress: params.contractAddress,
|
|
567
|
+
totalBalance: 0,
|
|
568
|
+
tokensRetrieved: 0,
|
|
569
|
+
tokens: [],
|
|
570
|
+
note: 'Loading...'
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Debug logging
|
|
575
|
+
*/
|
|
576
|
+
log(message, data) {
|
|
577
|
+
if (this.config.debug) {
|
|
578
|
+
console.log(`[TokenCollectionManager] ${message}`, data || '');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Cleanup resources
|
|
583
|
+
*/
|
|
584
|
+
destroy() {
|
|
585
|
+
// Clear pending refreshes
|
|
586
|
+
for (const timeout of this.pendingRefresh.values()) {
|
|
587
|
+
clearTimeout(timeout);
|
|
588
|
+
}
|
|
589
|
+
this.pendingRefresh.clear();
|
|
590
|
+
// Unsubscribe from events
|
|
591
|
+
this.eventUnsubscribe?.();
|
|
592
|
+
// Clear handlers and collections
|
|
593
|
+
this.handlers.clear();
|
|
594
|
+
this.collections.clear();
|
|
595
|
+
this.log('Destroyed');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* BalanceManager - Auto-refresh ERC-20 token balances on wallet events
|
|
601
|
+
*
|
|
602
|
+
* This manager provides:
|
|
603
|
+
* 1. Cached balance storage with automatic refresh
|
|
604
|
+
* 2. Auto-subscription to wallet events (transfers)
|
|
605
|
+
* 3. Targeted invalidation based on contract address
|
|
606
|
+
* 4. Subscriber pattern for reactive updates
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* ```typescript
|
|
610
|
+
* // Get from SDK
|
|
611
|
+
* const manager = sdk.tokens.balanceManager;
|
|
612
|
+
*
|
|
613
|
+
* // Subscribe to balance updates
|
|
614
|
+
* const unsubscribe = manager.subscribe((balance, contractAddress) => {
|
|
615
|
+
* console.log(`Balance updated for ${contractAddress}: ${balance}`);
|
|
616
|
+
* updateUI(balance);
|
|
617
|
+
* });
|
|
618
|
+
*
|
|
619
|
+
* // Fetch balance (auto-cached, auto-refreshed on wallet events)
|
|
620
|
+
* const balance = await manager.getBalance({
|
|
621
|
+
* accountAddress: '0x...',
|
|
622
|
+
* contractAddress: '0x...',
|
|
623
|
+
* abi: contractAbi,
|
|
624
|
+
* chainId: 39123
|
|
625
|
+
* });
|
|
626
|
+
*
|
|
627
|
+
* // Manual refresh
|
|
628
|
+
* await manager.refreshBalance(contractAddress);
|
|
629
|
+
*
|
|
630
|
+
* // Cleanup
|
|
631
|
+
* unsubscribe();
|
|
632
|
+
* ```
|
|
633
|
+
*/
|
|
634
|
+
/**
|
|
635
|
+
* Manages ERC-20 token balances with automatic refresh on wallet events
|
|
636
|
+
*/
|
|
637
|
+
class BalanceManager {
|
|
638
|
+
constructor(tokenService, eventEmitter, config) {
|
|
639
|
+
this.tokenService = tokenService;
|
|
640
|
+
this.eventEmitter = eventEmitter;
|
|
641
|
+
this.balances = new Map();
|
|
642
|
+
this.handlers = new Set();
|
|
643
|
+
this.pendingRefresh = new Map();
|
|
644
|
+
this.config = {
|
|
645
|
+
autoRefreshOnWalletEvents: config?.autoRefreshOnWalletEvents ?? true,
|
|
646
|
+
eventDebounceMs: config?.eventDebounceMs ?? 500,
|
|
647
|
+
debug: config?.debug ?? false
|
|
648
|
+
};
|
|
649
|
+
if (this.config.autoRefreshOnWalletEvents && eventEmitter) {
|
|
650
|
+
this.setupEventSubscription();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Subscribe to wallet events for auto-refresh
|
|
655
|
+
*/
|
|
656
|
+
setupEventSubscription() {
|
|
657
|
+
if (!this.eventEmitter) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
this.eventUnsubscribe = this.eventEmitter.subscribe((event) => {
|
|
661
|
+
const contractAddress = event.details?.contractAddress;
|
|
662
|
+
this.handleWalletEvent(contractAddress, event.type);
|
|
663
|
+
}, { domains: ['wallet'] });
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Get a token balance (cached, auto-refreshed on wallet events)
|
|
667
|
+
*
|
|
668
|
+
* @param params - Balance parameters
|
|
669
|
+
* @returns Promise resolving to the token balance
|
|
670
|
+
*/
|
|
671
|
+
async getBalance(params) {
|
|
672
|
+
const key = this.getCacheKey(params);
|
|
673
|
+
const cached = this.balances.get(key);
|
|
674
|
+
// Return cached if available and not stale
|
|
675
|
+
if (cached?.lastBalance) {
|
|
676
|
+
this.log(`Cache HIT for ${key}`);
|
|
677
|
+
return cached.lastBalance;
|
|
678
|
+
}
|
|
679
|
+
this.log(`Cache MISS for ${key}, fetching...`);
|
|
680
|
+
return this.fetchAndCache(params);
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Force refresh a specific balance
|
|
684
|
+
*
|
|
685
|
+
* @param contractAddress - Contract address to refresh
|
|
686
|
+
* @param accountAddress - Account address to refresh (optional, refreshes all if not provided)
|
|
687
|
+
* @returns Promise that resolves when refresh is complete
|
|
688
|
+
*/
|
|
689
|
+
async refreshBalance(contractAddress, accountAddress) {
|
|
690
|
+
const toRefresh = [];
|
|
691
|
+
for (const [key, entry] of this.balances) {
|
|
692
|
+
if (entry.params.contractAddress.toLowerCase() === contractAddress.toLowerCase()) {
|
|
693
|
+
if (!accountAddress || entry.params.accountAddress.toLowerCase() === accountAddress.toLowerCase()) {
|
|
694
|
+
toRefresh.push(key);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
await Promise.all(toRefresh.map(key => {
|
|
699
|
+
const entry = this.balances.get(key);
|
|
700
|
+
if (entry) {
|
|
701
|
+
return this.refreshEntry(key, entry, 'refresh');
|
|
702
|
+
}
|
|
703
|
+
}));
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Force refresh all tracked balances
|
|
707
|
+
*
|
|
708
|
+
* @returns Promise that resolves when all refreshes are complete
|
|
709
|
+
*/
|
|
710
|
+
async refreshAll() {
|
|
711
|
+
const refreshPromises = Array.from(this.balances.entries()).map(([key, entry]) => this.refreshEntry(key, entry, 'refresh'));
|
|
712
|
+
await Promise.all(refreshPromises);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Subscribe to balance change events
|
|
716
|
+
*
|
|
717
|
+
* @param handler - Callback when any tracked balance changes
|
|
718
|
+
* @returns Unsubscribe function
|
|
719
|
+
*/
|
|
720
|
+
subscribe(handler) {
|
|
721
|
+
this.handlers.add(handler);
|
|
722
|
+
return () => this.handlers.delete(handler);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Get all currently tracked balances
|
|
726
|
+
*/
|
|
727
|
+
getTrackedBalances() {
|
|
728
|
+
return Array.from(this.balances.entries()).map(([_, entry]) => ({
|
|
729
|
+
contractAddress: entry.params.contractAddress,
|
|
730
|
+
accountAddress: entry.params.accountAddress,
|
|
731
|
+
balance: entry.lastBalance
|
|
732
|
+
}));
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Check if a balance is currently being tracked
|
|
736
|
+
*/
|
|
737
|
+
isTracking(contractAddress, accountAddress) {
|
|
738
|
+
const key = `${contractAddress.toLowerCase()}:${accountAddress.toLowerCase()}`;
|
|
739
|
+
return this.balances.has(key);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Clear tracking for a specific balance
|
|
743
|
+
*/
|
|
744
|
+
untrack(contractAddress, accountAddress) {
|
|
745
|
+
if (accountAddress) {
|
|
746
|
+
const key = `${contractAddress.toLowerCase()}:${accountAddress.toLowerCase()}`;
|
|
747
|
+
this.balances.delete(key);
|
|
748
|
+
this.log(`Untracked ${key}`);
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
// Untrack all for this contract
|
|
752
|
+
const toDelete = [];
|
|
753
|
+
for (const key of this.balances.keys()) {
|
|
754
|
+
if (key.startsWith(`${contractAddress.toLowerCase()}:`)) {
|
|
755
|
+
toDelete.push(key);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
toDelete.forEach(key => this.balances.delete(key));
|
|
759
|
+
this.log(`Untracked all balances for ${contractAddress}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Clear all tracked balances
|
|
764
|
+
*/
|
|
765
|
+
clearAll() {
|
|
766
|
+
this.balances.clear();
|
|
767
|
+
this.log('Cleared all tracked balances');
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Handle wallet events (with debouncing)
|
|
771
|
+
*/
|
|
772
|
+
handleWalletEvent(contractAddress, eventType) {
|
|
773
|
+
// Determine which balances to refresh
|
|
774
|
+
const keysToRefresh = [];
|
|
775
|
+
for (const [key, entry] of this.balances) {
|
|
776
|
+
// If no specific contract, refresh all; otherwise match contract
|
|
777
|
+
if (!contractAddress || entry.params.contractAddress.toLowerCase() === contractAddress.toLowerCase()) {
|
|
778
|
+
keysToRefresh.push(key);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (keysToRefresh.length === 0) {
|
|
782
|
+
this.log(`No tracked balances affected by event for ${contractAddress || 'all contracts'}`);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
// Debounce refresh per key
|
|
786
|
+
for (const key of keysToRefresh) {
|
|
787
|
+
const existing = this.pendingRefresh.get(key);
|
|
788
|
+
if (existing) {
|
|
789
|
+
clearTimeout(existing);
|
|
790
|
+
}
|
|
791
|
+
const timeout = setTimeout(async () => {
|
|
792
|
+
this.pendingRefresh.delete(key);
|
|
793
|
+
const entry = this.balances.get(key);
|
|
794
|
+
if (entry) {
|
|
795
|
+
this.log(`Refreshing ${key} due to wallet event: ${eventType}`);
|
|
796
|
+
await this.refreshEntry(key, entry, eventType);
|
|
797
|
+
}
|
|
798
|
+
}, this.config.eventDebounceMs);
|
|
799
|
+
this.pendingRefresh.set(key, timeout);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Fetch and cache a balance
|
|
804
|
+
*/
|
|
805
|
+
async fetchAndCache(params) {
|
|
806
|
+
const key = this.getCacheKey(params);
|
|
807
|
+
// Mark as refreshing to prevent duplicate fetches
|
|
808
|
+
const existing = this.balances.get(key);
|
|
809
|
+
if (existing?.refreshing) {
|
|
810
|
+
// Wait for existing refresh to complete
|
|
811
|
+
return existing.lastBalance || this.emptyBalance();
|
|
812
|
+
}
|
|
813
|
+
this.balances.set(key, {
|
|
814
|
+
params,
|
|
815
|
+
lastBalance: null,
|
|
816
|
+
timestamp: Date.now(),
|
|
817
|
+
refreshing: true
|
|
818
|
+
});
|
|
819
|
+
try {
|
|
820
|
+
const balance = await this.tokenService.getTokenBalance({
|
|
821
|
+
...params,
|
|
822
|
+
tokenId: null // ERC-20 has no tokenId
|
|
823
|
+
});
|
|
824
|
+
this.balances.set(key, {
|
|
825
|
+
params,
|
|
826
|
+
lastBalance: balance,
|
|
827
|
+
timestamp: Date.now(),
|
|
828
|
+
refreshing: false
|
|
829
|
+
});
|
|
830
|
+
this.log(`Cached balance for ${key}: ${balance.balance}`);
|
|
831
|
+
return balance;
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
// Remove failed entry
|
|
835
|
+
this.balances.delete(key);
|
|
836
|
+
throw error;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Refresh a cached entry and notify handlers
|
|
841
|
+
*/
|
|
842
|
+
async refreshEntry(key, entry, source) {
|
|
843
|
+
if (entry.refreshing) {
|
|
844
|
+
this.log(`Skipping refresh for ${key} - already refreshing`);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
entry.refreshing = true;
|
|
848
|
+
try {
|
|
849
|
+
const newBalance = await this.tokenService.getTokenBalance({
|
|
850
|
+
...entry.params,
|
|
851
|
+
tokenId: null // ERC-20 has no tokenId
|
|
852
|
+
});
|
|
853
|
+
entry.lastBalance = newBalance;
|
|
854
|
+
entry.timestamp = Date.now();
|
|
855
|
+
entry.refreshing = false;
|
|
856
|
+
this.log(`Refreshed ${key}: ${newBalance.balance}`);
|
|
857
|
+
// Notify subscribers
|
|
858
|
+
for (const handler of this.handlers) {
|
|
859
|
+
try {
|
|
860
|
+
handler(newBalance, entry.params.contractAddress, {
|
|
861
|
+
type: source === 'refresh' ? 'refresh' : 'wallet_event',
|
|
862
|
+
source
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
catch (handlerError) {
|
|
866
|
+
console.error('[BalanceManager] Handler error:', handlerError);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
catch (error) {
|
|
871
|
+
entry.refreshing = false;
|
|
872
|
+
console.error(`[BalanceManager] Refresh failed for ${key}:`, error);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Generate cache key from params
|
|
877
|
+
*/
|
|
878
|
+
getCacheKey(params) {
|
|
879
|
+
return `${params.contractAddress.toLowerCase()}:${params.accountAddress.toLowerCase()}`;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Create empty balance placeholder
|
|
883
|
+
*/
|
|
884
|
+
emptyBalance() {
|
|
885
|
+
return {
|
|
886
|
+
tokenId: null,
|
|
887
|
+
balance: 0,
|
|
888
|
+
hasBalance: false,
|
|
889
|
+
metadata: null
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Debug logging
|
|
894
|
+
*/
|
|
895
|
+
log(message, data) {
|
|
896
|
+
if (this.config.debug) {
|
|
897
|
+
console.log(`[BalanceManager] ${message}`, data || '');
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Cleanup resources
|
|
902
|
+
*/
|
|
903
|
+
destroy() {
|
|
904
|
+
// Clear pending refreshes
|
|
905
|
+
for (const timeout of this.pendingRefresh.values()) {
|
|
906
|
+
clearTimeout(timeout);
|
|
907
|
+
}
|
|
908
|
+
this.pendingRefresh.clear();
|
|
909
|
+
// Unsubscribe from events
|
|
910
|
+
this.eventUnsubscribe?.();
|
|
911
|
+
// Clear handlers and balances
|
|
912
|
+
this.handlers.clear();
|
|
913
|
+
this.balances.clear();
|
|
914
|
+
this.log('Destroyed');
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
281
918
|
/**
|
|
282
919
|
* Web3ApplicationService - Application layer entrance point
|
|
283
920
|
* Orchestrates domain services and provides clean public interface
|
|
@@ -386,6 +1023,30 @@ class Web3InfrastructureApi {
|
|
|
386
1023
|
return 0;
|
|
387
1024
|
}
|
|
388
1025
|
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Get multiple ERC-1155 token balances in a single RPC call using native balanceOfBatch.
|
|
1028
|
+
* Falls back to individual calls if balanceOfBatch fails.
|
|
1029
|
+
*
|
|
1030
|
+
* @param request - Batch balance request
|
|
1031
|
+
* @returns Array of balance results per tokenId
|
|
1032
|
+
*/
|
|
1033
|
+
async getBatchTokenBalances(request) {
|
|
1034
|
+
try {
|
|
1035
|
+
const ethersProvider = await this.web3ChainService.getEthersProviderByChainId(request.chainId);
|
|
1036
|
+
const results = await ethers.getBatchBalancesWithFallback(ethersProvider, request.contractAddress, request.abi, request.accountAddress, request.tokenIds, request.isERC1155 ?? true);
|
|
1037
|
+
return results;
|
|
1038
|
+
}
|
|
1039
|
+
catch (error) {
|
|
1040
|
+
console.error(`Failed to get batch token balances:`, error);
|
|
1041
|
+
// Return empty results on failure
|
|
1042
|
+
return request.tokenIds.map(tokenId => ({
|
|
1043
|
+
tokenId,
|
|
1044
|
+
balance: 0n,
|
|
1045
|
+
success: false,
|
|
1046
|
+
error: `${error}`
|
|
1047
|
+
}));
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
389
1050
|
async getTokenUri(request) {
|
|
390
1051
|
try {
|
|
391
1052
|
const ethersProvider = await this.web3ChainService.getEthersProviderByChainId(request.chainId);
|
|
@@ -623,17 +1284,11 @@ async function getExplorerUrlByChainId(getChainData, chainId, address, type) {
|
|
|
623
1284
|
* ```
|
|
624
1285
|
*/
|
|
625
1286
|
class Web3Manager {
|
|
626
|
-
|
|
627
|
-
// When ready, add:
|
|
628
|
-
// 1. constructor param: private events?: PersEventEmitter
|
|
629
|
-
// 2. Subscribe to contract events (Transfer, Approval, etc.)
|
|
630
|
-
// 3. Emit via: this.events?.emitSuccess({ domain: 'web3', type: 'NFT_TRANSFERRED', ... })
|
|
631
|
-
// 4. Filter to only user's address (not all transfers)
|
|
632
|
-
// 5. Add cleanup for event listeners on SDK destroy
|
|
633
|
-
constructor(apiClient, tenantManager$1) {
|
|
1287
|
+
constructor(apiClient, tenantManager$1, eventEmitter) {
|
|
634
1288
|
this.apiClient = apiClient;
|
|
635
1289
|
// Use provided TenantManager or create one
|
|
636
1290
|
this.tenantManager = tenantManager$1 || new tenantManager.TenantManager(apiClient);
|
|
1291
|
+
this.eventEmitter = eventEmitter;
|
|
637
1292
|
// Initialize Web3 Chain service
|
|
638
1293
|
const web3ChainApi = new web3ChainService.Web3ChainApi(apiClient);
|
|
639
1294
|
this.web3ChainService = new web3ChainService.Web3ChainService(web3ChainApi);
|
|
@@ -641,6 +1296,75 @@ class Web3Manager {
|
|
|
641
1296
|
const web3InfrastructureApi = new Web3InfrastructureApi(this.web3ChainService);
|
|
642
1297
|
const ipfsInfrastructureApi = new IPFSInfrastructureApi(this.tenantManager);
|
|
643
1298
|
this.web3ApplicationService = new Web3ApplicationService(web3InfrastructureApi, ipfsInfrastructureApi);
|
|
1299
|
+
// Initialize domain services for managers
|
|
1300
|
+
const contractDomainService = new ContractDomainService();
|
|
1301
|
+
const metadataDomainService = new MetadataDomainService(ipfsInfrastructureApi);
|
|
1302
|
+
this.tokenDomainService = new TokenDomainService(web3InfrastructureApi, metadataDomainService, contractDomainService);
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Get the TokenCollectionManager for NFT collection operations.
|
|
1306
|
+
*
|
|
1307
|
+
* Features:
|
|
1308
|
+
* - Auto-refresh on wallet events (transfers, mints, burns)
|
|
1309
|
+
* - Cached collections
|
|
1310
|
+
* - Subscriber pattern for reactive updates
|
|
1311
|
+
*
|
|
1312
|
+
* @example
|
|
1313
|
+
* ```typescript
|
|
1314
|
+
* const manager = sdk.web3.collectionManager;
|
|
1315
|
+
*
|
|
1316
|
+
* // Subscribe to updates
|
|
1317
|
+
* const unsubscribe = manager.subscribe((collection, contractAddress) => {
|
|
1318
|
+
* console.log(`Collection updated for ${contractAddress}`);
|
|
1319
|
+
* });
|
|
1320
|
+
*
|
|
1321
|
+
* // Fetch collection (auto-cached, auto-refreshed)
|
|
1322
|
+
* const collection = await manager.getCollection({...});
|
|
1323
|
+
*
|
|
1324
|
+
* // Manual refresh
|
|
1325
|
+
* await manager.refreshCollection(contractAddress);
|
|
1326
|
+
* ```
|
|
1327
|
+
*/
|
|
1328
|
+
get collectionManager() {
|
|
1329
|
+
if (!this._collectionManager) {
|
|
1330
|
+
this._collectionManager = new TokenCollectionManager(this.tokenDomainService, this.eventEmitter);
|
|
1331
|
+
}
|
|
1332
|
+
return this._collectionManager;
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Get the BalanceManager for ERC-20 (credit token) balance operations.
|
|
1336
|
+
*
|
|
1337
|
+
* Features:
|
|
1338
|
+
* - Auto-refresh on wallet events (transfers)
|
|
1339
|
+
* - Cached balances
|
|
1340
|
+
* - Subscriber pattern for reactive updates
|
|
1341
|
+
*
|
|
1342
|
+
* @example
|
|
1343
|
+
* ```typescript
|
|
1344
|
+
* const manager = sdk.web3.balanceManager;
|
|
1345
|
+
*
|
|
1346
|
+
* // Subscribe to balance updates
|
|
1347
|
+
* const unsubscribe = manager.subscribe((balance, contractAddress) => {
|
|
1348
|
+
* console.log(`Balance updated: ${balance.balance}`);
|
|
1349
|
+
* });
|
|
1350
|
+
*
|
|
1351
|
+
* // Fetch balance (auto-cached, auto-refreshed)
|
|
1352
|
+
* const balance = await manager.getBalance({
|
|
1353
|
+
* accountAddress: '0x...',
|
|
1354
|
+
* contractAddress: '0x...',
|
|
1355
|
+
* abi: creditTokenAbi,
|
|
1356
|
+
* chainId: 39123
|
|
1357
|
+
* });
|
|
1358
|
+
*
|
|
1359
|
+
* // Manual refresh
|
|
1360
|
+
* await manager.refreshBalance(contractAddress);
|
|
1361
|
+
* ```
|
|
1362
|
+
*/
|
|
1363
|
+
get balanceManager() {
|
|
1364
|
+
if (!this._balanceManager) {
|
|
1365
|
+
this._balanceManager = new BalanceManager(this.tokenDomainService, this.eventEmitter);
|
|
1366
|
+
}
|
|
1367
|
+
return this._balanceManager;
|
|
644
1368
|
}
|
|
645
1369
|
/**
|
|
646
1370
|
* Get token balance for a specific token
|
|
@@ -839,12 +1563,26 @@ class Web3Manager {
|
|
|
839
1563
|
maxTokens
|
|
840
1564
|
};
|
|
841
1565
|
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Cleanup resources.
|
|
1568
|
+
*
|
|
1569
|
+
* Call this when the SDK is being destroyed to clean up
|
|
1570
|
+
* event subscriptions and cached data.
|
|
1571
|
+
*/
|
|
1572
|
+
destroy() {
|
|
1573
|
+
this._collectionManager?.destroy();
|
|
1574
|
+
this._balanceManager?.destroy();
|
|
1575
|
+
this._collectionManager = undefined;
|
|
1576
|
+
this._balanceManager = undefined;
|
|
1577
|
+
}
|
|
842
1578
|
}
|
|
843
1579
|
|
|
1580
|
+
exports.BalanceManager = BalanceManager;
|
|
844
1581
|
exports.IPFSInfrastructureApi = IPFSInfrastructureApi;
|
|
1582
|
+
exports.TokenCollectionManager = TokenCollectionManager;
|
|
845
1583
|
exports.Web3ApplicationService = Web3ApplicationService;
|
|
846
1584
|
exports.Web3InfrastructureApi = Web3InfrastructureApi;
|
|
847
1585
|
exports.Web3Manager = Web3Manager;
|
|
848
1586
|
exports.getExplorerUrl = getExplorerUrl;
|
|
849
1587
|
exports.getExplorerUrlByChainId = getExplorerUrlByChainId;
|
|
850
|
-
//# sourceMappingURL=web3-manager-
|
|
1588
|
+
//# sourceMappingURL=web3-manager-NJaeBrci.cjs.map
|