@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,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
|
+
}
|