@silentswap/sdk 0.0.67 → 0.0.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bridge.d.ts CHANGED
@@ -202,6 +202,12 @@ export interface DeBridgeOrderParams {
202
202
  srcChainOrderAuthorityAddress?: string;
203
203
  srcChainRefundAddress?: string;
204
204
  dstChainOrderAuthorityAddress?: string;
205
+ referralCode?: number;
206
+ additionalTakerRewardBps?: number;
207
+ allowedTaker?: string;
208
+ deBridgeApp?: string;
209
+ ptp?: boolean;
210
+ srcChainPriorityLevel?: 'normal' | 'aggressive';
205
211
  }
206
212
  export interface DeBridgeOrderResponse {
207
213
  tx: {
package/dist/bridge.js CHANGED
@@ -247,10 +247,29 @@ export async function fetchRelayQuote(params, signal) {
247
247
  });
248
248
  if (!response.ok) {
249
249
  const text = await response.text();
250
- throw new Error(`HTTP ${response.status}: ${text}`);
250
+ throw parseQuoteApiErrorResponse(response.status, text);
251
251
  }
252
252
  return response.json();
253
253
  }
254
+ /**
255
+ * Parse quote API error response (relay.link or deBridge) and return a user-friendly Error.
256
+ * Handles INSUFFICIENT_FUNDS / "No UTXOs available to spend" and other known error shapes.
257
+ */
258
+ function parseQuoteApiErrorResponse(status, text) {
259
+ try {
260
+ const body = JSON.parse(text);
261
+ if (body.errorCode === 'INSUFFICIENT_FUNDS' || (body.message && body.message.includes('UTXO'))) {
262
+ return new Error('Insufficient funds');
263
+ }
264
+ if (body.message && typeof body.message === 'string') {
265
+ return new Error(body.message);
266
+ }
267
+ }
268
+ catch {
269
+ // not JSON or invalid
270
+ }
271
+ return new Error(`HTTP ${status}: ${text}`);
272
+ }
254
273
  /**
255
274
  * Fetch a deBridge order with detailed response
256
275
  * Uses create-tx endpoint for cross-chain swaps
@@ -294,7 +313,7 @@ export async function fetchDebridgeOrder(params, signal) {
294
313
  const response = await fetch(`https://deswap.debridge.finance/v1.0/dln/order/create-tx?${queryParams}`, { signal });
295
314
  if (!response.ok) {
296
315
  const text = await response.text();
297
- throw new Error(`HTTP ${response.status}: ${text}`);
316
+ throw parseQuoteApiErrorResponse(response.status, text);
298
317
  }
299
318
  return response.json();
300
319
  }
@@ -323,7 +342,7 @@ async function fetchDebridgeSingleChainOrder(params, signal) {
323
342
  const response = await fetch(`https://deswap.debridge.finance/v1.0/chain/transaction?${queryParams}`, { signal });
324
343
  if (!response.ok) {
325
344
  const text = await response.text();
326
- throw new Error(`HTTP ${response.status}: ${text}`);
345
+ throw parseQuoteApiErrorResponse(response.status, text);
327
346
  }
328
347
  const data = await response.json();
329
348
  // Normalize single-chain response to match DeBridgeOrderResponse structure
@@ -66,21 +66,32 @@ export declare function calculateRelayMetrics(relayQuote: RelayQuoteResponse): Q
66
66
  */
67
67
  export declare function calculateDebridgeMetrics(debridgeQuote: DeBridgeOrderResponse): QuoteMetrics;
68
68
  /**
69
- * Select the best quote from multiple providers based on retention rate
69
+ * Select the best quote from multiple providers based on retention rate.
70
+ * A quote is viable if it has transaction data (for execution) or estimation data (for display).
71
+ * When both providers return estimation but no tx, we still pick the best so the app can show
72
+ * output and refetch on amount change instead of throwing and blocking further quotes.
73
+ * When both fail, uses relayRejection/debridgeRejection to throw a specific message (e.g. "Insufficient funds").
70
74
  */
