@explorins/pers-sdk 2.1.37 → 2.1.40

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