@silentswap/react 0.0.41

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 (86) hide show
  1. package/README.md +48 -0
  2. package/dist/contexts/AssetsContext.d.ts +24 -0
  3. package/dist/contexts/AssetsContext.js +83 -0
  4. package/dist/contexts/BalancesContext.d.ts +28 -0
  5. package/dist/contexts/BalancesContext.js +533 -0
  6. package/dist/contexts/OrdersContext.d.ts +53 -0
  7. package/dist/contexts/OrdersContext.js +240 -0
  8. package/dist/contexts/PricesContext.d.ts +12 -0
  9. package/dist/contexts/PricesContext.js +109 -0
  10. package/dist/contexts/SilentSwapContext.d.ts +58 -0
  11. package/dist/contexts/SilentSwapContext.js +205 -0
  12. package/dist/hooks/silent/orderTrackingWebSocketManager.d.ts +48 -0
  13. package/dist/hooks/silent/orderTrackingWebSocketManager.js +284 -0
  14. package/dist/hooks/silent/solana-transaction.d.ts +60 -0
  15. package/dist/hooks/silent/solana-transaction.js +236 -0
  16. package/dist/hooks/silent/useAuth.d.ts +90 -0
  17. package/dist/hooks/silent/useAuth.js +269 -0
  18. package/dist/hooks/silent/useBridgeExecution.d.ts +39 -0
  19. package/dist/hooks/silent/useBridgeExecution.js +877 -0
  20. package/dist/hooks/silent/useOrderSigning.d.ts +34 -0
  21. package/dist/hooks/silent/useOrderSigning.js +133 -0
  22. package/dist/hooks/silent/useOrderTracking.d.ts +174 -0
  23. package/dist/hooks/silent/useOrderTracking.js +524 -0
  24. package/dist/hooks/silent/useQuoteCalculation.d.ts +50 -0
  25. package/dist/hooks/silent/useQuoteCalculation.js +331 -0
  26. package/dist/hooks/silent/useQuoteFetching.d.ts +18 -0
  27. package/dist/hooks/silent/useQuoteFetching.js +54 -0
  28. package/dist/hooks/silent/useRefund.d.ts +26 -0
  29. package/dist/hooks/silent/useRefund.js +134 -0
  30. package/dist/hooks/silent/useSilentClient.d.ts +16 -0
  31. package/dist/hooks/silent/useSilentClient.js +32 -0
  32. package/dist/hooks/silent/useSilentOrders.d.ts +174 -0
  33. package/dist/hooks/silent/useSilentOrders.js +73 -0
  34. package/dist/hooks/silent/useSilentQuote.d.ts +88 -0
  35. package/dist/hooks/silent/useSilentQuote.js +381 -0
  36. package/dist/hooks/silent/useWallet.d.ts +76 -0
  37. package/dist/hooks/silent/useWallet.js +203 -0
  38. package/dist/hooks/useAssetPrice.d.ts +8 -0
  39. package/dist/hooks/useAssetPrice.js +47 -0
  40. package/dist/hooks/useContacts.d.ts +52 -0
  41. package/dist/hooks/useContacts.js +259 -0
  42. package/dist/hooks/useEgressEstimates.d.ts +32 -0
  43. package/dist/hooks/useEgressEstimates.js +230 -0
  44. package/dist/hooks/useHiddenSwapFees.d.ts +22 -0
  45. package/dist/hooks/useHiddenSwapFees.js +81 -0
  46. package/dist/hooks/useOrderEstimates.d.ts +37 -0
  47. package/dist/hooks/useOrderEstimates.js +393 -0
  48. package/dist/hooks/useOutputAssetInfo.d.ts +12 -0
  49. package/dist/hooks/useOutputAssetInfo.js +38 -0
  50. package/dist/hooks/usePrices.d.ts +60 -0
  51. package/dist/hooks/usePrices.js +188 -0
  52. package/dist/hooks/useQuote.d.ts +73 -0
  53. package/dist/hooks/useQuote.js +507 -0
  54. package/dist/hooks/useResetSwapForm.d.ts +16 -0
  55. package/dist/hooks/useResetSwapForm.js +68 -0
  56. package/dist/hooks/useSlippageUsd.d.ts +11 -0
  57. package/dist/hooks/useSlippageUsd.js +19 -0
  58. package/dist/hooks/useSolanaAdapter.d.ts +15 -0
  59. package/dist/hooks/useSolanaAdapter.js +55 -0
  60. package/dist/hooks/useStatus.d.ts +25 -0
  61. package/dist/hooks/useStatus.js +60 -0
  62. package/dist/hooks/useSwap.d.ts +67 -0
  63. package/dist/hooks/useSwap.js +285 -0
  64. package/dist/hooks/useTransaction.d.ts +119 -0
  65. package/dist/hooks/useTransaction.js +353 -0
  66. package/dist/hooks/useTransactionAddress.d.ts +11 -0
  67. package/dist/hooks/useTransactionAddress.js +26 -0
  68. package/dist/hooks/useUsdValue.d.ts +7 -0
  69. package/dist/hooks/useUsdValue.js +19 -0
  70. package/dist/index.d.ts +54 -0
  71. package/dist/index.js +41 -0
  72. package/dist/stories/SilentSwapOverview.stories.d.ts +10 -0
  73. package/dist/stories/SilentSwapOverview.stories.js +364 -0
  74. package/dist/stories/useAuth.stories.d.ts +6 -0
  75. package/dist/stories/useAuth.stories.js +55 -0
  76. package/dist/stories/useSilentClient.stories.d.ts +9 -0
  77. package/dist/stories/useSilentClient.stories.js +39 -0
  78. package/dist/stories/useSilentOrders.stories.d.ts +1 -0
  79. package/dist/stories/useSilentOrders.stories.js +1 -0
  80. package/dist/stories/useSilentQuote.stories.d.ts +6 -0
  81. package/dist/stories/useSilentQuote.stories.js +267 -0
  82. package/dist/stories/useTransaction.stories.d.ts +6 -0
  83. package/dist/stories/useTransaction.stories.js +121 -0
  84. package/dist/utils/formatters.d.ts +33 -0
  85. package/dist/utils/formatters.js +82 -0
  86. package/package.json +67 -0
