@silentswap/sdk 0.0.21 → 0.0.23

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/bridge.js ADDED
@@ -0,0 +1,615 @@
1
+ import { encodeFunctionData, erc20Abi } from 'viem';
2
+ import { NI_CHAIN_ID_AVALANCHE, S0X_ADDR_USDC_AVALANCHE, S0X_ADDR_DEPOSITOR, X_MAX_IMPACT_PERCENT, } from './constants.js';
3
+ import { createPhonyDepositCalldata } from './wallet.js';
4
+ /**
5
+ * Get a quote from relay.link
6
+ */
7
+ export async function getRelayQuote(params, apiKey) {
8
+ const { srcChainId, dstChainId, srcToken, dstToken, amount, tradeType, recipient, user, additionalTxs = [], } = params;
9
+ // relay.link API call - using the correct parameters from relay-link.ts
10
+ const response = await fetch('https://api.relay.link/quote', {
11
+ method: 'POST',
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ },
15
+ body: JSON.stringify({
16
+ user,
17
+ referrer: 'silentswap',
18
+ originChainId: srcChainId,
19
+ destinationChainId: dstChainId,
20
+ originCurrency: srcToken === '0x0000000000000000000000000000000000000000' ? '0x0000000000000000000000000000000000000000' : srcToken,
21
+ destinationCurrency: dstToken,
22
+ amount,
23
+ tradeType,
24
+ recipient,
25
+ txsGasLimit: additionalTxs.length > 0 ? 600_000 : undefined,
26
+ txs: additionalTxs.map(tx => ({
27
+ to: tx.to,
28
+ value: tx.value,
29
+ data: tx.data,
30
+ })),
31
+ }),
32
+ });
33
+ if (!response.ok) {
34
+ const errorText = await response.text();
35
+ throw new Error(`Relay API error: ${response.status} ${response.statusText} - ${errorText}`);
36
+ }
37
+ const data = await response.json();
38
+ return {
39
+ provider: 'relay',
40
+ estimatedTime: data.details?.timeEstimate || 300, // Use timeEstimate from response
41
+ fee: {
42
+ amount: data.fees?.gas?.amountFormatted || data.fees?.relayer?.amountFormatted || '0',
43
+ token: data.fees?.gas?.currency?.symbol || data.fees?.relayer?.currency?.symbol || 'ETH',
44
+ usdValue: data.fees?.gas?.amountUsd || data.fees?.relayer?.amountUsd,
45
+ },
46
+ slippage: parseFloat(data.details?.slippageTolerance?.percent || '0.005') * 100, // Convert to percentage
47
+ route: data,
48
+ txs: data.steps?.flatMap((step) => step.items?.map((item) => ({
49
+ to: item.data?.to,
50
+ value: item.data?.value || '0',
51
+ data: item.data?.data || '0x',
52
+ gasLimit: item.data?.gas,
53
+ chainId: item.data?.chainId || srcChainId,
54
+ })) || []) || [],
55
+ };
56
+ }
57
+ /**
58
+ * Get a quote from debridge (handles both cross-chain and same-chain swaps)
59
+ */
60
+ export async function getDebridgeQuote(params, apiKey) {
61
+ const { srcChainId, dstChainId, srcToken, dstToken, amount, recipient, user, additionalTxs = [], prependOperatingExpenses = true, enableEstimate = true, } = params;
62
+ // Use single-chain swap endpoint if source and destination chains are the same
63
+ if (srcChainId === dstChainId) {
64
+ return getDebridgeSingleChainQuote(params, apiKey);
65
+ }
66
+ // debridge cross-chain API call - using the correct parameters from original debridge.ts
67
+ const queryParams = new URLSearchParams({
68
+ srcChainId: srcChainId.toString(),
69
+ srcChainTokenIn: srcToken,
70
+ srcChainTokenInAmount: amount,
71
+ dstChainId: dstChainId.toString(),
72
+ dstChainTokenOut: dstToken,
73
+ dstChainTokenOutRecipient: recipient,
74
+ senderAddress: user, // Changed from 'account' to 'senderAddress' for enableEstimate=true
75
+ srcChainOrderAuthorityAddress: user,
76
+ dstChainOrderAuthorityAddress: user,
77
+ prependOperatingExpenses: prependOperatingExpenses.toString(),
78
+ enableEstimate: enableEstimate.toString(),
79
+ });
80
+ // Add dlnHook if additional transactions are provided
81
+ if (additionalTxs.length > 0) {
82
+ const hookData = {
83
+ type: 'evm_transaction_call',
84
+ data: {
85
+ to: additionalTxs[0].to,
86
+ calldata: additionalTxs[0].data,
87
+ gas: 500_000,
88
+ },
89
+ };
90
+ queryParams.append('dlnHook', JSON.stringify(hookData));
91
+ }
92
+ const response = await fetch(`https://dln.debridge.finance/v1.0/dln/order/create-tx?${queryParams.toString()}`, {
93
+ method: 'GET', // debridge uses GET with query parameters
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ },
97
+ });
98
+ if (!response.ok) {
99
+ const errorText = await response.text();
100
+ throw new Error(`Debridge API error: ${response.status} ${response.statusText} - ${errorText}`);
101
+ }
102
+ const data = await response.json();
103
+ return {
104
+ provider: 'debridge',
105
+ estimatedTime: data.order?.approximateFulfillmentDelay || 600, // Use approximateFulfillmentDelay from response
106
+ fee: {
107
+ amount: data.estimation?.srcChainTokenIn?.amount || '0',
108
+ token: data.estimation?.srcChainTokenIn?.currency?.symbol || 'UNKNOWN',
109
+ usdValue: data.estimation?.srcChainTokenIn?.approximateUsdValue,
110
+ },
111
+ slippage: data.recommendedSlippage || 0.5, // Use recommendedSlippage from response
112
+ route: data,
113
+ txs: data.tx ? [{
114
+ to: data.tx.to,
115
+ value: data.tx.value || '0',
116
+ data: data.tx.data || '0x',
117
+ chainId: srcChainId,
118
+ }] : [],
119
+ };
120
+ }
121
+ /**
122
+ * Get a quote for same-chain swap from deBridge single-chain swap endpoint
123
+ * Documentation: https://api.dln.trade/v1.0/#/single%20chain%20swap
124
+ */
125
+ async function getDebridgeSingleChainQuote(params, apiKey) {
126
+ const { srcChainId, srcToken, dstToken, amount, recipient, user, } = params;
127
+ // Single-chain swap API call
128
+ const queryParams = new URLSearchParams({
129
+ chainId: srcChainId.toString(),
130
+ tokenIn: srcToken,
131
+ tokenInAmount: amount,
132
+ tokenOut: dstToken,
133
+ recipient: recipient,
134
+ senderAddress: user,
135
+ });
136
+ const response = await fetch(`https://api.dln.trade/v1.0/chain/transaction?${queryParams.toString()}`, {
137
+ method: 'GET',
138
+ headers: {
139
+ 'Content-Type': 'application/json',
140
+ },
141
+ });
142
+ if (!response.ok) {
143
+ const errorText = await response.text();
144
+ throw new Error(`Debridge single-chain swap API error: ${response.status} ${response.statusText} - ${errorText}`);
145
+ }
146
+ const data = await response.json();
147
+ return {
148
+ provider: 'debridge',
149
+ estimatedTime: 60, // Single-chain swaps are faster
150
+ fee: {
151
+ amount: data.estimation?.srcChainTokenIn?.amount || '0',
152
+ token: data.estimation?.srcChainTokenIn?.currency?.symbol || 'UNKNOWN',
153
+ usdValue: data.estimation?.srcChainTokenIn?.approximateUsdValue,
154
+ },
155
+ slippage: data.recommendedSlippage || 0.5,
156
+ route: data,
157
+ txs: data.tx ? [{
158
+ to: data.tx.to,
159
+ value: data.tx.value || '0',
160
+ data: data.tx.data || '0x',
161
+ chainId: srcChainId,
162
+ }] : [],
163
+ };
164
+ }
165
+ /**
166
+ * Execute a relay.link bridge transaction
167
+ */
168
+ export async function executeRelayBridge(quote, executeTransaction, switchChain, setStep) {
169
+ const txHashes = [];
170
+ for (const tx of quote.txs) {
171
+ setStep(`Switching to chain ${tx.chainId}`);
172
+ // Switch to the correct chain
173
+ await switchChain(tx.chainId);
174
+ setStep('Sending bridge transaction');
175
+ // Send transaction
176
+ const hash = await executeTransaction(tx);
177
+ txHashes.push(hash);
178
+ }
179
+ // Extract request ID for status monitoring
180
+ const requestId = quote.route.steps?.find((s) => s.requestId)?.requestId;
181
+ return {
182
+ status: 'pending',
183
+ txHashes,
184
+ requestId,
185
+ };
186
+ }
187
+ /**
188
+ * Execute a debridge bridge transaction
189
+ */
190
+ export async function executeDebridgeBridge(quote, executeTransaction, switchChain, setStep) {
191
+ if (quote.txs.length === 0) {
192
+ throw new Error('No transactions in debridge quote');
193
+ }
194
+ const tx = quote.txs[0];
195
+ setStep(`Switching to chain ${tx.chainId}`);
196
+ // Switch to the correct chain
197
+ await switchChain(tx.chainId);
198
+ setStep('Sending bridge transaction');
199
+ // Send transaction
200
+ const hash = await executeTransaction(tx);
201
+ return {
202
+ status: 'pending',
203
+ txHashes: [hash],
204
+ requestId: quote.route.orderId,
205
+ };
206
+ }
207
+ /**
208
+ * Get relay.link bridge status
209
+ */
210
+ export async function getRelayStatus(requestId) {
211
+ const response = await fetch(`https://api.relay.link/intents/status?requestId=${encodeURIComponent(requestId)}`);
212
+ if (!response.ok) {
213
+ const errorText = await response.text();
214
+ throw new Error(`Relay status API error: ${response.status} ${response.statusText} - ${errorText}`);
215
+ }
216
+ const data = await response.json();
217
+ return {
218
+ status: mapRelayStatus(data.status),
219
+ txHashes: data.txHashes || data.inTxHashes,
220
+ details: data.details,
221
+ requestId,
222
+ };
223
+ }
224
+ /**
225
+ * Get debridge bridge status
226
+ */
227
+ export async function getDebridgeStatus(requestId) {
228
+ const response = await fetch(`https://dln.debridge.finance/v1.0/dln/order/status?orderId=${encodeURIComponent(requestId)}`);
229
+ if (!response.ok) {
230
+ const errorText = await response.text();
231
+ throw new Error(`Debridge status API error: ${response.status} ${response.statusText} - ${errorText}`);
232
+ }
233
+ const data = await response.json();
234
+ return {
235
+ status: mapDebridgeStatus(data.orderId ? 'Fulfilled' : 'Created'), // Simplified status mapping
236
+ txHashes: data.txHashes,
237
+ details: data.status,
238
+ requestId,
239
+ };
240
+ }
241
+ /**
242
+ * Map relay.link status to unified status
243
+ */
244
+ function mapRelayStatus(status) {
245
+ switch (status?.toLowerCase()) {
246
+ case 'success':
247
+ case 'completed':
248
+ return 'success';
249
+ case 'failed':
250
+ case 'error':
251
+ return 'failed';
252
+ case 'refund':
253
+ case 'refunded':
254
+ return 'refund';
255
+ case 'fallback':
256
+ return 'fallback';
257
+ case 'pending':
258
+ case 'processing':
259
+ default:
260
+ return 'pending';
261
+ }
262
+ }
263
+ /**
264
+ * Map debridge status to unified status
265
+ */
266
+ function mapDebridgeStatus(status) {
267
+ switch (status) {
268
+ case 'Fulfilled':
269
+ case 'ClaimedUnlock':
270
+ case 'Completed':
271
+ return 'success';
272
+ case 'OrderCancelled':
273
+ case 'SentOrderCancel':
274
+ case 'ClaimedOrderCancel':
275
+ case 'Cancelled':
276
+ return 'failed';
277
+ case 'Created':
278
+ case 'SentUnlock':
279
+ case 'Pending':
280
+ default:
281
+ return 'pending';
282
+ }
283
+ }
284
+ /**
285
+ * Fetch a relay.link quote with detailed response
286
+ */
287
+ export async function fetchRelayQuote(params, signal) {
288
+ const response = await fetch('https://api.relay.link/quote', {
289
+ method: 'POST',
290
+ headers: {
291
+ 'Content-Type': 'application/json',
292
+ },
293
+ body: JSON.stringify(params),
294
+ signal,
295
+ });
296
+ if (!response.ok) {
297
+ const text = await response.text();
298
+ throw new Error(`HTTP ${response.status}: ${text}`);
299
+ }
300
+ return response.json();
301
+ }
302
+ /**
303
+ * Fetch a deBridge order with detailed response (handles both cross-chain and same-chain swaps)
304
+ */
305
+ export async function fetchDebridgeOrder(params, signal) {
306
+ // Use single-chain swap endpoint if source and destination chains are the same
307
+ if (params.srcChainId === params.dstChainId) {
308
+ return fetchDebridgeSingleChainOrder(params, signal);
309
+ }
310
+ const queryParams = new URLSearchParams(Object.entries(params).reduce((acc, [key, value]) => {
311
+ if (value !== undefined) {
312
+ acc[key] = typeof value === 'object' ? JSON.stringify(value) : String(value);
313
+ }
314
+ return acc;
315
+ }, {}));
316
+ const response = await fetch(`https://dln.debridge.finance/v1.0/dln/order/create-tx?${queryParams}`, { signal });
317
+ if (!response.ok) {
318
+ const text = await response.text();
319
+ throw new Error(`HTTP ${response.status}: ${text}`);
320
+ }
321
+ return response.json();
322
+ }
323
+ /**
324
+ * Fetch a deBridge single-chain swap order
325
+ * Documentation: https://api.dln.trade/v1.0/#/single%20chain%20swap
326
+ */
327
+ async function fetchDebridgeSingleChainOrder(params, signal) {
328
+ const queryParams = new URLSearchParams({
329
+ chainId: params.srcChainId.toString(),
330
+ tokenIn: params.srcChainTokenIn,
331
+ tokenInAmount: params.srcChainTokenInAmount,
332
+ tokenOut: params.dstChainTokenOut,
333
+ recipient: params.dstChainTokenOutRecipient || params.account || '',
334
+ senderAddress: params.account || '',
335
+ });
336
+ const response = await fetch(`https://api.dln.trade/v1.0/chain/transaction?${queryParams}`, { signal });
337
+ if (!response.ok) {
338
+ const text = await response.text();
339
+ throw new Error(`HTTP ${response.status}: ${text}`);
340
+ }
341
+ return response.json();
342
+ }
343
+ const UINT256_MAX = 2n ** 256n - 1n;
344
+ /**
345
+ * Solve for optimal USDC amount using relay.link
346
+ */
347
+ async function solveRelayUsdcAmount(srcChainId, srcToken, srcAmount, userAddress, depositCalldata, maxImpactPercent) {
348
+ // Get initial quote to estimate expected output
349
+ const initialQuote = await fetchRelayQuote({
350
+ user: userAddress,
351
+ referrer: 'silentswap',
352
+ originChainId: srcChainId,
353
+ destinationChainId: NI_CHAIN_ID_AVALANCHE,
354
+ originCurrency: srcToken,
355
+ destinationCurrency: S0X_ADDR_USDC_AVALANCHE,
356
+ amount: srcAmount,
357
+ tradeType: 'EXACT_INPUT',
358
+ });
359
+ const baseUsdcOut = BigInt(initialQuote.details.currencyOut.amount);
360
+ // Overshoot by +1 USDC to +5% (max +100 USDC)
361
+ let targetUsdcOut = BigInt(Math.max(Number(baseUsdcOut) + 1_000_000, Math.min(Number(baseUsdcOut) + 100_000_000, Number(baseUsdcOut) * 1.05)));
362
+ // Try up to 4 times to find optimal amount
363
+ for (let attempt = 0; attempt < 4; attempt++) {
364
+ const quote = await fetchRelayQuote({
365
+ user: userAddress,
366
+ referrer: 'silentswap',
367
+ originChainId: srcChainId,
368
+ destinationChainId: NI_CHAIN_ID_AVALANCHE,
369
+ originCurrency: srcToken,
370
+ destinationCurrency: S0X_ADDR_USDC_AVALANCHE,
371
+ amount: targetUsdcOut.toString(),
372
+ tradeType: 'EXACT_OUTPUT',
373
+ recipient: S0X_ADDR_DEPOSITOR,
374
+ txsGasLimit: 500_000,
375
+ txs: [
376
+ {
377
+ to: S0X_ADDR_USDC_AVALANCHE,
378
+ value: '0',
379
+ data: encodeFunctionData({
380
+ abi: erc20Abi,
381
+ functionName: 'approve',
382
+ args: [S0X_ADDR_DEPOSITOR, UINT256_MAX],
383
+ }),
384
+ },
385
+ {
386
+ to: S0X_ADDR_DEPOSITOR,
387
+ value: '0',
388
+ data: depositCalldata,
389
+ },
390
+ ],
391
+ });
392
+ const impactPercent = Number(quote.details.totalImpact.percent);
393
+ if (impactPercent > maxImpactPercent) {
394
+ throw new Error(`Price impact too high: ${impactPercent.toFixed(2)}%`);
395
+ }
396
+ const amountIn = BigInt(quote.details.currencyIn.amount);
397
+ // Amount in is within our budget
398
+ if (amountIn <= BigInt(srcAmount)) {
399
+ return [BigInt(quote.details.currencyOut.amount), amountIn, ''];
400
+ }
401
+ // Calculate new target based on overshoot
402
+ const usdcPrice = quote.details.currencyOut.amountUsd / Number(quote.details.currencyOut.amount);
403
+ const tokenPrice = quote.details.currencyIn.amountUsd / Number(quote.details.currencyIn.amount);
404
+ const usdcOver = (Number(quote.details.currencyIn.amount) - Number(srcAmount)) * tokenPrice / usdcPrice;
405
+ const newTarget = Number(quote.details.currencyOut.amount) - usdcOver;
406
+ if (BigInt(newTarget) >= targetUsdcOut) {
407
+ throw new Error('Failed to find optimal bridge price');
408
+ }
409
+ targetUsdcOut = BigInt(newTarget);
410
+ }
411
+ throw new Error('Failed to find optimal bridge price after 4 attempts');
412
+ }
413
+ /**
414
+ * Solve for optimal USDC amount using deBridge
415
+ */
416
+ async function solveDebridgeUsdcAmount(srcChainId, srcToken, srcAmount, userAddress, depositCalldata, maxImpactPercent) {
417
+ // For same-chain swaps (Avalanche to Avalanche), use the single-chain endpoint
418
+ if (srcChainId === NI_CHAIN_ID_AVALANCHE) {
419
+ return solveDebridgeSingleChainUsdcAmount(srcChainId, srcToken, srcAmount, userAddress, depositCalldata, maxImpactPercent);
420
+ }
421
+ // Get initial quote for cross-chain swap
422
+ const initialQuote = await fetchDebridgeOrder({
423
+ srcChainId,
424
+ srcChainTokenIn: srcToken,
425
+ srcChainTokenInAmount: srcAmount,
426
+ dstChainId: NI_CHAIN_ID_AVALANCHE,
427
+ dstChainTokenOut: S0X_ADDR_USDC_AVALANCHE,
428
+ dstChainTokenOutAmount: 'auto',
429
+ account: userAddress,
430
+ prependOperatingExpenses: true,
431
+ });
432
+ const baseUsdcOut = BigInt(initialQuote.estimation.dstChainTokenOut.amount);
433
+ // Overshoot by +1 USDC to +5% (max +100 USDC)
434
+ let targetUsdcOut = BigInt(Math.max(Number(baseUsdcOut) + 1_000_000, Math.min(Number(baseUsdcOut) + 100_000_000, Number(baseUsdcOut) * 1.05)));
435
+ // Try up to 4 times to find optimal amount
436
+ for (let attempt = 0; attempt < 4; attempt++) {
437
+ const quote = await fetchDebridgeOrder({
438
+ srcChainId,
439
+ srcChainTokenIn: srcToken,
440
+ srcChainTokenInAmount: 'auto',
441
+ dstChainId: NI_CHAIN_ID_AVALANCHE,
442
+ dstChainTokenOut: S0X_ADDR_USDC_AVALANCHE,
443
+ dstChainTokenOutAmount: targetUsdcOut.toString(),
444
+ dstChainTokenOutRecipient: S0X_ADDR_DEPOSITOR,
445
+ account: userAddress,
446
+ prependOperatingExpenses: true,
447
+ dlnHook: JSON.stringify({
448
+ type: 'evm_transaction_call',
449
+ data: {
450
+ to: S0X_ADDR_DEPOSITOR,
451
+ calldata: depositCalldata,
452
+ gas: 500_000,
453
+ },
454
+ }),
455
+ });
456
+ // Calculate price impact
457
+ const usdIn = quote.estimation.srcChainTokenIn.approximateUsdValue;
458
+ const usdOut = quote.estimation.dstChainTokenOut.approximateUsdValue;
459
+ const impactPercent = 100 * (1 - usdOut / usdIn);
460
+ if (impactPercent > maxImpactPercent) {
461
+ throw new Error(`Price impact too high: ${impactPercent.toFixed(2)}%`);
462
+ }
463
+ const amountIn = BigInt(quote.estimation.srcChainTokenIn.amount);
464
+ // Amount in is within our budget
465
+ if (amountIn <= BigInt(srcAmount)) {
466
+ return [
467
+ BigInt(quote.estimation.dstChainTokenOut.amount),
468
+ amountIn,
469
+ quote.tx.allowanceTarget || '',
470
+ ];
471
+ }
472
+ // Calculate new target based on overshoot
473
+ const usdcPrice = usdOut / Number(quote.estimation.dstChainTokenOut.amount);
474
+ const tokenPrice = usdIn / Number(quote.estimation.srcChainTokenIn.amount);
475
+ const usdcOver = (Number(quote.estimation.srcChainTokenIn.amount) - Number(srcAmount)) * tokenPrice / usdcPrice;
476
+ const newTarget = Number(quote.estimation.dstChainTokenOut.amount) - usdcOver;
477
+ if (BigInt(newTarget) >= targetUsdcOut) {
478
+ throw new Error('Failed to find optimal bridge price');
479
+ }
480
+ targetUsdcOut = BigInt(newTarget);
481
+ }
482
+ throw new Error('Failed to find optimal bridge price after 4 attempts');
483
+ }
484
+ /**
485
+ * Solve for optimal USDC amount using deBridge single-chain swap
486
+ * For same-chain swaps like Avalanche to Avalanche
487
+ */
488
+ async function solveDebridgeSingleChainUsdcAmount(chainId, srcToken, srcAmount, userAddress, depositCalldata, maxImpactPercent) {
489
+ // Get initial quote for same-chain swap
490
+ const initialQuote = await fetchDebridgeSingleChainOrder({
491
+ srcChainId: chainId,
492
+ srcChainTokenIn: srcToken,
493
+ srcChainTokenInAmount: srcAmount,
494
+ dstChainId: chainId,
495
+ dstChainTokenOut: S0X_ADDR_USDC_AVALANCHE,
496
+ dstChainTokenOutAmount: 'auto',
497
+ account: userAddress,
498
+ prependOperatingExpenses: true,
499
+ });
500
+ const baseUsdcOut = BigInt(initialQuote.estimation.dstChainTokenOut.amount);
501
+ // Overshoot by +1 USDC to +5% (max +100 USDC)
502
+ let targetUsdcOut = BigInt(Math.max(Number(baseUsdcOut) + 1_000_000, Math.min(Number(baseUsdcOut) + 100_000_000, Number(baseUsdcOut) * 1.05)));
503
+ // Try up to 4 times to find optimal amount
504
+ for (let attempt = 0; attempt < 4; attempt++) {
505
+ const quote = await fetchDebridgeSingleChainOrder({
506
+ srcChainId: chainId,
507
+ srcChainTokenIn: srcToken,
508
+ srcChainTokenInAmount: 'auto',
509
+ dstChainId: chainId,
510
+ dstChainTokenOut: S0X_ADDR_USDC_AVALANCHE,
511
+ dstChainTokenOutAmount: targetUsdcOut.toString(),
512
+ dstChainTokenOutRecipient: S0X_ADDR_DEPOSITOR,
513
+ account: userAddress,
514
+ prependOperatingExpenses: true,
515
+ });
516
+ // Calculate price impact
517
+ const usdIn = quote.estimation.srcChainTokenIn.approximateUsdValue;
518
+ const usdOut = quote.estimation.dstChainTokenOut.approximateUsdValue;
519
+ const impactPercent = 100 * (1 - usdOut / usdIn);
520
+ if (impactPercent > maxImpactPercent) {
521
+ throw new Error(`Price impact too high: ${impactPercent.toFixed(2)}%`);
522
+ }
523
+ const amountIn = BigInt(quote.estimation.srcChainTokenIn.amount);
524
+ // Amount in is within our budget
525
+ if (amountIn <= BigInt(srcAmount)) {
526
+ return [
527
+ BigInt(quote.estimation.dstChainTokenOut.amount),
528
+ amountIn,
529
+ quote.tx.allowanceTarget || '',
530
+ ];
531
+ }
532
+ // Calculate new target based on overshoot
533
+ const usdcPrice = usdOut / Number(quote.estimation.dstChainTokenOut.amount);
534
+ const tokenPrice = usdIn / Number(quote.estimation.srcChainTokenIn.amount);
535
+ const usdcOver = (Number(quote.estimation.srcChainTokenIn.amount) - Number(srcAmount)) * tokenPrice / usdcPrice;
536
+ const newTarget = Number(quote.estimation.dstChainTokenOut.amount) - usdcOver;
537
+ if (BigInt(newTarget) >= targetUsdcOut) {
538
+ throw new Error('Failed to find optimal bridge price');
539
+ }
540
+ targetUsdcOut = BigInt(newTarget);
541
+ }
542
+ throw new Error('Failed to find optimal bridge price after 4 attempts');
543
+ }
544
+ /**
545
+ * Solve for optimal USDC amount using both relay and debridge providers
546
+ * Compares rates and returns the best option
547
+ *
548
+ * @param srcChainId - Source chain ID
549
+ * @param srcToken - Source token address (use 0x0 for native)
550
+ * @param srcAmount - Source amount in token units (as string)
551
+ * @param userAddress - User's EVM address
552
+ * @param depositCalldata - Optional deposit calldata (will use phony if not provided)
553
+ * @param maxImpactPercent - Maximum price impact percentage allowed (default from constants)
554
+ * @returns Promise resolving to solve result with optimal amounts
555
+ */
556
+ export async function solveOptimalUsdcAmount(srcChainId, srcToken, srcAmount, userAddress, depositCalldata, maxImpactPercent = X_MAX_IMPACT_PERCENT) {
557
+ // Create phony deposit calldata if not provided
558
+ const calldata = depositCalldata || createPhonyDepositCalldata(userAddress);
559
+ // Try both providers in parallel
560
+ const [relayResult, debridgeResult] = await Promise.allSettled([
561
+ solveRelayUsdcAmount(srcChainId, srcToken, srcAmount, userAddress, calldata, maxImpactPercent),
562
+ solveDebridgeUsdcAmount(srcChainId, srcToken, srcAmount, userAddress, calldata, maxImpactPercent),
563
+ ]);
564
+ // Extract results
565
+ const relayData = relayResult.status === 'fulfilled' ? relayResult.value : null;
566
+ const debridgeData = debridgeResult.status === 'fulfilled' ? debridgeResult.value : null;
567
+ // Both failed
568
+ if (!relayData && !debridgeData) {
569
+ const errors = [
570
+ relayResult.status === 'rejected' ? relayResult.reason : null,
571
+ debridgeResult.status === 'rejected' ? debridgeResult.reason : null,
572
+ ].filter(Boolean);
573
+ throw new AggregateError(errors, 'All bridge providers failed');
574
+ }
575
+ // Only one succeeded
576
+ if (!relayData) {
577
+ console.info('Using deBridge (relay failed)');
578
+ return {
579
+ usdcAmountOut: debridgeData[0],
580
+ actualAmountIn: debridgeData[1],
581
+ provider: 'debridge',
582
+ allowanceTarget: debridgeData[2],
583
+ };
584
+ }
585
+ if (!debridgeData) {
586
+ console.info('Using relay.link (deBridge failed)');
587
+ return {
588
+ usdcAmountOut: relayData[0],
589
+ actualAmountIn: relayData[1],
590
+ provider: 'relay',
591
+ allowanceTarget: relayData[2],
592
+ };
593
+ }
594
+ // Compare rates (USDC out / amount in)
595
+ const relayRate = Number(relayData[0]) / Number(relayData[1]);
596
+ const debridgeRate = Number(debridgeData[0]) / Number(debridgeData[1]);
597
+ if (relayRate >= debridgeRate) {
598
+ console.info(`Using relay.link: ${relayRate} >= ${debridgeRate}`);
599
+ return {
600
+ usdcAmountOut: relayData[0],
601
+ actualAmountIn: relayData[1],
602
+ provider: 'relay',
603
+ allowanceTarget: '',
604
+ };
605
+ }
606
+ else {
607
+ console.info(`Using deBridge: ${debridgeRate} >= ${relayRate}`);
608
+ return {
609
+ usdcAmountOut: debridgeData[0],
610
+ actualAmountIn: debridgeData[1],
611
+ provider: 'debridge',
612
+ allowanceTarget: debridgeData[2],
613
+ };
614
+ }
615
+ }
@@ -0,0 +1,35 @@
1
+ import type { WalletClient, SendTransactionParameters, Hex, PublicClient } from 'viem';
2
+ import type { Connector } from 'wagmi';
3
+ /**
4
+ * Ensure the correct chain is active
5
+ *
6
+ * @param chainId - The target chain ID
7
+ * @param walletClient - The wallet client
8
+ * @param connector - The wagmi connector for chain switching
9
+ * @returns The wallet client (after ensuring correct chain)
10
+ */
11
+ export declare function ensureChain(chainId: number, walletClient: WalletClient, connector: Connector): Promise<WalletClient>;
12
+ /**
13
+ * Wait for transaction confirmation using viem's waitForTransactionReceipt
14
+ *
15
+ * @param hash - The transaction hash to wait for
16
+ * @param publicClient - The public client to use for waiting
17
+ * @param timeoutMs - Timeout in milliseconds (default: 30000)
18
+ * @returns Promise resolving when transaction is confirmed
19
+ */
20
+ export declare function waitForTransactionConfirmation(hash: Hex, publicClient: PublicClient, timeoutMs?: number): Promise<{
21
+ status: 'success' | 'failed';
22
+ transactionHash: Hex;
23
+ }>;
24
+ /**
25
+ * Execute a transaction with confirmation waiting
26
+ *
27
+ * @param params - Transaction parameters
28
+ * @param walletClient - The wallet client
29
+ * @param publicClient - The public client to use for waiting
30
+ * @returns Promise resolving to transaction result
31
+ */
32
+ export declare function executeTransaction(params: SendTransactionParameters, walletClient: WalletClient, publicClient: PublicClient): Promise<{
33
+ hash: Hex;
34
+ status: 'success' | 'failed';
35
+ }>;