@persistenceone/bridgekitty 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/dist/backends/across.d.ts +10 -0
  4. package/dist/backends/across.js +285 -0
  5. package/dist/backends/debridge.d.ts +11 -0
  6. package/dist/backends/debridge.js +380 -0
  7. package/dist/backends/lifi.d.ts +19 -0
  8. package/dist/backends/lifi.js +295 -0
  9. package/dist/backends/persistence.d.ts +86 -0
  10. package/dist/backends/persistence.js +642 -0
  11. package/dist/backends/relay.d.ts +11 -0
  12. package/dist/backends/relay.js +292 -0
  13. package/dist/backends/squid.d.ts +31 -0
  14. package/dist/backends/squid.js +476 -0
  15. package/dist/backends/types.d.ts +125 -0
  16. package/dist/backends/types.js +11 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +154 -0
  19. package/dist/routing/engine.d.ts +49 -0
  20. package/dist/routing/engine.js +336 -0
  21. package/dist/tools/check-status.d.ts +3 -0
  22. package/dist/tools/check-status.js +93 -0
  23. package/dist/tools/execute-bridge.d.ts +3 -0
  24. package/dist/tools/execute-bridge.js +428 -0
  25. package/dist/tools/get-chains.d.ts +3 -0
  26. package/dist/tools/get-chains.js +162 -0
  27. package/dist/tools/get-quote.d.ts +3 -0
  28. package/dist/tools/get-quote.js +534 -0
  29. package/dist/tools/get-tokens.d.ts +3 -0
  30. package/dist/tools/get-tokens.js +128 -0
  31. package/dist/tools/help.d.ts +2 -0
  32. package/dist/tools/help.js +204 -0
  33. package/dist/tools/multi-quote.d.ts +3 -0
  34. package/dist/tools/multi-quote.js +310 -0
  35. package/dist/tools/onboard.d.ts +3 -0
  36. package/dist/tools/onboard.js +218 -0
  37. package/dist/tools/wallet.d.ts +14 -0
  38. package/dist/tools/wallet.js +744 -0
  39. package/dist/tools/xprt-farm.d.ts +3 -0
  40. package/dist/tools/xprt-farm.js +1308 -0
  41. package/dist/tools/xprt-rewards.d.ts +2 -0
  42. package/dist/tools/xprt-rewards.js +177 -0
  43. package/dist/tools/xprt-staking.d.ts +2 -0
  44. package/dist/tools/xprt-staking.js +565 -0
  45. package/dist/utils/chains.d.ts +22 -0
  46. package/dist/utils/chains.js +154 -0
  47. package/dist/utils/circuit-breaker.d.ts +64 -0
  48. package/dist/utils/circuit-breaker.js +160 -0
  49. package/dist/utils/evm.d.ts +18 -0
  50. package/dist/utils/evm.js +46 -0
  51. package/dist/utils/fill-detector.d.ts +70 -0
  52. package/dist/utils/fill-detector.js +298 -0
  53. package/dist/utils/gas-estimator.d.ts +67 -0
  54. package/dist/utils/gas-estimator.js +340 -0
  55. package/dist/utils/sanitize-error.d.ts +23 -0
  56. package/dist/utils/sanitize-error.js +101 -0
  57. package/dist/utils/token-registry.d.ts +70 -0
  58. package/dist/utils/token-registry.js +669 -0
  59. package/dist/utils/tokens.d.ts +17 -0
  60. package/dist/utils/tokens.js +37 -0
  61. package/dist/utils/tx-simulator.d.ts +27 -0
  62. package/dist/utils/tx-simulator.js +105 -0
  63. package/package.json +75 -0
