@provable-games/ekubo-sdk 0.1.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/README.md +336 -0
- package/dist/index.cjs +1592 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +927 -0
- package/dist/index.d.ts +927 -0
- package/dist/index.js +1533 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1533 @@
|
|
|
1
|
+
// src/errors/index.ts
|
|
2
|
+
var EkuboError = class _EkuboError extends Error {
|
|
3
|
+
constructor(message, code, cause) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.cause = cause;
|
|
7
|
+
this.name = "EkuboError";
|
|
8
|
+
Object.setPrototypeOf(this, _EkuboError.prototype);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var InsufficientLiquidityError = class _InsufficientLiquidityError extends EkuboError {
|
|
12
|
+
constructor(message = "Insufficient liquidity for swap") {
|
|
13
|
+
super(message, "INSUFFICIENT_LIQUIDITY");
|
|
14
|
+
this.name = "InsufficientLiquidityError";
|
|
15
|
+
Object.setPrototypeOf(this, _InsufficientLiquidityError.prototype);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var ApiError = class _ApiError extends EkuboError {
|
|
19
|
+
constructor(message, statusCode, cause) {
|
|
20
|
+
super(message, "API_ERROR", cause);
|
|
21
|
+
this.statusCode = statusCode;
|
|
22
|
+
this.name = "ApiError";
|
|
23
|
+
Object.setPrototypeOf(this, _ApiError.prototype);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var RateLimitError = class _RateLimitError extends EkuboError {
|
|
27
|
+
constructor(message = "Rate limited by API", retryAfter) {
|
|
28
|
+
super(message, "RATE_LIMIT");
|
|
29
|
+
this.retryAfter = retryAfter;
|
|
30
|
+
this.name = "RateLimitError";
|
|
31
|
+
Object.setPrototypeOf(this, _RateLimitError.prototype);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var TimeoutError = class _TimeoutError extends EkuboError {
|
|
35
|
+
constructor(message = "Request timed out", timeout) {
|
|
36
|
+
super(message, "TIMEOUT");
|
|
37
|
+
this.timeout = timeout;
|
|
38
|
+
this.name = "TimeoutError";
|
|
39
|
+
Object.setPrototypeOf(this, _TimeoutError.prototype);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var AbortError = class _AbortError extends EkuboError {
|
|
43
|
+
constructor(message = "Request aborted") {
|
|
44
|
+
super(message, "ABORTED");
|
|
45
|
+
this.name = "AbortError";
|
|
46
|
+
Object.setPrototypeOf(this, _AbortError.prototype);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var TokenNotFoundError = class _TokenNotFoundError extends EkuboError {
|
|
50
|
+
constructor(tokenIdentifier, message = `Token not found: ${tokenIdentifier}`) {
|
|
51
|
+
super(message, "TOKEN_NOT_FOUND");
|
|
52
|
+
this.tokenIdentifier = tokenIdentifier;
|
|
53
|
+
this.name = "TokenNotFoundError";
|
|
54
|
+
Object.setPrototypeOf(this, _TokenNotFoundError.prototype);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var InvalidChainError = class _InvalidChainError extends EkuboError {
|
|
58
|
+
constructor(chainId, message = `Invalid or unsupported chain: ${chainId}`) {
|
|
59
|
+
super(message, "INVALID_CHAIN");
|
|
60
|
+
this.chainId = chainId;
|
|
61
|
+
this.name = "InvalidChainError";
|
|
62
|
+
Object.setPrototypeOf(this, _InvalidChainError.prototype);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
function isNonRetryableError(error) {
|
|
66
|
+
if (error instanceof InsufficientLiquidityError) return true;
|
|
67
|
+
if (error instanceof AbortError) return true;
|
|
68
|
+
if (error instanceof TokenNotFoundError) return true;
|
|
69
|
+
if (error instanceof InvalidChainError) return true;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/utils/retry.ts
|
|
74
|
+
var DEFAULT_FETCH_CONFIG = {
|
|
75
|
+
timeout: 1e4,
|
|
76
|
+
maxRetries: 3,
|
|
77
|
+
baseBackoff: 1e3,
|
|
78
|
+
maxBackoff: 5e3,
|
|
79
|
+
fetch: globalThis.fetch
|
|
80
|
+
};
|
|
81
|
+
function parseRetryAfter(retryAfter) {
|
|
82
|
+
if (!retryAfter) return null;
|
|
83
|
+
const trimmed = retryAfter.trim();
|
|
84
|
+
if (!trimmed) return null;
|
|
85
|
+
const seconds = parseInt(trimmed, 10);
|
|
86
|
+
if (!isNaN(seconds) && seconds >= 0) {
|
|
87
|
+
return seconds * 1e3;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const date = new Date(trimmed);
|
|
91
|
+
const delay = date.getTime() - Date.now();
|
|
92
|
+
return delay > 0 ? delay : null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function calculateBackoff(attempt, baseBackoff, maxBackoff, retryAfter) {
|
|
98
|
+
if (retryAfter) {
|
|
99
|
+
const retryAfterDelay = parseRetryAfter(retryAfter);
|
|
100
|
+
if (retryAfterDelay !== null && retryAfterDelay > 0) {
|
|
101
|
+
return retryAfterDelay;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
let delay = baseBackoff * Math.pow(2, attempt);
|
|
105
|
+
if (delay > maxBackoff) {
|
|
106
|
+
delay = maxBackoff;
|
|
107
|
+
}
|
|
108
|
+
const minDelay = delay / 2;
|
|
109
|
+
const jitter = Math.random() * (delay - minDelay);
|
|
110
|
+
return minDelay + jitter;
|
|
111
|
+
}
|
|
112
|
+
function sleep(ms) {
|
|
113
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
114
|
+
}
|
|
115
|
+
async function withRetry(fn, options) {
|
|
116
|
+
const { maxRetries, baseBackoff, maxBackoff, signal, onRetry } = options;
|
|
117
|
+
let lastError = null;
|
|
118
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
119
|
+
if (signal?.aborted) {
|
|
120
|
+
throw new Error("Request aborted");
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
return await fn();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
lastError = error;
|
|
126
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
if (isNonRetryableError(error)) {
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
if (attempt === maxRetries - 1) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
const delay = calculateBackoff(attempt, baseBackoff, maxBackoff);
|
|
136
|
+
onRetry?.(attempt, lastError, delay);
|
|
137
|
+
await sleep(delay);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw lastError || new Error("Unknown error after retries");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/utils/bigint.ts
|
|
144
|
+
function parseTotalCalculated(totalCalculated) {
|
|
145
|
+
let value;
|
|
146
|
+
if (typeof totalCalculated === "string") {
|
|
147
|
+
value = BigInt(totalCalculated);
|
|
148
|
+
} else {
|
|
149
|
+
value = BigInt(Math.floor(totalCalculated));
|
|
150
|
+
}
|
|
151
|
+
return value < 0n ? -value : value;
|
|
152
|
+
}
|
|
153
|
+
function abs(value) {
|
|
154
|
+
return value < 0n ? -value : value;
|
|
155
|
+
}
|
|
156
|
+
function addSlippage(amount, slippagePercent) {
|
|
157
|
+
return amount + amount * slippagePercent / 100n;
|
|
158
|
+
}
|
|
159
|
+
function subtractSlippage(amount, slippagePercent) {
|
|
160
|
+
return amount - amount * slippagePercent / 100n;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/utils/hex.ts
|
|
164
|
+
function toHex(value) {
|
|
165
|
+
if (typeof value === "string") {
|
|
166
|
+
if (value.startsWith("0x") || value.startsWith("0X")) {
|
|
167
|
+
return "0x" + BigInt(value).toString(16);
|
|
168
|
+
}
|
|
169
|
+
return "0x" + BigInt(value).toString(16);
|
|
170
|
+
}
|
|
171
|
+
return "0x" + BigInt(value).toString(16);
|
|
172
|
+
}
|
|
173
|
+
function normalizeAddress(address) {
|
|
174
|
+
const normalized = toHex(address);
|
|
175
|
+
return normalized.toLowerCase();
|
|
176
|
+
}
|
|
177
|
+
function isAddress(value) {
|
|
178
|
+
return /^0x[0-9a-fA-F]+$/.test(value);
|
|
179
|
+
}
|
|
180
|
+
function splitU256(value) {
|
|
181
|
+
const low = value % 2n ** 128n;
|
|
182
|
+
const high = value >> 128n;
|
|
183
|
+
return {
|
|
184
|
+
low: toHex(low),
|
|
185
|
+
high: toHex(high)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/chains/constants.ts
|
|
190
|
+
var CHAIN_IDS = {
|
|
191
|
+
MAINNET: "23448594291968334",
|
|
192
|
+
SEPOLIA: "393402133025997798000961"
|
|
193
|
+
};
|
|
194
|
+
var STARKNET_CHAIN_IDS = {
|
|
195
|
+
SN_MAIN: "0x534e5f4d41494e",
|
|
196
|
+
SN_SEPOLIA: "0x534e5f5345504f4c4941"
|
|
197
|
+
};
|
|
198
|
+
var ROUTER_ADDRESSES = {
|
|
199
|
+
[CHAIN_IDS.MAINNET]: "0x0199741822c2dc722f6f605204f35e56dbc23bceed54818168c4c49e4fb8737e",
|
|
200
|
+
[CHAIN_IDS.SEPOLIA]: "0x0045f933adf0607292468ad1c1dedaa74d5ad166392590e72676a34d01d7b763"
|
|
201
|
+
};
|
|
202
|
+
var USDC_ADDRESSES = {
|
|
203
|
+
[CHAIN_IDS.MAINNET]: "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb",
|
|
204
|
+
[CHAIN_IDS.SEPOLIA]: "0x053b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080"
|
|
205
|
+
};
|
|
206
|
+
var API_URLS = {
|
|
207
|
+
/** Quoter API for swap quotes */
|
|
208
|
+
QUOTER: "https://prod-api-quoter.ekubo.org",
|
|
209
|
+
/** Main API for tokens, prices, stats, etc. */
|
|
210
|
+
API: "https://prod-api.ekubo.org"
|
|
211
|
+
};
|
|
212
|
+
var CHAIN_CONFIGS = {
|
|
213
|
+
mainnet: {
|
|
214
|
+
chainId: CHAIN_IDS.MAINNET,
|
|
215
|
+
quoterApiUrl: API_URLS.QUOTER,
|
|
216
|
+
apiUrl: API_URLS.API,
|
|
217
|
+
routerAddress: ROUTER_ADDRESSES[CHAIN_IDS.MAINNET],
|
|
218
|
+
usdcAddress: USDC_ADDRESSES[CHAIN_IDS.MAINNET]
|
|
219
|
+
},
|
|
220
|
+
sepolia: {
|
|
221
|
+
chainId: CHAIN_IDS.SEPOLIA,
|
|
222
|
+
quoterApiUrl: API_URLS.QUOTER,
|
|
223
|
+
apiUrl: API_URLS.API,
|
|
224
|
+
routerAddress: ROUTER_ADDRESSES[CHAIN_IDS.SEPOLIA],
|
|
225
|
+
usdcAddress: USDC_ADDRESSES[CHAIN_IDS.SEPOLIA]
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var STARKNET_TO_EKUBO_CHAIN = {
|
|
229
|
+
[STARKNET_CHAIN_IDS.SN_MAIN]: CHAIN_IDS.MAINNET,
|
|
230
|
+
[STARKNET_CHAIN_IDS.SN_SEPOLIA]: CHAIN_IDS.SEPOLIA
|
|
231
|
+
};
|
|
232
|
+
function getEkuboChainId(starknetChainId) {
|
|
233
|
+
return STARKNET_TO_EKUBO_CHAIN[starknetChainId];
|
|
234
|
+
}
|
|
235
|
+
function getChainConfig(chainOrId) {
|
|
236
|
+
if (chainOrId === "mainnet" || chainOrId === "sepolia") {
|
|
237
|
+
return CHAIN_CONFIGS[chainOrId];
|
|
238
|
+
}
|
|
239
|
+
if (chainOrId === CHAIN_IDS.MAINNET) {
|
|
240
|
+
return CHAIN_CONFIGS.mainnet;
|
|
241
|
+
}
|
|
242
|
+
if (chainOrId === CHAIN_IDS.SEPOLIA) {
|
|
243
|
+
return CHAIN_CONFIGS.sepolia;
|
|
244
|
+
}
|
|
245
|
+
const ekuboChainId = getEkuboChainId(chainOrId);
|
|
246
|
+
if (ekuboChainId === CHAIN_IDS.MAINNET) {
|
|
247
|
+
return CHAIN_CONFIGS.mainnet;
|
|
248
|
+
}
|
|
249
|
+
if (ekuboChainId === CHAIN_IDS.SEPOLIA) {
|
|
250
|
+
return CHAIN_CONFIGS.sepolia;
|
|
251
|
+
}
|
|
252
|
+
return void 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/api/quote.ts
|
|
256
|
+
async function fetchSwapQuote(params) {
|
|
257
|
+
const {
|
|
258
|
+
amount,
|
|
259
|
+
tokenFrom,
|
|
260
|
+
tokenTo,
|
|
261
|
+
chainId = CHAIN_IDS.MAINNET,
|
|
262
|
+
signal,
|
|
263
|
+
fetchConfig = {}
|
|
264
|
+
} = params;
|
|
265
|
+
const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
|
|
266
|
+
const fetchFn = config.fetch;
|
|
267
|
+
const receivedAmount = `-${amount.toString()}`;
|
|
268
|
+
const normalizedTokenFrom = normalizeAddress(tokenFrom);
|
|
269
|
+
const normalizedTokenTo = normalizeAddress(tokenTo);
|
|
270
|
+
const url = `${API_URLS.QUOTER}/${chainId}/${receivedAmount}/${normalizedTokenFrom}/${normalizedTokenTo}`;
|
|
271
|
+
let lastError = null;
|
|
272
|
+
for (let attempt = 0; attempt < config.maxRetries; attempt++) {
|
|
273
|
+
if (signal?.aborted) {
|
|
274
|
+
throw new AbortError("Request aborted");
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const controller = new AbortController();
|
|
278
|
+
const timeoutId = setTimeout(() => controller.abort(), config.timeout);
|
|
279
|
+
const combinedSignal = signal ? anySignal([signal, controller.signal]) : controller.signal;
|
|
280
|
+
const response = await fetchFn(url, {
|
|
281
|
+
method: "GET",
|
|
282
|
+
signal: combinedSignal,
|
|
283
|
+
mode: "cors",
|
|
284
|
+
credentials: "omit"
|
|
285
|
+
});
|
|
286
|
+
clearTimeout(timeoutId);
|
|
287
|
+
if (response.ok) {
|
|
288
|
+
const data = await response.json();
|
|
289
|
+
if ("error" in data) {
|
|
290
|
+
if (data.error.includes("Insufficient liquidity")) {
|
|
291
|
+
throw new InsufficientLiquidityError(data.error);
|
|
292
|
+
}
|
|
293
|
+
throw new ApiError(data.error);
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
impact: data.price_impact,
|
|
297
|
+
total: parseTotalCalculated(data.total_calculated),
|
|
298
|
+
splits: data.splits
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (response.status === 404) {
|
|
302
|
+
try {
|
|
303
|
+
const data = await response.json();
|
|
304
|
+
if (data.error?.includes("Insufficient liquidity")) {
|
|
305
|
+
throw new InsufficientLiquidityError(data.error);
|
|
306
|
+
}
|
|
307
|
+
} catch (e) {
|
|
308
|
+
if (e instanceof InsufficientLiquidityError) {
|
|
309
|
+
throw e;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
throw new ApiError(`Failed to fetch swap quote: 404 Not Found`, 404);
|
|
313
|
+
}
|
|
314
|
+
if (response.status === 429) {
|
|
315
|
+
if (attempt === config.maxRetries - 1) {
|
|
316
|
+
throw new RateLimitError("Rate limited (429) - max retries exceeded");
|
|
317
|
+
}
|
|
318
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
319
|
+
const delay = calculateBackoff(
|
|
320
|
+
attempt,
|
|
321
|
+
config.baseBackoff,
|
|
322
|
+
config.maxBackoff,
|
|
323
|
+
retryAfter
|
|
324
|
+
);
|
|
325
|
+
await sleep(delay);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
throw new ApiError(
|
|
329
|
+
`Failed to fetch swap quote: ${response.status}`,
|
|
330
|
+
response.status
|
|
331
|
+
);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
lastError = error;
|
|
334
|
+
if (error instanceof Error && (error.name === "AbortError" || error instanceof AbortError)) {
|
|
335
|
+
throw new AbortError("Request aborted");
|
|
336
|
+
}
|
|
337
|
+
if (error instanceof InsufficientLiquidityError) {
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
if (attempt === config.maxRetries - 1) {
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
const delay = calculateBackoff(
|
|
344
|
+
attempt,
|
|
345
|
+
config.baseBackoff,
|
|
346
|
+
config.maxBackoff
|
|
347
|
+
);
|
|
348
|
+
await sleep(delay);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
throw lastError || new ApiError("Failed to fetch swap quote: unknown error");
|
|
352
|
+
}
|
|
353
|
+
async function fetchSwapQuoteInUsdc(params) {
|
|
354
|
+
const { chainId = CHAIN_IDS.MAINNET, ...rest } = params;
|
|
355
|
+
const usdcAddress = USDC_ADDRESSES[chainId] ?? USDC_ADDRESSES[CHAIN_IDS.MAINNET];
|
|
356
|
+
const quote = await fetchSwapQuote({
|
|
357
|
+
...rest,
|
|
358
|
+
tokenTo: usdcAddress,
|
|
359
|
+
chainId
|
|
360
|
+
});
|
|
361
|
+
return quote.total;
|
|
362
|
+
}
|
|
363
|
+
function anySignal(signals) {
|
|
364
|
+
const controller = new AbortController();
|
|
365
|
+
for (const signal of signals) {
|
|
366
|
+
if (signal.aborted) {
|
|
367
|
+
controller.abort();
|
|
368
|
+
return controller.signal;
|
|
369
|
+
}
|
|
370
|
+
signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
371
|
+
}
|
|
372
|
+
return controller.signal;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/api/price.ts
|
|
376
|
+
async function getPriceHistory(params) {
|
|
377
|
+
const {
|
|
378
|
+
token,
|
|
379
|
+
otherToken,
|
|
380
|
+
chainId = CHAIN_IDS.MAINNET,
|
|
381
|
+
interval = 7e3,
|
|
382
|
+
apiUrl = API_URLS.API,
|
|
383
|
+
fetchConfig = {}
|
|
384
|
+
} = params;
|
|
385
|
+
const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
|
|
386
|
+
const fetchFn = config.fetch;
|
|
387
|
+
const normalizedToken = normalizeAddress(token);
|
|
388
|
+
const normalizedOtherToken = normalizeAddress(otherToken);
|
|
389
|
+
const url = `${apiUrl}/price/${chainId}/${normalizedToken}/${normalizedOtherToken}/history?interval=${interval}`;
|
|
390
|
+
try {
|
|
391
|
+
const response = await fetchFn(url, {
|
|
392
|
+
method: "GET",
|
|
393
|
+
mode: "cors",
|
|
394
|
+
credentials: "omit"
|
|
395
|
+
});
|
|
396
|
+
if (!response.ok) {
|
|
397
|
+
throw new ApiError(
|
|
398
|
+
`Failed to fetch price history: ${response.status}`,
|
|
399
|
+
response.status
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
const data = await response.json();
|
|
403
|
+
return {
|
|
404
|
+
data: data?.data || []
|
|
405
|
+
};
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if (error instanceof ApiError) {
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
throw new ApiError(
|
|
411
|
+
`Failed to fetch price history: ${error instanceof Error ? error.message : "unknown error"}`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/api/tokens.ts
|
|
417
|
+
async function fetchTokens(params = {}) {
|
|
418
|
+
const {
|
|
419
|
+
chainId = CHAIN_IDS.MAINNET,
|
|
420
|
+
apiUrl = API_URLS.API,
|
|
421
|
+
fetchConfig = {}
|
|
422
|
+
} = params;
|
|
423
|
+
const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
|
|
424
|
+
const fetchFn = config.fetch;
|
|
425
|
+
const url = `${apiUrl}/tokens?chainId=${chainId}`;
|
|
426
|
+
try {
|
|
427
|
+
const response = await fetchFn(url, {
|
|
428
|
+
method: "GET",
|
|
429
|
+
mode: "cors",
|
|
430
|
+
credentials: "omit"
|
|
431
|
+
});
|
|
432
|
+
if (!response.ok) {
|
|
433
|
+
throw new ApiError(
|
|
434
|
+
`Failed to fetch tokens: ${response.status}`,
|
|
435
|
+
response.status
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
const data = await response.json();
|
|
439
|
+
return data;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (error instanceof ApiError) {
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
throw new ApiError(
|
|
445
|
+
`Failed to fetch tokens: ${error instanceof Error ? error.message : "unknown error"}`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function fetchToken(params) {
|
|
450
|
+
const {
|
|
451
|
+
tokenAddress,
|
|
452
|
+
chainId = CHAIN_IDS.MAINNET,
|
|
453
|
+
apiUrl = API_URLS.API,
|
|
454
|
+
fetchConfig = {}
|
|
455
|
+
} = params;
|
|
456
|
+
const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
|
|
457
|
+
const fetchFn = config.fetch;
|
|
458
|
+
const url = `${apiUrl}/tokens/${chainId}/${tokenAddress}`;
|
|
459
|
+
try {
|
|
460
|
+
const response = await fetchFn(url, {
|
|
461
|
+
method: "GET",
|
|
462
|
+
mode: "cors",
|
|
463
|
+
credentials: "omit"
|
|
464
|
+
});
|
|
465
|
+
if (response.status === 404) {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
if (!response.ok) {
|
|
469
|
+
throw new ApiError(
|
|
470
|
+
`Failed to fetch token: ${response.status}`,
|
|
471
|
+
response.status
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const data = await response.json();
|
|
475
|
+
return data;
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (error instanceof ApiError) {
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
throw new ApiError(
|
|
481
|
+
`Failed to fetch token: ${error instanceof Error ? error.message : "unknown error"}`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async function fetchTokensBatch(params) {
|
|
486
|
+
const {
|
|
487
|
+
tokenAddresses,
|
|
488
|
+
chainId = CHAIN_IDS.MAINNET,
|
|
489
|
+
apiUrl = API_URLS.API,
|
|
490
|
+
fetchConfig = {}
|
|
491
|
+
} = params;
|
|
492
|
+
if (tokenAddresses.length === 0) {
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
|
|
496
|
+
const fetchFn = config.fetch;
|
|
497
|
+
const addressParams = tokenAddresses.map((addr) => `addresses=${encodeURIComponent(addr)}`).join("&");
|
|
498
|
+
const url = `${apiUrl}/tokens/batch?chainId=${chainId}&${addressParams}`;
|
|
499
|
+
try {
|
|
500
|
+
const response = await fetchFn(url, {
|
|
501
|
+
method: "GET",
|
|
502
|
+
mode: "cors",
|
|
503
|
+
credentials: "omit"
|
|
504
|
+
});
|
|
505
|
+
if (!response.ok) {
|
|
506
|
+
throw new ApiError(
|
|
507
|
+
`Failed to fetch tokens batch: ${response.status}`,
|
|
508
|
+
response.status
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
const data = await response.json();
|
|
512
|
+
return data;
|
|
513
|
+
} catch (error) {
|
|
514
|
+
if (error instanceof ApiError) {
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
throw new ApiError(
|
|
518
|
+
`Failed to fetch tokens batch: ${error instanceof Error ? error.message : "unknown error"}`
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/api/stats.ts
|
|
524
|
+
async function fetchTopPairs(params = {}) {
|
|
525
|
+
const {
|
|
526
|
+
chainId = CHAIN_IDS.MAINNET,
|
|
527
|
+
apiUrl = API_URLS.API,
|
|
528
|
+
fetchConfig = {}
|
|
529
|
+
} = params;
|
|
530
|
+
const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
|
|
531
|
+
const fetchFn = config.fetch;
|
|
532
|
+
const url = `${apiUrl}/overview/pairs?chainId=${chainId}`;
|
|
533
|
+
try {
|
|
534
|
+
const response = await fetchFn(url, {
|
|
535
|
+
method: "GET",
|
|
536
|
+
mode: "cors",
|
|
537
|
+
credentials: "omit"
|
|
538
|
+
});
|
|
539
|
+
if (!response.ok) {
|
|
540
|
+
throw new ApiError(
|
|
541
|
+
`Failed to fetch top pairs: ${response.status}`,
|
|
542
|
+
response.status
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
return await response.json();
|
|
546
|
+
} catch (error) {
|
|
547
|
+
if (error instanceof ApiError) throw error;
|
|
548
|
+
throw new ApiError(
|
|
549
|
+
`Failed to fetch top pairs: ${error instanceof Error ? error.message : "unknown error"}`
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async function fetchTvl(params = {}) {
|
|
554
|
+
const {
|
|
555
|
+
chainId = CHAIN_IDS.MAINNET,
|
|
556
|
+
apiUrl = API_URLS.API,
|
|
557
|
+
fetchConfig = {}
|
|
558
|
+
} = params;
|
|
559
|
+
const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
|
|
560
|
+
const fetchFn = config.fetch;
|
|
561
|
+
const url = `${apiUrl}/overview/tvl?chainId=${chainId}`;
|
|
562
|
+
try {
|
|
563
|
+
const response = await fetchFn(url, {
|
|
564
|
+
method: "GET",
|
|
565
|
+
mode: "cors",
|
|
566
|
+
credentials: "omit"
|
|
567
|
+
});
|
|
568
|
+
if (!response.ok) {
|
|
569
|
+
throw new ApiError(
|
|
570
|
+
`Failed to fetch TVL: ${response.status}`,
|
|
571
|
+
response.status
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
return await response.json();
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (error instanceof ApiError) throw error;
|
|
577
|
+
throw new ApiError(
|
|
578
|
+
`Failed to fetch TVL: ${error instanceof Error ? error.message : "unknown error"}`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
async function fetchVolume(params = {}) {
|
|
583
|
+
const {
|
|
584
|
+
chainId = CHAIN_IDS.MAINNET,
|
|
585
|
+
apiUrl = API_URLS.API,
|
|
586
|
+
fetchConfig = {}
|
|
587
|
+
} = params;
|
|
588
|
+
const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
|
|
589
|
+
const fetchFn = config.fetch;
|
|
590
|
+
const url = `${apiUrl}/overview/volume?chainId=${chainId}`;
|
|
591
|
+
try {
|
|
592
|
+
const response = await fetchFn(url, {
|
|
593
|
+
method: "GET",
|
|
594
|
+
mode: "cors",
|
|
595
|
+
credentials: "omit"
|
|
596
|
+
});
|
|
597
|
+
if (!response.ok) {
|
|
598
|
+
throw new ApiError(
|
|
599
|
+
`Failed to fetch volume: ${response.status}`,
|
|
600
|
+
response.status
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
return await response.json();
|
|
604
|
+
} catch (error) {
|
|
605
|
+
if (error instanceof ApiError) throw error;
|
|
606
|
+
throw new ApiError(
|
|
607
|
+
`Failed to fetch volume: ${error instanceof Error ? error.message : "unknown error"}`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
async function fetchPairTvl(params) {
|
|
612
|
+
const {
|
|
613
|
+
tokenA,
|
|
614
|
+
tokenB,
|
|
615
|
+
chainId = CHAIN_IDS.MAINNET,
|
|
616
|
+
apiUrl = API_URLS.API,
|
|
617
|
+
fetchConfig = {}
|
|
618
|
+
} = params;
|
|
619
|
+
const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
|
|
620
|
+
const fetchFn = config.fetch;
|
|
621
|
+
const normalizedA = normalizeAddress(tokenA);
|
|
622
|
+
const normalizedB = normalizeAddress(tokenB);
|
|
623
|
+
const url = `${apiUrl}/pair/${chainId}/${normalizedA}/${normalizedB}/tvl`;
|
|
624
|
+
try {
|
|
625
|
+
const response = await fetchFn(url, {
|
|
626
|
+
method: "GET",
|
|
627
|
+
mode: "cors",
|
|
628
|
+
credentials: "omit"
|
|
629
|
+
});
|
|
630
|
+
if (!response.ok) {
|
|
631
|
+
throw new ApiError(
|
|
632
|
+
`Failed to fetch pair TVL: ${response.status}`,
|
|
633
|
+
response.status
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
return await response.json();
|
|
637
|
+
} catch (error) {
|
|
638
|
+
if (error instanceof ApiError) throw error;
|
|
639
|
+
throw new ApiError(
|
|
640
|
+
`Failed to fetch pair TVL: ${error instanceof Error ? error.message : "unknown error"}`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
async function fetchPairVolume(params) {
|
|
645
|
+
const {
|
|
646
|
+
tokenA,
|
|
647
|
+
tokenB,
|
|
648
|
+
chainId = CHAIN_IDS.MAINNET,
|
|
649
|
+
apiUrl = API_URLS.API,
|
|
650
|
+
fetchConfig = {}
|
|
651
|
+
} = params;
|
|
652
|
+
const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
|
|
653
|
+
const fetchFn = config.fetch;
|
|
654
|
+
const normalizedA = normalizeAddress(tokenA);
|
|
655
|
+
const normalizedB = normalizeAddress(tokenB);
|
|
656
|
+
const url = `${apiUrl}/pair/${chainId}/${normalizedA}/${normalizedB}/volume`;
|
|
657
|
+
try {
|
|
658
|
+
const response = await fetchFn(url, {
|
|
659
|
+
method: "GET",
|
|
660
|
+
mode: "cors",
|
|
661
|
+
credentials: "omit"
|
|
662
|
+
});
|
|
663
|
+
if (!response.ok) {
|
|
664
|
+
throw new ApiError(
|
|
665
|
+
`Failed to fetch pair volume: ${response.status}`,
|
|
666
|
+
response.status
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
return await response.json();
|
|
670
|
+
} catch (error) {
|
|
671
|
+
if (error instanceof ApiError) throw error;
|
|
672
|
+
throw new ApiError(
|
|
673
|
+
`Failed to fetch pair volume: ${error instanceof Error ? error.message : "unknown error"}`
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async function fetchPairPools(params) {
|
|
678
|
+
const {
|
|
679
|
+
tokenA,
|
|
680
|
+
tokenB,
|
|
681
|
+
chainId = CHAIN_IDS.MAINNET,
|
|
682
|
+
apiUrl = API_URLS.API,
|
|
683
|
+
fetchConfig = {}
|
|
684
|
+
} = params;
|
|
685
|
+
const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
|
|
686
|
+
const fetchFn = config.fetch;
|
|
687
|
+
const normalizedA = normalizeAddress(tokenA);
|
|
688
|
+
const normalizedB = normalizeAddress(tokenB);
|
|
689
|
+
const url = `${apiUrl}/pair/${chainId}/${normalizedA}/${normalizedB}/pools`;
|
|
690
|
+
try {
|
|
691
|
+
const response = await fetchFn(url, {
|
|
692
|
+
method: "GET",
|
|
693
|
+
mode: "cors",
|
|
694
|
+
credentials: "omit"
|
|
695
|
+
});
|
|
696
|
+
if (!response.ok) {
|
|
697
|
+
throw new ApiError(
|
|
698
|
+
`Failed to fetch pair pools: ${response.status}`,
|
|
699
|
+
response.status
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
return await response.json();
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (error instanceof ApiError) throw error;
|
|
705
|
+
throw new ApiError(
|
|
706
|
+
`Failed to fetch pair pools: ${error instanceof Error ? error.message : "unknown error"}`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/calls/encoder.ts
|
|
712
|
+
function encodeRouteNode(routeNode, currentToken) {
|
|
713
|
+
const isToken1 = BigInt(currentToken) === BigInt(routeNode.pool_key.token1);
|
|
714
|
+
const nextToken = isToken1 ? routeNode.pool_key.token0 : routeNode.pool_key.token1;
|
|
715
|
+
const sqrtRatioLimit = BigInt(routeNode.sqrt_ratio_limit);
|
|
716
|
+
const calldata = [
|
|
717
|
+
routeNode.pool_key.token0,
|
|
718
|
+
routeNode.pool_key.token1,
|
|
719
|
+
routeNode.pool_key.fee,
|
|
720
|
+
toHex(routeNode.pool_key.tick_spacing),
|
|
721
|
+
routeNode.pool_key.extension,
|
|
722
|
+
toHex(sqrtRatioLimit % 2n ** 128n),
|
|
723
|
+
// low
|
|
724
|
+
toHex(sqrtRatioLimit >> 128n),
|
|
725
|
+
// high
|
|
726
|
+
toHex(routeNode.skip_ahead)
|
|
727
|
+
];
|
|
728
|
+
return { nextToken, calldata };
|
|
729
|
+
}
|
|
730
|
+
function encodeRoute(route, targetToken) {
|
|
731
|
+
return route.reduce(
|
|
732
|
+
(memo, routeNode) => {
|
|
733
|
+
const { nextToken, calldata } = encodeRouteNode(routeNode, memo.token);
|
|
734
|
+
return {
|
|
735
|
+
token: nextToken,
|
|
736
|
+
encoded: memo.encoded.concat(calldata)
|
|
737
|
+
};
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
token: targetToken,
|
|
741
|
+
encoded: []
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/calls/generator.ts
|
|
747
|
+
var DEFAULT_SLIPPAGE_PERCENT = 5n;
|
|
748
|
+
function generateSwapCalls(params) {
|
|
749
|
+
const {
|
|
750
|
+
sellToken,
|
|
751
|
+
buyToken,
|
|
752
|
+
minimumReceived,
|
|
753
|
+
quote,
|
|
754
|
+
chainId = CHAIN_IDS.MAINNET,
|
|
755
|
+
routerAddress: customRouterAddress,
|
|
756
|
+
slippagePercent = DEFAULT_SLIPPAGE_PERCENT
|
|
757
|
+
} = params;
|
|
758
|
+
const routerAddress = customRouterAddress ?? ROUTER_ADDRESSES[chainId];
|
|
759
|
+
if (!routerAddress) {
|
|
760
|
+
throw new InvalidChainError(
|
|
761
|
+
chainId,
|
|
762
|
+
`Router address not found for chain ID: ${chainId}`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
const normalizedSellToken = normalizeAddress(sellToken);
|
|
766
|
+
const normalizedBuyToken = normalizeAddress(buyToken);
|
|
767
|
+
const totalWithSlippage = addSlippage(quote.total, slippagePercent);
|
|
768
|
+
const transferCall = {
|
|
769
|
+
contractAddress: normalizedSellToken,
|
|
770
|
+
entrypoint: "transfer",
|
|
771
|
+
calldata: [routerAddress, toHex(totalWithSlippage), "0x0"]
|
|
772
|
+
};
|
|
773
|
+
const clearCall = {
|
|
774
|
+
contractAddress: routerAddress,
|
|
775
|
+
entrypoint: "clear",
|
|
776
|
+
calldata: [normalizedSellToken]
|
|
777
|
+
};
|
|
778
|
+
if (!quote || quote.splits.length === 0) {
|
|
779
|
+
return {
|
|
780
|
+
transferCall,
|
|
781
|
+
swapCalls: [],
|
|
782
|
+
clearCall,
|
|
783
|
+
allCalls: [transferCall, clearCall]
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
const { splits } = quote;
|
|
787
|
+
const clearMinimumCall = {
|
|
788
|
+
contractAddress: routerAddress,
|
|
789
|
+
entrypoint: "clear_minimum",
|
|
790
|
+
calldata: [normalizedBuyToken, toHex(minimumReceived), "0x0"]
|
|
791
|
+
};
|
|
792
|
+
let swapCalls;
|
|
793
|
+
if (splits.length === 1) {
|
|
794
|
+
swapCalls = generateSingleRouteSwap(
|
|
795
|
+
splits[0],
|
|
796
|
+
normalizedBuyToken,
|
|
797
|
+
routerAddress
|
|
798
|
+
);
|
|
799
|
+
} else {
|
|
800
|
+
swapCalls = generateMultiRouteSwap(
|
|
801
|
+
splits,
|
|
802
|
+
normalizedBuyToken,
|
|
803
|
+
routerAddress
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
swapCalls.push(clearMinimumCall);
|
|
807
|
+
return {
|
|
808
|
+
transferCall,
|
|
809
|
+
swapCalls,
|
|
810
|
+
clearCall,
|
|
811
|
+
allCalls: [transferCall, ...swapCalls, clearCall]
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function generateSingleRouteSwap(split, targetToken, routerAddress) {
|
|
815
|
+
const routeCalldata = encodeRoute(split.route, targetToken);
|
|
816
|
+
const amountSpecified = BigInt(split.amount_specified);
|
|
817
|
+
const absAmount = abs(amountSpecified);
|
|
818
|
+
return [
|
|
819
|
+
{
|
|
820
|
+
contractAddress: routerAddress,
|
|
821
|
+
entrypoint: "multihop_swap",
|
|
822
|
+
calldata: [
|
|
823
|
+
toHex(split.route.length),
|
|
824
|
+
...routeCalldata.encoded,
|
|
825
|
+
targetToken,
|
|
826
|
+
toHex(absAmount),
|
|
827
|
+
"0x1"
|
|
828
|
+
// is_exact_amount_received flag
|
|
829
|
+
]
|
|
830
|
+
}
|
|
831
|
+
];
|
|
832
|
+
}
|
|
833
|
+
function generateMultiRouteSwap(splits, targetToken, routerAddress) {
|
|
834
|
+
const multiRouteCalldata = splits.reduce((memo, split) => {
|
|
835
|
+
const routeCalldata = encodeRoute(split.route, targetToken);
|
|
836
|
+
const amountSpecified = BigInt(split.amount_specified);
|
|
837
|
+
const absAmount = abs(amountSpecified);
|
|
838
|
+
return memo.concat([
|
|
839
|
+
toHex(split.route.length),
|
|
840
|
+
...routeCalldata.encoded,
|
|
841
|
+
targetToken,
|
|
842
|
+
toHex(absAmount),
|
|
843
|
+
"0x1"
|
|
844
|
+
// is_exact_amount_received flag
|
|
845
|
+
]);
|
|
846
|
+
}, []);
|
|
847
|
+
return [
|
|
848
|
+
{
|
|
849
|
+
contractAddress: routerAddress,
|
|
850
|
+
entrypoint: "multi_multihop_swap",
|
|
851
|
+
calldata: [toHex(splits.length), ...multiRouteCalldata]
|
|
852
|
+
}
|
|
853
|
+
];
|
|
854
|
+
}
|
|
855
|
+
function prepareSwapCalls(params) {
|
|
856
|
+
const result = generateSwapCalls(params);
|
|
857
|
+
const {
|
|
858
|
+
sellToken,
|
|
859
|
+
quote,
|
|
860
|
+
chainId = CHAIN_IDS.MAINNET,
|
|
861
|
+
routerAddress: customRouterAddress,
|
|
862
|
+
slippagePercent = DEFAULT_SLIPPAGE_PERCENT
|
|
863
|
+
} = params;
|
|
864
|
+
const routerAddress = customRouterAddress ?? ROUTER_ADDRESSES[chainId];
|
|
865
|
+
if (!routerAddress) {
|
|
866
|
+
throw new InvalidChainError(
|
|
867
|
+
chainId,
|
|
868
|
+
`Router address not found for chain ID: ${chainId}`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
const normalizedSellToken = normalizeAddress(sellToken);
|
|
872
|
+
const totalWithSlippage = addSlippage(quote.total, slippagePercent);
|
|
873
|
+
const approveCall = {
|
|
874
|
+
contractAddress: normalizedSellToken,
|
|
875
|
+
entrypoint: "approve",
|
|
876
|
+
calldata: [routerAddress, toHex(totalWithSlippage), "0x0"]
|
|
877
|
+
};
|
|
878
|
+
return {
|
|
879
|
+
...result,
|
|
880
|
+
approveCall,
|
|
881
|
+
allCalls: [approveCall, ...result.allCalls]
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/tokens/mainnet.ts
|
|
886
|
+
var MAINNET_TOKENS = [
|
|
887
|
+
{
|
|
888
|
+
symbol: "ETH",
|
|
889
|
+
name: "Ether",
|
|
890
|
+
address: "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
|
|
891
|
+
decimals: 18
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
symbol: "STRK",
|
|
895
|
+
name: "Starknet Token",
|
|
896
|
+
address: "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
|
|
897
|
+
decimals: 18
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
symbol: "USDC",
|
|
901
|
+
name: "USD Coin",
|
|
902
|
+
address: "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb",
|
|
903
|
+
decimals: 6
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
symbol: "USDC.e",
|
|
907
|
+
name: "Bridged USDC",
|
|
908
|
+
address: "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8",
|
|
909
|
+
decimals: 6
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
symbol: "USDT",
|
|
913
|
+
name: "Tether USD",
|
|
914
|
+
address: "0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8",
|
|
915
|
+
decimals: 6
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
symbol: "DAI",
|
|
919
|
+
name: "Dai Stablecoin",
|
|
920
|
+
address: "0x00da114221cb83fa859dbdb4c44beeaa0bb37c7537ad5ae66fe5e0efd20e6eb3",
|
|
921
|
+
decimals: 18
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
symbol: "WBTC",
|
|
925
|
+
name: "Wrapped Bitcoin",
|
|
926
|
+
address: "0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac",
|
|
927
|
+
decimals: 8
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
symbol: "LORDS",
|
|
931
|
+
name: "Lords Token",
|
|
932
|
+
address: "0x0124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49",
|
|
933
|
+
decimals: 18
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
symbol: "wstETH",
|
|
937
|
+
name: "Wrapped stETH",
|
|
938
|
+
address: "0x042b8f0484674ca266ac5d08e4ac6a3fe65bd3129795def2dca5c34ecc5f96d2",
|
|
939
|
+
decimals: 18
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
symbol: "EKUBO",
|
|
943
|
+
name: "Ekubo Protocol",
|
|
944
|
+
address: "0x075afe6402ad5a5c20dd25e10ec3b3986acaa647b77e4ae24b0cbc9a54a27a87",
|
|
945
|
+
decimals: 18
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
symbol: "ZEND",
|
|
949
|
+
name: "zkLend",
|
|
950
|
+
address: "0x00585c32b625999e6e5e78645ff8df7a9001cf5cf3eb6b80ccdd16cb64bd3a34",
|
|
951
|
+
decimals: 18
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
symbol: "NSTR",
|
|
955
|
+
name: "Nostra",
|
|
956
|
+
address: "0x04619e9ce4109590219c5263787050726be63382148538f3f936c22aa87d2fc2",
|
|
957
|
+
decimals: 18
|
|
958
|
+
}
|
|
959
|
+
];
|
|
960
|
+
function getMainnetTokensMap() {
|
|
961
|
+
const map = /* @__PURE__ */ new Map();
|
|
962
|
+
for (const token of MAINNET_TOKENS) {
|
|
963
|
+
map.set(token.symbol.toUpperCase(), token);
|
|
964
|
+
}
|
|
965
|
+
return map;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/tokens/registry.ts
|
|
969
|
+
var TokenRegistry = class {
|
|
970
|
+
symbolMap = /* @__PURE__ */ new Map();
|
|
971
|
+
addressMap = /* @__PURE__ */ new Map();
|
|
972
|
+
constructor(tokens = []) {
|
|
973
|
+
for (const token of MAINNET_TOKENS) {
|
|
974
|
+
this.register(token);
|
|
975
|
+
}
|
|
976
|
+
for (const token of tokens) {
|
|
977
|
+
this.register(token);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Register a token
|
|
982
|
+
*/
|
|
983
|
+
register(token) {
|
|
984
|
+
const upperSymbol = token.symbol.toUpperCase();
|
|
985
|
+
const normalizedAddress = normalizeAddress(token.address);
|
|
986
|
+
const normalizedToken = {
|
|
987
|
+
...token,
|
|
988
|
+
address: normalizedAddress
|
|
989
|
+
};
|
|
990
|
+
this.symbolMap.set(upperSymbol, normalizedToken);
|
|
991
|
+
this.addressMap.set(normalizedAddress, normalizedToken);
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Get token by symbol (case-insensitive)
|
|
995
|
+
*/
|
|
996
|
+
getBySymbol(symbol) {
|
|
997
|
+
return this.symbolMap.get(symbol.toUpperCase());
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Get token by address (normalized)
|
|
1001
|
+
*/
|
|
1002
|
+
getByAddress(address) {
|
|
1003
|
+
return this.addressMap.get(normalizeAddress(address));
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Check if a token exists by symbol
|
|
1007
|
+
*/
|
|
1008
|
+
hasSymbol(symbol) {
|
|
1009
|
+
return this.symbolMap.has(symbol.toUpperCase());
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Check if a token exists by address
|
|
1013
|
+
*/
|
|
1014
|
+
hasAddress(address) {
|
|
1015
|
+
return this.addressMap.has(normalizeAddress(address));
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Get all registered tokens
|
|
1019
|
+
*/
|
|
1020
|
+
getAll() {
|
|
1021
|
+
return Array.from(this.symbolMap.values());
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Get all registered symbols
|
|
1025
|
+
*/
|
|
1026
|
+
getSymbols() {
|
|
1027
|
+
return Array.from(this.symbolMap.keys());
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
var defaultRegistry = null;
|
|
1031
|
+
function getDefaultRegistry() {
|
|
1032
|
+
if (!defaultRegistry) {
|
|
1033
|
+
defaultRegistry = new TokenRegistry();
|
|
1034
|
+
}
|
|
1035
|
+
return defaultRegistry;
|
|
1036
|
+
}
|
|
1037
|
+
function createTokenRegistry(customTokens = []) {
|
|
1038
|
+
return new TokenRegistry(customTokens);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// src/tokens/resolver.ts
|
|
1042
|
+
function resolveToken(tokenIdentifier, registry) {
|
|
1043
|
+
const reg = registry ?? getDefaultRegistry();
|
|
1044
|
+
if (isAddress(tokenIdentifier)) {
|
|
1045
|
+
return normalizeAddress(tokenIdentifier);
|
|
1046
|
+
}
|
|
1047
|
+
const token = reg.getBySymbol(tokenIdentifier);
|
|
1048
|
+
if (token) {
|
|
1049
|
+
return token.address;
|
|
1050
|
+
}
|
|
1051
|
+
throw new TokenNotFoundError(tokenIdentifier);
|
|
1052
|
+
}
|
|
1053
|
+
function resolveTokenInfo(tokenIdentifier, registry) {
|
|
1054
|
+
const reg = registry ?? getDefaultRegistry();
|
|
1055
|
+
if (isAddress(tokenIdentifier)) {
|
|
1056
|
+
return reg.getByAddress(tokenIdentifier);
|
|
1057
|
+
}
|
|
1058
|
+
return reg.getBySymbol(tokenIdentifier);
|
|
1059
|
+
}
|
|
1060
|
+
function canResolveToken(tokenIdentifier, registry) {
|
|
1061
|
+
if (isAddress(tokenIdentifier)) {
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
const reg = registry ?? getDefaultRegistry();
|
|
1065
|
+
return reg.hasSymbol(tokenIdentifier);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/polling/poller.ts
|
|
1069
|
+
var DEFAULT_POLLING_CONFIG = {
|
|
1070
|
+
interval: 5e3,
|
|
1071
|
+
maxConsecutiveErrors: 3
|
|
1072
|
+
};
|
|
1073
|
+
var QuotePoller = class {
|
|
1074
|
+
intervalId = null;
|
|
1075
|
+
abortController = null;
|
|
1076
|
+
consecutiveErrors = 0;
|
|
1077
|
+
isRunning = false;
|
|
1078
|
+
params;
|
|
1079
|
+
callbacks;
|
|
1080
|
+
config;
|
|
1081
|
+
fetchConfig;
|
|
1082
|
+
constructor(params, callbacks, config = {}, fetchConfig) {
|
|
1083
|
+
this.params = params;
|
|
1084
|
+
this.callbacks = callbacks;
|
|
1085
|
+
this.config = { ...DEFAULT_POLLING_CONFIG, ...config };
|
|
1086
|
+
this.fetchConfig = fetchConfig;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Start polling for quotes
|
|
1090
|
+
*/
|
|
1091
|
+
start() {
|
|
1092
|
+
if (this.isRunning) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
this.isRunning = true;
|
|
1096
|
+
this.consecutiveErrors = 0;
|
|
1097
|
+
this.abortController = new AbortController();
|
|
1098
|
+
void this.fetchQuote();
|
|
1099
|
+
this.intervalId = setInterval(() => {
|
|
1100
|
+
void this.fetchQuote();
|
|
1101
|
+
}, this.config.interval);
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Stop polling
|
|
1105
|
+
*/
|
|
1106
|
+
stop() {
|
|
1107
|
+
if (!this.isRunning) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
this.isRunning = false;
|
|
1111
|
+
if (this.intervalId) {
|
|
1112
|
+
clearInterval(this.intervalId);
|
|
1113
|
+
this.intervalId = null;
|
|
1114
|
+
}
|
|
1115
|
+
if (this.abortController) {
|
|
1116
|
+
this.abortController.abort();
|
|
1117
|
+
this.abortController = null;
|
|
1118
|
+
}
|
|
1119
|
+
this.callbacks.onStop?.("manual");
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Check if currently polling
|
|
1123
|
+
*/
|
|
1124
|
+
get running() {
|
|
1125
|
+
return this.isRunning;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Update polling parameters (requires restart)
|
|
1129
|
+
*/
|
|
1130
|
+
updateParams(params) {
|
|
1131
|
+
Object.assign(this.params, params);
|
|
1132
|
+
if (this.isRunning) {
|
|
1133
|
+
this.stop();
|
|
1134
|
+
this.start();
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Fetch a quote
|
|
1139
|
+
*/
|
|
1140
|
+
async fetchQuote() {
|
|
1141
|
+
if (!this.isRunning || !this.abortController) {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
const quote = await fetchSwapQuote({
|
|
1146
|
+
amount: this.params.amount,
|
|
1147
|
+
tokenFrom: this.params.tokenFrom,
|
|
1148
|
+
tokenTo: this.params.tokenTo,
|
|
1149
|
+
chainId: this.params.chainId,
|
|
1150
|
+
signal: this.abortController.signal,
|
|
1151
|
+
fetchConfig: this.fetchConfig
|
|
1152
|
+
});
|
|
1153
|
+
this.consecutiveErrors = 0;
|
|
1154
|
+
this.callbacks.onQuote(quote);
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
this.consecutiveErrors++;
|
|
1160
|
+
this.callbacks.onError?.(error);
|
|
1161
|
+
if (this.consecutiveErrors >= this.config.maxConsecutiveErrors) {
|
|
1162
|
+
this.stopDueToErrors();
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Stop due to consecutive errors
|
|
1168
|
+
*/
|
|
1169
|
+
stopDueToErrors() {
|
|
1170
|
+
this.isRunning = false;
|
|
1171
|
+
if (this.intervalId) {
|
|
1172
|
+
clearInterval(this.intervalId);
|
|
1173
|
+
this.intervalId = null;
|
|
1174
|
+
}
|
|
1175
|
+
if (this.abortController) {
|
|
1176
|
+
this.abortController.abort();
|
|
1177
|
+
this.abortController = null;
|
|
1178
|
+
}
|
|
1179
|
+
this.callbacks.onStop?.("errors");
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
function createQuotePoller(params, callbacks, config, fetchConfig) {
|
|
1183
|
+
return new QuotePoller(params, callbacks, config, fetchConfig);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/client.ts
|
|
1187
|
+
var DEFAULT_CONFIG = {
|
|
1188
|
+
chain: "mainnet",
|
|
1189
|
+
defaultSlippagePercent: 5n
|
|
1190
|
+
};
|
|
1191
|
+
var EkuboClient = class {
|
|
1192
|
+
config;
|
|
1193
|
+
tokenRegistry;
|
|
1194
|
+
constructor(config = {}) {
|
|
1195
|
+
this.config = this.resolveConfig(config);
|
|
1196
|
+
this.tokenRegistry = new TokenRegistry(config.customTokens);
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Resolve configuration with defaults
|
|
1200
|
+
*/
|
|
1201
|
+
resolveConfig(config) {
|
|
1202
|
+
const chain = config.chain ?? DEFAULT_CONFIG.chain;
|
|
1203
|
+
const chainConfig = config.chainId ? getChainConfig(config.chainId) : getChainConfig(chain);
|
|
1204
|
+
if (!chainConfig && !config.chainId) {
|
|
1205
|
+
throw new InvalidChainError(chain);
|
|
1206
|
+
}
|
|
1207
|
+
return {
|
|
1208
|
+
chainId: config.chainId ?? chainConfig?.chainId ?? CHAIN_IDS.MAINNET,
|
|
1209
|
+
quoterApiUrl: config.quoterApiUrl ?? chainConfig?.quoterApiUrl ?? "https://prod-api-quoter.ekubo.org",
|
|
1210
|
+
apiUrl: config.apiUrl ?? chainConfig?.apiUrl ?? "https://prod-api.ekubo.org",
|
|
1211
|
+
routerAddress: config.routerAddress ?? chainConfig?.routerAddress ?? "",
|
|
1212
|
+
defaultSlippagePercent: config.defaultSlippagePercent ?? DEFAULT_CONFIG.defaultSlippagePercent,
|
|
1213
|
+
fetch: { ...DEFAULT_FETCH_CONFIG, ...config.fetch },
|
|
1214
|
+
polling: { ...DEFAULT_POLLING_CONFIG, ...config.polling }
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Get the current chain ID
|
|
1219
|
+
*/
|
|
1220
|
+
get chainId() {
|
|
1221
|
+
return this.config.chainId;
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Get the router contract address
|
|
1225
|
+
*/
|
|
1226
|
+
get routerAddress() {
|
|
1227
|
+
return this.config.routerAddress;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Get the token registry
|
|
1231
|
+
*/
|
|
1232
|
+
get tokens() {
|
|
1233
|
+
return this.tokenRegistry;
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Resolve a token identifier (symbol or address) to an address
|
|
1237
|
+
*/
|
|
1238
|
+
resolveToken(tokenIdentifier) {
|
|
1239
|
+
return resolveToken(tokenIdentifier, this.tokenRegistry);
|
|
1240
|
+
}
|
|
1241
|
+
// ==========================================================================
|
|
1242
|
+
// Swap Quotes
|
|
1243
|
+
// ==========================================================================
|
|
1244
|
+
/**
|
|
1245
|
+
* Fetch a swap quote
|
|
1246
|
+
*
|
|
1247
|
+
* @param params - Quote parameters (supports symbols or addresses)
|
|
1248
|
+
* @returns Swap quote
|
|
1249
|
+
*/
|
|
1250
|
+
async getQuote(params) {
|
|
1251
|
+
const tokenFrom = this.resolveToken(params.tokenFrom);
|
|
1252
|
+
const tokenTo = this.resolveToken(params.tokenTo);
|
|
1253
|
+
return fetchSwapQuote({
|
|
1254
|
+
amount: params.amount,
|
|
1255
|
+
tokenFrom,
|
|
1256
|
+
tokenTo,
|
|
1257
|
+
chainId: this.config.chainId,
|
|
1258
|
+
signal: params.signal,
|
|
1259
|
+
fetchConfig: this.config.fetch
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Fetch a token price in USDC
|
|
1264
|
+
*
|
|
1265
|
+
* @param tokenIdentifier - Token symbol or address
|
|
1266
|
+
* @param amount - Amount of token
|
|
1267
|
+
* @param signal - Optional abort signal
|
|
1268
|
+
* @returns Price in USDC (as bigint)
|
|
1269
|
+
*/
|
|
1270
|
+
async getUsdcPrice(tokenIdentifier, amount, signal) {
|
|
1271
|
+
const tokenFrom = this.resolveToken(tokenIdentifier);
|
|
1272
|
+
return fetchSwapQuoteInUsdc({
|
|
1273
|
+
amount,
|
|
1274
|
+
tokenFrom,
|
|
1275
|
+
chainId: this.config.chainId,
|
|
1276
|
+
signal,
|
|
1277
|
+
fetchConfig: this.config.fetch
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Fetch price history
|
|
1282
|
+
*
|
|
1283
|
+
* @param token - Token symbol or address
|
|
1284
|
+
* @param otherToken - Quote token symbol or address
|
|
1285
|
+
* @param interval - Time interval in seconds (default: 7000)
|
|
1286
|
+
* @returns Price history data
|
|
1287
|
+
*/
|
|
1288
|
+
async getPriceHistory(token, otherToken, interval) {
|
|
1289
|
+
const resolvedToken = this.resolveToken(token);
|
|
1290
|
+
const resolvedOtherToken = this.resolveToken(otherToken);
|
|
1291
|
+
return getPriceHistory({
|
|
1292
|
+
token: resolvedToken,
|
|
1293
|
+
otherToken: resolvedOtherToken,
|
|
1294
|
+
chainId: this.config.chainId,
|
|
1295
|
+
interval,
|
|
1296
|
+
apiUrl: this.config.apiUrl,
|
|
1297
|
+
fetchConfig: this.config.fetch
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
// ==========================================================================
|
|
1301
|
+
// Swap Call Generation
|
|
1302
|
+
// ==========================================================================
|
|
1303
|
+
/**
|
|
1304
|
+
* Generate swap calls from a quote
|
|
1305
|
+
*
|
|
1306
|
+
* @param params - Call generation parameters (supports symbols or addresses)
|
|
1307
|
+
* @returns Swap calls result
|
|
1308
|
+
*/
|
|
1309
|
+
generateSwapCalls(params) {
|
|
1310
|
+
const sellToken = this.resolveToken(params.sellToken);
|
|
1311
|
+
const buyToken = this.resolveToken(params.buyToken);
|
|
1312
|
+
return generateSwapCalls({
|
|
1313
|
+
...params,
|
|
1314
|
+
sellToken,
|
|
1315
|
+
buyToken,
|
|
1316
|
+
chainId: this.config.chainId,
|
|
1317
|
+
routerAddress: this.config.routerAddress,
|
|
1318
|
+
slippagePercent: params.slippagePercent ?? this.config.defaultSlippagePercent
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Prepare swap calls with approval included
|
|
1323
|
+
*
|
|
1324
|
+
* @param params - Call generation parameters (supports symbols or addresses)
|
|
1325
|
+
* @returns Swap calls result with approve call
|
|
1326
|
+
*/
|
|
1327
|
+
prepareSwapCalls(params) {
|
|
1328
|
+
const sellToken = this.resolveToken(params.sellToken);
|
|
1329
|
+
const buyToken = this.resolveToken(params.buyToken);
|
|
1330
|
+
return prepareSwapCalls({
|
|
1331
|
+
...params,
|
|
1332
|
+
sellToken,
|
|
1333
|
+
buyToken,
|
|
1334
|
+
chainId: this.config.chainId,
|
|
1335
|
+
routerAddress: this.config.routerAddress,
|
|
1336
|
+
slippagePercent: params.slippagePercent ?? this.config.defaultSlippagePercent
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
// ==========================================================================
|
|
1340
|
+
// Quote Polling
|
|
1341
|
+
// ==========================================================================
|
|
1342
|
+
/**
|
|
1343
|
+
* Create a quote poller for real-time updates
|
|
1344
|
+
*
|
|
1345
|
+
* @param params - Quote parameters (supports symbols or addresses)
|
|
1346
|
+
* @param callbacks - Poller callbacks
|
|
1347
|
+
* @param config - Optional polling configuration
|
|
1348
|
+
* @returns QuotePoller instance
|
|
1349
|
+
*/
|
|
1350
|
+
createQuotePoller(params, callbacks, config) {
|
|
1351
|
+
const tokenFrom = this.resolveToken(params.tokenFrom);
|
|
1352
|
+
const tokenTo = this.resolveToken(params.tokenTo);
|
|
1353
|
+
return new QuotePoller(
|
|
1354
|
+
{
|
|
1355
|
+
amount: params.amount,
|
|
1356
|
+
tokenFrom,
|
|
1357
|
+
tokenTo,
|
|
1358
|
+
chainId: this.config.chainId
|
|
1359
|
+
},
|
|
1360
|
+
callbacks,
|
|
1361
|
+
{ ...this.config.polling, ...config },
|
|
1362
|
+
this.config.fetch
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
// ==========================================================================
|
|
1366
|
+
// Token Management
|
|
1367
|
+
// ==========================================================================
|
|
1368
|
+
/**
|
|
1369
|
+
* Register a custom token in the local registry
|
|
1370
|
+
*/
|
|
1371
|
+
registerToken(token) {
|
|
1372
|
+
this.tokenRegistry.register(token);
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Fetch all tokens from the Ekubo API
|
|
1376
|
+
*
|
|
1377
|
+
* @returns Array of token metadata from the API
|
|
1378
|
+
*/
|
|
1379
|
+
async fetchTokens() {
|
|
1380
|
+
return fetchTokens({
|
|
1381
|
+
chainId: this.config.chainId,
|
|
1382
|
+
apiUrl: this.config.apiUrl,
|
|
1383
|
+
fetchConfig: this.config.fetch
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Fetch metadata for a single token from the API
|
|
1388
|
+
*
|
|
1389
|
+
* @param tokenAddress - Token contract address
|
|
1390
|
+
* @returns Token metadata or null if not found
|
|
1391
|
+
*/
|
|
1392
|
+
async fetchToken(tokenAddress) {
|
|
1393
|
+
const resolved = this.resolveToken(tokenAddress);
|
|
1394
|
+
return fetchToken({
|
|
1395
|
+
tokenAddress: resolved,
|
|
1396
|
+
chainId: this.config.chainId,
|
|
1397
|
+
apiUrl: this.config.apiUrl,
|
|
1398
|
+
fetchConfig: this.config.fetch
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Fetch metadata for multiple tokens at once
|
|
1403
|
+
*
|
|
1404
|
+
* @param tokenAddresses - Array of token addresses
|
|
1405
|
+
* @returns Array of token metadata
|
|
1406
|
+
*/
|
|
1407
|
+
async fetchTokensBatch(tokenAddresses) {
|
|
1408
|
+
const resolved = tokenAddresses.map((addr) => this.resolveToken(addr));
|
|
1409
|
+
return fetchTokensBatch({
|
|
1410
|
+
tokenAddresses: resolved,
|
|
1411
|
+
chainId: this.config.chainId,
|
|
1412
|
+
apiUrl: this.config.apiUrl,
|
|
1413
|
+
fetchConfig: this.config.fetch
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Fetch tokens from API and register them in the local registry
|
|
1418
|
+
*
|
|
1419
|
+
* @returns Number of tokens registered
|
|
1420
|
+
*/
|
|
1421
|
+
async syncTokensFromApi() {
|
|
1422
|
+
const apiTokens = await this.fetchTokens();
|
|
1423
|
+
for (const token of apiTokens) {
|
|
1424
|
+
this.tokenRegistry.register({
|
|
1425
|
+
symbol: token.symbol,
|
|
1426
|
+
address: token.address,
|
|
1427
|
+
decimals: token.decimals,
|
|
1428
|
+
name: token.name
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
return apiTokens.length;
|
|
1432
|
+
}
|
|
1433
|
+
// ==========================================================================
|
|
1434
|
+
// Protocol Statistics
|
|
1435
|
+
// ==========================================================================
|
|
1436
|
+
/**
|
|
1437
|
+
* Fetch top trading pairs by volume
|
|
1438
|
+
*
|
|
1439
|
+
* @returns Array of pair statistics
|
|
1440
|
+
*/
|
|
1441
|
+
async getTopPairs() {
|
|
1442
|
+
return fetchTopPairs({
|
|
1443
|
+
chainId: this.config.chainId,
|
|
1444
|
+
apiUrl: this.config.apiUrl,
|
|
1445
|
+
fetchConfig: this.config.fetch
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Fetch protocol TVL overview
|
|
1450
|
+
*
|
|
1451
|
+
* @returns TVL statistics
|
|
1452
|
+
*/
|
|
1453
|
+
async getTvl() {
|
|
1454
|
+
return fetchTvl({
|
|
1455
|
+
chainId: this.config.chainId,
|
|
1456
|
+
apiUrl: this.config.apiUrl,
|
|
1457
|
+
fetchConfig: this.config.fetch
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Fetch protocol volume overview
|
|
1462
|
+
*
|
|
1463
|
+
* @returns Volume statistics
|
|
1464
|
+
*/
|
|
1465
|
+
async getVolume() {
|
|
1466
|
+
return fetchVolume({
|
|
1467
|
+
chainId: this.config.chainId,
|
|
1468
|
+
apiUrl: this.config.apiUrl,
|
|
1469
|
+
fetchConfig: this.config.fetch
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Fetch TVL history for a trading pair
|
|
1474
|
+
*
|
|
1475
|
+
* @param tokenA - First token (symbol or address)
|
|
1476
|
+
* @param tokenB - Second token (symbol or address)
|
|
1477
|
+
* @returns TVL data points
|
|
1478
|
+
*/
|
|
1479
|
+
async getPairTvl(tokenA, tokenB) {
|
|
1480
|
+
const resolvedA = this.resolveToken(tokenA);
|
|
1481
|
+
const resolvedB = this.resolveToken(tokenB);
|
|
1482
|
+
return fetchPairTvl({
|
|
1483
|
+
tokenA: resolvedA,
|
|
1484
|
+
tokenB: resolvedB,
|
|
1485
|
+
chainId: this.config.chainId,
|
|
1486
|
+
apiUrl: this.config.apiUrl,
|
|
1487
|
+
fetchConfig: this.config.fetch
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Fetch volume history for a trading pair
|
|
1492
|
+
*
|
|
1493
|
+
* @param tokenA - First token (symbol or address)
|
|
1494
|
+
* @param tokenB - Second token (symbol or address)
|
|
1495
|
+
* @returns Volume data points
|
|
1496
|
+
*/
|
|
1497
|
+
async getPairVolume(tokenA, tokenB) {
|
|
1498
|
+
const resolvedA = this.resolveToken(tokenA);
|
|
1499
|
+
const resolvedB = this.resolveToken(tokenB);
|
|
1500
|
+
return fetchPairVolume({
|
|
1501
|
+
tokenA: resolvedA,
|
|
1502
|
+
tokenB: resolvedB,
|
|
1503
|
+
chainId: this.config.chainId,
|
|
1504
|
+
apiUrl: this.config.apiUrl,
|
|
1505
|
+
fetchConfig: this.config.fetch
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Fetch pools for a trading pair
|
|
1510
|
+
*
|
|
1511
|
+
* @param tokenA - First token (symbol or address)
|
|
1512
|
+
* @param tokenB - Second token (symbol or address)
|
|
1513
|
+
* @returns Pool information
|
|
1514
|
+
*/
|
|
1515
|
+
async getPairPools(tokenA, tokenB) {
|
|
1516
|
+
const resolvedA = this.resolveToken(tokenA);
|
|
1517
|
+
const resolvedB = this.resolveToken(tokenB);
|
|
1518
|
+
return fetchPairPools({
|
|
1519
|
+
tokenA: resolvedA,
|
|
1520
|
+
tokenB: resolvedB,
|
|
1521
|
+
chainId: this.config.chainId,
|
|
1522
|
+
apiUrl: this.config.apiUrl,
|
|
1523
|
+
fetchConfig: this.config.fetch
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
function createEkuboClient(config) {
|
|
1528
|
+
return new EkuboClient(config);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
export { API_URLS, AbortError, ApiError, CHAIN_CONFIGS, CHAIN_IDS, DEFAULT_POLLING_CONFIG, EkuboClient, EkuboError, InsufficientLiquidityError, InvalidChainError, MAINNET_TOKENS, QuotePoller, ROUTER_ADDRESSES, RateLimitError, STARKNET_CHAIN_IDS, STARKNET_TO_EKUBO_CHAIN, TimeoutError, TokenNotFoundError, TokenRegistry, USDC_ADDRESSES, abs, addSlippage, calculateBackoff, canResolveToken, createEkuboClient, createQuotePoller, createTokenRegistry, encodeRoute, encodeRouteNode, fetchPairPools, fetchPairTvl, fetchPairVolume, fetchSwapQuote, fetchSwapQuoteInUsdc, fetchToken, fetchTokens, fetchTokensBatch, fetchTopPairs, fetchTvl, fetchVolume, generateSwapCalls, getChainConfig, getDefaultRegistry, getEkuboChainId, getMainnetTokensMap, getPriceHistory, isAddress, isNonRetryableError, normalizeAddress, parseTotalCalculated, prepareSwapCalls, resolveToken, resolveTokenInfo, sleep, splitU256, subtractSlippage, toHex, withRetry };
|
|
1532
|
+
//# sourceMappingURL=index.js.map
|
|
1533
|
+
//# sourceMappingURL=index.js.map
|