@provable-games/ekubo-sdk 0.1.0 → 0.1.8

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/dist/react.cjs ADDED
@@ -0,0 +1,1999 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/react/context.tsx
7
+
8
+ // src/errors/index.ts
9
+ var EkuboError = class _EkuboError extends Error {
10
+ constructor(message, code, cause) {
11
+ super(message);
12
+ this.code = code;
13
+ this.cause = cause;
14
+ this.name = "EkuboError";
15
+ Object.setPrototypeOf(this, _EkuboError.prototype);
16
+ }
17
+ };
18
+ var InsufficientLiquidityError = class _InsufficientLiquidityError extends EkuboError {
19
+ constructor(message = "Insufficient liquidity for swap") {
20
+ super(message, "INSUFFICIENT_LIQUIDITY");
21
+ this.name = "InsufficientLiquidityError";
22
+ Object.setPrototypeOf(this, _InsufficientLiquidityError.prototype);
23
+ }
24
+ };
25
+ var ApiError = class _ApiError extends EkuboError {
26
+ constructor(message, statusCode, cause) {
27
+ super(message, "API_ERROR", cause);
28
+ this.statusCode = statusCode;
29
+ this.name = "ApiError";
30
+ Object.setPrototypeOf(this, _ApiError.prototype);
31
+ }
32
+ };
33
+ var RateLimitError = class _RateLimitError extends EkuboError {
34
+ constructor(message = "Rate limited by API", retryAfter) {
35
+ super(message, "RATE_LIMIT");
36
+ this.retryAfter = retryAfter;
37
+ this.name = "RateLimitError";
38
+ Object.setPrototypeOf(this, _RateLimitError.prototype);
39
+ }
40
+ };
41
+ var AbortError = class _AbortError extends EkuboError {
42
+ constructor(message = "Request aborted") {
43
+ super(message, "ABORTED");
44
+ this.name = "AbortError";
45
+ Object.setPrototypeOf(this, _AbortError.prototype);
46
+ }
47
+ };
48
+ var TokenNotFoundError = class _TokenNotFoundError extends EkuboError {
49
+ constructor(tokenIdentifier, message = `Token not found: ${tokenIdentifier}`) {
50
+ super(message, "TOKEN_NOT_FOUND");
51
+ this.tokenIdentifier = tokenIdentifier;
52
+ this.name = "TokenNotFoundError";
53
+ Object.setPrototypeOf(this, _TokenNotFoundError.prototype);
54
+ }
55
+ };
56
+ var InvalidChainError = class _InvalidChainError extends EkuboError {
57
+ constructor(chainId, message = `Invalid or unsupported chain: ${chainId}`) {
58
+ super(message, "INVALID_CHAIN");
59
+ this.chainId = chainId;
60
+ this.name = "InvalidChainError";
61
+ Object.setPrototypeOf(this, _InvalidChainError.prototype);
62
+ }
63
+ };
64
+
65
+ // src/utils/retry.ts
66
+ var DEFAULT_FETCH_CONFIG = {
67
+ timeout: 1e4,
68
+ maxRetries: 3,
69
+ baseBackoff: 1e3,
70
+ maxBackoff: 5e3,
71
+ fetch: globalThis.fetch
72
+ };
73
+ function parseRetryAfter(retryAfter) {
74
+ if (!retryAfter) return null;
75
+ const trimmed = retryAfter.trim();
76
+ if (!trimmed) return null;
77
+ const seconds = parseInt(trimmed, 10);
78
+ if (!isNaN(seconds) && seconds >= 0) {
79
+ return seconds * 1e3;
80
+ }
81
+ try {
82
+ const date = new Date(trimmed);
83
+ const delay = date.getTime() - Date.now();
84
+ return delay > 0 ? delay : null;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+ function calculateBackoff(attempt, baseBackoff, maxBackoff, retryAfter) {
90
+ if (retryAfter) {
91
+ const retryAfterDelay = parseRetryAfter(retryAfter);
92
+ if (retryAfterDelay !== null && retryAfterDelay > 0) {
93
+ return retryAfterDelay;
94
+ }
95
+ }
96
+ let delay = baseBackoff * Math.pow(2, attempt);
97
+ if (delay > maxBackoff) {
98
+ delay = maxBackoff;
99
+ }
100
+ const minDelay = delay / 2;
101
+ const jitter = Math.random() * (delay - minDelay);
102
+ return minDelay + jitter;
103
+ }
104
+ function sleep(ms) {
105
+ return new Promise((resolve) => setTimeout(resolve, ms));
106
+ }
107
+
108
+ // src/utils/bigint.ts
109
+ function parseTotalCalculated(totalCalculated) {
110
+ let value;
111
+ if (typeof totalCalculated === "string") {
112
+ value = BigInt(totalCalculated);
113
+ } else {
114
+ value = BigInt(Math.floor(totalCalculated));
115
+ }
116
+ return value < 0n ? -value : value;
117
+ }
118
+ function abs(value) {
119
+ return value < 0n ? -value : value;
120
+ }
121
+ function addSlippage(amount, slippagePercent) {
122
+ return amount + amount * slippagePercent / 100n;
123
+ }
124
+
125
+ // src/utils/hex.ts
126
+ function toHex(value) {
127
+ if (typeof value === "string") {
128
+ if (value.startsWith("0x") || value.startsWith("0X")) {
129
+ return "0x" + BigInt(value).toString(16);
130
+ }
131
+ return "0x" + BigInt(value).toString(16);
132
+ }
133
+ return "0x" + BigInt(value).toString(16);
134
+ }
135
+ function normalizeAddress(address) {
136
+ const normalized = toHex(address);
137
+ return normalized.toLowerCase();
138
+ }
139
+ function isAddress(value) {
140
+ return /^0x[0-9a-fA-F]+$/.test(value);
141
+ }
142
+
143
+ // src/chains/constants.ts
144
+ var CHAIN_IDS = {
145
+ MAINNET: "23448594291968334",
146
+ SEPOLIA: "393402133025997798000961"
147
+ };
148
+ var STARKNET_CHAIN_IDS = {
149
+ SN_MAIN: "0x534e5f4d41494e",
150
+ SN_SEPOLIA: "0x534e5f5345504f4c4941"
151
+ };
152
+ var ROUTER_ADDRESSES = {
153
+ [CHAIN_IDS.MAINNET]: "0x0199741822c2dc722f6f605204f35e56dbc23bceed54818168c4c49e4fb8737e",
154
+ [CHAIN_IDS.SEPOLIA]: "0x0045f933adf0607292468ad1c1dedaa74d5ad166392590e72676a34d01d7b763"
155
+ };
156
+ var USDC_ADDRESSES = {
157
+ [CHAIN_IDS.MAINNET]: "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb",
158
+ [CHAIN_IDS.SEPOLIA]: "0x053b40a647cedfca6ca84f542a0fe36736031905a9639a7f19a3c1e66bfd5080"
159
+ };
160
+ var API_URLS = {
161
+ /** Quoter API for swap quotes */
162
+ QUOTER: "https://prod-api-quoter.ekubo.org",
163
+ /** Main API for tokens, prices, stats, etc. */
164
+ API: "https://prod-api.ekubo.org"
165
+ };
166
+ var CHAIN_CONFIGS = {
167
+ mainnet: {
168
+ chainId: CHAIN_IDS.MAINNET,
169
+ quoterApiUrl: API_URLS.QUOTER,
170
+ apiUrl: API_URLS.API,
171
+ routerAddress: ROUTER_ADDRESSES[CHAIN_IDS.MAINNET],
172
+ usdcAddress: USDC_ADDRESSES[CHAIN_IDS.MAINNET]
173
+ },
174
+ sepolia: {
175
+ chainId: CHAIN_IDS.SEPOLIA,
176
+ quoterApiUrl: API_URLS.QUOTER,
177
+ apiUrl: API_URLS.API,
178
+ routerAddress: ROUTER_ADDRESSES[CHAIN_IDS.SEPOLIA],
179
+ usdcAddress: USDC_ADDRESSES[CHAIN_IDS.SEPOLIA]
180
+ }
181
+ };
182
+ var STARKNET_TO_EKUBO_CHAIN = {
183
+ [STARKNET_CHAIN_IDS.SN_MAIN]: CHAIN_IDS.MAINNET,
184
+ [STARKNET_CHAIN_IDS.SN_SEPOLIA]: CHAIN_IDS.SEPOLIA
185
+ };
186
+ function getEkuboChainId(starknetChainId) {
187
+ return STARKNET_TO_EKUBO_CHAIN[starknetChainId];
188
+ }
189
+ function getChainConfig(chainOrId) {
190
+ if (chainOrId === "mainnet" || chainOrId === "sepolia") {
191
+ return CHAIN_CONFIGS[chainOrId];
192
+ }
193
+ if (chainOrId === CHAIN_IDS.MAINNET) {
194
+ return CHAIN_CONFIGS.mainnet;
195
+ }
196
+ if (chainOrId === CHAIN_IDS.SEPOLIA) {
197
+ return CHAIN_CONFIGS.sepolia;
198
+ }
199
+ const ekuboChainId = getEkuboChainId(chainOrId);
200
+ if (ekuboChainId === CHAIN_IDS.MAINNET) {
201
+ return CHAIN_CONFIGS.mainnet;
202
+ }
203
+ if (ekuboChainId === CHAIN_IDS.SEPOLIA) {
204
+ return CHAIN_CONFIGS.sepolia;
205
+ }
206
+ return void 0;
207
+ }
208
+
209
+ // src/api/quote.ts
210
+ async function fetchSwapQuote(params) {
211
+ const {
212
+ amount,
213
+ tokenFrom,
214
+ tokenTo,
215
+ chainId = CHAIN_IDS.MAINNET,
216
+ signal,
217
+ fetchConfig = {}
218
+ } = params;
219
+ const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
220
+ const fetchFn = config.fetch;
221
+ const receivedAmount = `-${amount.toString()}`;
222
+ const normalizedTokenFrom = normalizeAddress(tokenFrom);
223
+ const normalizedTokenTo = normalizeAddress(tokenTo);
224
+ const url = `${API_URLS.QUOTER}/${chainId}/${receivedAmount}/${normalizedTokenFrom}/${normalizedTokenTo}`;
225
+ let lastError = null;
226
+ for (let attempt = 0; attempt < config.maxRetries; attempt++) {
227
+ if (signal?.aborted) {
228
+ throw new AbortError("Request aborted");
229
+ }
230
+ try {
231
+ const controller = new AbortController();
232
+ const timeoutId = setTimeout(() => controller.abort(), config.timeout);
233
+ const combinedSignal = signal ? anySignal([signal, controller.signal]) : controller.signal;
234
+ const response = await fetchFn(url, {
235
+ method: "GET",
236
+ signal: combinedSignal,
237
+ mode: "cors",
238
+ credentials: "omit"
239
+ });
240
+ clearTimeout(timeoutId);
241
+ if (response.ok) {
242
+ const data = await response.json();
243
+ if ("error" in data) {
244
+ if (data.error.includes("Insufficient liquidity")) {
245
+ throw new InsufficientLiquidityError(data.error);
246
+ }
247
+ throw new ApiError(data.error);
248
+ }
249
+ return {
250
+ impact: data.price_impact,
251
+ total: parseTotalCalculated(data.total_calculated),
252
+ splits: data.splits
253
+ };
254
+ }
255
+ if (response.status === 404) {
256
+ try {
257
+ const data = await response.json();
258
+ if (data.error?.includes("Insufficient liquidity")) {
259
+ throw new InsufficientLiquidityError(data.error);
260
+ }
261
+ } catch (e) {
262
+ if (e instanceof InsufficientLiquidityError) {
263
+ throw e;
264
+ }
265
+ }
266
+ throw new ApiError(`Failed to fetch swap quote: 404 Not Found`, 404);
267
+ }
268
+ if (response.status === 429) {
269
+ if (attempt === config.maxRetries - 1) {
270
+ throw new RateLimitError("Rate limited (429) - max retries exceeded");
271
+ }
272
+ const retryAfter = response.headers.get("Retry-After");
273
+ const delay = calculateBackoff(
274
+ attempt,
275
+ config.baseBackoff,
276
+ config.maxBackoff,
277
+ retryAfter
278
+ );
279
+ await sleep(delay);
280
+ continue;
281
+ }
282
+ throw new ApiError(
283
+ `Failed to fetch swap quote: ${response.status}`,
284
+ response.status
285
+ );
286
+ } catch (error) {
287
+ lastError = error;
288
+ if (error instanceof Error && (error.name === "AbortError" || error instanceof AbortError)) {
289
+ throw new AbortError("Request aborted");
290
+ }
291
+ if (error instanceof InsufficientLiquidityError) {
292
+ throw error;
293
+ }
294
+ if (attempt === config.maxRetries - 1) {
295
+ break;
296
+ }
297
+ const delay = calculateBackoff(
298
+ attempt,
299
+ config.baseBackoff,
300
+ config.maxBackoff
301
+ );
302
+ await sleep(delay);
303
+ }
304
+ }
305
+ throw lastError || new ApiError("Failed to fetch swap quote: unknown error");
306
+ }
307
+ async function fetchSwapQuoteInUsdc(params) {
308
+ const { chainId = CHAIN_IDS.MAINNET, ...rest } = params;
309
+ const usdcAddress = USDC_ADDRESSES[chainId] ?? USDC_ADDRESSES[CHAIN_IDS.MAINNET];
310
+ const quote = await fetchSwapQuote({
311
+ ...rest,
312
+ tokenTo: usdcAddress,
313
+ chainId
314
+ });
315
+ return quote.total;
316
+ }
317
+ function anySignal(signals) {
318
+ const controller = new AbortController();
319
+ for (const signal of signals) {
320
+ if (signal.aborted) {
321
+ controller.abort();
322
+ return controller.signal;
323
+ }
324
+ signal.addEventListener("abort", () => controller.abort(), { once: true });
325
+ }
326
+ return controller.signal;
327
+ }
328
+
329
+ // src/api/price.ts
330
+ async function getPriceHistory(params) {
331
+ const {
332
+ token,
333
+ otherToken,
334
+ chainId = CHAIN_IDS.MAINNET,
335
+ interval = 7e3,
336
+ apiUrl = API_URLS.API,
337
+ fetchConfig = {}
338
+ } = params;
339
+ const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
340
+ const fetchFn = config.fetch;
341
+ const normalizedToken = normalizeAddress(token);
342
+ const normalizedOtherToken = normalizeAddress(otherToken);
343
+ const url = `${apiUrl}/price/${chainId}/${normalizedToken}/${normalizedOtherToken}/history?interval=${interval}`;
344
+ try {
345
+ const response = await fetchFn(url, {
346
+ method: "GET",
347
+ mode: "cors",
348
+ credentials: "omit"
349
+ });
350
+ if (!response.ok) {
351
+ throw new ApiError(
352
+ `Failed to fetch price history: ${response.status}`,
353
+ response.status
354
+ );
355
+ }
356
+ const data = await response.json();
357
+ return {
358
+ data: data?.data || []
359
+ };
360
+ } catch (error) {
361
+ if (error instanceof ApiError) {
362
+ throw error;
363
+ }
364
+ throw new ApiError(
365
+ `Failed to fetch price history: ${error instanceof Error ? error.message : "unknown error"}`
366
+ );
367
+ }
368
+ }
369
+
370
+ // src/api/tokens.ts
371
+ async function fetchTokens(params = {}) {
372
+ const {
373
+ chainId = CHAIN_IDS.MAINNET,
374
+ apiUrl = API_URLS.API,
375
+ fetchConfig = {}
376
+ } = params;
377
+ const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
378
+ const fetchFn = config.fetch;
379
+ const url = `${apiUrl}/tokens?chainId=${chainId}`;
380
+ try {
381
+ const response = await fetchFn(url, {
382
+ method: "GET",
383
+ mode: "cors",
384
+ credentials: "omit"
385
+ });
386
+ if (!response.ok) {
387
+ throw new ApiError(
388
+ `Failed to fetch tokens: ${response.status}`,
389
+ response.status
390
+ );
391
+ }
392
+ const data = await response.json();
393
+ return data;
394
+ } catch (error) {
395
+ if (error instanceof ApiError) {
396
+ throw error;
397
+ }
398
+ throw new ApiError(
399
+ `Failed to fetch tokens: ${error instanceof Error ? error.message : "unknown error"}`
400
+ );
401
+ }
402
+ }
403
+ async function fetchToken(params) {
404
+ const {
405
+ tokenAddress,
406
+ chainId = CHAIN_IDS.MAINNET,
407
+ apiUrl = API_URLS.API,
408
+ fetchConfig = {}
409
+ } = params;
410
+ const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
411
+ const fetchFn = config.fetch;
412
+ const url = `${apiUrl}/tokens/${chainId}/${tokenAddress}`;
413
+ try {
414
+ const response = await fetchFn(url, {
415
+ method: "GET",
416
+ mode: "cors",
417
+ credentials: "omit"
418
+ });
419
+ if (response.status === 404) {
420
+ return null;
421
+ }
422
+ if (!response.ok) {
423
+ throw new ApiError(
424
+ `Failed to fetch token: ${response.status}`,
425
+ response.status
426
+ );
427
+ }
428
+ const data = await response.json();
429
+ return data;
430
+ } catch (error) {
431
+ if (error instanceof ApiError) {
432
+ throw error;
433
+ }
434
+ throw new ApiError(
435
+ `Failed to fetch token: ${error instanceof Error ? error.message : "unknown error"}`
436
+ );
437
+ }
438
+ }
439
+ async function fetchTokensBatch(params) {
440
+ const {
441
+ tokenAddresses,
442
+ chainId = CHAIN_IDS.MAINNET,
443
+ apiUrl = API_URLS.API,
444
+ fetchConfig = {}
445
+ } = params;
446
+ if (tokenAddresses.length === 0) {
447
+ return [];
448
+ }
449
+ const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
450
+ const fetchFn = config.fetch;
451
+ const addressParams = tokenAddresses.map((addr) => `addresses=${encodeURIComponent(addr)}`).join("&");
452
+ const url = `${apiUrl}/tokens/batch?chainId=${chainId}&${addressParams}`;
453
+ try {
454
+ const response = await fetchFn(url, {
455
+ method: "GET",
456
+ mode: "cors",
457
+ credentials: "omit"
458
+ });
459
+ if (!response.ok) {
460
+ throw new ApiError(
461
+ `Failed to fetch tokens batch: ${response.status}`,
462
+ response.status
463
+ );
464
+ }
465
+ const data = await response.json();
466
+ return data;
467
+ } catch (error) {
468
+ if (error instanceof ApiError) {
469
+ throw error;
470
+ }
471
+ throw new ApiError(
472
+ `Failed to fetch tokens batch: ${error instanceof Error ? error.message : "unknown error"}`
473
+ );
474
+ }
475
+ }
476
+
477
+ // src/api/stats.ts
478
+ async function fetchTopPairs(params = {}) {
479
+ const {
480
+ chainId = CHAIN_IDS.MAINNET,
481
+ apiUrl = API_URLS.API,
482
+ fetchConfig = {}
483
+ } = params;
484
+ const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
485
+ const fetchFn = config.fetch;
486
+ const url = `${apiUrl}/overview/pairs?chainId=${chainId}`;
487
+ try {
488
+ const response = await fetchFn(url, {
489
+ method: "GET",
490
+ mode: "cors",
491
+ credentials: "omit"
492
+ });
493
+ if (!response.ok) {
494
+ throw new ApiError(
495
+ `Failed to fetch top pairs: ${response.status}`,
496
+ response.status
497
+ );
498
+ }
499
+ return await response.json();
500
+ } catch (error) {
501
+ if (error instanceof ApiError) throw error;
502
+ throw new ApiError(
503
+ `Failed to fetch top pairs: ${error instanceof Error ? error.message : "unknown error"}`
504
+ );
505
+ }
506
+ }
507
+ async function fetchTvl(params = {}) {
508
+ const {
509
+ chainId = CHAIN_IDS.MAINNET,
510
+ apiUrl = API_URLS.API,
511
+ fetchConfig = {}
512
+ } = params;
513
+ const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
514
+ const fetchFn = config.fetch;
515
+ const url = `${apiUrl}/overview/tvl?chainId=${chainId}`;
516
+ try {
517
+ const response = await fetchFn(url, {
518
+ method: "GET",
519
+ mode: "cors",
520
+ credentials: "omit"
521
+ });
522
+ if (!response.ok) {
523
+ throw new ApiError(
524
+ `Failed to fetch TVL: ${response.status}`,
525
+ response.status
526
+ );
527
+ }
528
+ return await response.json();
529
+ } catch (error) {
530
+ if (error instanceof ApiError) throw error;
531
+ throw new ApiError(
532
+ `Failed to fetch TVL: ${error instanceof Error ? error.message : "unknown error"}`
533
+ );
534
+ }
535
+ }
536
+ async function fetchVolume(params = {}) {
537
+ const {
538
+ chainId = CHAIN_IDS.MAINNET,
539
+ apiUrl = API_URLS.API,
540
+ fetchConfig = {}
541
+ } = params;
542
+ const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
543
+ const fetchFn = config.fetch;
544
+ const url = `${apiUrl}/overview/volume?chainId=${chainId}`;
545
+ try {
546
+ const response = await fetchFn(url, {
547
+ method: "GET",
548
+ mode: "cors",
549
+ credentials: "omit"
550
+ });
551
+ if (!response.ok) {
552
+ throw new ApiError(
553
+ `Failed to fetch volume: ${response.status}`,
554
+ response.status
555
+ );
556
+ }
557
+ return await response.json();
558
+ } catch (error) {
559
+ if (error instanceof ApiError) throw error;
560
+ throw new ApiError(
561
+ `Failed to fetch volume: ${error instanceof Error ? error.message : "unknown error"}`
562
+ );
563
+ }
564
+ }
565
+ async function fetchPairTvl(params) {
566
+ const {
567
+ tokenA,
568
+ tokenB,
569
+ chainId = CHAIN_IDS.MAINNET,
570
+ apiUrl = API_URLS.API,
571
+ fetchConfig = {}
572
+ } = params;
573
+ const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
574
+ const fetchFn = config.fetch;
575
+ const normalizedA = normalizeAddress(tokenA);
576
+ const normalizedB = normalizeAddress(tokenB);
577
+ const url = `${apiUrl}/pair/${chainId}/${normalizedA}/${normalizedB}/tvl`;
578
+ try {
579
+ const response = await fetchFn(url, {
580
+ method: "GET",
581
+ mode: "cors",
582
+ credentials: "omit"
583
+ });
584
+ if (!response.ok) {
585
+ throw new ApiError(
586
+ `Failed to fetch pair TVL: ${response.status}`,
587
+ response.status
588
+ );
589
+ }
590
+ return await response.json();
591
+ } catch (error) {
592
+ if (error instanceof ApiError) throw error;
593
+ throw new ApiError(
594
+ `Failed to fetch pair TVL: ${error instanceof Error ? error.message : "unknown error"}`
595
+ );
596
+ }
597
+ }
598
+ async function fetchPairVolume(params) {
599
+ const {
600
+ tokenA,
601
+ tokenB,
602
+ chainId = CHAIN_IDS.MAINNET,
603
+ apiUrl = API_URLS.API,
604
+ fetchConfig = {}
605
+ } = params;
606
+ const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
607
+ const fetchFn = config.fetch;
608
+ const normalizedA = normalizeAddress(tokenA);
609
+ const normalizedB = normalizeAddress(tokenB);
610
+ const url = `${apiUrl}/pair/${chainId}/${normalizedA}/${normalizedB}/volume`;
611
+ try {
612
+ const response = await fetchFn(url, {
613
+ method: "GET",
614
+ mode: "cors",
615
+ credentials: "omit"
616
+ });
617
+ if (!response.ok) {
618
+ throw new ApiError(
619
+ `Failed to fetch pair volume: ${response.status}`,
620
+ response.status
621
+ );
622
+ }
623
+ return await response.json();
624
+ } catch (error) {
625
+ if (error instanceof ApiError) throw error;
626
+ throw new ApiError(
627
+ `Failed to fetch pair volume: ${error instanceof Error ? error.message : "unknown error"}`
628
+ );
629
+ }
630
+ }
631
+ async function fetchPairPools(params) {
632
+ const {
633
+ tokenA,
634
+ tokenB,
635
+ chainId = CHAIN_IDS.MAINNET,
636
+ apiUrl = API_URLS.API,
637
+ fetchConfig = {}
638
+ } = params;
639
+ const config = { ...DEFAULT_FETCH_CONFIG, ...fetchConfig };
640
+ const fetchFn = config.fetch;
641
+ const normalizedA = normalizeAddress(tokenA);
642
+ const normalizedB = normalizeAddress(tokenB);
643
+ const url = `${apiUrl}/pair/${chainId}/${normalizedA}/${normalizedB}/pools`;
644
+ try {
645
+ const response = await fetchFn(url, {
646
+ method: "GET",
647
+ mode: "cors",
648
+ credentials: "omit"
649
+ });
650
+ if (!response.ok) {
651
+ throw new ApiError(
652
+ `Failed to fetch pair pools: ${response.status}`,
653
+ response.status
654
+ );
655
+ }
656
+ return await response.json();
657
+ } catch (error) {
658
+ if (error instanceof ApiError) throw error;
659
+ throw new ApiError(
660
+ `Failed to fetch pair pools: ${error instanceof Error ? error.message : "unknown error"}`
661
+ );
662
+ }
663
+ }
664
+
665
+ // src/calls/encoder.ts
666
+ function encodeRouteNode(routeNode, currentToken) {
667
+ const isToken1 = BigInt(currentToken) === BigInt(routeNode.pool_key.token1);
668
+ const nextToken = isToken1 ? routeNode.pool_key.token0 : routeNode.pool_key.token1;
669
+ const sqrtRatioLimit = BigInt(routeNode.sqrt_ratio_limit);
670
+ const calldata = [
671
+ routeNode.pool_key.token0,
672
+ routeNode.pool_key.token1,
673
+ routeNode.pool_key.fee,
674
+ toHex(routeNode.pool_key.tick_spacing),
675
+ routeNode.pool_key.extension,
676
+ toHex(sqrtRatioLimit % 2n ** 128n),
677
+ // low
678
+ toHex(sqrtRatioLimit >> 128n),
679
+ // high
680
+ toHex(routeNode.skip_ahead)
681
+ ];
682
+ return { nextToken, calldata };
683
+ }
684
+ function encodeRoute(route, targetToken) {
685
+ return route.reduce(
686
+ (memo, routeNode) => {
687
+ const { nextToken, calldata } = encodeRouteNode(routeNode, memo.token);
688
+ return {
689
+ token: nextToken,
690
+ encoded: memo.encoded.concat(calldata)
691
+ };
692
+ },
693
+ {
694
+ token: targetToken,
695
+ encoded: []
696
+ }
697
+ );
698
+ }
699
+
700
+ // src/calls/generator.ts
701
+ var DEFAULT_SLIPPAGE_PERCENT = 5n;
702
+ function generateSwapCalls(params) {
703
+ const {
704
+ sellToken,
705
+ buyToken,
706
+ minimumReceived,
707
+ quote,
708
+ chainId = CHAIN_IDS.MAINNET,
709
+ routerAddress: customRouterAddress,
710
+ slippagePercent = DEFAULT_SLIPPAGE_PERCENT
711
+ } = params;
712
+ const routerAddress = customRouterAddress ?? ROUTER_ADDRESSES[chainId];
713
+ if (!routerAddress) {
714
+ throw new InvalidChainError(
715
+ chainId,
716
+ `Router address not found for chain ID: ${chainId}`
717
+ );
718
+ }
719
+ const normalizedSellToken = normalizeAddress(sellToken);
720
+ const normalizedBuyToken = normalizeAddress(buyToken);
721
+ const totalWithSlippage = addSlippage(quote.total, slippagePercent);
722
+ const transferCall = {
723
+ contractAddress: normalizedSellToken,
724
+ entrypoint: "transfer",
725
+ calldata: [routerAddress, toHex(totalWithSlippage), "0x0"]
726
+ };
727
+ const clearCall = {
728
+ contractAddress: routerAddress,
729
+ entrypoint: "clear",
730
+ calldata: [normalizedSellToken]
731
+ };
732
+ if (!quote || quote.splits.length === 0) {
733
+ return {
734
+ transferCall,
735
+ swapCalls: [],
736
+ clearCall,
737
+ allCalls: [transferCall, clearCall]
738
+ };
739
+ }
740
+ const { splits } = quote;
741
+ const clearMinimumCall = {
742
+ contractAddress: routerAddress,
743
+ entrypoint: "clear_minimum",
744
+ calldata: [normalizedBuyToken, toHex(minimumReceived), "0x0"]
745
+ };
746
+ let swapCalls;
747
+ if (splits.length === 1) {
748
+ swapCalls = generateSingleRouteSwap(
749
+ splits[0],
750
+ normalizedBuyToken,
751
+ routerAddress
752
+ );
753
+ } else {
754
+ swapCalls = generateMultiRouteSwap(
755
+ splits,
756
+ normalizedBuyToken,
757
+ routerAddress
758
+ );
759
+ }
760
+ swapCalls.push(clearMinimumCall);
761
+ return {
762
+ transferCall,
763
+ swapCalls,
764
+ clearCall,
765
+ allCalls: [transferCall, ...swapCalls, clearCall]
766
+ };
767
+ }
768
+ function generateSingleRouteSwap(split, targetToken, routerAddress) {
769
+ const routeCalldata = encodeRoute(split.route, targetToken);
770
+ const amountSpecified = BigInt(split.amount_specified);
771
+ const absAmount = abs(amountSpecified);
772
+ return [
773
+ {
774
+ contractAddress: routerAddress,
775
+ entrypoint: "multihop_swap",
776
+ calldata: [
777
+ toHex(split.route.length),
778
+ ...routeCalldata.encoded,
779
+ targetToken,
780
+ toHex(absAmount),
781
+ "0x1"
782
+ // is_exact_amount_received flag
783
+ ]
784
+ }
785
+ ];
786
+ }
787
+ function generateMultiRouteSwap(splits, targetToken, routerAddress) {
788
+ const multiRouteCalldata = splits.reduce((memo, split) => {
789
+ const routeCalldata = encodeRoute(split.route, targetToken);
790
+ const amountSpecified = BigInt(split.amount_specified);
791
+ const absAmount = abs(amountSpecified);
792
+ return memo.concat([
793
+ toHex(split.route.length),
794
+ ...routeCalldata.encoded,
795
+ targetToken,
796
+ toHex(absAmount),
797
+ "0x1"
798
+ // is_exact_amount_received flag
799
+ ]);
800
+ }, []);
801
+ return [
802
+ {
803
+ contractAddress: routerAddress,
804
+ entrypoint: "multi_multihop_swap",
805
+ calldata: [toHex(splits.length), ...multiRouteCalldata]
806
+ }
807
+ ];
808
+ }
809
+ function prepareSwapCalls(params) {
810
+ const result = generateSwapCalls(params);
811
+ const {
812
+ sellToken,
813
+ quote,
814
+ chainId = CHAIN_IDS.MAINNET,
815
+ routerAddress: customRouterAddress,
816
+ slippagePercent = DEFAULT_SLIPPAGE_PERCENT
817
+ } = params;
818
+ const routerAddress = customRouterAddress ?? ROUTER_ADDRESSES[chainId];
819
+ if (!routerAddress) {
820
+ throw new InvalidChainError(
821
+ chainId,
822
+ `Router address not found for chain ID: ${chainId}`
823
+ );
824
+ }
825
+ const normalizedSellToken = normalizeAddress(sellToken);
826
+ const totalWithSlippage = addSlippage(quote.total, slippagePercent);
827
+ const approveCall = {
828
+ contractAddress: normalizedSellToken,
829
+ entrypoint: "approve",
830
+ calldata: [routerAddress, toHex(totalWithSlippage), "0x0"]
831
+ };
832
+ return {
833
+ ...result,
834
+ approveCall,
835
+ allCalls: [approveCall, ...result.allCalls]
836
+ };
837
+ }
838
+
839
+ // src/tokens/mainnet.ts
840
+ var MAINNET_TOKENS = [
841
+ {
842
+ symbol: "ETH",
843
+ name: "Ether",
844
+ address: "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
845
+ decimals: 18
846
+ },
847
+ {
848
+ symbol: "STRK",
849
+ name: "Starknet Token",
850
+ address: "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
851
+ decimals: 18
852
+ },
853
+ {
854
+ symbol: "USDC",
855
+ name: "USD Coin",
856
+ address: "0x033068F6539f8e6e6b131e6B2B814e6c34A5224bC66947c47DaB9dFeE93b35fb",
857
+ decimals: 6
858
+ },
859
+ {
860
+ symbol: "USDC.e",
861
+ name: "Bridged USDC",
862
+ address: "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8",
863
+ decimals: 6
864
+ },
865
+ {
866
+ symbol: "USDT",
867
+ name: "Tether USD",
868
+ address: "0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8",
869
+ decimals: 6
870
+ },
871
+ {
872
+ symbol: "DAI",
873
+ name: "Dai Stablecoin",
874
+ address: "0x00da114221cb83fa859dbdb4c44beeaa0bb37c7537ad5ae66fe5e0efd20e6eb3",
875
+ decimals: 18
876
+ },
877
+ {
878
+ symbol: "WBTC",
879
+ name: "Wrapped Bitcoin",
880
+ address: "0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac",
881
+ decimals: 8
882
+ },
883
+ {
884
+ symbol: "LORDS",
885
+ name: "Lords Token",
886
+ address: "0x0124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49",
887
+ decimals: 18
888
+ },
889
+ {
890
+ symbol: "wstETH",
891
+ name: "Wrapped stETH",
892
+ address: "0x042b8f0484674ca266ac5d08e4ac6a3fe65bd3129795def2dca5c34ecc5f96d2",
893
+ decimals: 18
894
+ },
895
+ {
896
+ symbol: "EKUBO",
897
+ name: "Ekubo Protocol",
898
+ address: "0x075afe6402ad5a5c20dd25e10ec3b3986acaa647b77e4ae24b0cbc9a54a27a87",
899
+ decimals: 18
900
+ },
901
+ {
902
+ symbol: "ZEND",
903
+ name: "zkLend",
904
+ address: "0x00585c32b625999e6e5e78645ff8df7a9001cf5cf3eb6b80ccdd16cb64bd3a34",
905
+ decimals: 18
906
+ },
907
+ {
908
+ symbol: "NSTR",
909
+ name: "Nostra",
910
+ address: "0x04619e9ce4109590219c5263787050726be63382148538f3f936c22aa87d2fc2",
911
+ decimals: 18
912
+ }
913
+ ];
914
+
915
+ // src/tokens/registry.ts
916
+ var TokenRegistry = class {
917
+ symbolMap = /* @__PURE__ */ new Map();
918
+ addressMap = /* @__PURE__ */ new Map();
919
+ constructor(tokens = []) {
920
+ for (const token of MAINNET_TOKENS) {
921
+ this.register(token);
922
+ }
923
+ for (const token of tokens) {
924
+ this.register(token);
925
+ }
926
+ }
927
+ /**
928
+ * Register a token
929
+ */
930
+ register(token) {
931
+ const upperSymbol = token.symbol.toUpperCase();
932
+ const normalizedAddress = normalizeAddress(token.address);
933
+ const normalizedToken = {
934
+ ...token,
935
+ address: normalizedAddress
936
+ };
937
+ this.symbolMap.set(upperSymbol, normalizedToken);
938
+ this.addressMap.set(normalizedAddress, normalizedToken);
939
+ }
940
+ /**
941
+ * Get token by symbol (case-insensitive)
942
+ */
943
+ getBySymbol(symbol) {
944
+ return this.symbolMap.get(symbol.toUpperCase());
945
+ }
946
+ /**
947
+ * Get token by address (normalized)
948
+ */
949
+ getByAddress(address) {
950
+ return this.addressMap.get(normalizeAddress(address));
951
+ }
952
+ /**
953
+ * Check if a token exists by symbol
954
+ */
955
+ hasSymbol(symbol) {
956
+ return this.symbolMap.has(symbol.toUpperCase());
957
+ }
958
+ /**
959
+ * Check if a token exists by address
960
+ */
961
+ hasAddress(address) {
962
+ return this.addressMap.has(normalizeAddress(address));
963
+ }
964
+ /**
965
+ * Get all registered tokens
966
+ */
967
+ getAll() {
968
+ return Array.from(this.symbolMap.values());
969
+ }
970
+ /**
971
+ * Get all registered symbols
972
+ */
973
+ getSymbols() {
974
+ return Array.from(this.symbolMap.keys());
975
+ }
976
+ };
977
+ var defaultRegistry = null;
978
+ function getDefaultRegistry() {
979
+ if (!defaultRegistry) {
980
+ defaultRegistry = new TokenRegistry();
981
+ }
982
+ return defaultRegistry;
983
+ }
984
+
985
+ // src/tokens/resolver.ts
986
+ function resolveToken(tokenIdentifier, registry) {
987
+ const reg = registry ?? getDefaultRegistry();
988
+ if (isAddress(tokenIdentifier)) {
989
+ return normalizeAddress(tokenIdentifier);
990
+ }
991
+ const token = reg.getBySymbol(tokenIdentifier);
992
+ if (token) {
993
+ return token.address;
994
+ }
995
+ throw new TokenNotFoundError(tokenIdentifier);
996
+ }
997
+
998
+ // src/polling/poller.ts
999
+ var DEFAULT_POLLING_CONFIG = {
1000
+ interval: 5e3,
1001
+ maxConsecutiveErrors: 3
1002
+ };
1003
+ var QuotePoller = class {
1004
+ intervalId = null;
1005
+ abortController = null;
1006
+ consecutiveErrors = 0;
1007
+ isRunning = false;
1008
+ params;
1009
+ callbacks;
1010
+ config;
1011
+ fetchConfig;
1012
+ constructor(params, callbacks, config = {}, fetchConfig) {
1013
+ this.params = params;
1014
+ this.callbacks = callbacks;
1015
+ this.config = { ...DEFAULT_POLLING_CONFIG, ...config };
1016
+ this.fetchConfig = fetchConfig;
1017
+ }
1018
+ /**
1019
+ * Start polling for quotes
1020
+ */
1021
+ start() {
1022
+ if (this.isRunning) {
1023
+ return;
1024
+ }
1025
+ this.isRunning = true;
1026
+ this.consecutiveErrors = 0;
1027
+ this.abortController = new AbortController();
1028
+ void this.fetchQuote();
1029
+ this.intervalId = setInterval(() => {
1030
+ void this.fetchQuote();
1031
+ }, this.config.interval);
1032
+ }
1033
+ /**
1034
+ * Stop polling
1035
+ */
1036
+ stop() {
1037
+ if (!this.isRunning) {
1038
+ return;
1039
+ }
1040
+ this.isRunning = false;
1041
+ if (this.intervalId) {
1042
+ clearInterval(this.intervalId);
1043
+ this.intervalId = null;
1044
+ }
1045
+ if (this.abortController) {
1046
+ this.abortController.abort();
1047
+ this.abortController = null;
1048
+ }
1049
+ this.callbacks.onStop?.("manual");
1050
+ }
1051
+ /**
1052
+ * Check if currently polling
1053
+ */
1054
+ get running() {
1055
+ return this.isRunning;
1056
+ }
1057
+ /**
1058
+ * Update polling parameters (requires restart)
1059
+ */
1060
+ updateParams(params) {
1061
+ Object.assign(this.params, params);
1062
+ if (this.isRunning) {
1063
+ this.stop();
1064
+ this.start();
1065
+ }
1066
+ }
1067
+ /**
1068
+ * Fetch a quote
1069
+ */
1070
+ async fetchQuote() {
1071
+ if (!this.isRunning || !this.abortController) {
1072
+ return;
1073
+ }
1074
+ try {
1075
+ const quote = await fetchSwapQuote({
1076
+ amount: this.params.amount,
1077
+ tokenFrom: this.params.tokenFrom,
1078
+ tokenTo: this.params.tokenTo,
1079
+ chainId: this.params.chainId,
1080
+ signal: this.abortController.signal,
1081
+ fetchConfig: this.fetchConfig
1082
+ });
1083
+ this.consecutiveErrors = 0;
1084
+ this.callbacks.onQuote(quote);
1085
+ } catch (error) {
1086
+ if (error instanceof Error && error.name === "AbortError") {
1087
+ return;
1088
+ }
1089
+ this.consecutiveErrors++;
1090
+ this.callbacks.onError?.(error);
1091
+ if (this.consecutiveErrors >= this.config.maxConsecutiveErrors) {
1092
+ this.stopDueToErrors();
1093
+ }
1094
+ }
1095
+ }
1096
+ /**
1097
+ * Stop due to consecutive errors
1098
+ */
1099
+ stopDueToErrors() {
1100
+ this.isRunning = false;
1101
+ if (this.intervalId) {
1102
+ clearInterval(this.intervalId);
1103
+ this.intervalId = null;
1104
+ }
1105
+ if (this.abortController) {
1106
+ this.abortController.abort();
1107
+ this.abortController = null;
1108
+ }
1109
+ this.callbacks.onStop?.("errors");
1110
+ }
1111
+ };
1112
+
1113
+ // src/client.ts
1114
+ var DEFAULT_CONFIG = {
1115
+ chain: "mainnet",
1116
+ defaultSlippagePercent: 5n
1117
+ };
1118
+ var EkuboClient = class {
1119
+ config;
1120
+ tokenRegistry;
1121
+ constructor(config = {}) {
1122
+ this.config = this.resolveConfig(config);
1123
+ this.tokenRegistry = new TokenRegistry(config.customTokens);
1124
+ }
1125
+ /**
1126
+ * Resolve configuration with defaults
1127
+ */
1128
+ resolveConfig(config) {
1129
+ const chain = config.chain ?? DEFAULT_CONFIG.chain;
1130
+ const chainConfig = config.chainId ? getChainConfig(config.chainId) : getChainConfig(chain);
1131
+ if (!chainConfig && !config.chainId) {
1132
+ throw new InvalidChainError(chain);
1133
+ }
1134
+ return {
1135
+ chainId: config.chainId ?? chainConfig?.chainId ?? CHAIN_IDS.MAINNET,
1136
+ quoterApiUrl: config.quoterApiUrl ?? chainConfig?.quoterApiUrl ?? "https://prod-api-quoter.ekubo.org",
1137
+ apiUrl: config.apiUrl ?? chainConfig?.apiUrl ?? "https://prod-api.ekubo.org",
1138
+ routerAddress: config.routerAddress ?? chainConfig?.routerAddress ?? "",
1139
+ defaultSlippagePercent: config.defaultSlippagePercent ?? DEFAULT_CONFIG.defaultSlippagePercent,
1140
+ fetch: { ...DEFAULT_FETCH_CONFIG, ...config.fetch },
1141
+ polling: { ...DEFAULT_POLLING_CONFIG, ...config.polling }
1142
+ };
1143
+ }
1144
+ /**
1145
+ * Get the current chain ID
1146
+ */
1147
+ get chainId() {
1148
+ return this.config.chainId;
1149
+ }
1150
+ /**
1151
+ * Get the router contract address
1152
+ */
1153
+ get routerAddress() {
1154
+ return this.config.routerAddress;
1155
+ }
1156
+ /**
1157
+ * Get the token registry
1158
+ */
1159
+ get tokens() {
1160
+ return this.tokenRegistry;
1161
+ }
1162
+ /**
1163
+ * Resolve a token identifier (symbol or address) to an address
1164
+ */
1165
+ resolveToken(tokenIdentifier) {
1166
+ return resolveToken(tokenIdentifier, this.tokenRegistry);
1167
+ }
1168
+ // ==========================================================================
1169
+ // Swap Quotes
1170
+ // ==========================================================================
1171
+ /**
1172
+ * Fetch a swap quote
1173
+ *
1174
+ * @param params - Quote parameters (supports symbols or addresses)
1175
+ * @returns Swap quote
1176
+ */
1177
+ async getQuote(params) {
1178
+ const tokenFrom = this.resolveToken(params.tokenFrom);
1179
+ const tokenTo = this.resolveToken(params.tokenTo);
1180
+ return fetchSwapQuote({
1181
+ amount: params.amount,
1182
+ tokenFrom,
1183
+ tokenTo,
1184
+ chainId: this.config.chainId,
1185
+ signal: params.signal,
1186
+ fetchConfig: this.config.fetch
1187
+ });
1188
+ }
1189
+ /**
1190
+ * Fetch a token price in USDC
1191
+ *
1192
+ * @param tokenIdentifier - Token symbol or address
1193
+ * @param amount - Amount of token
1194
+ * @param signal - Optional abort signal
1195
+ * @returns Price in USDC (as bigint)
1196
+ */
1197
+ async getUsdcPrice(tokenIdentifier, amount, signal) {
1198
+ const tokenFrom = this.resolveToken(tokenIdentifier);
1199
+ return fetchSwapQuoteInUsdc({
1200
+ amount,
1201
+ tokenFrom,
1202
+ chainId: this.config.chainId,
1203
+ signal,
1204
+ fetchConfig: this.config.fetch
1205
+ });
1206
+ }
1207
+ /**
1208
+ * Fetch price history
1209
+ *
1210
+ * @param token - Token symbol or address
1211
+ * @param otherToken - Quote token symbol or address
1212
+ * @param interval - Time interval in seconds (default: 7000)
1213
+ * @returns Price history data
1214
+ */
1215
+ async getPriceHistory(token, otherToken, interval) {
1216
+ const resolvedToken = this.resolveToken(token);
1217
+ const resolvedOtherToken = this.resolveToken(otherToken);
1218
+ return getPriceHistory({
1219
+ token: resolvedToken,
1220
+ otherToken: resolvedOtherToken,
1221
+ chainId: this.config.chainId,
1222
+ interval,
1223
+ apiUrl: this.config.apiUrl,
1224
+ fetchConfig: this.config.fetch
1225
+ });
1226
+ }
1227
+ // ==========================================================================
1228
+ // Swap Call Generation
1229
+ // ==========================================================================
1230
+ /**
1231
+ * Generate swap calls from a quote
1232
+ *
1233
+ * @param params - Call generation parameters (supports symbols or addresses)
1234
+ * @returns Swap calls result
1235
+ */
1236
+ generateSwapCalls(params) {
1237
+ const sellToken = this.resolveToken(params.sellToken);
1238
+ const buyToken = this.resolveToken(params.buyToken);
1239
+ return generateSwapCalls({
1240
+ ...params,
1241
+ sellToken,
1242
+ buyToken,
1243
+ chainId: this.config.chainId,
1244
+ routerAddress: this.config.routerAddress,
1245
+ slippagePercent: params.slippagePercent ?? this.config.defaultSlippagePercent
1246
+ });
1247
+ }
1248
+ /**
1249
+ * Prepare swap calls with approval included
1250
+ *
1251
+ * @param params - Call generation parameters (supports symbols or addresses)
1252
+ * @returns Swap calls result with approve call
1253
+ */
1254
+ prepareSwapCalls(params) {
1255
+ const sellToken = this.resolveToken(params.sellToken);
1256
+ const buyToken = this.resolveToken(params.buyToken);
1257
+ return prepareSwapCalls({
1258
+ ...params,
1259
+ sellToken,
1260
+ buyToken,
1261
+ chainId: this.config.chainId,
1262
+ routerAddress: this.config.routerAddress,
1263
+ slippagePercent: params.slippagePercent ?? this.config.defaultSlippagePercent
1264
+ });
1265
+ }
1266
+ // ==========================================================================
1267
+ // Quote Polling
1268
+ // ==========================================================================
1269
+ /**
1270
+ * Create a quote poller for real-time updates
1271
+ *
1272
+ * @param params - Quote parameters (supports symbols or addresses)
1273
+ * @param callbacks - Poller callbacks
1274
+ * @param config - Optional polling configuration
1275
+ * @returns QuotePoller instance
1276
+ */
1277
+ createQuotePoller(params, callbacks, config) {
1278
+ const tokenFrom = this.resolveToken(params.tokenFrom);
1279
+ const tokenTo = this.resolveToken(params.tokenTo);
1280
+ return new QuotePoller(
1281
+ {
1282
+ amount: params.amount,
1283
+ tokenFrom,
1284
+ tokenTo,
1285
+ chainId: this.config.chainId
1286
+ },
1287
+ callbacks,
1288
+ { ...this.config.polling, ...config },
1289
+ this.config.fetch
1290
+ );
1291
+ }
1292
+ // ==========================================================================
1293
+ // Token Management
1294
+ // ==========================================================================
1295
+ /**
1296
+ * Register a custom token in the local registry
1297
+ */
1298
+ registerToken(token) {
1299
+ this.tokenRegistry.register(token);
1300
+ }
1301
+ /**
1302
+ * Fetch all tokens from the Ekubo API
1303
+ *
1304
+ * @returns Array of token metadata from the API
1305
+ */
1306
+ async fetchTokens() {
1307
+ return fetchTokens({
1308
+ chainId: this.config.chainId,
1309
+ apiUrl: this.config.apiUrl,
1310
+ fetchConfig: this.config.fetch
1311
+ });
1312
+ }
1313
+ /**
1314
+ * Fetch metadata for a single token from the API
1315
+ *
1316
+ * @param tokenAddress - Token contract address
1317
+ * @returns Token metadata or null if not found
1318
+ */
1319
+ async fetchToken(tokenAddress) {
1320
+ const resolved = this.resolveToken(tokenAddress);
1321
+ return fetchToken({
1322
+ tokenAddress: resolved,
1323
+ chainId: this.config.chainId,
1324
+ apiUrl: this.config.apiUrl,
1325
+ fetchConfig: this.config.fetch
1326
+ });
1327
+ }
1328
+ /**
1329
+ * Fetch metadata for multiple tokens at once
1330
+ *
1331
+ * @param tokenAddresses - Array of token addresses
1332
+ * @returns Array of token metadata
1333
+ */
1334
+ async fetchTokensBatch(tokenAddresses) {
1335
+ const resolved = tokenAddresses.map((addr) => this.resolveToken(addr));
1336
+ return fetchTokensBatch({
1337
+ tokenAddresses: resolved,
1338
+ chainId: this.config.chainId,
1339
+ apiUrl: this.config.apiUrl,
1340
+ fetchConfig: this.config.fetch
1341
+ });
1342
+ }
1343
+ /**
1344
+ * Fetch tokens from API and register them in the local registry
1345
+ *
1346
+ * @returns Number of tokens registered
1347
+ */
1348
+ async syncTokensFromApi() {
1349
+ const apiTokens = await this.fetchTokens();
1350
+ for (const token of apiTokens) {
1351
+ this.tokenRegistry.register({
1352
+ symbol: token.symbol,
1353
+ address: token.address,
1354
+ decimals: token.decimals,
1355
+ name: token.name
1356
+ });
1357
+ }
1358
+ return apiTokens.length;
1359
+ }
1360
+ // ==========================================================================
1361
+ // Protocol Statistics
1362
+ // ==========================================================================
1363
+ /**
1364
+ * Fetch top trading pairs by volume
1365
+ *
1366
+ * @returns Array of pair statistics
1367
+ */
1368
+ async getTopPairs() {
1369
+ return fetchTopPairs({
1370
+ chainId: this.config.chainId,
1371
+ apiUrl: this.config.apiUrl,
1372
+ fetchConfig: this.config.fetch
1373
+ });
1374
+ }
1375
+ /**
1376
+ * Fetch protocol TVL overview
1377
+ *
1378
+ * @returns TVL statistics
1379
+ */
1380
+ async getTvl() {
1381
+ return fetchTvl({
1382
+ chainId: this.config.chainId,
1383
+ apiUrl: this.config.apiUrl,
1384
+ fetchConfig: this.config.fetch
1385
+ });
1386
+ }
1387
+ /**
1388
+ * Fetch protocol volume overview
1389
+ *
1390
+ * @returns Volume statistics
1391
+ */
1392
+ async getVolume() {
1393
+ return fetchVolume({
1394
+ chainId: this.config.chainId,
1395
+ apiUrl: this.config.apiUrl,
1396
+ fetchConfig: this.config.fetch
1397
+ });
1398
+ }
1399
+ /**
1400
+ * Fetch TVL history for a trading pair
1401
+ *
1402
+ * @param tokenA - First token (symbol or address)
1403
+ * @param tokenB - Second token (symbol or address)
1404
+ * @returns TVL data points
1405
+ */
1406
+ async getPairTvl(tokenA, tokenB) {
1407
+ const resolvedA = this.resolveToken(tokenA);
1408
+ const resolvedB = this.resolveToken(tokenB);
1409
+ return fetchPairTvl({
1410
+ tokenA: resolvedA,
1411
+ tokenB: resolvedB,
1412
+ chainId: this.config.chainId,
1413
+ apiUrl: this.config.apiUrl,
1414
+ fetchConfig: this.config.fetch
1415
+ });
1416
+ }
1417
+ /**
1418
+ * Fetch volume history for a trading pair
1419
+ *
1420
+ * @param tokenA - First token (symbol or address)
1421
+ * @param tokenB - Second token (symbol or address)
1422
+ * @returns Volume data points
1423
+ */
1424
+ async getPairVolume(tokenA, tokenB) {
1425
+ const resolvedA = this.resolveToken(tokenA);
1426
+ const resolvedB = this.resolveToken(tokenB);
1427
+ return fetchPairVolume({
1428
+ tokenA: resolvedA,
1429
+ tokenB: resolvedB,
1430
+ chainId: this.config.chainId,
1431
+ apiUrl: this.config.apiUrl,
1432
+ fetchConfig: this.config.fetch
1433
+ });
1434
+ }
1435
+ /**
1436
+ * Fetch pools for a trading pair
1437
+ *
1438
+ * @param tokenA - First token (symbol or address)
1439
+ * @param tokenB - Second token (symbol or address)
1440
+ * @returns Pool information
1441
+ */
1442
+ async getPairPools(tokenA, tokenB) {
1443
+ const resolvedA = this.resolveToken(tokenA);
1444
+ const resolvedB = this.resolveToken(tokenB);
1445
+ return fetchPairPools({
1446
+ tokenA: resolvedA,
1447
+ tokenB: resolvedB,
1448
+ chainId: this.config.chainId,
1449
+ apiUrl: this.config.apiUrl,
1450
+ fetchConfig: this.config.fetch
1451
+ });
1452
+ }
1453
+ };
1454
+ function createEkuboClient(config) {
1455
+ return new EkuboClient(config);
1456
+ }
1457
+ var EkuboContext = react.createContext(null);
1458
+ function EkuboProvider({
1459
+ children,
1460
+ config,
1461
+ client: providedClient
1462
+ }) {
1463
+ const client = react.useMemo(() => {
1464
+ if (providedClient) {
1465
+ return providedClient;
1466
+ }
1467
+ return createEkuboClient(config);
1468
+ }, [providedClient, config]);
1469
+ return /* @__PURE__ */ jsxRuntime.jsx(EkuboContext.Provider, { value: client, children });
1470
+ }
1471
+ function useEkuboClient() {
1472
+ const client = react.useContext(EkuboContext);
1473
+ if (!client) {
1474
+ throw new Error("useEkuboClient must be used within an EkuboProvider");
1475
+ }
1476
+ return client;
1477
+ }
1478
+ function useOptionalEkuboClient() {
1479
+ return react.useContext(EkuboContext);
1480
+ }
1481
+ var defaultClientInstance = null;
1482
+ function getDefaultClient(config) {
1483
+ if (!defaultClientInstance) {
1484
+ defaultClientInstance = createEkuboClient(config);
1485
+ }
1486
+ return defaultClientInstance;
1487
+ }
1488
+ function useEkuboSwap({
1489
+ sellToken,
1490
+ buyToken,
1491
+ amount,
1492
+ enabled = true,
1493
+ pollingInterval = 5e3,
1494
+ defaultSlippagePercent = 5n,
1495
+ config
1496
+ }) {
1497
+ const contextClient = useOptionalEkuboClient();
1498
+ const client = contextClient ?? getDefaultClient(config);
1499
+ const [state, setState] = react.useState({
1500
+ quote: null,
1501
+ loading: false,
1502
+ error: null,
1503
+ priceImpact: null,
1504
+ insufficientLiquidity: false
1505
+ });
1506
+ const pollerRef = react.useRef(null);
1507
+ const abortControllerRef = react.useRef(null);
1508
+ const latestQuoteRef = react.useRef(null);
1509
+ const isSameToken = react.useMemo(() => {
1510
+ if (!sellToken || !buyToken) return true;
1511
+ try {
1512
+ return normalizeAddress(sellToken).toLowerCase() === normalizeAddress(buyToken).toLowerCase();
1513
+ } catch {
1514
+ return false;
1515
+ }
1516
+ }, [sellToken, buyToken]);
1517
+ const canFetch = react.useMemo(() => {
1518
+ return !isSameToken && sellToken !== null && buyToken !== null && amount !== 0n;
1519
+ }, [isSameToken, sellToken, buyToken, amount]);
1520
+ const shouldPoll = enabled && canFetch;
1521
+ const fetchQuote = react.useCallback(async () => {
1522
+ if (!canFetch || !sellToken || !buyToken) return;
1523
+ if (abortControllerRef.current) {
1524
+ abortControllerRef.current.abort();
1525
+ }
1526
+ abortControllerRef.current = new AbortController();
1527
+ setState((prev) => ({ ...prev, loading: true, error: null }));
1528
+ try {
1529
+ const quote = await client.getQuote({
1530
+ amount,
1531
+ tokenFrom: sellToken,
1532
+ tokenTo: buyToken,
1533
+ signal: abortControllerRef.current.signal
1534
+ });
1535
+ latestQuoteRef.current = quote;
1536
+ setState({
1537
+ quote,
1538
+ loading: false,
1539
+ error: null,
1540
+ priceImpact: quote.impact,
1541
+ insufficientLiquidity: false
1542
+ });
1543
+ } catch (err) {
1544
+ if (err instanceof Error && err.name === "AbortError") {
1545
+ return;
1546
+ }
1547
+ const isInsufficient = err instanceof InsufficientLiquidityError;
1548
+ setState((prev) => ({
1549
+ ...prev,
1550
+ quote: null,
1551
+ loading: false,
1552
+ error: err instanceof Error ? err : new Error("Unknown error"),
1553
+ priceImpact: null,
1554
+ insufficientLiquidity: isInsufficient
1555
+ }));
1556
+ }
1557
+ }, [canFetch, sellToken, buyToken, amount, client]);
1558
+ react.useEffect(() => {
1559
+ if (pollerRef.current) {
1560
+ clearInterval(pollerRef.current);
1561
+ pollerRef.current = null;
1562
+ }
1563
+ if (!shouldPoll) {
1564
+ if (!canFetch) {
1565
+ setState({
1566
+ quote: null,
1567
+ loading: false,
1568
+ error: null,
1569
+ priceImpact: null,
1570
+ insufficientLiquidity: false
1571
+ });
1572
+ }
1573
+ return;
1574
+ }
1575
+ fetchQuote();
1576
+ pollerRef.current = setInterval(fetchQuote, pollingInterval);
1577
+ return () => {
1578
+ if (pollerRef.current) {
1579
+ clearInterval(pollerRef.current);
1580
+ pollerRef.current = null;
1581
+ }
1582
+ if (abortControllerRef.current) {
1583
+ abortControllerRef.current.abort();
1584
+ abortControllerRef.current = null;
1585
+ }
1586
+ };
1587
+ }, [shouldPoll, canFetch, fetchQuote, pollingInterval]);
1588
+ const generateCalls = react.useCallback(
1589
+ (slippagePercent, minimumReceived) => {
1590
+ const quote = latestQuoteRef.current;
1591
+ if (!quote || !sellToken || !buyToken) return null;
1592
+ try {
1593
+ const minReceived = minimumReceived ?? (amount > 0n ? quote.total : amount);
1594
+ return client.prepareSwapCalls({
1595
+ sellToken,
1596
+ buyToken,
1597
+ quote,
1598
+ minimumReceived: minReceived < 0n ? -minReceived : minReceived,
1599
+ slippagePercent: slippagePercent ?? defaultSlippagePercent
1600
+ });
1601
+ } catch (err) {
1602
+ console.error("Error generating swap calls:", err);
1603
+ return null;
1604
+ }
1605
+ },
1606
+ [sellToken, buyToken, amount, defaultSlippagePercent, client]
1607
+ );
1608
+ const refetch = react.useCallback(() => {
1609
+ fetchQuote();
1610
+ }, [fetchQuote]);
1611
+ return react.useMemo(
1612
+ () => ({
1613
+ state,
1614
+ generateCalls,
1615
+ refetch
1616
+ }),
1617
+ [state, generateCalls, refetch]
1618
+ );
1619
+ }
1620
+ var defaultClientInstance2 = null;
1621
+ function getDefaultClient2(config) {
1622
+ if (!defaultClientInstance2) {
1623
+ defaultClientInstance2 = createEkuboClient(config);
1624
+ }
1625
+ return defaultClientInstance2;
1626
+ }
1627
+ function useEkuboPrices({
1628
+ tokens,
1629
+ timeoutMs = 1e4,
1630
+ config,
1631
+ enabled = true,
1632
+ standardAmount = 1000000000000000000n
1633
+ // 1e18
1634
+ }) {
1635
+ const contextClient = useOptionalEkuboClient();
1636
+ const client = contextClient ?? getDefaultClient2(config);
1637
+ const [prices, setPrices] = react.useState({});
1638
+ const [isLoading, setIsLoading] = react.useState(true);
1639
+ const [tokenLoadingStates, setTokenLoadingStates] = react.useState({});
1640
+ const [tokenErrorStates, setTokenErrorStates] = react.useState({});
1641
+ const [fetchKey, setFetchKey] = react.useState(0);
1642
+ const tokensKey = react.useMemo(() => JSON.stringify([...tokens].sort()), [tokens]);
1643
+ const isTokenAvailable = react.useCallback(
1644
+ (tokenAddress) => {
1645
+ if (!tokens.includes(tokenAddress)) return false;
1646
+ if (tokenLoadingStates[tokenAddress] === true) return false;
1647
+ if (tokenErrorStates[tokenAddress] === true) return false;
1648
+ if (prices[tokenAddress] === void 0) return false;
1649
+ return true;
1650
+ },
1651
+ [tokens, tokenLoadingStates, tokenErrorStates, prices]
1652
+ );
1653
+ const isTokenLoading = react.useCallback(
1654
+ (tokenAddress) => {
1655
+ if (!tokens.includes(tokenAddress)) return true;
1656
+ return tokenLoadingStates[tokenAddress] === true;
1657
+ },
1658
+ [tokens, tokenLoadingStates]
1659
+ );
1660
+ const hasTokenError = react.useCallback(
1661
+ (tokenAddress) => {
1662
+ if (!tokens.includes(tokenAddress)) return false;
1663
+ return tokenErrorStates[tokenAddress] === true;
1664
+ },
1665
+ [tokens, tokenErrorStates]
1666
+ );
1667
+ const getPrice = react.useCallback(
1668
+ (tokenAddress) => {
1669
+ if (!isTokenAvailable(tokenAddress)) {
1670
+ return void 0;
1671
+ }
1672
+ return prices[tokenAddress];
1673
+ },
1674
+ [isTokenAvailable, prices]
1675
+ );
1676
+ const refetch = react.useCallback(() => {
1677
+ setFetchKey((prev) => prev + 1);
1678
+ }, []);
1679
+ react.useEffect(() => {
1680
+ if (!enabled || !tokens || tokens.length === 0) {
1681
+ setIsLoading(false);
1682
+ setPrices({});
1683
+ setTokenLoadingStates({});
1684
+ setTokenErrorStates({});
1685
+ return;
1686
+ }
1687
+ setIsLoading(true);
1688
+ setTokenLoadingStates(
1689
+ tokens.reduce((acc, address) => ({ ...acc, [address]: true }), {})
1690
+ );
1691
+ setTokenErrorStates(
1692
+ tokens.reduce((acc, address) => ({ ...acc, [address]: false }), {})
1693
+ );
1694
+ let cancelled = false;
1695
+ const fetchPrices = async () => {
1696
+ const pricePromises = tokens.map(async (tokenAddress) => {
1697
+ const timeoutPromise = new Promise((resolve) => {
1698
+ setTimeout(() => {
1699
+ resolve({
1700
+ tokenAddress,
1701
+ price: void 0,
1702
+ error: true
1703
+ });
1704
+ }, timeoutMs);
1705
+ });
1706
+ const fetchPromise = (async () => {
1707
+ try {
1708
+ const usdcAmount = await client.getUsdcPrice(tokenAddress, standardAmount);
1709
+ const price = Number(usdcAmount) / 1e6;
1710
+ return { tokenAddress, price, error: false };
1711
+ } catch (error) {
1712
+ console.error(`Error fetching ${tokenAddress} price:`, error);
1713
+ return { tokenAddress, price: void 0, error: true };
1714
+ }
1715
+ })();
1716
+ return Promise.race([fetchPromise, timeoutPromise]);
1717
+ });
1718
+ const results = await Promise.all(pricePromises);
1719
+ if (cancelled) return;
1720
+ const newPrices = {};
1721
+ const newLoadingStates = {};
1722
+ const newErrorStates = {};
1723
+ results.forEach(({ tokenAddress, price, error }) => {
1724
+ newPrices[tokenAddress] = price;
1725
+ newLoadingStates[tokenAddress] = false;
1726
+ newErrorStates[tokenAddress] = error;
1727
+ });
1728
+ setPrices(newPrices);
1729
+ setTokenLoadingStates(newLoadingStates);
1730
+ setTokenErrorStates(newErrorStates);
1731
+ setIsLoading(false);
1732
+ };
1733
+ fetchPrices();
1734
+ return () => {
1735
+ cancelled = true;
1736
+ };
1737
+ }, [tokensKey, enabled, timeoutMs, client, standardAmount, fetchKey]);
1738
+ return react.useMemo(
1739
+ () => ({
1740
+ prices,
1741
+ isLoading,
1742
+ isTokenLoading,
1743
+ hasTokenError,
1744
+ isTokenAvailable,
1745
+ getPrice,
1746
+ refetch
1747
+ }),
1748
+ [prices, isLoading, isTokenLoading, hasTokenError, isTokenAvailable, getPrice, refetch]
1749
+ );
1750
+ }
1751
+ var defaultClientInstance3 = null;
1752
+ function getDefaultClient3(config) {
1753
+ if (!defaultClientInstance3) {
1754
+ defaultClientInstance3 = createEkuboClient(config);
1755
+ }
1756
+ return defaultClientInstance3;
1757
+ }
1758
+ function useEkuboTokens({
1759
+ config,
1760
+ enabled = true
1761
+ } = {}) {
1762
+ const contextClient = useOptionalEkuboClient();
1763
+ const client = contextClient ?? getDefaultClient3(config);
1764
+ const [tokens, setTokens] = react.useState([]);
1765
+ const [isLoading, setIsLoading] = react.useState(true);
1766
+ const [error, setError] = react.useState(null);
1767
+ const [fetchKey, setFetchKey] = react.useState(0);
1768
+ const refetch = react.useCallback(() => {
1769
+ setFetchKey((prev) => prev + 1);
1770
+ }, []);
1771
+ react.useEffect(() => {
1772
+ if (!enabled) {
1773
+ setIsLoading(false);
1774
+ setTokens([]);
1775
+ setError(null);
1776
+ return;
1777
+ }
1778
+ let cancelled = false;
1779
+ const fetchTokens2 = async () => {
1780
+ setIsLoading(true);
1781
+ setError(null);
1782
+ try {
1783
+ const result = await client.fetchTokens();
1784
+ if (!cancelled) {
1785
+ setTokens(result);
1786
+ setIsLoading(false);
1787
+ }
1788
+ } catch (err) {
1789
+ if (!cancelled) {
1790
+ setError(err instanceof Error ? err : new Error("Failed to fetch tokens"));
1791
+ setIsLoading(false);
1792
+ }
1793
+ }
1794
+ };
1795
+ fetchTokens2();
1796
+ return () => {
1797
+ cancelled = true;
1798
+ };
1799
+ }, [enabled, client, fetchKey]);
1800
+ const tokensByAddress = react.useMemo(() => {
1801
+ const map = /* @__PURE__ */ new Map();
1802
+ for (const token of tokens) {
1803
+ map.set(token.address.toLowerCase(), token);
1804
+ }
1805
+ return map;
1806
+ }, [tokens]);
1807
+ const tokensBySymbol = react.useMemo(() => {
1808
+ const map = /* @__PURE__ */ new Map();
1809
+ for (const token of tokens) {
1810
+ map.set(token.symbol.toUpperCase(), token);
1811
+ }
1812
+ return map;
1813
+ }, [tokens]);
1814
+ const getToken = react.useCallback(
1815
+ (address) => {
1816
+ return tokensByAddress.get(address.toLowerCase());
1817
+ },
1818
+ [tokensByAddress]
1819
+ );
1820
+ const getTokenBySymbol = react.useCallback(
1821
+ (symbol) => {
1822
+ return tokensBySymbol.get(symbol.toUpperCase());
1823
+ },
1824
+ [tokensBySymbol]
1825
+ );
1826
+ return react.useMemo(
1827
+ () => ({
1828
+ tokens,
1829
+ isLoading,
1830
+ error,
1831
+ refetch,
1832
+ getToken,
1833
+ getTokenBySymbol
1834
+ }),
1835
+ [tokens, isLoading, error, refetch, getToken, getTokenBySymbol]
1836
+ );
1837
+ }
1838
+ var defaultClientInstance4 = null;
1839
+ function getDefaultClient4(config) {
1840
+ if (!defaultClientInstance4) {
1841
+ defaultClientInstance4 = createEkuboClient(config);
1842
+ }
1843
+ return defaultClientInstance4;
1844
+ }
1845
+ function useEkuboStats({
1846
+ config,
1847
+ enabled = true,
1848
+ fetchTvl: fetchTvl2 = true,
1849
+ fetchVolume: fetchVolume2 = true,
1850
+ fetchTopPairs: fetchTopPairs2 = true
1851
+ } = {}) {
1852
+ const contextClient = useOptionalEkuboClient();
1853
+ const client = contextClient ?? getDefaultClient4(config);
1854
+ const [tvl, setTvl] = react.useState(null);
1855
+ const [volume, setVolume] = react.useState(null);
1856
+ const [topPairs, setTopPairs] = react.useState([]);
1857
+ const [isLoading, setIsLoading] = react.useState(true);
1858
+ const [error, setError] = react.useState(null);
1859
+ const [fetchKey, setFetchKey] = react.useState(0);
1860
+ const refetch = react.useCallback(() => {
1861
+ setFetchKey((prev) => prev + 1);
1862
+ }, []);
1863
+ react.useEffect(() => {
1864
+ if (!enabled) {
1865
+ setIsLoading(false);
1866
+ setTvl(null);
1867
+ setVolume(null);
1868
+ setTopPairs([]);
1869
+ setError(null);
1870
+ return;
1871
+ }
1872
+ let cancelled = false;
1873
+ const fetchStats = async () => {
1874
+ setIsLoading(true);
1875
+ setError(null);
1876
+ try {
1877
+ const promises = [];
1878
+ if (fetchTvl2) {
1879
+ promises.push(
1880
+ client.getTvl().then((result) => {
1881
+ if (!cancelled) setTvl(result);
1882
+ })
1883
+ );
1884
+ }
1885
+ if (fetchVolume2) {
1886
+ promises.push(
1887
+ client.getVolume().then((result) => {
1888
+ if (!cancelled) setVolume(result);
1889
+ })
1890
+ );
1891
+ }
1892
+ if (fetchTopPairs2) {
1893
+ promises.push(
1894
+ client.getTopPairs().then((result) => {
1895
+ if (!cancelled) setTopPairs(result);
1896
+ })
1897
+ );
1898
+ }
1899
+ await Promise.all(promises);
1900
+ if (!cancelled) {
1901
+ setIsLoading(false);
1902
+ }
1903
+ } catch (err) {
1904
+ if (!cancelled) {
1905
+ setError(err instanceof Error ? err : new Error("Failed to fetch stats"));
1906
+ setIsLoading(false);
1907
+ }
1908
+ }
1909
+ };
1910
+ fetchStats();
1911
+ return () => {
1912
+ cancelled = true;
1913
+ };
1914
+ }, [enabled, client, fetchKey, fetchTvl2, fetchVolume2, fetchTopPairs2]);
1915
+ return react.useMemo(
1916
+ () => ({
1917
+ tvl,
1918
+ volume,
1919
+ topPairs,
1920
+ isLoading,
1921
+ error,
1922
+ refetch
1923
+ }),
1924
+ [tvl, volume, topPairs, isLoading, error, refetch]
1925
+ );
1926
+ }
1927
+ var defaultClientInstance5 = null;
1928
+ function getDefaultClient5(config) {
1929
+ if (!defaultClientInstance5) {
1930
+ defaultClientInstance5 = createEkuboClient(config);
1931
+ }
1932
+ return defaultClientInstance5;
1933
+ }
1934
+ function useEkuboPriceHistory({
1935
+ token,
1936
+ quoteToken,
1937
+ interval = 7e3,
1938
+ config,
1939
+ enabled = true
1940
+ }) {
1941
+ const contextClient = useOptionalEkuboClient();
1942
+ const client = contextClient ?? getDefaultClient5(config);
1943
+ const [data, setData] = react.useState([]);
1944
+ const [isLoading, setIsLoading] = react.useState(true);
1945
+ const [error, setError] = react.useState(null);
1946
+ const [fetchKey, setFetchKey] = react.useState(0);
1947
+ const refetch = react.useCallback(() => {
1948
+ setFetchKey((prev) => prev + 1);
1949
+ }, []);
1950
+ react.useEffect(() => {
1951
+ if (!enabled || !token || !quoteToken) {
1952
+ setIsLoading(false);
1953
+ setData([]);
1954
+ setError(null);
1955
+ return;
1956
+ }
1957
+ let cancelled = false;
1958
+ const fetchPriceHistory = async () => {
1959
+ setIsLoading(true);
1960
+ setError(null);
1961
+ try {
1962
+ const result = await client.getPriceHistory(token, quoteToken, interval);
1963
+ if (!cancelled) {
1964
+ setData(result.data);
1965
+ setIsLoading(false);
1966
+ }
1967
+ } catch (err) {
1968
+ if (!cancelled) {
1969
+ setError(err instanceof Error ? err : new Error("Failed to fetch price history"));
1970
+ setIsLoading(false);
1971
+ }
1972
+ }
1973
+ };
1974
+ fetchPriceHistory();
1975
+ return () => {
1976
+ cancelled = true;
1977
+ };
1978
+ }, [enabled, token, quoteToken, interval, client, fetchKey]);
1979
+ return react.useMemo(
1980
+ () => ({
1981
+ data,
1982
+ isLoading,
1983
+ error,
1984
+ refetch
1985
+ }),
1986
+ [data, isLoading, error, refetch]
1987
+ );
1988
+ }
1989
+
1990
+ exports.EkuboProvider = EkuboProvider;
1991
+ exports.useEkuboClient = useEkuboClient;
1992
+ exports.useEkuboPriceHistory = useEkuboPriceHistory;
1993
+ exports.useEkuboPrices = useEkuboPrices;
1994
+ exports.useEkuboStats = useEkuboStats;
1995
+ exports.useEkuboSwap = useEkuboSwap;
1996
+ exports.useEkuboTokens = useEkuboTokens;
1997
+ exports.useOptionalEkuboClient = useOptionalEkuboClient;
1998
+ //# sourceMappingURL=react.cjs.map
1999
+ //# sourceMappingURL=react.cjs.map