@@ -0,0 +1,877 @@
1
+ import { useCallback } from 'react';
2
+ import { createPublicClient, http, encodeFunctionData, erc20Abi, getAddress } from 'viem';
3
+ import { ensureChain, waitForTransactionConfirmation, NI_CHAIN_ID_AVALANCHE, S0X_ADDR_USDC_AVALANCHE, X_MAX_IMPACT_PERCENT, getRelayStatus, getDebridgeStatus, fetchDebridgeOrder, fetchRelayQuote, createPhonyDepositCalldata, isSolanaAsset, parseSolanaCaip19, isSolanaNativeToken, isSplToken, N_RELAY_CHAIN_ID_SOLANA, SB58_ADDR_SOL_PROGRAM_SYSTEM, getChainById, } from '@silentswap/sdk';
4
+ import { createSolanaTransactionExecutor, convertRelaySolanaStepToTransaction } from './solana-transaction.js';
5
+ const S0X_ADDR_EVM_ZERO = '0x0000000000000000000000000000000000000000';
6
+ const XG_UINT256_MAX = (1n << 256n) - 1n;
7
+ /**
8
+ * Get normalized wallet account address from wallet client
9
+ * This ensures the address matches the account that will send transactions
10
+ *
11
+ * Note: This pattern is similar to useTransaction.ts but kept here for bridge-specific needs
12
+ */
13
+ function getWalletAccountAddress(walletClient) {
14
+ const walletAccountAddress = walletClient.account?.address;
15
+ if (!walletAccountAddress) {
16
+ throw new Error('Wallet account address is required');
17
+ }
18
+ return getAddress(walletAccountAddress);
19
+ }
20
+ /**
21
+ * Ensure chain is active and create public client
22
+ * Returns both wallet client (after chain switch) and public client
23
+ *
24
+ * Note: This follows the same pattern as useTransaction.ts but is optimized for bridge execution
25
+ * with gas estimation and confirmation waiting built-in
26
+ */
27
+ async function ensureChainAndCreateClient(chainId, walletClient, connector) {
28
+ const wallet = await ensureChain(chainId, walletClient, connector);
29
+ // Use wallet's chain if available, otherwise look up chain by ID
30
+ const chain = wallet.chain || getChainById(chainId);
31
+ if (!chain) {
32
+ throw new Error(`Unsupported chain ID: ${chainId}. Please ensure the chain is supported.`);
33
+ }
34
+ // Create public client - viem has complex union types that TypeScript struggles with
35
+ const publicClient = createPublicClient({
36
+ chain,
37
+ transport: http(),
38
+ });
39
+ return { wallet, publicClient };
40
+ }
41
+ /**
42
+ * Estimate and adjust gas fees, using the greater of estimated vs provided fees
43
+ * This matches Svelte's behavior to ensure sufficient gas fees
44
+ */
45
+ async function estimateAndAdjustGasFees(publicClient, providedMaxFeePerGas, providedMaxPriorityFeePerGas) {
46
+ if (providedMaxFeePerGas === undefined && providedMaxPriorityFeePerGas === undefined) {
47
+ return {};
48
+ }
49
+ try {
50
+ const estimatedFees = await publicClient.estimateFeesPerGas();
51
+ // Use the greater of estimated fees or provided fees (matching Svelte's bigint_greater)
52
+ const maxFeePerGas = providedMaxFeePerGas !== undefined && estimatedFees.maxFeePerGas
53
+ ? estimatedFees.maxFeePerGas > providedMaxFeePerGas
54
+ ? estimatedFees.maxFeePerGas
55
+ : providedMaxFeePerGas
56
+ : estimatedFees.maxFeePerGas || providedMaxFeePerGas;
57
+ const maxPriorityFeePerGas = providedMaxPriorityFeePerGas !== undefined && estimatedFees.maxPriorityFeePerGas
58
+ ? estimatedFees.maxPriorityFeePerGas > providedMaxPriorityFeePerGas
59
+ ? estimatedFees.maxPriorityFeePerGas
60
+ : providedMaxPriorityFeePerGas
61
+ : estimatedFees.maxPriorityFeePerGas || providedMaxPriorityFeePerGas;
62
+ return { maxFeePerGas, maxPriorityFeePerGas };
63
+ }
64
+ catch (error) {
65
+ // If gas estimation fails, use provided fees (or let wallet handle it)
66
+ console.warn('Failed to estimate gas fees, using provided values:', {
67
+ error: error instanceof Error ? error.message : String(error),
68
+ });
69
+ return {
70
+ maxFeePerGas: providedMaxFeePerGas,
71
+ maxPriorityFeePerGas: providedMaxPriorityFeePerGas,
72
+ };
73
+ }
74
+ }
75
+ /**
76
+ * Send EVM transaction and wait for confirmation
77
+ * Handles chain switching, gas estimation, and confirmation waiting
78
+ *
79
+ * Note: This is more feature-rich than createTransactionExecutor from the SDK
80
+ * because it includes gas fee estimation and transaction confirmation,
81
+ * which are essential for bridge execution reliability.
82
+ *
83
+ * Similar pattern to useTransaction.ts executeSwapTransaction but optimized
84
+ * for bridge execution with gas estimation built-in.
85
+ */
86
+ async function sendTransactionAndWait(walletClient, connector, chainId, transaction, setCurrentStep, onStatus) {
87
+ setCurrentStep?.('Switching to chain...');
88
+ onStatus?.('Switching to chain...');
89
+ const { publicClient } = await ensureChainAndCreateClient(chainId, walletClient, connector);
90
+ // Convert string gas values to bigint
91
+ let maxFeePerGas;
92
+ let maxPriorityFeePerGas;
93
+ if (transaction.maxFeePerGas) {
94
+ maxFeePerGas = BigInt(transaction.maxFeePerGas);
95
+ }
96
+ if (transaction.maxPriorityFeePerGas) {
97
+ maxPriorityFeePerGas = BigInt(transaction.maxPriorityFeePerGas);
98
+ }
99
+ // Estimate and adjust gas fees if provided
100
+ if (maxFeePerGas !== undefined || maxPriorityFeePerGas !== undefined) {
101
+ setCurrentStep?.('Estimating gas...');
102
+ onStatus?.('Estimating gas...');
103
+ const adjustedFees = await estimateAndAdjustGasFees(publicClient, maxFeePerGas, maxPriorityFeePerGas);
104
+ maxFeePerGas = adjustedFees.maxFeePerGas;
105
+ maxPriorityFeePerGas = adjustedFees.maxPriorityFeePerGas;
106
+ }
107
+ setCurrentStep?.('Sending transaction...');
108
+ onStatus?.('Sending transaction...');
109
+ const hash = await walletClient.sendTransaction({
110
+ account: walletClient.account,
111
+ chain: null,
112
+ to: transaction.to,
113
+ value: BigInt(transaction.value || '0'),
114
+ data: transaction.data,
115
+ gas: transaction.gas ? BigInt(transaction.gas) : undefined,
116
+ maxFeePerGas,
117
+ maxPriorityFeePerGas,
118
+ });
119
+ setCurrentStep?.('Waiting for confirmation...');
120
+ onStatus?.('Waiting for confirmation...');
121
+ const receipt = await waitForTransactionConfirmation(hash, publicClient);
122
+ if (receipt.status !== 'success') {
123
+ console.error('Transaction failed:', {
124
+ hash: receipt.transactionHash,
125
+ status: receipt.status,
126
+ });
127
+ // Note: We return the receipt even if failed, allowing caller to decide whether to throw
128
+ }
129
+ return { hash, receipt };
130
+ }
131
+ /**
132
+ * Get relay.link origin asset parameters from CAIP-19
133
+ */
134
+ function getRelayOriginAsset(caip19) {
135
+ if (!isSolanaAsset(caip19)) {
136
+ throw new Error('Only Solana assets are supported by this helper');
137
+ }
138
+ const parsed = parseSolanaCaip19(caip19);
139
+ if (!parsed) {
140
+ throw new Error(`Invalid Solana CAIP-19: ${caip19}`);
141
+ }
142
+ const originCurrency = isSolanaNativeToken(caip19)
143
+ ? SB58_ADDR_SOL_PROGRAM_SYSTEM
144
+ : isSplToken(caip19)
145
+ ? parsed.tokenAddress ||
146
+ (() => {
147
+ throw new Error(`Missing token address in Solana CAIP-19: ${caip19}`);
148
+ })()
149
+ : (() => {
150
+ throw new Error(`Unsupported Solana asset type: ${caip19}`);
151
+ })();
152
+ return {
153
+ originChainId: N_RELAY_CHAIN_ID_SOLANA,
154
+ originCurrency,
155
+ };
156
+ }
157
+ // Depositor ABI for encoding deposit calldata
158
+ const DEPOSITOR_ABI = [
159
+ {
160
+ inputs: [
161
+ {
162
+ components: [
163
+ { internalType: 'address', name: 'signer', type: 'address' },
164
+ { internalType: 'bytes32', name: 'orderId', type: 'bytes32' },
165
+ { internalType: 'address', name: 'notary', type: 'address' },
166
+ { internalType: 'address', name: 'approver', type: 'address' },
167
+ { internalType: 'bytes', name: 'orderApproval', type: 'bytes' },
168
+ { internalType: 'uint256', name: 'approvalExpiration', type: 'uint256' },
169
+ { internalType: 'uint256', name: 'duration', type: 'uint256' },
170
+ { internalType: 'bytes32', name: 'domainSepHash', type: 'bytes32' },
171
+ { internalType: 'bytes32', name: 'payloadHash', type: 'bytes32' },
172
+ { internalType: 'bytes', name: 'typedDataSignature', type: 'bytes' },
173
+ { internalType: 'bytes', name: 'receiveAuthorization', type: 'bytes' },
174
+ ],
175
+ internalType: 'struct SilentSwapV2Gateway.DepositParams',
176
+ name: 'params',
177
+ type: 'tuple',
178
+ },
179
+ ],
180
+ name: 'depositProxy2',
181
+ outputs: [],
182
+ stateMutability: 'nonpayable',
183
+ type: 'function',
184
+ },
185
+ ];
186
+ /**
187
+ * Hook for executing bridge transactions (Solana → Avalanche or EVM → Avalanche)
188
+ *
189
+ * This hook handles the complete bridge execution flow:
190
+ * - Solving for optimal USDC amount (if not provided)
191
+ * - Fetching quotes from both relay.link and deBridge
192
+ * - Selecting the best provider based on rates
193
+ * - Executing bridge transactions (Solana or EVM)
194
+ * - Monitoring bridge status until deposit arrives
195
+ *
196
+ * Supports both Solana and EVM source chains, automatically selecting
197
+ * the best bridge provider (relay.link or deBridge) based on rates.
198
+ *
199
+ * @param walletClient - Wallet client for EVM transactions
200
+ * @param connector - Wagmi connector for chain switching
201
+ * @param solanaConnector - Solana wallet connector (required for Solana swaps)
202
+ * @param solanaConnection - Solana RPC connection (required for Solana swaps)
203
+ * @param solanaRpcUrl - Optional Solana RPC URL
204
+ * @param setCurrentStep - Callback to set current step message
205
+ * @param onStatus - Optional status update callback
206
+ * @returns Functions for executing bridge transactions
207
+ */
208
+ export function useBridgeExecution(walletClient, connector, solanaConnector, solanaConnection, solanaRpcUrl, setCurrentStep, depositorAddress, onStatus) {
209
+ /**
210
+ * Execute Solana bridge transaction
211
+ *
212
+ * Handles bridge execution for Solana source assets:
213
+ * 1. Solves for optimal USDC amount (if not provided)
214
+ * 2. Gets quotes from both relay.link and deBridge
215
+ * 3. Selects best provider
216
+ * 4. Executes Solana transactions
217
+ * 5. Monitors bridge status
218
+ *
219
+ * @param sourceAsset - Source asset CAIP-19 (Solana)
220
+ * @param sourceAmount - Source amount in token units
221
+ * @param usdcAmount - Optional USDC amount (will be solved if not provided)
222
+ * @param depositParams - Deposit parameters from order response
223
+ * @param solanaSenderAddress - Solana sender address (base58)
224
+ * @param evmSignerAddress - EVM signer address for deposit operations
225
+ * @returns Promise resolving to bridge execution result
226
+ */
227
+ const executeSolanaBridge = useCallback(async (sourceAsset, sourceAmount, usdcAmount, solanaSenderAddress, evmSignerAddress, depositParams) => {
228
+ try {
229
+ if (!solanaConnector || !solanaConnection) {
230
+ throw new Error('Solana connector and connection are required for Solana swaps');
231
+ }
232
+ // Get relay origin asset parameters
233
+ const { originChainId, originCurrency } = getRelayOriginAsset(sourceAsset);
234
+ // CRITICAL: In Svelte, solve_uusdc_amount is called ONCE before order creation,
235
+ // and the result (usdcAmount) is used directly in bridge execution.
236
+ // We should NEVER solve again here - the usdcAmount must be provided
237
+ // from the initial solve in handleGetQuote (matching Svelte Form.svelte line 2535 and 2576-2584).
238
+ if (!usdcAmount) {
239
+ throw new Error('USDC amount is required for Solana bridge execution. ' +
240
+ 'It should be provided from the initial solveOptimalUsdcAmount call in handleGetQuote. ' +
241
+ 'This matches Svelte behavior where solve_uusdc_amount is called once before order creation.');
242
+ }
243
+ // Use the provided usdcAmount directly (matches Svelte behavior)
244
+ // In Svelte: zg_amount_src_usdc is used directly without re-solving
245
+ const bridgeUsdcAmount = usdcAmount;
246
+ setCurrentStep('Fetching bridge quote');
247
+ onStatus?.('Fetching bridge quote');
248
+ // Encode USDC on Avalanche approval calldata (matches Svelte line 896-903)
249
+ const approveUsdcCalldata = encodeFunctionData({
250
+ abi: erc20Abi,
251
+ functionName: 'approve',
252
+ args: [depositorAddress, XG_UINT256_MAX],
253
+ });
254
+ // Encode depositProxy2 calldata from depositParams (matches Svelte line 914-925)
255
+ // CRITICAL: The signer must match EXACTLY (including checksum) the one used in the SilentSwap quote request
256
+ // Use getAddress() to ensure checksummed format matches quote request
257
+ // In Svelte (line 919-922): spreads g_params and only overrides signer, approvalExpiration, and duration
258
+ // IMPORTANT: The approver is NOT overridden - it comes from the API response (g_params.approver) and is used as-is
259
+ let depositCalldataForExecution;
260
+ if (depositParams) {
261
+ const checksummedSigner = getAddress(evmSignerAddress);
262
+ const finalDepositParams = {
263
+ ...depositParams,
264
+ signer: checksummedSigner,
265
+ // Convert to BigInt to match Svelte's BigInt() conversion (lines 921-922)
266
+ approvalExpiration: typeof depositParams.approvalExpiration === 'bigint'
267
+ ? depositParams.approvalExpiration
268
+ : BigInt(String(depositParams.approvalExpiration)),
269
+ duration: typeof depositParams.duration === 'bigint'
270
+ ? depositParams.duration
271
+ : BigInt(String(depositParams.duration)),
272
+ // NOTE: approver is NOT overridden - it comes from depositParams as-is (matches Svelte line 919)
273
+ };
274
+ depositCalldataForExecution = encodeFunctionData({
275
+ abi: DEPOSITOR_ABI,
276
+ functionName: 'depositProxy2',
277
+ args: [finalDepositParams],
278
+ });
279
+ }
280
+ else {
281
+ // Fallback to phony calldata if depositParams not provided (should not happen in normal flow)
282
+ console.warn('DepositParams not provided, using phony calldata for bridge quote');
283
+ depositCalldataForExecution = createPhonyDepositCalldata(evmSignerAddress);
284
+ }
285
+ // Request bridge quote for Solana → USDC Avalanche using EXACT_OUTPUT (matches Svelte line 1302-1323)
286
+ // CRITICAL: For execution, we must use EXACT_OUTPUT with txs parameter, not getBridgeQuote
287
+ // getBridgeQuote uses EXACT_INPUT and doesn't support txs parameter for execution
288
+ const relayQuote = await fetchRelayQuote({
289
+ user: solanaSenderAddress, // Solana address (matches Svelte line 1303: user: sb58_pk_sender)
290
+ originChainId, // Matches Svelte line 1304: ...relay_origin_asset(k_asset_src)
291
+ originCurrency, // Matches Svelte line 1304: ...relay_origin_asset(k_asset_src)
292
+ destinationChainId: NI_CHAIN_ID_AVALANCHE, // Matches Svelte line 1305
293
+ destinationCurrency: S0X_ADDR_USDC_AVALANCHE, // Matches Svelte line 1306
294
+ amount: bridgeUsdcAmount, // USDC amount in micro units (EXACT_OUTPUT target) - matches Svelte line 1307
295
+ recipient: depositorAddress, // EVM depositor address (matches Svelte line 1308: recipient: S0X_ADDR_DEPOSITOR)
296
+ tradeType: 'EXACT_OUTPUT', // CRITICAL: Must use EXACT_OUTPUT for execution (matches Svelte line 1309)
297
+ txsGasLimit: 600_000, // Matches Svelte line 1310
298
+ txs: [
299
+ {
300
+ to: S0X_ADDR_USDC_AVALANCHE, // Matches Svelte line 1313
301
+ value: '0', // Matches Svelte line 1314
302
+ data: approveUsdcCalldata, // USDC approval calldata (matches Svelte line 1315)
303
+ },
304
+ {
305
+ to: depositorAddress, // Matches Svelte line 1318: to: S0X_ADDR_DEPOSITOR
306
+ value: '0', // Matches Svelte line 1319
307
+ data: depositCalldataForExecution, // Deposit calldata (matches Svelte line 1320)
308
+ },
309
+ ],
310
+ });
311
+ // Check price impact (matches Svelte line 1343-1348)
312
+ const impactPercent = Number(relayQuote.details.totalImpact.percent);
313
+ // TODO: check negative impact
314
+ if (impactPercent > X_MAX_IMPACT_PERCENT) {
315
+ throw new Error(`Price impact across bridge too high: ${impactPercent.toFixed(2)}%`);
316
+ }
317
+ const rawResponse = relayQuote;
318
+ const selectedProvider = 'relay';
319
+ // Create Solana transaction executor
320
+ const solanaExecutor = createSolanaTransactionExecutor(solanaConnector, solanaConnection);
321
+ setCurrentStep('Executing bridge transaction');
322
+ onStatus?.('Executing bridge transaction');
323
+ // Execute transactions based on selected provider
324
+ if (selectedProvider === 'relay') {
325
+ const relayQuote = rawResponse;
326
+ if (!relayQuote.steps) {
327
+ throw new Error('No steps in relay quote response');
328
+ }
329
+ // Execute each step from relay.link
330
+ for (const step of relayQuote.steps) {
331
+ if (step.kind !== 'transaction') {
332
+ throw new Error(`Unsupported relay step kind: ${step.kind}`);
333
+ }
334
+ if (step.items.length > 1) {
335
+ throw new Error('Multiple items in transaction step not implemented');
336
+ }
337
+ const item = step.items[0];
338
+ const itemData = item.data;
339
+ // Solana transaction
340
+ if ('instructions' in itemData) {
341
+ setCurrentStep(step.id === 'approve'
342
+ ? 'Requesting approval...'
343
+ : step.id === 'deposit'
344
+ ? 'Requesting deposit...'
345
+ : 'Requesting bridge...');
346
+ onStatus?.(step.id === 'approve'
347
+ ? 'Requesting approval...'
348
+ : step.id === 'deposit'
349
+ ? 'Requesting deposit...'
350
+ : 'Requesting bridge...');
351
+ // Convert relay step to BridgeTransaction
352
+ const solanaTx = convertRelaySolanaStepToTransaction(itemData, solanaSenderAddress, N_RELAY_CHAIN_ID_SOLANA);
353
+ // Execute Solana transaction
354
+ await solanaExecutor(solanaTx);
355
+ }
356
+ }
357
+ // Find request ID for status monitoring
358
+ const requestId = relayQuote.steps.find((s) => s.requestId)?.requestId;
359
+ if (!requestId) {
360
+ throw new Error('Missing relay.link request ID');
361
+ }
362
+ // Monitor bridge status
363
+ const depositTxHash = await monitorRelayBridgeStatus(requestId, setCurrentStep, onStatus);
364
+ return {
365
+ depositTxHash: depositTxHash,
366
+ provider: 'relay',
367
+ };
368
+ }
369
+ else if (selectedProvider === 'debridge') {
370
+ // DeBridge Solana execution not yet fully implemented
371
+ throw new Error('DeBridge Solana transaction execution not yet fully implemented');
372
+ }
373
+ else {
374
+ throw new Error(`Unsupported bridge provider: ${selectedProvider}`);
375
+ }
376
+ }
377
+ catch (error) {
378
+ console.error('Solana bridge execution failed:', {
379
+ sourceAsset,
380
+ sourceAmount,
381
+ usdcAmount,
382
+ solanaSenderAddress,
383
+ evmSignerAddress,
384
+ error: error instanceof Error ? error.message : String(error),
385
+ stack: error instanceof Error ? error.stack : undefined,
386
+ });
387
+ throw error;
388
+ }
389
+ }, [solanaConnector, solanaConnection, solanaRpcUrl, setCurrentStep, onStatus, depositorAddress]);
390
+ /**
391
+ * Execute EVM bridge transaction
392
+ *
393
+ * Handles bridge execution for EVM source assets:
394
+ * 1. Solves for optimal USDC amount (if not provided)
395
+ * 2. Gets quotes from both relay.link and deBridge
396
+ * 3. Selects best provider
397
+ * 4. Executes EVM transactions
398
+ * 5. Monitors bridge status
399
+ *
400
+ * @param sourceChainId - Source chain ID
401
+ * @param sourceTokenAddress - Source token address (0x0 for native)
402
+ * @param sourceAmount - Source amount in token units
403
+ * @param usdcAmount - Optional USDC amount (will be solved if not provided)
404
+ * @param depositParams - Deposit parameters from order response
405
+ * @param evmSignerAddress - EVM signer address (must match quote request signer, used for deposit calldata)
406
+ * @param provider - Bridge provider to use
407
+ * @param evmSenderAddress - Optional EVM sender address (used for bridge quotes, defaults to signer address)
408
+ * @returns Promise resolving to bridge execution result
409
+ */
410
+ const executeEvmBridge = useCallback(async (sourceChainId, sourceTokenAddress, sourceAmount, usdcAmount, depositParams, evmSignerAddress, // EVM signer address (must match quote request signer, used for deposit calldata)
411
+ evmSenderAddress, // Optional EVM sender address (used for bridge quotes, matches Svelte's s0x_sender)
412
+ provider) => {
413
+ try {
414
+ if (!walletClient || !connector) {
415
+ throw new Error('Wallet client and connector required for EVM bridge execution');
416
+ }
417
+ if (!provider) {
418
+ throw new Error('Provider is required for EVM bridge execution');
419
+ }
420
+ // CRITICAL: The user address in bridge quotes MUST match the wallet account address
421
+ // that will actually send the transaction. This is required for relay.link to properly
422
+ // route and execute the transaction. In Svelte: s0x_sender matches y_viem.account.address
423
+ const evmUserForBridge = getWalletAccountAddress(walletClient);
424
+ // Verify that wallet account matches expected sender/signer (for debugging)
425
+ // In most cases they should match, but we use wallet account as the source of truth
426
+ if (evmSenderAddress && getAddress(evmSenderAddress) !== evmUserForBridge) {
427
+ console.warn(`Wallet account address (${evmUserForBridge}) differs from sender address (${evmSenderAddress}). Using wallet account address.`);
428
+ }
429
+ if (getAddress(evmSignerAddress) !== evmUserForBridge) {
430
+ console.warn(`Wallet account address (${evmUserForBridge}) differs from signer address (${evmSignerAddress}). Using wallet account address for bridge quotes.`);
431
+ }
432
+ // Encode USDC on Avalanche approval calldata
433
+ const approveUsdcCalldata = encodeFunctionData({
434
+ abi: erc20Abi,
435
+ functionName: 'approve',
436
+ args: [depositorAddress, XG_UINT256_MAX],
437
+ });
438
+ // Match Svelte implementation exactly: spread g_params, then override signer, approvalExpiration, and duration
439
+ // CRITICAL: The signer must match EXACTLY (including checksum) the one used in the SilentSwap quote request
440
+ // Use getAddress() to ensure checksummed format matches quote request (see useQuoteFetching.ts line 327)
441
+ // In Svelte (line 919-922): spreads g_params and only overrides signer, approvalExpiration, and duration
442
+ // IMPORTANT: The approver is NOT overridden - it comes from the API response (g_params.approver) and is used as-is
443
+ const checksummedSigner = getAddress(evmSignerAddress);
444
+ const finalDepositParams = {
445
+ ...depositParams,
446
+ signer: checksummedSigner,
447
+ // Convert to BigInt to match Svelte's BigInt() conversion (lines 921-922)
448
+ // These should already be numbers/strings from the API, but ensure BigInt conversion
449
+ approvalExpiration: typeof depositParams.approvalExpiration === 'bigint'
450
+ ? depositParams.approvalExpiration
451
+ : BigInt(String(depositParams.approvalExpiration)),
452
+ duration: typeof depositParams.duration === 'bigint' ? depositParams.duration : BigInt(String(depositParams.duration)),
453
+ // NOTE: approver is NOT overridden - it comes from depositParams as-is (matches Svelte line 919)
454
+ };
455
+ const isSourceNative = sourceTokenAddress === S0X_ADDR_EVM_ZERO;
456
+ // CRITICAL: In Svelte, solve_uusdc_amount is called ONCE before order creation,
457
+ // and the result (usdcAmount + provider) is used directly in bridge execution.
458
+ // We should NEVER solve again here - the usdcAmount and provider must be provided
459
+ // from the initial solve in handleGetQuote (matching Svelte Form.svelte line 2535 and 2576-2584).
460
+ if (!usdcAmount) {
461
+ throw new Error('USDC amount is required for bridge execution. ' +
462
+ 'It should be provided from the initial solveOptimalUsdcAmount call in handleGetQuote. ' +
463
+ 'This matches Svelte behavior where solve_uusdc_amount is called once before order creation.');
464
+ }
465
+ if (!provider) {
466
+ throw new Error('Provider is required for bridge execution. ' +
467
+ 'It should be provided from the initial solveOptimalUsdcAmount call in handleGetQuote. ' +
468
+ 'This matches Svelte behavior where the provider is selected once during solve_uusdc_amount.');
469
+ }
470
+ // Use the provided usdcAmount and provider directly (matches Svelte behavior)
471
+ // In Svelte: zg_amount_src_usdc and s_source are used directly without re-solving
472
+ // (see Svelte silentswap.ts lines 990-1012 for relay, 1015-1039 for deBridge)
473
+ const bridgeUsdcAmount = usdcAmount;
474
+ const selectedProvider = provider;
475
+ // Encode depositProxy2 calldata - use the same calldata for both relay and deBridge (matches Svelte)
476
+ const depositCalldata = encodeFunctionData({
477
+ abi: DEPOSITOR_ABI,
478
+ functionName: 'depositProxy2',
479
+ args: [finalDepositParams],
480
+ });
481
+ // Handle bridge execution based on selected provider
482
+ if (selectedProvider === 'debridge') {
483
+ // Note: allowanceTarget is not needed here because approval is already handled
484
+ // in executeSwap before bridge execution (matching Svelte silentswap.ts line 985-986).
485
+ // DeBridge will provide its own allowanceTarget from the response if needed.
486
+ return await executeDebridgeBridge(sourceChainId, sourceTokenAddress, isSourceNative, bridgeUsdcAmount, depositCalldata, evmSignerAddress, // Use EVM signer address for deposit calldata
487
+ '', // allowanceTarget not needed - approval handled before bridge execution
488
+ depositorAddress, // Pass environment-based depositor address
489
+ walletClient, connector, setCurrentStep, onStatus);
490
+ }
491
+ else if (selectedProvider === 'relay') {
492
+ return await executeRelayBridge(walletClient, connector, sourceChainId, sourceTokenAddress, isSourceNative, bridgeUsdcAmount, approveUsdcCalldata, depositCalldata, evmUserForBridge, // Use wallet account address for bridge quotes
493
+ depositorAddress, // Pass environment-based depositor address
494
+ setCurrentStep, onStatus);
495
+ }
496
+ else {
497
+ throw new Error(`Unsupported bridge provider: ${selectedProvider}`);
498
+ }
499
+ }
500
+ catch (error) {
501
+ console.error('EVM bridge execution failed:', {
502
+ sourceChainId,
503
+ sourceTokenAddress,
504
+ sourceAmount,
505
+ usdcAmount,
506
+ provider,
507
+ evmSignerAddress,
508
+ evmSenderAddress,
509
+ error: error instanceof Error ? error.message : String(error),
510
+ stack: error instanceof Error ? error.stack : undefined,
511
+ });
512
+ throw error;
513
+ }
514
+ }, [walletClient, connector, setCurrentStep, onStatus, depositorAddress]);
515
+ return {
516
+ executeSolanaBridge,
517
+ executeEvmBridge,
518
+ };
519
+ }
520
+ /**
521
+ * Execute relay.link bridge transaction for EVM
522
+ */
523
+ async function executeRelayBridge(walletClient, connector, sourceChainId, sourceTokenAddress, isSourceNative, bridgeUsdcAmount, approveUsdcCalldata, depositCalldata, evmSenderAddress, // Optional EVM sender address (matches Svelte's s0x_sender, used for bridge quotes)
524
+ depositorAddress, // Environment-based depositor address
525
+ setCurrentStep, onStatus) {
526
+ console.log('executeRelayBridge:', {
527
+ sourceChainId,
528
+ sourceTokenAddress,
529
+ isSourceNative,
530
+ bridgeUsdcAmount,
531
+ evmSenderAddress,
532
+ depositorAddress,
533
+ });
534
+ setCurrentStep('Executing bridge transaction');
535
+ onStatus?.('Executing bridge transaction');
536
+ // Request relay.link quote (matching Svelte app parameters)
537
+ // Note: Svelte's create_relay_deposit does NOT include referrer for execution quotes
538
+ // referrer is only used for estimates/solving, not for actual execution
539
+ // In Svelte: user: s0x_sender (sender address) - matches line 992 in silentswap.ts
540
+ // CRITICAL: The user address must match the wallet account address that will send the transaction
541
+ // This ensures relay.link can properly route and execute the transaction
542
+ const evmUserForBridge = getWalletAccountAddress(walletClient);
543
+ const relayQuote = await fetchRelayQuote({
544
+ user: evmUserForBridge, // Use wallet account address (must match transaction sender)
545
+ originChainId: sourceChainId,
546
+ destinationChainId: NI_CHAIN_ID_AVALANCHE,
547
+ originCurrency: isSourceNative ? S0X_ADDR_EVM_ZERO : sourceTokenAddress,
548
+ destinationCurrency: S0X_ADDR_USDC_AVALANCHE,
549
+ amount: bridgeUsdcAmount, // Already a string, no need for BigInt conversion
550
+ recipient: depositorAddress,
551
+ tradeType: 'EXACT_OUTPUT',
552
+ txsGasLimit: 600_000,
553
+ txs: [
554
+ {
555
+ to: S0X_ADDR_USDC_AVALANCHE,
556
+ value: '0',
557
+ data: approveUsdcCalldata,
558
+ },
559
+ {
560
+ to: depositorAddress,
561
+ value: '0',
562
+ data: depositCalldata,
563
+ },
564
+ ],
565
+ });
566
+ // Check price impact
567
+ const impactPercent = Number(relayQuote.details.totalImpact.percent);
568
+ // TODO: check negative impact
569
+ if (impactPercent > X_MAX_IMPACT_PERCENT) {
570
+ throw new Error(`Price impact across bridge too high: ${impactPercent.toFixed(2)}%`);
571
+ }
572
+ // Execute each step from relay.link
573
+ if (!relayQuote.steps) {
574
+ throw new Error('No steps in relay quote response');
575
+ }
576
+ for (const step of relayQuote.steps) {
577
+ if (step.kind !== 'transaction') {
578
+ throw new Error(`Unsupported relay step kind: ${step.kind}`);
579
+ }
580
+ if (step.items.length > 1) {
581
+ throw new Error('Multiple items in transaction step not implemented');
582
+ }
583
+ const item = step.items[0];
584
+ const itemData = item.data;
585
+ // EVM transaction
586
+ if ('chainId' in itemData && typeof itemData.chainId === 'number') {
587
+ const stepMessage = step.id === 'approve'
588
+ ? 'Requesting approval...'
589
+ : step.id === 'deposit'
590
+ ? 'Requesting deposit...'
591
+ : 'Requesting bridge...';
592
+ setCurrentStep(stepMessage);
593
+ onStatus?.(stepMessage);
594
+ console.log('Sending EVM transaction:', {
595
+ stepId: step.id,
596
+ chainId: itemData.chainId,
597
+ to: itemData.to,
598
+ value: itemData.value,
599
+ gas: itemData.gas,
600
+ });
601
+ const { hash, receipt } = await sendTransactionAndWait(walletClient, connector, itemData.chainId, {
602
+ to: itemData.to,
603
+ value: itemData.value || '0',
604
+ data: itemData.data,
605
+ gas: itemData.gas,
606
+ maxFeePerGas: itemData.maxFeePerGas,
607
+ maxPriorityFeePerGas: itemData.maxPriorityFeePerGas,
608
+ }, setCurrentStep, onStatus);
609
+ if (receipt.status === 'success') {
610
+ console.log('Transaction confirmed:', { stepId: step.id, hash: receipt.transactionHash });
611
+ }
612
+ else {
613
+ // Transaction confirmation failed or timed out, but transaction may still be pending
614
+ // Log warning and continue - status monitoring will check if it succeeded
615
+ console.warn('Transaction confirmation failed or timed out, proceeding to status checks:', {
616
+ stepId: step.id,
617
+ hash: receipt.transactionHash,
618
+ status: receipt.status,
619
+ });
620
+ }
621
+ }
622
+ }
623
+ // Find request ID for status monitoring
624
+ const requestId = relayQuote.steps.find((s) => s.requestId)?.requestId;
625
+ if (!requestId) {
626
+ throw new Error('Missing relay.link request ID');
627
+ }
628
+ // Monitor bridge status
629
+ const depositTxHash = await monitorRelayBridgeStatus(requestId, setCurrentStep, onStatus);
630
+ return {
631
+ depositTxHash: depositTxHash,
632
+ provider: 'relay',
633
+ };
634
+ }
635
+ /**
636
+ * Execute deBridge bridge transaction for EVM
637
+ */
638
+ async function executeDebridgeBridge(sourceChainId, sourceTokenAddress, isSourceNative, bridgeUsdcAmount, depositCalldata, evmSignerAddress, // EVM signer address (matches Svelte's s0x_signer, used for deposit calldata)
639
+ allowanceTarget, depositorAddress, // Environment-based depositor address
640
+ walletClient, connector, setCurrentStep, onStatus) {
641
+ setCurrentStep('Fetching bridge quote');
642
+ onStatus?.('Fetching bridge quote...');
643
+ // Request deBridge quote
644
+ let debridgeQuote;
645
+ let debridgeResponse;
646
+ let debridgeTx;
647
+ let retryCount = 0;
648
+ const maxRetries = 2;
649
+ // CRITICAL: The account address must match the wallet account address that will send the transaction
650
+ // In Svelte: s0x_sender is used for bridge quotes (line 1020), s0x_signer is used for deposit calldata
651
+ // For deBridge, we need to use the wallet account address, not the extracted sender address
652
+ // This ensures the transaction can be properly executed by the connected wallet
653
+ const evmUserForBridge = getWalletAccountAddress(walletClient);
654
+ while (retryCount < maxRetries) {
655
+ debridgeQuote = await fetchDebridgeOrder({
656
+ account: evmUserForBridge, // Use wallet account address (must match transaction sender)
657
+ srcChainId: sourceChainId,
658
+ dstChainId: NI_CHAIN_ID_AVALANCHE,
659
+ srcChainTokenIn: isSourceNative ? S0X_ADDR_EVM_ZERO : sourceTokenAddress,
660
+ dstChainTokenOut: S0X_ADDR_USDC_AVALANCHE,
661
+ srcChainTokenInAmount: 'auto',
662
+ dstChainTokenOutAmount: `${BigInt(bridgeUsdcAmount)}`,
663
+ dstChainTokenOutRecipient: depositorAddress,
664
+ prependOperatingExpenses: true,
665
+ enableEstimate: true,
666
+ srcChainOrderAuthorityAddress: evmUserForBridge, // Use wallet account address (must match transaction sender)
667
+ srcChainRefundAddress: evmUserForBridge, // Use wallet account address (must match transaction sender)
668
+ dstChainOrderAuthorityAddress: evmUserForBridge, // Use wallet account address (must match transaction sender)
669
+ dlnHook: JSON.stringify({
670
+ type: 'evm_transaction_call',
671
+ data: {
672
+ to: depositorAddress,
673
+ calldata: depositCalldata,
674
+ gas: 500_000,
675
+ },
676
+ }),
677
+ });
678
+ debridgeTx = debridgeQuote.tx;
679
+ debridgeResponse = debridgeQuote;
680
+ // Check if approval is needed
681
+ const targetAllowance = debridgeTx.allowanceTarget || allowanceTarget;
682
+ if (targetAllowance && !isSourceNative) {
683
+ const allowanceValue = debridgeTx.allowanceValue;
684
+ if (!allowanceValue) {
685
+ throw new Error('DeBridge response missing allowance value');
686
+ }
687
+ setCurrentStep('Approving token spending');
688
+ onStatus?.('Approving token spending');
689
+ // Switch to source chain for approval
690
+ const { publicClient } = await ensureChainAndCreateClient(sourceChainId, walletClient, connector);
691
+ const currentAllowance = (await publicClient.readContract({
692
+ address: sourceTokenAddress,
693
+ abi: erc20Abi,
694
+ functionName: 'allowance',
695
+ args: [evmSignerAddress, targetAllowance],
696
+ }));
697
+ // Approve if needed
698
+ if (currentAllowance < BigInt(allowanceValue)) {
699
+ const hash = await walletClient.writeContract({
700
+ address: sourceTokenAddress,
701
+ abi: erc20Abi,
702
+ functionName: 'approve',
703
+ args: [targetAllowance, BigInt(allowanceValue)],
704
+ account: walletClient.account,
705
+ chain: null,
706
+ });
707
+ await waitForTransactionConfirmation(hash, publicClient);
708
+ }
709
+ retryCount++;
710
+ continue;
711
+ }
712
+ // Check price impact
713
+ const impactPercent = (100 * (debridgeQuote.usdPriceImpact ?? 0)) / (debridgeQuote.estimation.srcChainTokenIn.approximateUsdValue || 1);
714
+ // TODO: check negative impact
715
+ if (impactPercent > X_MAX_IMPACT_PERCENT) {
716
+ throw new Error(`Price impact across bridge too high: ${impactPercent.toFixed(2)}%`);
717
+ }
718
+ break;
719
+ }
720
+ if (!debridgeQuote) {
721
+ throw new Error('Failed to get deBridge quote after approvals');
722
+ }
723
+ setCurrentStep('Switching to source chain');
724
+ onStatus?.('Switching to source chain');
725
+ setCurrentStep('Sending bridge transaction');
726
+ onStatus?.('Sending bridge transaction');
727
+ // Send deBridge transaction
728
+ console.log('Sending deBridge transaction:', {
729
+ to: debridgeQuote.tx.to,
730
+ value: debridgeQuote.tx.value,
731
+ sourceChainId,
732
+ });
733
+ const { hash: debridgeHash, receipt } = await sendTransactionAndWait(walletClient, connector, sourceChainId, {
734
+ to: debridgeQuote.tx.to,
735
+ value: debridgeQuote.tx.value || '0',
736
+ data: debridgeQuote.tx.data,
737
+ }, setCurrentStep, onStatus);
738
+ if (receipt.status !== 'success') {
739
+ throw new Error(`DeBridge transaction failed: ${receipt.transactionHash}`);
740
+ }
741
+ console.log('DeBridge transaction confirmed:', { hash: receipt.transactionHash });
742
+ setCurrentStep('Waiting for bridge');
743
+ onStatus?.('Waiting for bridge');
744
+ // Monitor deBridge status
745
+ const debridgeOrderId = debridgeResponse.orderId;
746
+ if (!debridgeOrderId) {
747
+ throw new Error('Missing deBridge order ID');
748
+ }
749
+ // Poll for deBridge status with exponential backoff
750
+ let depositTxHash;
751
+ const startTime = Date.now();
752
+ const timeout = 10 * 60 * 1000; // 10 minutes (bridges can take several minutes)
753
+ let pollInterval = 2_000; // Start with 2 seconds
754
+ const maxPollInterval = 10_000; // Max 10 seconds between polls
755
+ let attemptCount = 0;
756
+ while (Date.now() - startTime < timeout) {
757
+ attemptCount++;
758
+ try {
759
+ const status = await getDebridgeStatus(debridgeOrderId);
760
+ if (status.status === 'success') {
761
+ depositTxHash = status.txHashes?.[0] || '0x';
762
+ break;
763
+ }
764
+ else if (status.status === 'failed' || status.status === 'refund') {
765
+ throw new Error(`DeBridge transaction failed: ${status.details || 'Unknown error'}`);
766
+ }
767
+ }
768
+ catch (error) {
769
+ // If error is thrown from status check, re-throw it
770
+ if (error instanceof Error && error.message.includes('DeBridge transaction failed')) {
771
+ throw error;
772
+ }
773
+ // Otherwise log and continue polling
774
+ console.error('Failed to fetch deBridge status:', {
775
+ debridgeOrderId,
776
+ error: error instanceof Error ? error.message : String(error),
777
+ });
778
+ }
779
+ // Update status message with elapsed time
780
+ const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
781
+ setCurrentStep(`Waiting for bridge (${elapsedSeconds}s)...`);
782
+ onStatus?.(`Waiting for bridge (${elapsedSeconds}s)...`);
783
+ // Exponential backoff: increase interval after every 3 attempts
784
+ if (attemptCount % 3 === 0) {
785
+ pollInterval = Math.min(pollInterval * 1.5, maxPollInterval);
786
+ }
787
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
788
+ }
789
+ if (!depositTxHash) {
790
+ throw new Error('DeBridge transaction timed out after 10 minutes');
791
+ }
792
+ return {
793
+ depositTxHash: depositTxHash,
794
+ provider: 'debridge',
795
+ };
796
+ }
797
+ /**
798
+ * Monitor relay.link bridge status until deposit arrives
799
+ * With exponential backoff and timeout
800
+ */
801
+ async function monitorRelayBridgeStatus(requestId, setCurrentStep, onStatus) {
802
+ console.log('Starting relay.link bridge status monitoring:', { requestId });
803
+ setCurrentStep('Waiting for bridge');
804
+ onStatus?.('Waiting for bridge');
805
+ let depositTxHash;
806
+ let bridgeStatus;
807
+ const startTime = Date.now();
808
+ const timeout = 10 * 60 * 1000; // 10 minutes (bridges can take several minutes)
809
+ let pollInterval = 2_000; // Start with 2 seconds
810
+ const maxPollInterval = 10_000; // Max 10 seconds between polls
811
+ let attemptCount = 0;
812
+ // Poll for bridge status with timeout
813
+ while (Date.now() - startTime < timeout) {
814
+ attemptCount++;
815
+ try {
816
+ bridgeStatus = await getRelayStatus(requestId);
817
+ }
818
+ catch (error) {
819
+ // If status check fails, log and retry with backoff
820
+ console.warn('Failed to get relay status, retrying...', error);
821
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
822
+ continue;
823
+ }
824
+ const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
825
+ switch (bridgeStatus.status) {
826
+ case 'success': {
827
+ depositTxHash = bridgeStatus.txHashes?.[0] || '0x';
828
+ break;
829
+ }
830
+ case 'failed': {
831
+ throw new Error(`Bridge tx failed: ${bridgeStatus.details || 'Unknown error'}`);
832
+ }
833
+ case 'refund': {
834
+ throw new Error('Bridge tx refunded due to an error');
835
+ }
836
+ case 'fallback': {
837
+ throw new Error('Bridge tx is refunding due to a fallback');
838
+ }
839
+ case 'delayed': {
840
+ setCurrentStep(`Bridge delayed (${elapsedSeconds}s)...`);
841
+ onStatus?.(`Bridge delayed (${elapsedSeconds}s)...`);
842
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
843
+ continue;
844
+ }
845
+ case 'received': {
846
+ setCurrentStep(`Bridge received (${elapsedSeconds}s)...`);
847
+ onStatus?.(`Bridge received (${elapsedSeconds}s)...`);
848
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
849
+ continue;
850
+ }
851
+ case 'pending':
852
+ case 'unknown':
853
+ default: {
854
+ setCurrentStep(`Waiting for bridge (${elapsedSeconds}s)...`);
855
+ onStatus?.(`Waiting for bridge (${elapsedSeconds}s)...`);
856
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
857
+ continue;
858
+ }
859
+ }
860
+ // If we got here with success status, break
861
+ if (bridgeStatus.status === 'success') {
862
+ break;
863
+ }
864
+ // Exponential backoff: increase interval after every 3 attempts
865
+ if (attemptCount % 3 === 0) {
866
+ pollInterval = Math.min(pollInterval * 1.5, maxPollInterval);
867
+ }
868
+ }
869
+ // Check if we timed out
870
+ if (!depositTxHash && Date.now() - startTime >= timeout) {
871
+ throw new Error('Bridge transaction timed out after 10 minutes');
872
+ }
873
+ if (!depositTxHash) {
874
+ throw new Error('Failed to get deposit transaction hash');
875
+ }
876
+ return depositTxHash;
877
+ }