71
- export declare function selectBestQuote(relayQuote: RelayQuoteResponse | null, debridgeQuote: DeBridgeOrderResponse | null, srcAmount: string): BridgeQuoteResult;
75
+ export declare function selectBestQuote(relayQuote: RelayQuoteResponse | null, debridgeQuote: DeBridgeOrderResponse | null, srcAmount: string, relayRejection?: unknown, debridgeRejection?: unknown): BridgeQuoteResult;
72
76
  /**
73
77
  * Interpolate retention rate from samples
74
78
  */
75
79
  export declare function interpolateSamples(samples: EstimateSample[], usdValue: number, marginHi?: number): number;
80
+ /**
81
+ * When set, only the specified provider is used for quotes (for testing).
82
+ */
83
+ export type ForceBridgeProvider = 'relay' | 'debridge';
76
84
  /**
77
85
  * Get quote for cross-chain swap from multiple providers
78
86
  * This is the core logic shared between React and Vue
79
87
  * @param recipientAddress - Optional recipient address (required for Solana destinations)
88
+ * @param sourceAddress - Optional source chain address (if different from userAddress)
89
+ * @param forceProvider - Optional: use only this provider (for testing; skips the other)
80
90
  */
81
91
  export declare function getBridgeQuote(srcChainId: number, srcToken: string, srcAmount: string, dstChainId: number, dstToken: string, userAddress: `0x${string}` | string, // Can be EVM (0x) or Solana (base58) address
82
92
  signal?: AbortSignal, recipientAddress?: string, // Optional recipient address (required for Solana destinations)
83
- sourceAddress?: string): Promise<BridgeQuoteResult>;
93
+ sourceAddress?: string, // Optional source chain address (if different from userAddress)
94
+ forceProvider?: ForceBridgeProvider): Promise<BridgeQuoteResult>;
84
95
  /**
85
96
  * Convert BridgeQuoteResult to BridgeQuote with transaction data
86
97
  * Extracts transactions from the raw response based on provider
@@ -113,18 +113,74 @@ function debridgeHasTransactionData(quote) {
113
113
  return !!(quote.tx && (quote.tx.to || quote.tx.data));
114
114
  }
115
115
  /**
116
- * Select the best quote from multiple providers based on retention rate
116
+ * Check if a relay quote has estimation data (output amount) even without tx.
117
+ * Allows selecting a quote for display when API returned estimation but no tx.
117
118
  */
