@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.
- package/CHANGELOG.md +108 -0
- package/MIGRATION.md +307 -0
- package/README.md +395 -0
- package/dist/index.d.mts +2327 -0
- package/dist/index.d.ts +2327 -0
- package/dist/index.js +5815 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5759 -0
- package/dist/index.mjs.map +1 -0
- package/examples/basic-agent.ts +126 -0
- package/examples/mcp-claude.ts +75 -0
- package/examples/ucp-checkout.ts +92 -0
- package/examples/x402-integration.ts +75 -0
- package/package.json +36 -0
- package/src/AgentWallet.ts +432 -0
- package/src/chains/AptosChainClient.ts +29 -0
- package/src/chains/ChainClient.ts +73 -0
- package/src/chains/ChainClientFactory.ts +113 -0
- package/src/chains/EVMChainClient.ts +39 -0
- package/src/chains/SolanaChainClient.ts +37 -0
- package/src/chains/StarknetChainClient.ts +36 -0
- package/src/chains/SuiChainClient.ts +28 -0
- package/src/index.ts +83 -0
- package/src/mcp/MCPServer.ts +73 -0
- package/src/mcp/schemas.ts +60 -0
- package/src/monitoring/AlertManager.ts +258 -0
- package/src/monitoring/AuditLogger.ts +86 -0
- package/src/monitoring/BalanceCache.ts +44 -0
- package/src/monitoring/ComplianceExporter.ts +52 -0
- package/src/oracle/PythFeeds.ts +60 -0
- package/src/oracle/PythOracle.ts +121 -0
- package/src/performance/ConnectionPool.ts +217 -0
- package/src/performance/NonceManager.ts +91 -0
- package/src/performance/ParallelRouteFinder.ts +438 -0
- package/src/performance/TransactionPoller.ts +201 -0
- package/src/performance/TransactionQueue.ts +565 -0
- package/src/performance/index.ts +46 -0
- package/src/react/hooks.ts +298 -0
- package/src/routing/BridgeOrchestrator.ts +18 -0
- package/src/routing/CrossChainRouter.ts +501 -0
- package/src/routing/DEXAggregator.ts +448 -0
- package/src/routing/FeeEstimator.ts +43 -0
- package/src/session/SessionKeyManager.ts +312 -0
- package/src/session/SessionStorage.ts +80 -0
- package/src/session/SpendingTracker.ts +71 -0
- package/src/types/agent.ts +105 -0
- package/src/types/errors.ts +115 -0
- package/src/types/mcp.ts +22 -0
- package/src/types/ucp.ts +47 -0
- package/src/types/x402.ts +170 -0
- package/src/ucp/CapabilityNegotiator.ts +44 -0
- package/src/ucp/CredentialProvider.ts +73 -0
- package/src/ucp/PaymentTokenizer.ts +169 -0
- package/src/ucp/TransportAdapter.ts +18 -0
- package/src/ucp/UCPClient.ts +143 -0
- package/src/x402/NonceManager.ts +26 -0
- package/src/x402/PaymentParser.ts +225 -0
- package/src/x402/PaymentSigner.ts +305 -0
- package/src/x402/X402Client.ts +364 -0
- package/src/x402/adapters/CronosFacilitatorAdapter.ts +109 -0
- package/tests/alerts.test.ts +208 -0
- package/tests/chains.test.ts +242 -0
- package/tests/integration.test.ts +315 -0
- package/tests/monitoring.test.ts +435 -0
- package/tests/performance.test.ts +303 -0
- package/tests/property.test.ts +186 -0
- package/tests/react-hooks.test.ts +262 -0
- package/tests/session.test.ts +376 -0
- package/tests/ucp.test.ts +253 -0
- package/tests/x402.test.ts +385 -0
- package/tsconfig.json +26 -0
- package/tsup.config.ts +10 -0
- 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
|
+
}
|