@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.
- package/README.md +48 -0
- package/dist/contexts/AssetsContext.d.ts +24 -0
- package/dist/contexts/AssetsContext.js +83 -0
- package/dist/contexts/BalancesContext.d.ts +28 -0
- package/dist/contexts/BalancesContext.js +533 -0
- package/dist/contexts/OrdersContext.d.ts +53 -0
- package/dist/contexts/OrdersContext.js +240 -0
- package/dist/contexts/PricesContext.d.ts +12 -0
- package/dist/contexts/PricesContext.js +109 -0
- package/dist/contexts/SilentSwapContext.d.ts +58 -0
- package/dist/contexts/SilentSwapContext.js +205 -0
- package/dist/hooks/silent/orderTrackingWebSocketManager.d.ts +48 -0
- package/dist/hooks/silent/orderTrackingWebSocketManager.js +284 -0
- package/dist/hooks/silent/solana-transaction.d.ts +60 -0
- package/dist/hooks/silent/solana-transaction.js +236 -0
- package/dist/hooks/silent/useAuth.d.ts +90 -0
- package/dist/hooks/silent/useAuth.js +269 -0
- package/dist/hooks/silent/useBridgeExecution.d.ts +39 -0
- package/dist/hooks/silent/useBridgeExecution.js +877 -0
- package/dist/hooks/silent/useOrderSigning.d.ts +34 -0
- package/dist/hooks/silent/useOrderSigning.js +133 -0
- package/dist/hooks/silent/useOrderTracking.d.ts +174 -0
- package/dist/hooks/silent/useOrderTracking.js +524 -0
- package/dist/hooks/silent/useQuoteCalculation.d.ts +50 -0
- package/dist/hooks/silent/useQuoteCalculation.js +331 -0
- package/dist/hooks/silent/useQuoteFetching.d.ts +18 -0
- package/dist/hooks/silent/useQuoteFetching.js +54 -0
- package/dist/hooks/silent/useRefund.d.ts +26 -0
- package/dist/hooks/silent/useRefund.js +134 -0
- package/dist/hooks/silent/useSilentClient.d.ts +16 -0
- package/dist/hooks/silent/useSilentClient.js +32 -0
- package/dist/hooks/silent/useSilentOrders.d.ts +174 -0
- package/dist/hooks/silent/useSilentOrders.js +73 -0
- package/dist/hooks/silent/useSilentQuote.d.ts +88 -0
- package/dist/hooks/silent/useSilentQuote.js +381 -0
- package/dist/hooks/silent/useWallet.d.ts +76 -0
- package/dist/hooks/silent/useWallet.js +203 -0
- package/dist/hooks/useAssetPrice.d.ts +8 -0
- package/dist/hooks/useAssetPrice.js +47 -0
- package/dist/hooks/useContacts.d.ts +52 -0
- package/dist/hooks/useContacts.js +259 -0
- package/dist/hooks/useEgressEstimates.d.ts +32 -0
- package/dist/hooks/useEgressEstimates.js +230 -0
- package/dist/hooks/useHiddenSwapFees.d.ts +22 -0
- package/dist/hooks/useHiddenSwapFees.js +81 -0
- package/dist/hooks/useOrderEstimates.d.ts +37 -0
- package/dist/hooks/useOrderEstimates.js +393 -0
- package/dist/hooks/useOutputAssetInfo.d.ts +12 -0
- package/dist/hooks/useOutputAssetInfo.js +38 -0
- package/dist/hooks/usePrices.d.ts +60 -0
- package/dist/hooks/usePrices.js +188 -0
- package/dist/hooks/useQuote.d.ts +73 -0
- package/dist/hooks/useQuote.js +507 -0
- package/dist/hooks/useResetSwapForm.d.ts +16 -0
- package/dist/hooks/useResetSwapForm.js +68 -0
- package/dist/hooks/useSlippageUsd.d.ts +11 -0
- package/dist/hooks/useSlippageUsd.js +19 -0
- package/dist/hooks/useSolanaAdapter.d.ts +15 -0
- package/dist/hooks/useSolanaAdapter.js +55 -0
- package/dist/hooks/useStatus.d.ts +25 -0
- package/dist/hooks/useStatus.js +60 -0
- package/dist/hooks/useSwap.d.ts +67 -0
- package/dist/hooks/useSwap.js +285 -0
- package/dist/hooks/useTransaction.d.ts +119 -0
- package/dist/hooks/useTransaction.js +353 -0
- package/dist/hooks/useTransactionAddress.d.ts +11 -0
- package/dist/hooks/useTransactionAddress.js +26 -0
- package/dist/hooks/useUsdValue.d.ts +7 -0
- package/dist/hooks/useUsdValue.js +19 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +41 -0
- package/dist/stories/SilentSwapOverview.stories.d.ts +10 -0
- package/dist/stories/SilentSwapOverview.stories.js +364 -0
- package/dist/stories/useAuth.stories.d.ts +6 -0
- package/dist/stories/useAuth.stories.js +55 -0
- package/dist/stories/useSilentClient.stories.d.ts +9 -0
- package/dist/stories/useSilentClient.stories.js +39 -0
- package/dist/stories/useSilentOrders.stories.d.ts +1 -0
- package/dist/stories/useSilentOrders.stories.js +1 -0
- package/dist/stories/useSilentQuote.stories.d.ts +6 -0
- package/dist/stories/useSilentQuote.stories.js +267 -0
- package/dist/stories/useTransaction.stories.d.ts +6 -0
- package/dist/stories/useTransaction.stories.js +121 -0
- package/dist/utils/formatters.d.ts +33 -0
- package/dist/utils/formatters.js +82 -0
- 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
|
+
}
|