@@ -0,0 +1,380 @@
1
+ import { formatTokenAmount } from "../utils/tokens.js";
2
+ import { getBackendChainId, getAllChains, isSolanaChain } from "../utils/chains.js";
3
+ import { buildApproveData, isNativeToken } from "../utils/evm.js";
4
+ import { estimateGasCostUsd, getGasUnits } from "../utils/gas-estimator.js";
5
+ import { sanitizeError } from "../utils/sanitize-error.js";
6
+ const BASE_URL = "https://api.dln.trade/v1.0";
7
+ const TIMEOUT_MS = 15_000;
8
+ async function fetchJson(url, init) {
9
+ const controller = new AbortController();
10
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
11
+ try {
12
+ const res = await fetch(url, { ...init, signal: controller.signal });
13
+ if (!res.ok) {
14
+ const text = await res.text().catch(() => "");
15
+ throw new Error(`deBridge ${res.status}: ${text.slice(0, 200)}`);
16
+ }
17
+ return res.json();
18
+ }
19
+ finally {
20
+ clearTimeout(timer);
21
+ }
22
+ }
23
+ // buildApproveData imported from ../utils/evm.js
24
+ export class DeBridgeBackend {
25
+ name = "debridge";
26
+ affiliateFeePercent;
27
+ affiliateFeeRecipient;
28
+ constructor(affiliateFeePercent, affiliateFeeRecipient) {
29
+ this.affiliateFeePercent = affiliateFeePercent;
30
+ this.affiliateFeeRecipient = affiliateFeeRecipient;
31
+ }
32
+ async getQuote(params) {
33
+ try {
34
+ const url = new URL(`${BASE_URL}/dln/order/quote`);
35
+ const srcChainId = getBackendChainId("debridge", params.fromChainId);
36
+ const dstChainId = getBackendChainId("debridge", params.toChainId);
37
+ url.searchParams.set("srcChainId", String(srcChainId));
38
+ // deBridge uses 0x000...0 for native EVM tokens (not 0xEeee...); convert.
39
+ const srcToken = isNativeToken(params.fromTokenAddress)
40
+ ? "0x0000000000000000000000000000000000000000"
41
+ : params.fromTokenAddress;
42
+ url.searchParams.set("srcChainTokenIn", srcToken);
43
+ url.searchParams.set("srcChainTokenInAmount", params.amountRaw);
44
+ url.searchParams.set("dstChainId", String(dstChainId));
45
+ // deBridge uses native SOL address (system program) instead of wrapped SOL mint.
46
+ // Using the native address ensures the solver delivers actual SOL, not wSOL.
47
+ const WRAPPED_SOL_MINT = "So11111111111111111111111111111111111111112";
48
+ const NATIVE_SOL_ADDRESS = "11111111111111111111111111111111";
49
+ let dstToken = params.toTokenAddress;
50
+ if (isSolanaChain(params.toChainId) && params.toTokenAddress === WRAPPED_SOL_MINT) {
51
+ dstToken = NATIVE_SOL_ADDRESS;
52
+ }
53
+ else if (!isSolanaChain(params.toChainId) && isNativeToken(params.toTokenAddress)) {
54
+ // deBridge uses 0x000...0 for native EVM tokens on destination too
55
+ dstToken = "0x0000000000000000000000000000000000000000";
56
+ }
57
+ url.searchParams.set("dstChainTokenOut", dstToken);
58
+ url.searchParams.set("prependOperatingExpenses", "true");
59
+ if (this.affiliateFeePercent && this.affiliateFeeRecipient) {
60
+ url.searchParams.set("affiliateFeePercent", this.affiliateFeePercent);
61
+ url.searchParams.set("affiliateFeeRecipient", this.affiliateFeeRecipient);
62
+ }
63
+ let data;
64
+ try {
65
+ data = await fetchJson(url.toString());
66
+ }
67
+ catch (err) {
68
+ // If affiliate fee params caused the error, retry without them
69
+ if (this.affiliateFeePercent && this.affiliateFeeRecipient) {
70
+ console.warn("[debridge] quote failed with affiliate fee, retrying without fee:", err.message);
71
+ url.searchParams.delete("affiliateFeePercent");
72
+ url.searchParams.delete("affiliateFeeRecipient");
73
+ data = await fetchJson(url.toString());
74
+ }
75
+ else {
76
+ throw err;
77
+ }
78
+ }
79
+ if (!data.estimation)
80
+ return null;
81
+ const dstAmount = data.estimation.dstChainTokenOut?.amount ?? "0";
82
+ const dstDecimals = data.estimation.dstChainTokenOut?.decimals ?? 18;
83
+ const srcTokenSymbol = data.estimation.srcChainTokenIn?.symbol ?? "?";
84
+ const dstTokenSymbol = data.estimation.dstChainTokenOut?.symbol ?? "?";
85
+ // Calculate fee from operating expenses (with safe parsing)
86
+ let totalFeeUsd = 0;
87
+ const costsDetails = data.estimation.costsDetails;
88
+ if (Array.isArray(costsDetails)) {
89
+ for (const c of costsDetails) {
90
+ const usd = Number(c?.payload?.feeApproximateUsdValue ?? 0);
91
+ if (!isNaN(usd))
92
+ totalFeeUsd += usd;
93
+ }
94
+ }
95
+ // Estimate source chain gas cost (chain-aware)
96
+ const gasUnits = getGasUnits("debridge", params.fromChainId);
97
+ const gasEstimate = await estimateGasCostUsd(params.fromChainId, gasUnits);
98
+ const gasCostUsd = gasEstimate?.costUsd ?? null;
99
+ // deBridge DLN is intent-based: the recommended amount is what the solver commits to deliver.
100
+ // Use recommendedAmount if available (guaranteed), otherwise apply 0.5% slippage to estimated.
101
+ const recommendedRaw = data.estimation.dstChainTokenOut?.recommendedAmount;
102
+ let minOutputRaw;
103
+ if (recommendedRaw) {
104
+ minOutputRaw = recommendedRaw;
105
+ }
106
+ else {
107
+ // Apply 0.5% slippage tolerance
108
+ try {
109
+ const outputBig = BigInt(dstAmount);
110
+ minOutputRaw = (outputBig * 995n / 1000n).toString();
111
+ }
112
+ catch {
113
+ minOutputRaw = dstAmount;
114
+ }
115
+ }
116
+ // Extract fixFee (flat protocol fee in native token, e.g. 0.001 ETH)
117
+ // This is added to tx.value on top of the bridge amount — critical for balance checks
118
+ const fixFee = data.fixFee ? String(data.fixFee) : "0";
119
+ const operatingExpense = data.estimation.srcChainTokenIn?.approximateOperatingExpense ?? "0";
120
+ // Total amount the user actually needs (input + operating expenses + fixFee for native)
121
+ const totalSourceAmount = data.estimation.srcChainTokenIn?.amount ?? params.amountRaw;
122
+ return {
123
+ backendName: "debridge",
124
+ provider: "deBridge (direct)",
125
+ outputAmount: formatTokenAmount(dstAmount, dstDecimals),
126
+ outputAmountRaw: dstAmount,
127
+ minOutputAmount: formatTokenAmount(minOutputRaw, dstDecimals),
128
+ minOutputAmountRaw: minOutputRaw,
129
+ outputDecimals: dstDecimals,
130
+ estimatedGasCostUsd: gasCostUsd,
131
+ usingFallbackPrices: gasEstimate?.usingFallbackPrices,
132
+ estimatedFeeUsd: gasCostUsd !== null ? totalFeeUsd + gasCostUsd : null,
133
+ feeBreakdown: {
134
+ gasCostUsd,
135
+ protocolFeeUsd: totalFeeUsd,
136
+ integratorFeeUsd: 0,
137
+ integratorFeePercent: null,
138
+ totalFeeUsd: gasCostUsd !== null ? totalFeeUsd + gasCostUsd : null,
139
+ // deBridge-specific: flat fee in native token (e.g. 0.001 ETH on Base)
140
+ fixFeeNativeRaw: fixFee,
141
+ operatingExpenseRaw: operatingExpense,
142
+ totalSourceAmountRaw: totalSourceAmount,
143
+ },
144
+ estimatedTimeSeconds: data.estimation.estimatedFulfillmentDelay ?? 30,
145
+ route: `${srcTokenSymbol} → deBridge DLN → ${dstTokenSymbol}`,
146
+ quoteData: {
147
+ estimation: data.estimation,
148
+ order: data.order,
149
+ fixFee,
150
+ params: {
151
+ srcChainId: params.fromChainId,
152
+ dstChainId: params.toChainId,
153
+ srcChainTokenIn: params.fromTokenAddress,
154
+ dstChainTokenOut: dstToken,
155
+ srcChainTokenInAmount: params.amountRaw,
156
+ fromAddress: params.fromAddress,
157
+ toAddress: params.toAddress || params.fromAddress,
158
+ },
159
+ },
160
+ // deBridge DLN quotes: use estimation expiry if available, else conservative 30s
161
+ expiresAt: data.estimation?.expiration
162
+ ? new Date(data.estimation.expiration).getTime()
163
+ : Date.now() + 60_000,
164
+ };
165
+ }
166
+ catch (err) {
167
+ console.error("[debridge] quote error:", err.message);
168
+ return null;
169
+ }
170
+ }
171
+ async buildTransaction(quote) {
172
+ const qd = quote.quoteData;
173
+ const p = qd.params;
174
+ // Use create-tx endpoint to get the actual transaction
175
+ // Apply backend-specific chain ID mapping (same as getQuote)
176
+ const srcChainId = getBackendChainId("debridge", p.srcChainId);
177
+ const dstChainId = getBackendChainId("debridge", p.dstChainId);
178
+ const url = new URL(`${BASE_URL}/dln/order/create-tx`);
179
+ url.searchParams.set("srcChainId", String(srcChainId));
180
+ url.searchParams.set("srcChainTokenIn", p.srcChainTokenIn);
181
+ url.searchParams.set("srcChainTokenInAmount", p.srcChainTokenInAmount);
182
+ url.searchParams.set("dstChainId", String(dstChainId));
183
+ url.searchParams.set("dstChainTokenOut", p.dstChainTokenOut);
184
+ url.searchParams.set("dstChainTokenOutAmount", "auto");
185
+ url.searchParams.set("srcChainOrderAuthorityAddress", p.fromAddress);
186
+ url.searchParams.set("dstChainTokenOutRecipient", p.toAddress);
187
+ // senderAddress is REQUIRED for the API to return tx.to/tx.data/tx.value
188
+ url.searchParams.set("senderAddress", p.fromAddress);
189
+ url.searchParams.set("srcChainRefundAddress", p.fromAddress);
190
+ url.searchParams.set("dstChainOrderAuthorityAddress", p.toAddress);
191
+ url.searchParams.set("prependOperatingExpenses", "true");
192
+ if (this.affiliateFeePercent && this.affiliateFeeRecipient) {
193
+ url.searchParams.set("affiliateFeePercent", this.affiliateFeePercent);
194
+ url.searchParams.set("affiliateFeeRecipient", this.affiliateFeeRecipient);
195
+ }
196
+ let data;
197
+ try {
198
+ data = await fetchJson(url.toString());
199
+ }
200
+ catch (err) {
201
+ if (this.affiliateFeePercent && this.affiliateFeeRecipient) {
202
+ console.warn("[debridge] create-tx failed with affiliate fee, retrying without:", err.message);
203
+ url.searchParams.delete("affiliateFeePercent");
204
+ url.searchParams.delete("affiliateFeeRecipient");
205
+ data = await fetchJson(url.toString());
206
+ }
207
+ else {
208
+ throw err;
209
+ }
210
+ }
211
+ const orderId = data.orderId ?? `${Date.now()}`;
212
+ // Handle Solana source chains — deBridge returns serialized Solana transaction
213
+ if (isSolanaChain(p.srcChainId)) {
214
+ // For Solana, deBridge returns data.tx.data as a hex-encoded (0x-prefixed) serialized
215
+ // VersionedTransaction. Caller must decode hex, replace recentBlockhash, sign, and send.
216
+ const serializedTx = data.tx.data || data.tx.serializedTx;
217
+ if (!serializedTx) {
218
+ throw new Error("Invalid Solana transaction data in deBridge create-tx response. " +
219
+ "Expected serialized transaction in tx.data.");
220
+ }
221
+ return {
222
+ // Use placeholder values for EVM-specific fields (not used for Solana)
223
+ to: "solana",
224
+ data: "0x",
225
+ value: "0x0",
226
+ chainId: p.srcChainId,
227
+ provider: "debridge",
228
+ trackingId: `debridge:${orderId}`,
229
+ solanaTransaction: {
230
+ serializedTx,
231
+ },
232
+ };
233
+ }
234
+ // EVM source chain handling
235
+ if (!data.tx || !data.tx.to || !data.tx.data) {
236
+ throw new Error("Invalid or missing transaction data in deBridge create-tx response. " +
237
+ "Ensure senderAddress is provided.");
238
+ }
239
+ const result = {
240
+ to: data.tx.to,
241
+ data: data.tx.data,
242
+ value: data.tx.value ? `0x${BigInt(data.tx.value).toString(16)}` : "0x0",
243
+ chainId: p.srcChainId,
244
+ provider: "debridge",
245
+ trackingId: `debridge:${orderId}`,
246
+ };
247
+ // Check if ERC20 approval is needed (non-native token)
248
+ // deBridge API doesn't return allowanceTarget — use tx.to (the DlnSource contract)
249
+ // as the spender for the ERC20 approval.
250
+ // When prependOperatingExpenses=true, the contract pulls MORE than the user's input amount
251
+ // (input + operating expenses). The estimation.srcChainTokenIn.amount includes expenses,
252
+ // but the exact tx-encoded amount can differ slightly due to gas price fluctuation
253
+ // between the estimation and tx encoding. Add a 5% buffer to prevent allowance failures.
254
+ const approvalSpender = data.tx.allowanceTarget ?? data.tx.to;
255
+ if (approvalSpender && !isNativeToken(p.srcChainTokenIn)) {
256
+ const estimatedAmount = data.estimation?.srcChainTokenIn?.amount ?? p.srcChainTokenInAmount;
257
+ // Buffer the approval by 5% to account for operating expense fluctuation.
258
+ // This is still a per-transaction approval (not unlimited) — safe and scoped.
259
+ const approvalAmount = (BigInt(estimatedAmount) * 105n / 100n).toString();
260
+ result.approvalTx = {
261
+ to: p.srcChainTokenIn,
262
+ data: buildApproveData(approvalSpender, approvalAmount),
263
+ value: "0x0",
264
+ chainId: p.srcChainId,
265
+ };
266
+ // deBridge's create-tx embeds the nonce from when it was called.
267
+ // After approval is sent, the nonce becomes stale.
268
+ // Caller must re-fetch the bridge tx after approval confirms.
269
+ result.needsPostApprovalBuild = true;
270
+ }
271
+ return result;
272
+ }
273
+ async getStatus(trackingId, meta) {
274
+ try {
275
+ const orderId = trackingId.replace("debridge:", "");
276
+ const url = new URL(`${BASE_URL}/dln/order/${orderId}/status`);
277
+ const data = await fetchJson(url.toString());
278
+ const stateMap = {
279
+ None: "pending",
280
+ Created: "pending",
281
+ Fulfilled: "completed",
282
+ SentUnlock: "completed",
283
+ OrderCancelled: "failed",
284
+ SentOrderCancel: "failed",
285
+ ClaimedUnlock: "completed",
286
+ ClaimedOrderCancel: "refunded",
287
+ };
288
+ return {
289
+ state: stateMap[data.status] ?? "in_progress",
290
+ humanReadable: `deBridge DLN order: ${data.status ?? "unknown"}`,
291
+ sourceTxHash: meta?.txHash,
292
+ destTxHash: data.fulfillTxHash,
293
+ provider: "debridge",
294
+ elapsed: 0,
295
+ };
296
+ }
297
+ catch (err) {
298
+ // Fallback: check on-chain tx receipt if we have a txHash
299
+ if (meta?.txHash && meta?.fromChain) {
300
+ try {
301
+ const { getProvider } = await import("../utils/gas-estimator.js");
302
+ const chainId = Number(meta.fromChain);
303
+ if (!isNaN(chainId) && !isSolanaChain(chainId)) {
304
+ const provider = await getProvider(chainId);
305
+ const receipt = await provider.getTransactionReceipt(meta.txHash);
306
+ if (receipt) {
307
+ const confirmed = receipt.status === 1;
308
+ return {
309
+ state: confirmed ? "pending" : "failed",
310
+ humanReadable: confirmed
311
+ ? `Transaction confirmed on-chain (block ${receipt.blockNumber}). Bridge provider hasn't indexed the order yet — this is normal, check again in 1-2 minutes.`
312
+ : `Transaction reverted on-chain (block ${receipt.blockNumber}). The bridge transaction failed.`,
313
+ sourceTxHash: meta.txHash,
314
+ provider: "debridge",
315
+ elapsed: 0,
316
+ };
317
+ }
318
+ else {
319
+ return {
320
+ state: "pending",
321
+ humanReadable: "Transaction submitted but not yet confirmed on-chain. Wait for block confirmation.",
322
+ sourceTxHash: meta.txHash,
323
+ provider: "debridge",
324
+ elapsed: 0,
325
+ };
326
+ }
327
+ }
328
+ }
329
+ catch {
330
+ // On-chain check failed too — fall through to unknown
331
+ }
332
+ }
333
+ return {
334
+ state: "unknown",
335
+ humanReadable: `Status check failed: ${sanitizeError(err)}. If you just submitted the transaction, wait 1-2 minutes for the bridge provider to index it.`,
336
+ provider: "debridge",
337
+ elapsed: 0,
338
+ };
339
+ }
340
+ }
341
+ async getSupportedChains() {
342
+ try {
343
+ const allChains = getAllChains();
344
+ // Name-to-canonical lookup (for chains where originalChainId is missing/wrong)
345
+ const nameToCanonical = new Map(allChains.map((c) => [c.name.toLowerCase(), c]));
346
+ const data = await fetchJson(`${BASE_URL}/supported-chains-info`);
347
+ if (Array.isArray(data.chains)) {
348
+ const seen = new Set();
349
+ const result = [];
350
+ for (const chain of data.chains) {
351
+ // deBridge: originalChainId = EVM canonical ID, chainId = deBridge internal ID
352
+ const rawId = chain.originalChainId ?? chain.chainId;
353
+ const chainName = chain.chainName ?? "";
354
+ // Match canonical entry by ID first, then by name (catches wrong/missing originalChainId)
355
+ const canonical = allChains.find((c) => c.id === rawId) ??
356
+ nameToCanonical.get(chainName.toLowerCase());
357
+ const id = canonical?.id ?? rawId;
358
+ // Deduplicate: skip if we already have this canonical chain
359
+ if (seen.has(id))
360
+ continue;
361
+ seen.add(id);
362
+ result.push({
363
+ id,
364
+ name: canonical?.name ?? chainName,
365
+ key: canonical?.key ?? chainName.toLowerCase().replace(/\s+/g, "-"),
366
+ providers: ["debridge"],
367
+ });
368
+ }
369
+ return result;
370
+ }
371
+ }
372
+ catch {
373
+ // Fallback to hardcoded chains if API fails
374
+ }
375
+ return getAllChains().map((c) => ({
376
+ ...c,
377
+ providers: ["debridge"],
378
+ }));
379
+ }
380
+ }
@@ -0,0 +1,19 @@
1
+ import type { BridgeBackend, BridgeQuote, BridgeStatus, ChainInfo, QuoteParams, TokenInfo, TransactionRequest } from "./types.js";
2
+ export declare class LiFiBackend implements BridgeBackend {
3
+ name: string;
4
+ private apiKey?;
5
+ private integrator?;
6
+ private integratorFee?;
7
+ constructor(apiKey?: string, integrator?: string, integratorFee?: string);
8
+ private headers;
9
+ getQuote(params: QuoteParams): Promise<BridgeQuote | null>;
10
+ /**
11
+ * Fetch multiple routes via LI.FI /advanced/routes endpoint.
12
+ * Returns up to 5 route options with full fee breakdowns.
13
+ */
14
+ getQuotes(params: QuoteParams): Promise<BridgeQuote[]>;
15
+ buildTransaction(quote: BridgeQuote): Promise<TransactionRequest>;
16
+ getStatus(trackingId: string, meta?: Record<string, string>): Promise<BridgeStatus>;
17
+ getSupportedChains(): Promise<ChainInfo[]>;
18
+ getSupportedTokens(chainId: number): Promise<TokenInfo[]>;
19
+ }