118
- export function selectBestQuote(relayQuote, debridgeQuote, srcAmount) {
119
- // Only consider providers that have executable transaction data
120
- const viableRelay = relayQuote && relayHasTransactionData(relayQuote) ? relayQuote : null;
121
- const viableDebridge = debridgeQuote && debridgeHasTransactionData(debridgeQuote) ? debridgeQuote : null;
119
+ function relayHasEstimationData(quote) {
120
+ return !!(quote.details?.currencyOut?.amount != null && quote.details?.currencyOut?.amount !== '');
121
+ }
122
+ /**
123
+ * Check if a deBridge quote has estimation data (output amount) even without tx.
124
+ */
125
+ function debridgeHasEstimationData(quote) {
126
+ return !!(quote.estimation?.dstChainTokenOut?.amount != null &&
127
+ quote.estimation.dstChainTokenOut.amount !== '');
128
+ }
129
+ /**
130
+ * Derive a user-facing message from quote provider rejection reasons.
131
+ * When any provider returns INSUFFICIENT_FUNDS / "No UTXOs available to spend", return "Insufficient funds".
132
+ */
133
+ function getQuoteProviderErrorMessage(relayRejection, debridgeRejection) {
134
+ const check = (reason) => {
135
+ if (reason instanceof Error) {
136
+ const msg = reason.message;
137
+ if (msg.includes('Insufficient funds') ||
138
+ msg.includes('INSUFFICIENT_FUNDS') ||
139
+ msg.includes('No UTXOs available to spend')) {
140
+ return true;
141
+ }
142
+ // Parse JSON from "HTTP 400: {...}" style message
143
+ const jsonMatch = msg.match(/\{[\s\S]*\}/);
144
+ if (jsonMatch) {
145
+ try {
146
+ const body = JSON.parse(jsonMatch[0]);
147
+ if (body.errorCode === 'INSUFFICIENT_FUNDS' || (body.message && body.message.includes('UTXO'))) {
148
+ return true;
149
+ }
150
+ }
151
+ catch {
152
+ // ignore
153
+ }
154
+ }
155
+ }
156
+ return false;
157
+ };
158
+ if (check(relayRejection) || check(debridgeRejection)) {
159
+ return 'Insufficient funds';
160
+ }
161
+ return 'All quote providers failed';
162
+ }
163
+ /**
164
+ * Select the best quote from multiple providers based on retention rate.
165
+ * A quote is viable if it has transaction data (for execution) or estimation data (for display).
166
+ * When both providers return estimation but no tx, we still pick the best so the app can show
167
+ * output and refetch on amount change instead of throwing and blocking further quotes.
168
+ * When both fail, uses relayRejection/debridgeRejection to throw a specific message (e.g. "Insufficient funds").
169
+ */
170
+ export function selectBestQuote(relayQuote, debridgeQuote, srcAmount, relayRejection, debridgeRejection) {
171
+ const hasRelayTx = relayQuote && relayHasTransactionData(relayQuote);
172
+ const hasRelayEst = relayQuote && relayHasEstimationData(relayQuote);
173
+ const hasDebridgeTx = debridgeQuote && debridgeHasTransactionData(debridgeQuote);
174
+ const hasDebridgeEst = debridgeQuote && debridgeHasEstimationData(debridgeQuote);
175
+ const viableRelay = (hasRelayTx || hasRelayEst) ? relayQuote : null;
176
+ const viableDebridge = (hasDebridgeTx || hasDebridgeEst) ? debridgeQuote : null;
122
177
  // Calculate metrics only for viable providers
123
178
  const relayMetrics = viableRelay ? calculateRelayMetrics(viableRelay) : null;
124
179
  const debridgeMetrics = viableDebridge ? calculateDebridgeMetrics(viableDebridge) : null;
125
- // Both providers failed or have no executable transactions
180
+ // Both providers failed or have no usable response (no tx and no estimation)
126
181
  if (!viableRelay && !viableDebridge) {
127
- throw new Error('All quote providers failed');
182
+ const message = getQuoteProviderErrorMessage(relayRejection, debridgeRejection);
183
+ throw new Error(message);
128
184
  }
129
185
  // Determine best provider based on retention rate
130
186
  let bestProvider;
@@ -221,14 +277,13 @@ export function interpolateSamples(samples, usdValue, marginHi = Infinity) {
221
277
  * Get quote for cross-chain swap from multiple providers
222
278
  * This is the core logic shared between React and Vue
223
279
  * @param recipientAddress - Optional recipient address (required for Solana destinations)
280
+ * @param sourceAddress - Optional source chain address (if different from userAddress)
281
+ * @param forceProvider - Optional: use only this provider (for testing; skips the other)
224
282
  */
225
283
  export async function getBridgeQuote(srcChainId, srcToken, srcAmount, dstChainId, dstToken, userAddress, // Can be EVM (0x) or Solana (base58) address
226
284
  signal, recipientAddress, // Optional recipient address (required for Solana destinations)
227
- sourceAddress // Optional source chain address (if different from userAddress)
228
- ) {
229
- console.log('recipientAddress', recipientAddress);
230
- console.log('sourceAddress', sourceAddress);
231
- console.log('userAddress', userAddress);
285
+ sourceAddress, // Optional source chain address (if different from userAddress)
286
+ forceProvider) {
232
287
  // Normalize token addresses for bridge API (handles both EVM and Solana)
233
288
  const srcTokenNorm = normalizeTokenAddressForQuote(srcChainId, srcToken);
234
289
  const dstTokenNorm = normalizeTokenAddressForQuote(dstChainId, dstToken);
@@ -300,10 +355,14 @@ sourceAddress // Optional source chain address (if different from userAddress)
300
355
  relayUserAddress = DEAD_ADDRESS;
301
356
  }
302
357
  }
303
- // Fetch quotes from both providers in parallel
358
+ // Fetch quotes from both providers in parallel (skip one if forceProvider is set)
359
+ const useRelay = forceProvider !== 'debridge';
360
+ const useDebridge = forceProvider !== 'relay';
304
361
  const [relayResult, debridgeResult] = await Promise.allSettled([
305
- // Relay.link quote
362
+ // Relay.link quote (skipped when forceProvider === 'debridge')
306
363
  (async () => {
364
+ if (!useRelay)
365
+ return null;
307
366
  try {
308
367
  // Map chain IDs to Relay chain IDs if needed
309
368
  // For Solana, Relay.link uses N_RELAY_CHAIN_ID_SOLANA (792703809)
@@ -406,11 +465,14 @@ sourceAddress // Optional source chain address (if different from userAddress)
406
465
  if (err instanceof Error && err.name !== 'AbortError') {
407
466
  console.warn('Relay quote failed:', err);
408
467
  }
409
- return null;
468
+ // Rethrow so Promise.allSettled captures the reason (e.g. "Insufficient funds")
469
+ throw err;
410
470
  }
411
471
  })(),
412
- // deBridge quote
472
+ // deBridge quote (skipped when forceProvider === 'relay')
413
473
  (async () => {
474
+ if (!useDebridge)
475
+ return null;
414
476
  try {
415
477
  // Skip deBridge for Bitcoin - Bitcoin only supports relay
416
478
  if (isSrcBitcoin || isDestBitcoin) {
@@ -462,9 +524,22 @@ sourceAddress // Optional source chain address (if different from userAddress)
462
524
  }
463
525
  }
464
526
  else {
465
- // Both source and destination are EVM, use EXACT_OUTPUT mode with auto
527
+ // Both source and destination are EVM
528
+ // API requires dstChainTokenOutRecipient, srcChainOrderAuthorityAddress, dstChainOrderAuthorityAddress for transaction construction
529
+ debridgeParams.dstChainTokenOutRecipient = userAddress;
530
+ debridgeParams.srcChainOrderAuthorityAddress = userAddress;
531
+ debridgeParams.dstChainOrderAuthorityAddress = userAddress;
532
+ debridgeParams.srcChainRefundAddress = userAddress;
533
+ debridgeParams.senderAddress = userAddress;
466
534
  debridgeParams.dstChainTokenOutAmount = 'auto';
467
535
  debridgeParams.enableEstimate = true;
536
+ // Optional params matching deSwap app for consistent API behavior
537
+ debridgeParams.additionalTakerRewardBps = 0;
538
+ debridgeParams.deBridgeApp = 'DESWAP';
539
+ debridgeParams.ptp = false;
540
+ debridgeParams.srcChainPriorityLevel = 'normal';
541
+ // Use standard API param names (senderAddress, dstChainTokenOutRecipient); omit account
542
+ // delete debridgeParams.account;
468
543
  }
469
544
  const quote = await fetchDebridgeOrder(debridgeParams, signal);
470
545
  return quote;
@@ -473,15 +548,16 @@ sourceAddress // Optional source chain address (if different from userAddress)
473
548
  if (err instanceof Error && err.name !== 'AbortError') {
474
549
  console.warn('DeBridge quote failed:', err);
475
550
  }
476
- return null;
551
+ // Rethrow so Promise.allSettled captures the reason (e.g. "Insufficient funds")
552
+ throw err;
477
553
  }
478
554
  })(),
479
555
  ]);
480
556
  const relayQuote = relayResult.status === 'fulfilled' ? relayResult.value : null;
481
557
  const debridgeQuote = debridgeResult.status === 'fulfilled' ? debridgeResult.value : null;
482
- // Select best quote
483
- const result = selectBestQuote(relayQuote, debridgeQuote, srcAmount);
484
- return result;
558
+ const relayRejection = relayResult.status === 'rejected' ? relayResult.reason : undefined;
559
+ const debridgeRejection = debridgeResult.status === 'rejected' ? debridgeResult.reason : undefined;
560
+ return selectBestQuote(relayQuote, debridgeQuote, srcAmount, relayRejection, debridgeRejection);
485
561
  }
486
562
  /**
487
563
  * Convert BridgeQuoteResult to BridgeQuote with transaction data
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@silentswap/sdk",
3
3
  "type": "module",
4
- "version": "0.0.67",
4
+ "version": "0.0.69",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
7
7
  "files": [