@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,476 @@
1
+ import { formatTokenAmount } from "../utils/tokens.js";
2
+ import { buildApproveData, isNativeToken } from "../utils/evm.js";
3
+ import { estimateGasCostUsd, getGasUnits } from "../utils/gas-estimator.js";
4
+ import { sanitizeError } from "../utils/sanitize-error.js";
5
+ import { getCosmosChainIdFromSynthetic, isCosmosChain, PERSISTENCE_CHAIN_ID, COSMOSHUB_CHAIN_ID } from "../utils/chains.js";
6
+ import { getKey } from "../tools/wallet.js";
7
+ // Cosmos bech32 prefixes by synthetic chain ID
8
+ const COSMOS_BECH32_PREFIX = {
9
+ [PERSISTENCE_CHAIN_ID]: "persistence",
10
+ [COSMOSHUB_CHAIN_ID]: "cosmos",
11
+ };
12
+ // Valid placeholder addresses for quote-only requests (derived from well-known "abandon" mnemonic)
13
+ const COSMOS_PLACEHOLDER_ADDRESS = {
14
+ [PERSISTENCE_CHAIN_ID]: "persistence19rl4cm2hmr8afy4kldpxz3fka4jguq0ajvtw33",
15
+ [COSMOSHUB_CHAIN_ID]: "cosmos19rl4cm2hmr8afy4kldpxz3fka4jguq0auqdal4",
16
+ };
17
+ const BASE_URL = "https://v2.api.squidrouter.com";
18
+ const TIMEOUT_MS = 15_000;
19
+ /**
20
+ * Convert native token address from zero address to EVM sentinel address for Squid API.
21
+ * Squid rejects 0x0000...0000 and requires 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE for native tokens.
22
+ */
23
+ function convertNativeTokenForSquid(tokenAddress) {
24
+ if (tokenAddress === "0x0000000000000000000000000000000000000000") {
25
+ return "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
26
+ }
27
+ return tokenAddress;
28
+ }
29
+ async function fetchJson(url, init) {
30
+ const controller = new AbortController();
31
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
32
+ try {
33
+ const res = await fetch(url, { ...init, signal: controller.signal });
34
+ if (!res.ok) {
35
+ const text = await res.text().catch(() => "");
36
+ throw new Error(`Squid ${res.status}: ${text.slice(0, 200)}`);
37
+ }
38
+ return res.json();
39
+ }
40
+ finally {
41
+ clearTimeout(timer);
42
+ }
43
+ }
44
+ export class SquidBackend {
45
+ name = "squid";
46
+ integratorId;
47
+ constructor(integratorId) {
48
+ this.integratorId = integratorId;
49
+ }
50
+ headers() {
51
+ const h = {
52
+ Accept: "application/json",
53
+ "Content-Type": "application/json",
54
+ };
55
+ if (this.integratorId) {
56
+ h["x-integrator-id"] = this.integratorId;
57
+ }
58
+ return h;
59
+ }
60
+ /**
61
+ * Map a chain ID to the string Squid Router expects.
62
+ * For EVM chains, this is just the numeric string (e.g. "8453").
63
+ * For Cosmos chains, this is the Cosmos chain ID string (e.g. "persistence-core-1").
64
+ */
65
+ resolveSquidChainId(chainId) {
66
+ const cosmosId = getCosmosChainIdFromSynthetic(chainId);
67
+ return cosmosId ?? String(chainId);
68
+ }
69
+ /**
70
+ * Resolve the recipient address for a Cosmos destination chain.
71
+ * If the provided toAddress is an EVM address (0x...), derive the
72
+ * Cosmos bech32 address from the wallet mnemonic.
73
+ * Falls back to a placeholder for quote-only requests.
74
+ */
75
+ async resolveCosmosToAddress(toAddress, toChainId, forExecution) {
76
+ // If already a bech32 Cosmos address, use it directly
77
+ if (toAddress && !toAddress.startsWith("0x")) {
78
+ return toAddress;
79
+ }
80
+ // Try to derive from wallet mnemonic
81
+ const mnemonic = getKey("mnemonic");
82
+ const prefix = COSMOS_BECH32_PREFIX[toChainId];
83
+ if (mnemonic && prefix) {
84
+ try {
85
+ const { Secp256k1HdWallet } = await import("@cosmjs/amino");
86
+ const wallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, { prefix });
87
+ const [account] = await wallet.getAccounts();
88
+ return account.address;
89
+ }
90
+ catch {
91
+ // Fall through to placeholder
92
+ }
93
+ }
94
+ if (forExecution) {
95
+ throw new Error(`Cosmos destination requires a valid bech32 address (e.g. ${prefix || "cosmos"}1...). ` +
96
+ `Configure a wallet with wallet_setup to auto-derive, or pass toAddress explicitly.`);
97
+ }
98
+ // For quoting, use a valid placeholder address — Squid validates bech32 checksums
99
+ // but the quote amount doesn't depend on the specific address
100
+ const placeholder = COSMOS_PLACEHOLDER_ADDRESS[toChainId];
101
+ if (placeholder)
102
+ return placeholder;
103
+ return `${prefix || "cosmos"}1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq0wejkl`;
104
+ }
105
+ async getQuote(params) {
106
+ try {
107
+ const fromChainStr = this.resolveSquidChainId(params.fromChainId);
108
+ const toChainStr = this.resolveSquidChainId(params.toChainId);
109
+ // Resolve toAddress for Cosmos destinations
110
+ const toIsCosmos = isCosmosChain(params.toChainId);
111
+ let resolvedToAddress = params.toAddress || params.fromAddress;
112
+ if (toIsCosmos) {
113
+ resolvedToAddress = await this.resolveCosmosToAddress(params.toAddress, params.toChainId, false);
114
+ }
115
+ const fromIsCosmos = isCosmosChain(params.fromChainId);
116
+ const body = {
117
+ fromChain: fromChainStr,
118
+ toChain: toChainStr,
119
+ fromToken: convertNativeTokenForSquid(params.fromTokenAddress),
120
+ toToken: convertNativeTokenForSquid(params.toTokenAddress),
121
+ fromAmount: params.amountRaw,
122
+ fromAddress: params.fromAddress,
123
+ toAddress: resolvedToAddress,
124
+ slippageConfig: {
125
+ autoMode: 1, // 1 = normal auto-slippage
126
+ },
127
+ };
128
+ // Note: "prefer" field removed — Squid v2 API rejects it for many route types
129
+ // ("speed invalid dex" / "output invalid dex"). Squid auto-selects optimal routing.
130
+ const data = await fetchJson(`${BASE_URL}/v2/route`, {
131
+ method: "POST",
132
+ headers: this.headers(),
133
+ body: JSON.stringify(body),
134
+ });
135
+ const route = data.route;
136
+ if (!route)
137
+ return null;
138
+ const estimate = route.estimate;
139
+ if (!estimate)
140
+ return null;
141
+ // Extract output amounts
142
+ const toAmount = estimate.toAmount ?? "0";
143
+ const toAmountMin = estimate.toAmountMin ?? toAmount;
144
+ const toToken = estimate.toToken ?? {};
145
+ const fromToken = estimate.fromToken ?? {};
146
+ const outputDecimals = toToken.decimals ?? params.toTokenDecimals ?? 18;
147
+ const srcSymbol = fromToken.symbol ?? "?";
148
+ const dstSymbol = toToken.symbol ?? "?";
149
+ // Fee breakdown from estimate
150
+ const gasCosts = estimate.gasCosts ?? [];
151
+ let gasCostUsd = 0;
152
+ for (const gc of gasCosts) {
153
+ gasCostUsd += Number(gc.amountUsd ?? gc.amountUSD ?? 0);
154
+ }
155
+ const feeCosts = estimate.feeCosts ?? [];
156
+ let protocolFeeUsd = 0;
157
+ let integratorFeeUsd = 0;
158
+ for (const fc of feeCosts) {
159
+ const usd = Number(fc.amountUsd ?? fc.amountUSD ?? 0);
160
+ if (fc.name?.toLowerCase().includes("integrator") ||
161
+ fc.name?.toLowerCase().includes("affiliate")) {
162
+ integratorFeeUsd += usd;
163
+ }
164
+ else {
165
+ protocolFeeUsd += usd;
166
+ }
167
+ }
168
+ // If API didn't provide USD gas cost, estimate it ourselves
169
+ if (gasCostUsd < 0.001) {
170
+ const gasUnits = getGasUnits("squid", params.fromChainId);
171
+ if (gasUnits) {
172
+ const gasEstimate = await estimateGasCostUsd(params.fromChainId, gasUnits);
173
+ if (gasEstimate) {
174
+ gasCostUsd = gasEstimate.costUsd;
175
+ }
176
+ }
177
+ }
178
+ const totalFeeUsd = gasCostUsd + protocolFeeUsd + integratorFeeUsd;
179
+ const feeBreakdown = {
180
+ gasCostUsd: gasCostUsd > 0 ? Math.round(gasCostUsd * 100) / 100 : null,
181
+ protocolFeeUsd: Math.round(protocolFeeUsd * 100) / 100,
182
+ integratorFeeUsd: Math.round(integratorFeeUsd * 100) / 100,
183
+ integratorFeePercent: null,
184
+ totalFeeUsd: gasCostUsd > 0 ? Math.round(totalFeeUsd * 100) / 100 : null,
185
+ };
186
+ // Estimated time -- Squid provides estimatedRouteDuration in seconds
187
+ const estimatedTimeSeconds = estimate.estimatedRouteDuration ?? estimate.estimatedTime ?? 300;
188
+ // Build route description from actions/steps
189
+ const actions = estimate.actions ?? [];
190
+ const routeDescription = actions.length > 0
191
+ ? actions
192
+ .map((a) => a.provider ?? a.type ?? "?")
193
+ .join(" -> ")
194
+ : "Squid Router";
195
+ return {
196
+ backendName: "squid",
197
+ provider: `${routeDescription} via Squid`,
198
+ outputAmount: formatTokenAmount(toAmount, outputDecimals),
199
+ outputAmountRaw: toAmount,
200
+ minOutputAmount: formatTokenAmount(toAmountMin, outputDecimals),
201
+ minOutputAmountRaw: toAmountMin,
202
+ outputDecimals,
203
+ estimatedGasCostUsd: gasCostUsd > 0 ? Math.round(gasCostUsd * 100) / 100 : null,
204
+ estimatedFeeUsd: gasCostUsd > 0 ? Math.round(totalFeeUsd * 100) / 100 : null,
205
+ feeBreakdown,
206
+ estimatedTimeSeconds,
207
+ route: `${srcSymbol} -> Squid Router -> ${dstSymbol}`,
208
+ quoteData: {
209
+ route: data.route,
210
+ requestId: data.requestId,
211
+ params: {
212
+ fromChainId: params.fromChainId,
213
+ toChainId: params.toChainId,
214
+ fromTokenAddress: params.fromTokenAddress,
215
+ toTokenAddress: params.toTokenAddress,
216
+ amountRaw: params.amountRaw,
217
+ fromAddress: params.fromAddress,
218
+ toAddress: resolvedToAddress,
219
+ },
220
+ },
221
+ // Squid quotes are relatively stable -- use 60s expiry
222
+ expiresAt: Date.now() + 60_000,
223
+ };
224
+ }
225
+ catch (err) {
226
+ console.error("[squid] route error:", err.message);
227
+ return null;
228
+ }
229
+ }
230
+ async buildTransaction(quote) {
231
+ const qd = quote.quoteData;
232
+ const route = qd.route;
233
+ const p = qd.params;
234
+ if (!route) {
235
+ throw new Error("No route data in Squid quote");
236
+ }
237
+ // Squid V2 includes transactionRequest directly in the route response
238
+ const txReq = route.transactionRequest;
239
+ if (!txReq || !txReq.target || !txReq.data) {
240
+ // If transactionRequest is missing, re-fetch the route to get fresh tx data
241
+ const body = {
242
+ fromChain: this.resolveSquidChainId(p.fromChainId),
243
+ toChain: this.resolveSquidChainId(p.toChainId),
244
+ fromToken: convertNativeTokenForSquid(p.fromTokenAddress),
245
+ toToken: convertNativeTokenForSquid(p.toTokenAddress),
246
+ fromAmount: p.amountRaw,
247
+ fromAddress: p.fromAddress,
248
+ toAddress: p.toAddress,
249
+ slippageConfig: {
250
+ autoMode: 1,
251
+ },
252
+ };
253
+ const data = await fetchJson(`${BASE_URL}/v2/route`, {
254
+ method: "POST",
255
+ headers: this.headers(),
256
+ body: JSON.stringify(body),
257
+ });
258
+ const freshTx = data.route?.transactionRequest;
259
+ if (!freshTx || !freshTx.target || !freshTx.data) {
260
+ throw new Error("No transactionRequest in Squid route response");
261
+ }
262
+ return this.buildTxResult(freshTx, p, data.requestId);
263
+ }
264
+ return this.buildTxResult(txReq, p, qd.requestId);
265
+ }
266
+ buildTxResult(txReq, params, requestId) {
267
+ const result = {
268
+ // Squid V2 uses "target" instead of "to" in transactionRequest
269
+ to: txReq.target ?? txReq.to,
270
+ data: txReq.data,
271
+ value: txReq.value ? `0x${BigInt(txReq.value).toString(16)}` : "0x0",
272
+ chainId: Number(txReq.chainId ?? params.fromChainId),
273
+ gasLimit: txReq.gasLimit?.toString(),
274
+ provider: "squid",
275
+ trackingId: `squid:${requestId ?? Date.now()}`,
276
+ };
277
+ // Check if ERC20 approval is needed (non-native tokens)
278
+ if (!isNativeToken(params.fromTokenAddress)) {
279
+ // Squid provides the approval target (router contract address)
280
+ const approvalTarget = txReq.target ?? txReq.to;
281
+ if (approvalTarget) {
282
+ // MEDIUM-001: Cap approval to 110% of quoted input to prevent excessive approvals
283
+ let approvalAmount = params.amountRaw;
284
+ try {
285
+ const inputBn = BigInt(params.amountRaw);
286
+ approvalAmount = ((inputBn * 110n) / 100n).toString();
287
+ }
288
+ catch {
289
+ // Keep original amount if BigInt conversion fails
290
+ }
291
+ result.approvalTx = {
292
+ to: params.fromTokenAddress,
293
+ data: buildApproveData(approvalTarget, approvalAmount),
294
+ value: "0x0",
295
+ chainId: Number(txReq.chainId ?? params.fromChainId),
296
+ };
297
+ }
298
+ }
299
+ return result;
300
+ }
301
+ async getStatus(trackingId, meta) {
302
+ try {
303
+ const txHash = meta?.txHash;
304
+ // Extract requestId from trackingId ("squid:<requestId>")
305
+ const requestId = trackingId.startsWith("squid:")
306
+ ? trackingId.slice("squid:".length)
307
+ : undefined;
308
+ if (!requestId && !txHash) {
309
+ return {
310
+ state: "unknown",
311
+ humanReadable: "No requestId or txHash for Squid status check",
312
+ provider: "squid",
313
+ elapsed: 0,
314
+ };
315
+ }
316
+ // Build status query params
317
+ const url = new URL(`${BASE_URL}/v2/status`);
318
+ if (txHash) {
319
+ url.searchParams.set("transactionId", txHash);
320
+ }
321
+ if (requestId && requestId !== String(Number(requestId))) {
322
+ // Only use requestId if it looks like a real ID (not a timestamp fallback)
323
+ url.searchParams.set("requestId", requestId);
324
+ }
325
+ if (meta?.fromChain) {
326
+ url.searchParams.set("fromChainId", meta.fromChain);
327
+ }
328
+ if (meta?.toChain) {
329
+ url.searchParams.set("toChainId", meta.toChain);
330
+ }
331
+ const data = await fetchJson(url.toString(), {
332
+ headers: this.headers(),
333
+ });
334
+ // Squid V2 status states
335
+ const stateMap = {
336
+ not_found: "pending",
337
+ ongoing: "in_progress",
338
+ partial_success: "in_progress",
339
+ success: "completed",
340
+ needs_gas: "in_progress",
341
+ confirmed: "completed",
342
+ express_executed: "completed",
343
+ executed: "completed",
344
+ error: "failed",
345
+ refunded: "refunded",
346
+ };
347
+ // Normalize status string (Squid may return uppercase or mixed case)
348
+ const rawStatus = (data.squidTransactionStatus ?? data.status ?? "unknown").toLowerCase();
349
+ const mappedState = stateMap[rawStatus];
350
+ if (!mappedState && rawStatus !== "unknown") {
351
+ console.warn(`[squid] unmapped status: "${rawStatus}" -- treating as in_progress`);
352
+ }
353
+ const elapsed = data.fromChain?.transactionTimestamp
354
+ ? Math.floor((Date.now() - new Date(data.fromChain.transactionTimestamp).getTime()) / 1000)
355
+ : 0;
356
+ return {
357
+ state: mappedState ?? (rawStatus === "unknown" ? "unknown" : "in_progress"),
358
+ humanReadable: `Squid bridge: ${rawStatus}`,
359
+ sourceTxHash: txHash ?? data.fromChain?.transactionId,
360
+ destTxHash: data.toChain?.transactionId,
361
+ provider: "squid",
362
+ elapsed,
363
+ };
364
+ }
365
+ catch (err) {
366
+ return {
367
+ state: "unknown",
368
+ humanReadable: `Status check failed: ${sanitizeError(err)}`,
369
+ provider: "squid",
370
+ elapsed: 0,
371
+ };
372
+ }
373
+ }
374
+ async getSupportedChains() {
375
+ try {
376
+ const data = await fetchJson(`${BASE_URL}/v2/chains`, {
377
+ headers: this.headers(),
378
+ });
379
+ const chains = data.chains ?? [];
380
+ if (chains.length === 0)
381
+ return this.fallbackChains();
382
+ const result = chains.map((c) => {
383
+ const rawId = String(c.chainId);
384
+ // Map known Cosmos chain ID strings to our synthetic numeric IDs
385
+ let numericId;
386
+ if (rawId === "core-1") {
387
+ numericId = PERSISTENCE_CHAIN_ID;
388
+ }
389
+ else if (rawId === "cosmoshub-4") {
390
+ numericId = COSMOSHUB_CHAIN_ID;
391
+ }
392
+ else if (isNaN(Number(rawId))) {
393
+ // Other Cosmos chains — skip for now (not in our chain registry)
394
+ numericId = NaN;
395
+ }
396
+ else {
397
+ numericId = Number(rawId);
398
+ }
399
+ // Resolve proper name for known Cosmos chains
400
+ let name = c.chainName ?? c.networkName ?? `Chain ${c.chainId}`;
401
+ if (rawId === "core-1")
402
+ name = "Persistence";
403
+ else if (rawId === "cosmoshub-4")
404
+ name = "Cosmos Hub";
405
+ let key = (name).toLowerCase().replace(/\s+/g, "-");
406
+ if (rawId === "core-1")
407
+ key = "persistence";
408
+ else if (rawId === "cosmoshub-4")
409
+ key = "cosmoshub";
410
+ return {
411
+ id: numericId,
412
+ name,
413
+ key,
414
+ logoURI: c.chainIconURI ?? c.iconUrl,
415
+ providers: ["squid"],
416
+ };
417
+ }).filter(c => !isNaN(c.id)); // Filter out unmapped Cosmos chains
418
+ return result;
419
+ }
420
+ catch (err) {
421
+ console.error("[squid] chains error:", err.message);
422
+ return this.fallbackChains();
423
+ }
424
+ }
425
+ async getSupportedTokens(chainId) {
426
+ try {
427
+ // Convert synthetic chain ID to the string Squid expects
428
+ const squidChainId = this.resolveSquidChainId(chainId);
429
+ const data = await fetchJson(`${BASE_URL}/v2/tokens`, {
430
+ headers: this.headers(),
431
+ });
432
+ const tokens = data.tokens ?? [];
433
+ // Filter to requested chain (compare as strings since Cosmos chain IDs are strings)
434
+ const chainTokens = tokens.filter((t) => String(t.chainId) === squidChainId);
435
+ return chainTokens.slice(0, 50).map((t) => ({
436
+ symbol: t.symbol,
437
+ name: t.name ?? t.symbol,
438
+ address: t.address,
439
+ decimals: t.decimals,
440
+ chainId: chainId, // Return our synthetic ID, not Squid's string
441
+ logoURI: t.logoURI ?? t.iconUrl,
442
+ }));
443
+ }
444
+ catch (err) {
445
+ console.error("[squid] tokens error:", err.message);
446
+ return [];
447
+ }
448
+ }
449
+ /**
450
+ * Fallback chain list when the API is unavailable.
451
+ * Squid supports EVM chains and Cosmos chains (via Axelar GMP).
452
+ */
453
+ fallbackChains() {
454
+ return [
455
+ { id: 1, name: "Ethereum", key: "ethereum", providers: ["squid"] },
456
+ { id: 10, name: "Optimism", key: "optimism", providers: ["squid"] },
457
+ { id: 56, name: "BNB Chain", key: "bsc", providers: ["squid"] },
458
+ { id: 137, name: "Polygon", key: "polygon", providers: ["squid"] },
459
+ { id: 42161, name: "Arbitrum", key: "arbitrum", providers: ["squid"] },
460
+ { id: 43114, name: "Avalanche", key: "avalanche", providers: ["squid"] },
461
+ { id: 8453, name: "Base", key: "base", providers: ["squid"] },
462
+ { id: 59144, name: "Linea", key: "linea", providers: ["squid"] },
463
+ { id: 534352, name: "Scroll", key: "scroll", providers: ["squid"] },
464
+ { id: 5000, name: "Mantle", key: "mantle", providers: ["squid"] },
465
+ { id: 81457, name: "Blast", key: "blast", providers: ["squid"] },
466
+ { id: 250, name: "Fantom", key: "fantom", providers: ["squid"] },
467
+ { id: 1284, name: "Moonbeam", key: "moonbeam", providers: ["squid"] },
468
+ { id: 2222, name: "Kava", key: "kava", providers: ["squid"] },
469
+ { id: 314, name: "Filecoin", key: "filecoin", providers: ["squid"] },
470
+ { id: 42220, name: "Celo", key: "celo", providers: ["squid"] },
471
+ // Cosmos chains (synthetic IDs mapped to real chain IDs in API calls)
472
+ { id: PERSISTENCE_CHAIN_ID, name: "Persistence", key: "persistence", providers: ["squid"] },
473
+ { id: COSMOSHUB_CHAIN_ID, name: "Cosmos Hub", key: "cosmoshub", providers: ["squid"] },
474
+ ];
475
+ }
476
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Generic validation error that any backend can throw.
3
+ * Routing engine catches these and propagates them to the tool layer
4
+ * with user-friendly messages.
5
+ */
6
+ export declare class BackendValidationError extends Error {
7
+ constructor(message: string);
8
+ }
9
+ export interface QuoteParams {
10
+ fromChainId: number;
11
+ toChainId: number;
12
+ fromTokenAddress: string;
13
+ toTokenAddress: string;
14
+ amountRaw: string;
15
+ fromAddress: string;
16
+ toAddress?: string;
17
+ preference: "cheapest" | "fastest";
18
+ fromTokenDecimals?: number;
19
+ toTokenDecimals?: number;
20
+ /** Optional filter: only query these providers (by backend name, e.g. "lifi", "squid") */
21
+ providers?: string[];
22
+ }
23
+ export interface FeeBreakdown {
24
+ gasCostUsd: number | null;
25
+ protocolFeeUsd: number;
26
+ integratorFeeUsd: number;
27
+ integratorFeePercent: string | null;
28
+ totalFeeUsd: number | null;
29
+ fixFeeNativeRaw?: string;
30
+ operatingExpenseRaw?: string;
31
+ totalSourceAmountRaw?: string;
32
+ }
33
+ export interface BridgeQuote {
34
+ /** Machine-readable backend name (e.g. "lifi", "debridge") for routing/lookup */
35
+ backendName: string;
36
+ /** Human-readable provider description (e.g. "Stargate via LI.FI") */
37
+ provider: string;
38
+ outputAmount: string;
39
+ outputAmountRaw: string;
40
+ /** Minimum guaranteed output (after slippage/fees). Worst-case amount that lands in wallet. */
41
+ minOutputAmount: string;
42
+ minOutputAmountRaw: string;
43
+ /** Number of decimals for the output token. Used to normalize cross-backend comparisons. */
44
+ outputDecimals?: number;
45
+ /** Estimated gas cost in USD for the on-chain transaction. null = unknown. */
46
+ estimatedGasCostUsd: number | null;
47
+ /** True if fallback (hardcoded) prices were used for gas estimation instead of live data */
48
+ usingFallbackPrices?: boolean;
49
+ estimatedFeeUsd: number | null;
50
+ feeBreakdown: FeeBreakdown;
51
+ estimatedTimeSeconds: number;
52
+ route: string;
53
+ quoteData: unknown;
54
+ expiresAt: number;
55
+ }
56
+ export interface TransactionRequest {
57
+ to: string;
58
+ data: string;
59
+ value: string;
60
+ chainId: number;
61
+ gasLimit?: string;
62
+ approvalTx?: {
63
+ to: string;
64
+ data: string;
65
+ value: string;
66
+ chainId: number;
67
+ };
68
+ provider: string;
69
+ trackingId: string;
70
+ /** If true, caller must re-fetch bridge tx after approval confirms (avoids stale nonce). */
71
+ needsPostApprovalBuild?: boolean;
72
+ /**
73
+ * EIP-712 typed data for backends that require off-chain signing (e.g. Persistence Interop).
74
+ * When present, skip tx simulation — the agent must sign this data with their wallet
75
+ * then submit the resulting signature to the backend.
76
+ */
77
+ eip712?: {
78
+ domain: Record<string, unknown>;
79
+ types: Record<string, unknown>;
80
+ value: Record<string, unknown>;
81
+ description: string;
82
+ };
83
+ /**
84
+ * Solana transaction data. When present, the source chain is Solana and the agent
85
+ * must sign/send this as a Solana transaction (not EVM).
86
+ * `serializedTx` is a base58-encoded versioned transaction.
87
+ */
88
+ solanaTransaction?: {
89
+ serializedTx: string;
90
+ };
91
+ }
92
+ export interface BridgeStatus {
93
+ state: "pending" | "in_progress" | "completed" | "failed" | "refunded" | "unknown";
94
+ humanReadable: string;
95
+ sourceTxHash?: string;
96
+ destTxHash?: string;
97
+ provider: string;
98
+ elapsed: number;
99
+ estimatedRemaining?: number;
100
+ }
101
+ export interface ChainInfo {
102
+ id: number;
103
+ name: string;
104
+ key: string;
105
+ logoURI?: string;
106
+ providers: string[];
107
+ }
108
+ export interface TokenInfo {
109
+ symbol: string;
110
+ name: string;
111
+ address: string;
112
+ decimals: number;
113
+ chainId: number;
114
+ logoURI?: string;
115
+ }
116
+ export interface BridgeBackend {
117
+ name: string;
118
+ getQuote(params: QuoteParams): Promise<BridgeQuote | null>;
119
+ /** Return multiple route options. Default implementation wraps getQuote. */
120
+ getQuotes?(params: QuoteParams): Promise<BridgeQuote[]>;
121
+ buildTransaction(quote: BridgeQuote): Promise<TransactionRequest>;
122
+ getStatus(trackingId: string, meta?: Record<string, string>): Promise<BridgeStatus>;
123
+ getSupportedChains(): Promise<ChainInfo[]>;
124
+ getSupportedTokens?(chainId: number): Promise<TokenInfo[]>;
125
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Generic validation error that any backend can throw.
3
+ * Routing engine catches these and propagates them to the tool layer
4
+ * with user-friendly messages.
5
+ */
6
+ export class BackendValidationError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "BackendValidationError";
10
+ }
11
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};