@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,295 @@
1
+ import { formatTokenAmount } from "../utils/tokens.js";
2
+ import { buildApproveData } from "../utils/evm.js";
3
+ import { sanitizeError } from "../utils/sanitize-error.js";
4
+ const BASE_URL = "https://li.quest/v1";
5
+ const TIMEOUT_MS = 30_000;
6
+ async function fetchJson(url, init) {
7
+ const controller = new AbortController();
8
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
9
+ try {
10
+ const res = await fetch(url, { ...init, signal: controller.signal });
11
+ if (!res.ok) {
12
+ const text = await res.text().catch(() => "");
13
+ throw new Error(`LI.FI ${res.status}: ${text.slice(0, 200)}`);
14
+ }
15
+ return res.json();
16
+ }
17
+ finally {
18
+ clearTimeout(timer);
19
+ }
20
+ }
21
+ export class LiFiBackend {
22
+ name = "lifi";
23
+ apiKey;
24
+ integrator;
25
+ integratorFee;
26
+ constructor(apiKey, integrator, integratorFee) {
27
+ this.apiKey = apiKey;
28
+ this.integrator = integrator;
29
+ this.integratorFee = integratorFee;
30
+ }
31
+ headers() {
32
+ const h = { Accept: "application/json" };
33
+ if (this.apiKey)
34
+ h["x-lifi-api-key"] = this.apiKey;
35
+ return h;
36
+ }
37
+ async getQuote(params) {
38
+ const quotes = await this.getQuotes(params);
39
+ return quotes.length > 0 ? quotes[0] : null;
40
+ }
41
+ /**
42
+ * Fetch multiple routes via LI.FI /advanced/routes endpoint.
43
+ * Returns up to 5 route options with full fee breakdowns.
44
+ */
45
+ async getQuotes(params) {
46
+ try {
47
+ const body = {
48
+ fromChainId: params.fromChainId,
49
+ toChainId: params.toChainId,
50
+ fromTokenAddress: params.fromTokenAddress,
51
+ toTokenAddress: params.toTokenAddress,
52
+ fromAmount: params.amountRaw,
53
+ fromAddress: params.fromAddress,
54
+ toAddress: params.toAddress || params.fromAddress,
55
+ options: {
56
+ order: params.preference === "fastest" ? "FASTEST" : "CHEAPEST",
57
+ slippage: 0.03,
58
+ maxPriceImpact: 0.4,
59
+ allowSwitchChain: false,
60
+ },
61
+ };
62
+ if (this.integrator) {
63
+ body.options.integrator = this.integrator;
64
+ if (this.integratorFee)
65
+ body.options.fee = parseFloat(this.integratorFee);
66
+ }
67
+ let data;
68
+ try {
69
+ data = await fetchJson(`${BASE_URL}/advanced/routes`, {
70
+ method: "POST",
71
+ headers: { ...this.headers(), "Content-Type": "application/json" },
72
+ body: JSON.stringify(body),
73
+ });
74
+ }
75
+ catch (err) {
76
+ // If fee param caused the error, retry without it
77
+ if (this.integratorFee && body.options?.fee !== undefined) {
78
+ console.warn("[lifi] quote failed with integrator fee, retrying without fee:", err.message);
79
+ delete body.options.fee;
80
+ data = await fetchJson(`${BASE_URL}/advanced/routes`, {
81
+ method: "POST",
82
+ headers: { ...this.headers(), "Content-Type": "application/json" },
83
+ body: JSON.stringify(body),
84
+ });
85
+ }
86
+ else {
87
+ throw err;
88
+ }
89
+ }
90
+ const routes = data.routes ?? [];
91
+ if (routes.length === 0)
92
+ return [];
93
+ // Filter to single-step routes only — our buildTransaction() only handles step[0],
94
+ // so multi-step routes would silently drop subsequent steps. Let LI.FI handle
95
+ // complex multi-hop routes internally; we only expose atomic single-step bridges.
96
+ const singleStepRoutes = routes.filter((r) => (r.steps?.length ?? 0) === 1);
97
+ if (singleStepRoutes.length === 0)
98
+ return [];
99
+ const integratorFeePercent = this.integratorFee
100
+ ? `${(parseFloat(this.integratorFee) * 100).toFixed(1)}%`
101
+ : null;
102
+ return singleStepRoutes.slice(0, 5).map((route) => {
103
+ const steps = route.steps ?? [];
104
+ const firstStep = steps[0];
105
+ const lastStep = steps[steps.length - 1];
106
+ // Fee breakdown from route data
107
+ const gasCostUsd = route.gasCostUSD ? parseFloat(route.gasCostUSD) : 0;
108
+ // Extract protocol fees from step fee costs
109
+ let protocolFeeUsd = 0;
110
+ let integratorFeeUsd = 0;
111
+ for (const step of steps) {
112
+ const estimate = step.estimate ?? {};
113
+ const feeCosts = estimate.feeCosts ?? [];
114
+ for (const fee of feeCosts) {
115
+ const usd = parseFloat(fee.amountUSD || "0");
116
+ if (fee.name?.toLowerCase().includes("integrator") ||
117
+ fee.name?.toLowerCase().includes("affiliate")) {
118
+ integratorFeeUsd += usd;
119
+ }
120
+ else {
121
+ protocolFeeUsd += usd;
122
+ }
123
+ }
124
+ }
125
+ const totalFeeUsd = gasCostUsd + protocolFeeUsd + integratorFeeUsd;
126
+ const feeBreakdown = {
127
+ gasCostUsd: Math.round(gasCostUsd * 100) / 100,
128
+ protocolFeeUsd: Math.round(protocolFeeUsd * 100) / 100,
129
+ integratorFeeUsd: Math.round(integratorFeeUsd * 100) / 100,
130
+ integratorFeePercent,
131
+ totalFeeUsd: Math.round(totalFeeUsd * 100) / 100,
132
+ };
133
+ // Build human-readable route description
134
+ const toolNames = steps
135
+ .map((s) => s.toolDetails?.name ?? s.tool ?? "?")
136
+ .join(" → ");
137
+ const fromSymbol = firstStep?.action?.fromToken?.symbol ?? "?";
138
+ const toSymbol = lastStep?.action?.toToken?.symbol ?? "?";
139
+ const toDecimals = lastStep?.action?.toToken?.decimals ?? 18;
140
+ // Extract minimum output amount (after slippage)
141
+ // LI.FI provides toAmountMin at the route level
142
+ const minOutputRaw = route.toAmountMin ?? route.toAmount ?? "0";
143
+ return {
144
+ backendName: "lifi",
145
+ provider: `${toolNames} via LI.FI`,
146
+ outputAmount: route.toAmount
147
+ ? formatTokenAmount(route.toAmount, toDecimals)
148
+ : "0",
149
+ outputAmountRaw: route.toAmount ?? "0",
150
+ minOutputAmount: formatTokenAmount(minOutputRaw, toDecimals),
151
+ minOutputAmountRaw: minOutputRaw,
152
+ outputDecimals: toDecimals,
153
+ estimatedGasCostUsd: Math.round(gasCostUsd * 100) / 100,
154
+ estimatedFeeUsd: totalFeeUsd,
155
+ feeBreakdown,
156
+ estimatedTimeSeconds: steps.reduce((sum, s) => sum + (s.estimate?.executionDuration ?? 0), 0) || 300,
157
+ route: `${fromSymbol} → ${toolNames} → ${toSymbol}`,
158
+ quoteData: route,
159
+ // LI.FI quotes are volatile (DEX prices shift rapidly) — use 30s expiry
160
+ expiresAt: Date.now() + 60_000,
161
+ };
162
+ });
163
+ }
164
+ catch (err) {
165
+ console.error("[lifi] advanced/routes error:", err.message);
166
+ return [];
167
+ }
168
+ }
169
+ async buildTransaction(quote) {
170
+ const route = quote.quoteData;
171
+ // For /advanced/routes, get step transaction from the first step
172
+ const step = route.steps?.[0];
173
+ if (!step)
174
+ throw new Error("No steps in LI.FI route");
175
+ // Call /advanced/stepTransaction to get the actual tx data
176
+ const stepData = await fetchJson(`${BASE_URL}/advanced/stepTransaction`, {
177
+ method: "POST",
178
+ headers: { ...this.headers(), "Content-Type": "application/json" },
179
+ body: JSON.stringify(step),
180
+ });
181
+ const txReq = stepData.transactionRequest;
182
+ if (!txReq)
183
+ throw new Error("No transactionRequest in LI.FI step response");
184
+ const toolName = step.tool ?? step.toolDetails?.name ?? "unknown";
185
+ const result = {
186
+ to: txReq.to,
187
+ data: txReq.data,
188
+ value: txReq.value ?? "0x0",
189
+ chainId: txReq.chainId,
190
+ gasLimit: txReq.gasLimit,
191
+ provider: "lifi",
192
+ trackingId: `lifi:${toolName}:${Date.now()}`,
193
+ };
194
+ // Check if approval is needed
195
+ const estimate = stepData.estimate ?? step.estimate;
196
+ const action = stepData.action ?? step.action;
197
+ if (estimate?.approvalAddress && action?.fromToken?.address) {
198
+ const tokenAddr = action.fromToken.address;
199
+ // Non-native tokens may need approval
200
+ if (tokenAddr !== "0x0000000000000000000000000000000000000000") {
201
+ // MEDIUM-001: Sanity-check approval amount against quoted input
202
+ let approvalAmount = action.fromAmount;
203
+ const quotedInput = route.steps?.[0]?.action?.fromAmount ?? route.fromAmount;
204
+ if (quotedInput && approvalAmount) {
205
+ const approvalBn = BigInt(approvalAmount);
206
+ const quotedBn = BigInt(quotedInput);
207
+ const maxAllowed = (quotedBn * 110n) / 100n; // 110% of quoted
208
+ if (approvalBn > maxAllowed) {
209
+ console.warn(`[lifi] Approval amount ${approvalAmount} exceeds 110% of quoted input ${quotedInput}. Capping to ${maxAllowed.toString()}.`);
210
+ approvalAmount = maxAllowed.toString();
211
+ }
212
+ }
213
+ result.approvalTx = {
214
+ to: tokenAddr,
215
+ data: buildApproveData(estimate.approvalAddress, approvalAmount),
216
+ value: "0x0",
217
+ chainId: txReq.chainId,
218
+ };
219
+ }
220
+ }
221
+ return result;
222
+ }
223
+ async getStatus(trackingId, meta) {
224
+ try {
225
+ const txHash = meta?.txHash;
226
+ const fromChain = meta?.fromChain;
227
+ const toChain = meta?.toChain;
228
+ if (!txHash) {
229
+ return {
230
+ state: "unknown",
231
+ humanReadable: "No transaction hash provided for status check",
232
+ provider: "lifi",
233
+ elapsed: 0,
234
+ };
235
+ }
236
+ const url = new URL(`${BASE_URL}/status`);
237
+ url.searchParams.set("txHash", txHash);
238
+ if (fromChain)
239
+ url.searchParams.set("fromChain", fromChain);
240
+ if (toChain)
241
+ url.searchParams.set("toChain", toChain);
242
+ const data = await fetchJson(url.toString(), { headers: this.headers() });
243
+ const stateMap = {
244
+ NOT_FOUND: "pending",
245
+ PENDING: "pending",
246
+ DONE: "completed",
247
+ FAILED: "failed",
248
+ };
249
+ const elapsed = data.sending?.timestamp
250
+ ? Math.floor((Date.now() - data.sending.timestamp * 1000) / 1000)
251
+ : 0;
252
+ return {
253
+ state: stateMap[data.status] ?? "in_progress",
254
+ humanReadable: `Bridge via ${data.tool ?? "LI.FI"}: ${data.status ?? "unknown"}${data.substatus ? ` (${data.substatus})` : ""}`,
255
+ sourceTxHash: data.sending?.txHash,
256
+ destTxHash: data.receiving?.txHash,
257
+ provider: "lifi",
258
+ elapsed,
259
+ };
260
+ }
261
+ catch (err) {
262
+ return {
263
+ state: "unknown",
264
+ humanReadable: `Status check failed: ${sanitizeError(err)}`,
265
+ provider: "lifi",
266
+ elapsed: 0,
267
+ };
268
+ }
269
+ }
270
+ async getSupportedChains() {
271
+ const data = await fetchJson(`${BASE_URL}/chains`, {
272
+ headers: this.headers(),
273
+ });
274
+ return (data.chains ?? []).map((c) => ({
275
+ id: c.id,
276
+ name: c.name,
277
+ key: c.key,
278
+ logoURI: c.logoURI,
279
+ providers: ["lifi"],
280
+ }));
281
+ }
282
+ async getSupportedTokens(chainId) {
283
+ const data = await fetchJson(`${BASE_URL}/tokens?chains=${chainId}`, { headers: this.headers() });
284
+ const tokens = data.tokens?.[String(chainId)] ?? [];
285
+ return tokens.slice(0, 50).map((t) => ({
286
+ symbol: t.symbol,
287
+ name: t.name,
288
+ address: t.address,
289
+ decimals: t.decimals,
290
+ chainId: t.chainId,
291
+ logoURI: t.logoURI,
292
+ }));
293
+ }
294
+ }
295
+ // buildApproveData imported from ../utils/evm.js
@@ -0,0 +1,86 @@
1
+ import { ethers } from "ethers";
2
+ import type { BridgeBackend, BridgeQuote, BridgeStatus, ChainInfo, QuoteParams, TransactionRequest } from "./types.js";
3
+ import { BackendValidationError } from "./types.js";
4
+ /**
5
+ * @deprecated Use BackendValidationError from types.ts instead.
6
+ * Kept as re-export for backward compatibility.
7
+ */
8
+ export declare const PersistenceValidationError: typeof BackendValidationError;
9
+ declare const PERMIT2_WITNESS_TYPES: {
10
+ CrossChainOrder: {
11
+ name: string;
12
+ type: string;
13
+ }[];
14
+ PermitWitnessTransferFrom: {
15
+ name: string;
16
+ type: string;
17
+ }[];
18
+ TokenPermissions: {
19
+ name: string;
20
+ type: string;
21
+ }[];
22
+ };
23
+ /** Prepared order data ready for signing */
24
+ export interface PreparedOrder {
25
+ order: {
26
+ settlementContract: string;
27
+ swapper: string;
28
+ nonce: bigint;
29
+ originChainId: number;
30
+ initiateDeadline: number;
31
+ fillDeadline: number;
32
+ orderData: string;
33
+ };
34
+ eip712Domain: {
35
+ name: string;
36
+ chainId: number;
37
+ verifyingContract: string;
38
+ };
39
+ eip712Types: typeof PERMIT2_WITNESS_TYPES;
40
+ eip712Value: Record<string, unknown>;
41
+ inputToken: string;
42
+ inputAmount: string;
43
+ approvalTx: {
44
+ to: string;
45
+ data: string;
46
+ value: string;
47
+ chainId: number;
48
+ };
49
+ }
50
+ export declare class PersistenceBackend implements BridgeBackend {
51
+ name: string;
52
+ /**
53
+ * Validate amount against Persistence Interop caps.
54
+ * Caps are defined in 8-decimal BTC units (MIN_AMOUNT_RAW=5000, MAX_AMOUNT_RAW=100000).
55
+ * BTCB uses 18 decimals, cbBTC uses 8 decimals — normalize before comparing.
56
+ */
57
+ private validateAmount;
58
+ getQuote(params: QuoteParams): Promise<BridgeQuote | null>;
59
+ /**
60
+ * Prepare a CrossChainOrder for signing. This calls the settlement contract
61
+ * on-chain to get a properly formed order with nonce and orderData.
62
+ */
63
+ prepareOrder(quote: BridgeQuote, swapperAddress: string): Promise<PreparedOrder>;
64
+ /**
65
+ * Build transaction data for the MCP flow (no server-side signing).
66
+ * Calls prepareOrder() via on-chain view to get the EIP-712 typed data
67
+ * and Permit2 approval tx that the agent/wallet must sign externally.
68
+ *
69
+ * Use signAndExecute() for flows where a signer (private key) is available.
70
+ */
71
+ buildTransaction(quote: BridgeQuote): Promise<TransactionRequest>;
72
+ /**
73
+ * Full sign-and-execute flow for when a signer (private key) is available.
74
+ * This is used by test scripts and the ACP listener.
75
+ *
76
+ * Returns the source chain tx hash and order ID for tracking.
77
+ */
78
+ signAndExecute(quote: BridgeQuote, signer: ethers.Wallet): Promise<{
79
+ txHash: string;
80
+ orderId: string;
81
+ trackingId: string;
82
+ }>;
83
+ getStatus(trackingId: string, meta?: Record<string, string>): Promise<BridgeStatus>;
84
+ getSupportedChains(): Promise<ChainInfo[]>;
85
+ }
86
+ export {};