@veridex/agentic-payments 0.1.1-beta.1

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 (73) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/MIGRATION.md +307 -0
  3. package/README.md +395 -0
  4. package/dist/index.d.mts +2327 -0
  5. package/dist/index.d.ts +2327 -0
  6. package/dist/index.js +5815 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/index.mjs +5759 -0
  9. package/dist/index.mjs.map +1 -0
  10. package/examples/basic-agent.ts +126 -0
  11. package/examples/mcp-claude.ts +75 -0
  12. package/examples/ucp-checkout.ts +92 -0
  13. package/examples/x402-integration.ts +75 -0
  14. package/package.json +36 -0
  15. package/src/AgentWallet.ts +432 -0
  16. package/src/chains/AptosChainClient.ts +29 -0
  17. package/src/chains/ChainClient.ts +73 -0
  18. package/src/chains/ChainClientFactory.ts +113 -0
  19. package/src/chains/EVMChainClient.ts +39 -0
  20. package/src/chains/SolanaChainClient.ts +37 -0
  21. package/src/chains/StarknetChainClient.ts +36 -0
  22. package/src/chains/SuiChainClient.ts +28 -0
  23. package/src/index.ts +83 -0
  24. package/src/mcp/MCPServer.ts +73 -0
  25. package/src/mcp/schemas.ts +60 -0
  26. package/src/monitoring/AlertManager.ts +258 -0
  27. package/src/monitoring/AuditLogger.ts +86 -0
  28. package/src/monitoring/BalanceCache.ts +44 -0
  29. package/src/monitoring/ComplianceExporter.ts +52 -0
  30. package/src/oracle/PythFeeds.ts +60 -0
  31. package/src/oracle/PythOracle.ts +121 -0
  32. package/src/performance/ConnectionPool.ts +217 -0
  33. package/src/performance/NonceManager.ts +91 -0
  34. package/src/performance/ParallelRouteFinder.ts +438 -0
  35. package/src/performance/TransactionPoller.ts +201 -0
  36. package/src/performance/TransactionQueue.ts +565 -0
  37. package/src/performance/index.ts +46 -0
  38. package/src/react/hooks.ts +298 -0
  39. package/src/routing/BridgeOrchestrator.ts +18 -0
  40. package/src/routing/CrossChainRouter.ts +501 -0
  41. package/src/routing/DEXAggregator.ts +448 -0
  42. package/src/routing/FeeEstimator.ts +43 -0
  43. package/src/session/SessionKeyManager.ts +312 -0
  44. package/src/session/SessionStorage.ts +80 -0
  45. package/src/session/SpendingTracker.ts +71 -0
  46. package/src/types/agent.ts +105 -0
  47. package/src/types/errors.ts +115 -0
  48. package/src/types/mcp.ts +22 -0
  49. package/src/types/ucp.ts +47 -0
  50. package/src/types/x402.ts +170 -0
  51. package/src/ucp/CapabilityNegotiator.ts +44 -0
  52. package/src/ucp/CredentialProvider.ts +73 -0
  53. package/src/ucp/PaymentTokenizer.ts +169 -0
  54. package/src/ucp/TransportAdapter.ts +18 -0
  55. package/src/ucp/UCPClient.ts +143 -0
  56. package/src/x402/NonceManager.ts +26 -0
  57. package/src/x402/PaymentParser.ts +225 -0
  58. package/src/x402/PaymentSigner.ts +305 -0
  59. package/src/x402/X402Client.ts +364 -0
  60. package/src/x402/adapters/CronosFacilitatorAdapter.ts +109 -0
  61. package/tests/alerts.test.ts +208 -0
  62. package/tests/chains.test.ts +242 -0
  63. package/tests/integration.test.ts +315 -0
  64. package/tests/monitoring.test.ts +435 -0
  65. package/tests/performance.test.ts +303 -0
  66. package/tests/property.test.ts +186 -0
  67. package/tests/react-hooks.test.ts +262 -0
  68. package/tests/session.test.ts +376 -0
  69. package/tests/ucp.test.ts +253 -0
  70. package/tests/x402.test.ts +385 -0
  71. package/tsconfig.json +26 -0
  72. package/tsup.config.ts +10 -0
  73. package/vitest.config.ts +16 -0
