@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,292 @@
1
+ import { formatTokenAmount } from "../utils/tokens.js";
2
+ import { getAllChains, isSolanaChain } from "../utils/chains.js";
3
+ import { buildApproveData, NATIVE_ADDRESS, 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.relay.link";
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(`Relay ${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 RelayBackend {
25
+ name = "relay";
26
+ appFeeRecipient;
27
+ appFeeBps;
28
+ constructor(appFeeRecipient, appFeeBps) {
29
+ this.appFeeRecipient = appFeeRecipient;
30
+ this.appFeeBps = appFeeBps;
31
+ }
32
+ async getQuote(params) {
33
+ try {
34
+ const body = {
35
+ user: params.fromAddress,
36
+ originChainId: params.fromChainId,
37
+ destinationChainId: params.toChainId,
38
+ originCurrency: params.fromTokenAddress === NATIVE_ADDRESS
39
+ ? NATIVE_ADDRESS
40
+ : params.fromTokenAddress,
41
+ destinationCurrency: params.toTokenAddress === NATIVE_ADDRESS
42
+ ? NATIVE_ADDRESS
43
+ // Relay uses native SOL address for actual SOL delivery (not wrapped SOL)
44
+ : (isSolanaChain(params.toChainId) && params.toTokenAddress === "So11111111111111111111111111111111111111112")
45
+ ? "11111111111111111111111111111111"
46
+ : params.toTokenAddress,
47
+ amount: params.amountRaw,
48
+ tradeType: "EXACT_INPUT",
49
+ };
50
+ if (params.toAddress) {
51
+ body.recipient = params.toAddress;
52
+ }
53
+ if (this.appFeeRecipient && this.appFeeBps) {
54
+ body.appFees = [{ recipient: this.appFeeRecipient, fee: this.appFeeBps }];
55
+ }
56
+ const data = await fetchJson(`${BASE_URL}/quote`, {
57
+ method: "POST",
58
+ headers: { "Content-Type": "application/json" },
59
+ body: JSON.stringify(body),
60
+ });
61
+ if (!data.steps || data.steps.length === 0)
62
+ return null;
63
+ // Extract output details from the quote
64
+ const details = data.details ?? {};
65
+ const outputRaw = details.currencyOut?.amount ?? "0";
66
+ const outputDecimals = details.currencyOut?.currency?.decimals ?? 18;
67
+ const srcSymbol = details.currencyIn?.currency?.symbol ?? "?";
68
+ const dstSymbol = details.currencyOut?.currency?.symbol ?? "?";
69
+ const feeUsd = Number(details.totalFee?.usd ?? 0);
70
+ let gasFeeUsd = Number(details.gasFee?.usd ?? 0);
71
+ const timeEstimate = details.timeEstimate ?? 60;
72
+ // Relay includes relayer gas in their fee, so API often reports 0 for gas.
73
+ // But the user still pays source chain gas to submit the tx — estimate it.
74
+ let usingFallbackPrices;
75
+ if (gasFeeUsd < 0.001) {
76
+ const gasUnits = getGasUnits("relay", params.fromChainId);
77
+ const gasEstimate = await estimateGasCostUsd(params.fromChainId, gasUnits);
78
+ if (gasEstimate !== null) {
79
+ gasFeeUsd = gasEstimate.costUsd;
80
+ usingFallbackPrices = gasEstimate.usingFallbackPrices || undefined;
81
+ }
82
+ }
83
+ // Relay is intent-based with deterministic pricing — min = estimated
84
+ return {
85
+ backendName: "relay",
86
+ provider: "Relay (direct)",
87
+ outputAmount: formatTokenAmount(outputRaw, outputDecimals),
88
+ outputAmountRaw: outputRaw,
89
+ minOutputAmount: formatTokenAmount(outputRaw, outputDecimals),
90
+ minOutputAmountRaw: outputRaw,
91
+ outputDecimals,
92
+ estimatedGasCostUsd: Math.round(gasFeeUsd * 100) / 100,
93
+ usingFallbackPrices,
94
+ estimatedFeeUsd: feeUsd + gasFeeUsd,
95
+ feeBreakdown: { gasCostUsd: gasFeeUsd, protocolFeeUsd: feeUsd, integratorFeeUsd: 0, integratorFeePercent: null, totalFeeUsd: feeUsd + gasFeeUsd },
96
+ estimatedTimeSeconds: timeEstimate,
97
+ route: `${srcSymbol} → Relay → ${dstSymbol}`,
98
+ quoteData: data,
99
+ // Relay quotes include step-level expiry. Use it if available, else 30s default.
100
+ expiresAt: data.steps?.[0]?.items?.[0]?.data?.expiresAt
101
+ ? new Date(data.steps[0].items[0].data.expiresAt).getTime()
102
+ : Date.now() + 60_000,
103
+ };
104
+ }
105
+ catch (err) {
106
+ console.error("[relay] quote error:", err.message);
107
+ return null;
108
+ }
109
+ }
110
+ async buildTransaction(quote) {
111
+ const data = quote.quoteData;
112
+ // Relay returns steps, each step has items, each item has a transaction
113
+ const steps = data.steps ?? [];
114
+ if (steps.length === 0)
115
+ throw new Error("No steps in Relay quote");
116
+ // Find the main transaction step (usually the first or only step)
117
+ let mainTx = null;
118
+ let approvalTx = null;
119
+ for (const step of steps) {
120
+ for (const item of step.items ?? []) {
121
+ if (item.data?.data) {
122
+ if (step.id === "approve" || item.id === "approve") {
123
+ approvalTx = item.data;
124
+ }
125
+ else {
126
+ mainTx = item.data;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ if (!mainTx || !mainTx.to || !mainTx.data) {
132
+ throw new Error("No valid transaction data in Relay quote steps");
133
+ }
134
+ const result = {
135
+ to: mainTx.to,
136
+ data: mainTx.data,
137
+ value: mainTx.value ? `0x${BigInt(mainTx.value).toString(16)}` : "0x0",
138
+ chainId: mainTx.chainId ?? data.details?.currencyIn?.currency?.chainId ?? 0,
139
+ provider: "relay",
140
+ trackingId: `relay:${data.requestId ?? Date.now()}`,
141
+ };
142
+ // If Relay didn't include an approval step, generate one for ERC-20 tokens.
143
+ // Relay sometimes omits the approval step assuming the user already has allowance.
144
+ if (!approvalTx) {
145
+ const srcToken = data.details?.currencyIn?.currency?.address;
146
+ const inputAmount = data.details?.currencyIn?.amount;
147
+ if (srcToken && !isNativeToken(srcToken) && inputAmount && mainTx.to) {
148
+ // Generate an approval for the exact amount + 5% buffer to the Relay contract
149
+ const approvalAmount = (BigInt(inputAmount) * 105n / 100n).toString();
150
+ result.approvalTx = {
151
+ to: srcToken,
152
+ data: buildApproveData(mainTx.to, approvalAmount),
153
+ value: "0x0",
154
+ chainId: result.chainId,
155
+ };
156
+ }
157
+ }
158
+ if (approvalTx) {
159
+ // Validate: reject unlimited approvals from API (amount = MaxUint256)
160
+ // ERC20 approve calldata: 0x095ea7b3 + spender(32 bytes) + amount(32 bytes)
161
+ let approvalData = approvalTx.data;
162
+ if (typeof approvalData === "string" && approvalData.length === 138) {
163
+ const amountHex = approvalData.slice(74); // last 64 hex chars = amount
164
+ const MAX_UINT256_HEX = "f".repeat(64);
165
+ if (amountHex.toLowerCase() === MAX_UINT256_HEX) {
166
+ // Relay sent unlimited approval — rebuild with exact amount from quote
167
+ const inputAmount = data.details?.currencyIn?.amount;
168
+ if (inputAmount) {
169
+ const spender = "0x" + approvalData.slice(10, 74).replace(/^0+/, "");
170
+ approvalData = buildApproveData(spender, inputAmount);
171
+ console.warn("[relay] Replaced unlimited approval with exact amount");
172
+ }
173
+ }
174
+ }
175
+ result.approvalTx = {
176
+ to: approvalTx.to,
177
+ data: approvalData,
178
+ value: "0x0",
179
+ chainId: approvalTx.chainId ?? result.chainId,
180
+ };
181
+ }
182
+ return result;
183
+ }
184
+ async getStatus(trackingId, meta) {
185
+ try {
186
+ const txHash = meta?.txHash;
187
+ // Extract requestId from trackingId ("relay:<requestId>")
188
+ const requestId = trackingId.startsWith("relay:")
189
+ ? trackingId.slice("relay:".length)
190
+ : undefined;
191
+ if (!requestId && !txHash) {
192
+ return {
193
+ state: "unknown",
194
+ humanReadable: "No requestId or txHash for Relay status check",
195
+ provider: "relay",
196
+ elapsed: 0,
197
+ };
198
+ }
199
+ // Prefer txHash (always available after submission), fall back to requestId
200
+ // The intents/status endpoint is unreliable for txHash lookups,
201
+ // so we also try the requests/v2 endpoint as a fallback.
202
+ let data;
203
+ if (txHash) {
204
+ // Try intents/status/v3 first
205
+ data = await fetchJson(`${BASE_URL}/intents/status/v3?txHash=${txHash}`);
206
+ // If status is unknown, try requests/v2 endpoint filtered by user
207
+ if (data?.status === "unknown") {
208
+ try {
209
+ const fromAddress = meta?.fromAddress;
210
+ if (fromAddress) {
211
+ const reqData = await fetchJson(`${BASE_URL}/requests/v2?user=${fromAddress}`);
212
+ // Find the request matching our txHash
213
+ const match = reqData?.requests?.find((r) => r.inTxHash === txHash || r.data?.inTxHashes?.includes(txHash));
214
+ if (match) {
215
+ data = { status: match.status };
216
+ }
217
+ }
218
+ }
219
+ catch { /* fallback failed, use original result */ }
220
+ }
221
+ }
222
+ else if (requestId) {
223
+ data = await fetchJson(`${BASE_URL}/intents/status/v3?requestId=${requestId}`);
224
+ }
225
+ else {
226
+ data = { status: "unknown" };
227
+ }
228
+ // Relay v3 status values (from docs):
229
+ // waiting — Waiting for deposit confirmation
230
+ // pending — Deposit confirmed, pending destination chain submission
231
+ // submitted — Destination transaction submitted
232
+ // success — Successful fill on destination
233
+ // delayed — Destination fill delayed, still processing
234
+ // refunded — Successfully refunded
235
+ // refund — Refund alias
236
+ // failure — Unsuccessful fill
237
+ const stateMap = {
238
+ waiting: "pending",
239
+ pending: "in_progress",
240
+ submitted: "in_progress",
241
+ delayed: "in_progress",
242
+ success: "completed",
243
+ failure: "failed",
244
+ refund: "refunded",
245
+ refunded: "refunded",
246
+ };
247
+ const mappedState = stateMap[data.status];
248
+ if (!mappedState && data.status && data.status !== "unknown") {
249
+ console.warn(`[relay] unmapped status: "${data.status}" — treating as pending`);
250
+ }
251
+ const destChainId = data.destinationChainId;
252
+ const sourceChainId = data.originChainId;
253
+ return {
254
+ state: mappedState ?? (data.status === "unknown" ? "unknown" : "pending"),
255
+ humanReadable: `Relay bridge: ${data.status ?? "unknown"}`,
256
+ sourceTxHash: txHash ?? data.inTxHashes?.[0],
257
+ destTxHash: data.txHashes?.[0],
258
+ provider: "relay",
259
+ elapsed: 0,
260
+ };
261
+ }
262
+ catch (err) {
263
+ return {
264
+ state: "unknown",
265
+ humanReadable: `Status check failed: ${sanitizeError(err)}`,
266
+ provider: "relay",
267
+ elapsed: 0,
268
+ };
269
+ }
270
+ }
271
+ async getSupportedChains() {
272
+ try {
273
+ const data = await fetchJson(`${BASE_URL}/chains`);
274
+ if (Array.isArray(data.chains)) {
275
+ return data.chains.map((c) => ({
276
+ id: c.id,
277
+ name: c.name ?? `Chain ${c.id}`,
278
+ key: (c.name ?? `chain-${c.id}`).toLowerCase().replace(/\s+/g, "-"),
279
+ logoURI: c.icon,
280
+ providers: ["relay"],
281
+ }));
282
+ }
283
+ }
284
+ catch {
285
+ // Fallback
286
+ }
287
+ return getAllChains().map((c) => ({
288
+ ...c,
289
+ providers: ["relay"],
290
+ }));
291
+ }
292
+ }
@@ -0,0 +1,31 @@
1
+ import type { BridgeBackend, BridgeQuote, BridgeStatus, ChainInfo, QuoteParams, TokenInfo, TransactionRequest } from "./types.js";
2
+ export declare class SquidBackend implements BridgeBackend {
3
+ name: string;
4
+ private integratorId?;
5
+ constructor(integratorId?: string);
6
+ private headers;
7
+ /**
8
+ * Map a chain ID to the string Squid Router expects.
9
+ * For EVM chains, this is just the numeric string (e.g. "8453").
10
+ * For Cosmos chains, this is the Cosmos chain ID string (e.g. "persistence-core-1").
11
+ */
12
+ private resolveSquidChainId;
13
+ /**
14
+ * Resolve the recipient address for a Cosmos destination chain.
15
+ * If the provided toAddress is an EVM address (0x...), derive the
16
+ * Cosmos bech32 address from the wallet mnemonic.
17
+ * Falls back to a placeholder for quote-only requests.
18
+ */
19
+ private resolveCosmosToAddress;
20
+ getQuote(params: QuoteParams): Promise<BridgeQuote | null>;
21
+ buildTransaction(quote: BridgeQuote): Promise<TransactionRequest>;
22
+ private buildTxResult;
23
+ getStatus(trackingId: string, meta?: Record<string, string>): Promise<BridgeStatus>;
24
+ getSupportedChains(): Promise<ChainInfo[]>;
25
+ getSupportedTokens(chainId: number): Promise<TokenInfo[]>;
26
+ /**
27
+ * Fallback chain list when the API is unavailable.
28
+ * Squid supports EVM chains and Cosmos chains (via Axelar GMP).
29
+ */
30
+ private fallbackChains;
31
+ }