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