@@ -0,0 +1,438 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * @module ParallelRouteFinder
4
+ * @description
5
+ * High-performance engine for discovering cross-chain payment paths.
6
+ *
7
+ * Instead of querying chains sequentially, this module:
8
+ * 1. Multicasts balance queries to all configured chains simultaneously.
9
+ * 2. Races requests against a configurable timeout (default 5s).
10
+ * 3. Aggregates results and scores them based on the User's preference (Speed vs Cost).
11
+ *
12
+ * Result: Finds optimal routes in milliseconds rather than seconds.
13
+ */
14
+
15
+ export interface ChainBalance {
16
+ chain: number;
17
+ chainName: string;
18
+ token: string;
19
+ balance: bigint;
20
+ balanceUSD: number;
21
+ }
22
+
23
+ export interface RouteCandidate {
24
+ id: string;
25
+ sourceChain: number;
26
+ targetChain: number;
27
+ sourceToken: string;
28
+ targetToken: string;
29
+ estimatedFeeUSD: number;
30
+ estimatedTimeMs: number;
31
+ hops: RouteHop[];
32
+ score: number;
33
+ metadata?: Record<string, unknown>;
34
+ }
35
+
36
+ export interface RouteHop {
37
+ type: 'transfer' | 'bridge' | 'swap';
38
+ fromChain: number;
39
+ toChain: number;
40
+ fromToken: string;
41
+ toToken: string;
42
+ estimatedFeeUSD: number;
43
+ estimatedTimeMs: number;
44
+ protocol?: string;
45
+ }
46
+
47
+ export interface RouteFindingConfig {
48
+ /** Maximum time to wait for all queries in ms */
49
+ timeoutMs: number;
50
+ /** Number of top routes to return */
51
+ maxRoutes: number;
52
+ /** Whether to include swap routes */
53
+ includeSwaps: boolean;
54
+ /** Whether to include bridge routes */
55
+ includeBridges: boolean;
56
+ /** Maximum number of hops allowed */
57
+ maxHops: number;
58
+ /** Prefer speed over cost (0-1) */
59
+ speedPreference: number;
60
+ }
61
+
62
+ export interface ChainClient {
63
+ getChainId(): number;
64
+ getChainName(): string;
65
+ getBalance(address: string, token?: string): Promise<bigint>;
66
+ getTokenPriceUSD(token: string): Promise<number>;
67
+ estimateTransferFee(to: string, amount: string, token: string): Promise<bigint>;
68
+ }
69
+
70
+ export interface RouteFindingResult {
71
+ routes: RouteCandidate[];
72
+ balances: ChainBalance[];
73
+ queryTimeMs: number;
74
+ timedOut: boolean;
75
+ errors: Array<{ chain: number; error: string }>;
76
+ }
77
+
78
+ const DEFAULT_CONFIG: RouteFindingConfig = {
79
+ timeoutMs: 5000,
80
+ maxRoutes: 5,
81
+ includeSwaps: true,
82
+ includeBridges: true,
83
+ maxHops: 3,
84
+ speedPreference: 0.5,
85
+ };
86
+
87
+ // Cache for balance queries
88
+ interface CacheEntry<T> {
89
+ value: T;
90
+ expiresAt: number;
91
+ }
92
+
93
+ export class ParallelRouteFinder {
94
+ private clients: Map<number, ChainClient> = new Map();
95
+ private config: RouteFindingConfig;
96
+ private balanceCache: Map<string, CacheEntry<ChainBalance>> = new Map();
97
+ private readonly CACHE_TTL_MS = 10000; // 10 seconds
98
+
99
+ constructor(
100
+ clients: ChainClient[],
101
+ config: Partial<RouteFindingConfig> = {}
102
+ ) {
103
+ this.config = { ...DEFAULT_CONFIG, ...config };
104
+ for (const client of clients) {
105
+ this.clients.set(client.getChainId(), client);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Add a chain client.
111
+ */
112
+ addClient(client: ChainClient): void {
113
+ this.clients.set(client.getChainId(), client);
114
+ }
115
+
116
+ /**
117
+ * Remove a chain client.
118
+ */
119
+ removeClient(chainId: number): boolean {
120
+ return this.clients.delete(chainId);
121
+ }
122
+
123
+ /**
124
+ * Find optimal routes for a payment.
125
+ */
126
+ async findRoutes(
127
+ address: string,
128
+ targetChain: number,
129
+ targetToken: string,
130
+ amountUSD: number,
131
+ options: Partial<RouteFindingConfig> = {}
132
+ ): Promise<RouteFindingResult> {
133
+ const config = { ...this.config, ...options };
134
+ const startTime = Date.now();
135
+ const errors: Array<{ chain: number; error: string }> = [];
136
+
137
+ // Create timeout promise
138
+ const timeoutPromise = new Promise<'timeout'>((resolve) =>
139
+ setTimeout(() => resolve('timeout'), config.timeoutMs)
140
+ );
141
+
142
+ // Query all balances in parallel
143
+ const balancePromises = Array.from(this.clients.entries()).map(
144
+ async ([chainId, client]): Promise<ChainBalance | null> => {
145
+ // Check cache first
146
+ const cacheKey = `${address}:${chainId}:${targetToken}`;
147
+ const cached = this.balanceCache.get(cacheKey);
148
+ if (cached && cached.expiresAt > Date.now()) {
149
+ return cached.value;
150
+ }
151
+
152
+ try {
153
+ const [balance, priceUSD] = await Promise.all([
154
+ client.getBalance(address, targetToken),
155
+ client.getTokenPriceUSD(targetToken),
156
+ ]);
157
+
158
+ const result: ChainBalance = {
159
+ chain: chainId,
160
+ chainName: client.getChainName(),
161
+ token: targetToken,
162
+ balance,
163
+ balanceUSD: Number(balance) * priceUSD / 1e6, // Assuming 6 decimals
164
+ };
165
+
166
+ // Cache the result
167
+ this.balanceCache.set(cacheKey, {
168
+ value: result,
169
+ expiresAt: Date.now() + this.CACHE_TTL_MS,
170
+ });
171
+
172
+ return result;
173
+ } catch (error) {
174
+ errors.push({
175
+ chain: chainId,
176
+ error: error instanceof Error ? error.message : 'Unknown error',
177
+ });
178
+ return null;
179
+ }
180
+ }
181
+ );
182
+
183
+ // Race against timeout
184
+ const balanceResults = await Promise.race([
185
+ Promise.all(balancePromises),
186
+ timeoutPromise,
187
+ ]);
188
+
189
+ let timedOut = false;
190
+ let balances: ChainBalance[];
191
+
192
+ if (balanceResults === 'timeout') {
193
+ timedOut = true;
194
+ // Get whatever results we have so far
195
+ balances = [];
196
+ for (const [chainId] of this.clients) {
197
+ const cacheKey = `${address}:${chainId}:${targetToken}`;
198
+ const cached = this.balanceCache.get(cacheKey);
199
+ if (cached) {
200
+ balances.push(cached.value);
201
+ }
202
+ }
203
+ } else {
204
+ balances = balanceResults.filter((b): b is ChainBalance => b !== null);
205
+ }
206
+
207
+ // Find candidate routes
208
+ const routes = await this.generateRoutes(
209
+ balances,
210
+ targetChain,
211
+ targetToken,
212
+ amountUSD,
213
+ config
214
+ );
215
+
216
+ // Sort routes by score (lower is better)
217
+ routes.sort((a, b) => a.score - b.score);
218
+
219
+ return {
220
+ routes: routes.slice(0, config.maxRoutes),
221
+ balances,
222
+ queryTimeMs: Date.now() - startTime,
223
+ timedOut,
224
+ errors,
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Generate candidate routes from available balances.
230
+ */
231
+ private async generateRoutes(
232
+ balances: ChainBalance[],
233
+ targetChain: number,
234
+ targetToken: string,
235
+ amountUSD: number,
236
+ config: RouteFindingConfig
237
+ ): Promise<RouteCandidate[]> {
238
+ const routes: RouteCandidate[] = [];
239
+
240
+ // Filter balances with sufficient funds
241
+ const sufficientBalances = balances.filter(b => b.balanceUSD >= amountUSD);
242
+
243
+ for (const balance of sufficientBalances) {
244
+ // Direct transfer (same chain)
245
+ if (balance.chain === targetChain) {
246
+ const route = await this.createDirectRoute(
247
+ balance,
248
+ targetChain,
249
+ targetToken,
250
+ amountUSD,
251
+ config
252
+ );
253
+ if (route) routes.push(route);
254
+ }
255
+ // Bridge route (cross-chain)
256
+ else if (config.includeBridges) {
257
+ const route = await this.createBridgeRoute(
258
+ balance,
259
+ targetChain,
260
+ targetToken,
261
+ amountUSD,
262
+ config
263
+ );
264
+ if (route) routes.push(route);
265
+ }
266
+ }
267
+
268
+ return routes;
269
+ }
270
+
271
+ /**
272
+ * Create a direct transfer route.
273
+ */
274
+ private async createDirectRoute(
275
+ balance: ChainBalance,
276
+ targetChain: number,
277
+ targetToken: string,
278
+ amountUSD: number,
279
+ config: RouteFindingConfig
280
+ ): Promise<RouteCandidate | null> {
281
+ const client = this.clients.get(balance.chain);
282
+ if (!client) return null;
283
+
284
+ try {
285
+ // Estimate transfer fee
286
+ const fee = await client.estimateTransferFee(
287
+ '0x0000000000000000000000000000000000000000', // Placeholder recipient
288
+ String(BigInt(Math.floor(amountUSD * 1e6))),
289
+ targetToken
290
+ );
291
+
292
+ const feeUSD = Number(fee) / 1e18 * 2000; // Rough ETH to USD conversion
293
+
294
+ const hop: RouteHop = {
295
+ type: 'transfer',
296
+ fromChain: balance.chain,
297
+ toChain: targetChain,
298
+ fromToken: targetToken,
299
+ toToken: targetToken,
300
+ estimatedFeeUSD: feeUSD,
301
+ estimatedTimeMs: balance.chain === 1 ? 15000 : 2000, // L1 vs L2
302
+ };
303
+
304
+ return {
305
+ id: `direct_${balance.chain}_${targetChain}`,
306
+ sourceChain: balance.chain,
307
+ targetChain,
308
+ sourceToken: targetToken,
309
+ targetToken,
310
+ estimatedFeeUSD: feeUSD,
311
+ estimatedTimeMs: hop.estimatedTimeMs,
312
+ hops: [hop],
313
+ score: this.calculateScore(feeUSD, hop.estimatedTimeMs, config),
314
+ };
315
+ } catch {
316
+ return null;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Create a bridge route.
322
+ */
323
+ private async createBridgeRoute(
324
+ balance: ChainBalance,
325
+ targetChain: number,
326
+ targetToken: string,
327
+ amountUSD: number,
328
+ config: RouteFindingConfig
329
+ ): Promise<RouteCandidate | null> {
330
+ // Estimate bridge parameters
331
+ const bridgeFeeUSD = 2.5; // Typical Wormhole fee
332
+ const bridgeTimeMs = 180000; // 3 minutes typical
333
+
334
+ const hops: RouteHop[] = [
335
+ {
336
+ type: 'bridge',
337
+ fromChain: balance.chain,
338
+ toChain: targetChain,
339
+ fromToken: targetToken,
340
+ toToken: targetToken,
341
+ estimatedFeeUSD: bridgeFeeUSD,
342
+ estimatedTimeMs: bridgeTimeMs,
343
+ protocol: 'wormhole',
344
+ },
345
+ ];
346
+
347
+ const totalFeeUSD = hops.reduce((sum, h) => sum + h.estimatedFeeUSD, 0);
348
+ const totalTimeMs = hops.reduce((sum, h) => sum + h.estimatedTimeMs, 0);
349
+
350
+ return {
351
+ id: `bridge_${balance.chain}_${targetChain}`,
352
+ sourceChain: balance.chain,
353
+ targetChain,
354
+ sourceToken: targetToken,
355
+ targetToken,
356
+ estimatedFeeUSD: totalFeeUSD,
357
+ estimatedTimeMs: totalTimeMs,
358
+ hops,
359
+ score: this.calculateScore(totalFeeUSD, totalTimeMs, config),
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Calculate route score (lower is better).
365
+ */
366
+ private calculateScore(
367
+ feeUSD: number,
368
+ timeMs: number,
369
+ config: RouteFindingConfig
370
+ ): number {
371
+ // Normalize fee (assume max fee of $10)
372
+ const normalizedFee = Math.min(feeUSD / 10, 1);
373
+
374
+ // Normalize time (assume max time of 5 minutes)
375
+ const normalizedTime = Math.min(timeMs / (5 * 60 * 1000), 1);
376
+
377
+ // Weighted score based on preference
378
+ const costWeight = 1 - config.speedPreference;
379
+ const speedWeight = config.speedPreference;
380
+
381
+ return normalizedFee * costWeight + normalizedTime * speedWeight;
382
+ }
383
+
384
+ /**
385
+ * Get all chain balances in parallel.
386
+ */
387
+ async getAllBalances(
388
+ address: string,
389
+ tokens: string[] = ['USDC']
390
+ ): Promise<ChainBalance[]> {
391
+ const promises: Promise<ChainBalance | null>[] = [];
392
+
393
+ for (const [chainId, client] of this.clients) {
394
+ for (const token of tokens) {
395
+ promises.push(
396
+ (async () => {
397
+ try {
398
+ const [balance, priceUSD] = await Promise.all([
399
+ client.getBalance(address, token),
400
+ client.getTokenPriceUSD(token),
401
+ ]);
402
+
403
+ return {
404
+ chain: chainId,
405
+ chainName: client.getChainName(),
406
+ token,
407
+ balance,
408
+ balanceUSD: Number(balance) * priceUSD / 1e6,
409
+ };
410
+ } catch {
411
+ return null;
412
+ }
413
+ })()
414
+ );
415
+ }
416
+ }
417
+
418
+ const results = await Promise.all(promises);
419
+ return results.filter((r): r is ChainBalance => r !== null);
420
+ }
421
+
422
+ /**
423
+ * Clear balance cache.
424
+ */
425
+ clearCache(): void {
426
+ this.balanceCache.clear();
427
+ }
428
+
429
+ /**
430
+ * Get cache statistics.
431
+ */
432
+ getCacheStats(): { size: number; hitRate: number } {
433
+ return {
434
+ size: this.balanceCache.size,
435
+ hitRate: 0, // Would need to track hits/misses
436
+ };
437
+ }
438
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * @module TransactionPoller
4
+ * @description
5
+ * Reliable transaction confirmation tracking.
6
+ *
7
+ * Unlike standard `await tx.wait()`, this poller:
8
+ * - Does NOT block the main thread.
9
+ * - Polls for status updates in the background (every 2s).
10
+ * - Emits events (`pending`, `confirmed`, `failed`) for UI updates.
11
+ * - Handles long-running cross-chain epochs without timing out the application.
12
+ */
13
+
14
+ export type TransactionStatus = 'pending' | 'confirmed' | 'failed' | 'timeout';
15
+
16
+ export interface PendingTransaction {
17
+ txHash: string;
18
+ chain: number;
19
+ submittedAt: number;
20
+ status: TransactionStatus;
21
+ confirmations?: number;
22
+ blockNumber?: number;
23
+ }
24
+
25
+ export interface ConfirmationEvent {
26
+ txHash: string;
27
+ status: TransactionStatus;
28
+ confirmations?: number;
29
+ blockNumber?: number;
30
+ error?: string;
31
+ }
32
+
33
+ type ConfirmationCallback = (event: ConfirmationEvent) => void;
34
+
35
+ export class TransactionPoller {
36
+ private pending: Map<string, PendingTransaction> = new Map();
37
+ private callbacks: Map<string, ConfirmationCallback[]> = new Map();
38
+ private globalCallbacks: ConfirmationCallback[] = [];
39
+ private pollInterval: ReturnType<typeof setInterval> | null = null;
40
+ private readonly POLL_INTERVAL_MS = 2000;
41
+ private readonly TIMEOUT_MS = 300000; // 5 minutes
42
+ private checkConfirmation: (txHash: string, chain: number) => Promise<{ confirmed: boolean; confirmations?: number; blockNumber?: number }>;
43
+
44
+ constructor(
45
+ confirmationChecker: (txHash: string, chain: number) => Promise<{ confirmed: boolean; confirmations?: number; blockNumber?: number }>
46
+ ) {
47
+ this.checkConfirmation = confirmationChecker;
48
+ }
49
+
50
+ /**
51
+ * Start tracking a transaction.
52
+ */
53
+ track(txHash: string, chain: number, callback?: ConfirmationCallback): void {
54
+ const tx: PendingTransaction = {
55
+ txHash,
56
+ chain,
57
+ submittedAt: Date.now(),
58
+ status: 'pending',
59
+ };
60
+ this.pending.set(txHash, tx);
61
+
62
+ if (callback) {
63
+ if (!this.callbacks.has(txHash)) {
64
+ this.callbacks.set(txHash, []);
65
+ }
66
+ this.callbacks.get(txHash)!.push(callback);
67
+ }
68
+
69
+ this.startPolling();
70
+ }
71
+
72
+ /**
73
+ * Subscribe to all confirmation events.
74
+ */
75
+ onConfirmation(callback: ConfirmationCallback): () => void {
76
+ this.globalCallbacks.push(callback);
77
+ return () => {
78
+ const index = this.globalCallbacks.indexOf(callback);
79
+ if (index > -1) {
80
+ this.globalCallbacks.splice(index, 1);
81
+ }
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Get status of a tracked transaction.
87
+ */
88
+ getStatus(txHash: string): PendingTransaction | undefined {
89
+ return this.pending.get(txHash);
90
+ }
91
+
92
+ /**
93
+ * Get all pending transactions.
94
+ */
95
+ getPending(): PendingTransaction[] {
96
+ return Array.from(this.pending.values()).filter(tx => tx.status === 'pending');
97
+ }
98
+
99
+ /**
100
+ * Stop tracking a transaction.
101
+ */
102
+ untrack(txHash: string): void {
103
+ this.pending.delete(txHash);
104
+ this.callbacks.delete(txHash);
105
+ this.maybeStopPolling();
106
+ }
107
+
108
+ /**
109
+ * Start the polling loop.
110
+ */
111
+ private startPolling(): void {
112
+ if (this.pollInterval) return;
113
+
114
+ this.pollInterval = setInterval(() => {
115
+ this.poll();
116
+ }, this.POLL_INTERVAL_MS);
117
+ }
118
+
119
+ /**
120
+ * Stop polling if no pending transactions.
121
+ */
122
+ private maybeStopPolling(): void {
123
+ if (this.getPending().length === 0 && this.pollInterval) {
124
+ clearInterval(this.pollInterval);
125
+ this.pollInterval = null;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Poll all pending transactions.
131
+ */
132
+ private async poll(): Promise<void> {
133
+ const pending = this.getPending();
134
+
135
+ for (const tx of pending) {
136
+ try {
137
+ // Check for timeout
138
+ if (Date.now() - tx.submittedAt > this.TIMEOUT_MS) {
139
+ this.updateStatus(tx.txHash, 'timeout');
140
+ continue;
141
+ }
142
+
143
+ const result = await this.checkConfirmation(tx.txHash, tx.chain);
144
+
145
+ if (result.confirmed) {
146
+ tx.confirmations = result.confirmations;
147
+ tx.blockNumber = result.blockNumber;
148
+ this.updateStatus(tx.txHash, 'confirmed');
149
+ }
150
+ } catch (error) {
151
+ console.error(`[TransactionPoller] Error checking ${tx.txHash}:`, error);
152
+ // Don't fail immediately on network errors, just log
153
+ }
154
+ }
155
+
156
+ this.maybeStopPolling();
157
+ }
158
+
159
+ /**
160
+ * Update transaction status and emit events.
161
+ */
162
+ private updateStatus(txHash: string, status: TransactionStatus, error?: string): void {
163
+ const tx = this.pending.get(txHash);
164
+ if (!tx) return;
165
+
166
+ tx.status = status;
167
+
168
+ const event: ConfirmationEvent = {
169
+ txHash,
170
+ status,
171
+ confirmations: tx.confirmations,
172
+ blockNumber: tx.blockNumber,
173
+ error,
174
+ };
175
+
176
+ // Notify specific callbacks
177
+ const callbacks = this.callbacks.get(txHash) || [];
178
+ callbacks.forEach(cb => cb(event));
179
+
180
+ // Notify global callbacks
181
+ this.globalCallbacks.forEach(cb => cb(event));
182
+
183
+ // Clean up completed transactions
184
+ if (status !== 'pending') {
185
+ this.callbacks.delete(txHash);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Clean up all resources.
191
+ */
192
+ destroy(): void {
193
+ if (this.pollInterval) {
194
+ clearInterval(this.pollInterval);
195
+ this.pollInterval = null;
196
+ }
197
+ this.pending.clear();
198
+ this.callbacks.clear();
199
+ this.globalCallbacks = [];
200
+ }
201
+ }