@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.
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/dist/backends/across.d.ts +10 -0
- package/dist/backends/across.js +285 -0
- package/dist/backends/debridge.d.ts +11 -0
- package/dist/backends/debridge.js +380 -0
- package/dist/backends/lifi.d.ts +19 -0
- package/dist/backends/lifi.js +295 -0
- package/dist/backends/persistence.d.ts +86 -0
- package/dist/backends/persistence.js +642 -0
- package/dist/backends/relay.d.ts +11 -0
- package/dist/backends/relay.js +292 -0
- package/dist/backends/squid.d.ts +31 -0
- package/dist/backends/squid.js +476 -0
- package/dist/backends/types.d.ts +125 -0
- package/dist/backends/types.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +154 -0
- package/dist/routing/engine.d.ts +49 -0
- package/dist/routing/engine.js +336 -0
- package/dist/tools/check-status.d.ts +3 -0
- package/dist/tools/check-status.js +93 -0
- package/dist/tools/execute-bridge.d.ts +3 -0
- package/dist/tools/execute-bridge.js +428 -0
- package/dist/tools/get-chains.d.ts +3 -0
- package/dist/tools/get-chains.js +162 -0
- package/dist/tools/get-quote.d.ts +3 -0
- package/dist/tools/get-quote.js +534 -0
- package/dist/tools/get-tokens.d.ts +3 -0
- package/dist/tools/get-tokens.js +128 -0
- package/dist/tools/help.d.ts +2 -0
- package/dist/tools/help.js +204 -0
- package/dist/tools/multi-quote.d.ts +3 -0
- package/dist/tools/multi-quote.js +310 -0
- package/dist/tools/onboard.d.ts +3 -0
- package/dist/tools/onboard.js +218 -0
- package/dist/tools/wallet.d.ts +14 -0
- package/dist/tools/wallet.js +744 -0
- package/dist/tools/xprt-farm.d.ts +3 -0
- package/dist/tools/xprt-farm.js +1308 -0
- package/dist/tools/xprt-rewards.d.ts +2 -0
- package/dist/tools/xprt-rewards.js +177 -0
- package/dist/tools/xprt-staking.d.ts +2 -0
- package/dist/tools/xprt-staking.js +565 -0
- package/dist/utils/chains.d.ts +22 -0
- package/dist/utils/chains.js +154 -0
- package/dist/utils/circuit-breaker.d.ts +64 -0
- package/dist/utils/circuit-breaker.js +160 -0
- package/dist/utils/evm.d.ts +18 -0
- package/dist/utils/evm.js +46 -0
- package/dist/utils/fill-detector.d.ts +70 -0
- package/dist/utils/fill-detector.js +298 -0
- package/dist/utils/gas-estimator.d.ts +67 -0
- package/dist/utils/gas-estimator.js +340 -0
- package/dist/utils/sanitize-error.d.ts +23 -0
- package/dist/utils/sanitize-error.js +101 -0
- package/dist/utils/token-registry.d.ts +70 -0
- package/dist/utils/token-registry.js +669 -0
- package/dist/utils/tokens.d.ts +17 -0
- package/dist/utils/tokens.js +37 -0
- package/dist/utils/tx-simulator.d.ts +27 -0
- package/dist/utils/tx-simulator.js +105 -0
- 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
|
+
}
|
package/dist/index.d.ts
ADDED