@alcorexchange/alcor-swap-sdk 1.1.3 → 1.1.5

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.
@@ -217,5 +217,7 @@ export declare class Trade<TInput extends Currency, TOutput extends Currency, TT
217
217
  static bestTradeWithSplit<TInput extends Currency, TOutput extends Currency>(_routes: Route<TInput, TOutput>[], amount: CurrencyAmount<Currency>, percents: number[], tradeType: TradeType, swapConfig?: {
218
218
  minSplits: number;
219
219
  maxSplits: number;
220
+ branchFactor?: number;
221
+ candidateLimit?: number;
220
222
  }): Trade<Currency, Currency, TradeType> | null;
221
223
  }
@@ -440,8 +440,14 @@ let Trade = exports.Trade = /*#__PURE__*/function () {
440
440
  value: function bestTradeExactIn(routes, currencyAmountIn, maxNumResults = 1) {
441
441
  (0, _tinyInvariant.default)(routes.length > 0, 'ROUTES');
442
442
 
443
- // Pre-filter: remove routes with zero-liquidity pools
444
- const validRoutes = routes.filter(route => route.pools.every(pool => pool.active && pool.liquidity > _internalConstants.ZERO));
443
+ // Pre-filter: drop routes touching empty pools (no positions at all).
444
+ // Use ticks presence, NOT current in-range liquidity: a pool can have
445
+ // liquidity === 0 at the current tick while all positions are out of range,
446
+ // yet still be tradeable (the swap crosses the gap into a position).
447
+ const validRoutes = routes.filter(route => route.pools.every(pool => {
448
+ var _ticks$length, _pool$tickDataProvide;
449
+ return pool.active && ((_ticks$length = (_pool$tickDataProvide = pool.tickDataProvider) === null || _pool$tickDataProvide === void 0 || (_pool$tickDataProvide = _pool$tickDataProvide.ticks) === null || _pool$tickDataProvide === void 0 ? void 0 : _pool$tickDataProvide.length) !== null && _ticks$length !== void 0 ? _ticks$length : 0) > 0;
450
+ }));
445
451
 
446
452
  // Helper: compute min liquidity (no overflow)
447
453
  const getMinLiquidity = route => {
@@ -493,8 +499,14 @@ let Trade = exports.Trade = /*#__PURE__*/function () {
493
499
  value: function bestTradeExactOut(routes, currencyAmountOut, maxNumResults = 1) {
494
500
  (0, _tinyInvariant.default)(routes.length > 0, 'ROUTES');
495
501
 
496
- // Pre-filter: remove routes with zero-liquidity pools
497
- const validRoutes = routes.filter(route => route.pools.every(pool => pool.active && pool.liquidity > _internalConstants.ZERO));
502
+ // Pre-filter: drop routes touching empty pools (no positions at all).
503
+ // Use ticks presence, NOT current in-range liquidity: a pool can have
504
+ // liquidity === 0 at the current tick while all positions are out of range,
505
+ // yet still be tradeable (the swap crosses the gap into a position).
506
+ const validRoutes = routes.filter(route => route.pools.every(pool => {
507
+ var _ticks$length2, _pool$tickDataProvide2;
508
+ return pool.active && ((_ticks$length2 = (_pool$tickDataProvide2 = pool.tickDataProvider) === null || _pool$tickDataProvide2 === void 0 || (_pool$tickDataProvide2 = _pool$tickDataProvide2.ticks) === null || _pool$tickDataProvide2 === void 0 ? void 0 : _pool$tickDataProvide2.length) !== null && _ticks$length2 !== void 0 ? _ticks$length2 : 0) > 0;
509
+ }));
498
510
 
499
511
  // Helper: compute min liquidity (no overflow)
500
512
  const getMinLiquidity = route => {
@@ -570,8 +582,14 @@ let Trade = exports.Trade = /*#__PURE__*/function () {
570
582
  (0, _tinyInvariant.default)(_routes.length > 0, 'ROUTES');
571
583
  (0, _tinyInvariant.default)(percents.length > 0, 'PERCENTS');
572
584
 
573
- // Pre-filter: remove routes with zero-liquidity or inactive pools
574
- const validRoutes = _routes.filter(route => route.pools.every(pool => pool.active && pool.liquidity > _internalConstants.ZERO));
585
+ // Pre-filter: drop routes touching empty pools (no positions at all).
586
+ // Use ticks presence, NOT current in-range liquidity: a pool can have
587
+ // liquidity === 0 at the current tick while all positions are out of range,
588
+ // yet still be tradeable (the swap crosses the gap into a position).
589
+ const validRoutes = _routes.filter(route => route.pools.every(pool => {
590
+ var _ticks$length3, _pool$tickDataProvide3;
591
+ return pool.active && ((_ticks$length3 = (_pool$tickDataProvide3 = pool.tickDataProvider) === null || _pool$tickDataProvide3 === void 0 || (_pool$tickDataProvide3 = _pool$tickDataProvide3.ticks) === null || _pool$tickDataProvide3 === void 0 ? void 0 : _pool$tickDataProvide3.length) !== null && _ticks$length3 !== void 0 ? _ticks$length3 : 0) > 0;
592
+ }));
575
593
 
576
594
  // Helper: compute min liquidity for a route (no overflow)
577
595
  const getMinLiquidity = route => {
@@ -604,23 +622,46 @@ let Trade = exports.Trade = /*#__PURE__*/function () {
604
622
  for (const percent of percents) {
605
623
  percentToAmount.set(percent, amount.multiply(percent).divide(100));
606
624
  }
607
-
608
- // Используем Map вместо объекта для лучшей производительности
609
- const percentToTrades = new Map();
625
+ const quoteRoute = (route, splitAmount, percent) => {
626
+ const amounts = new Array(route.tokenPath.length);
627
+ let inputAmount;
628
+ let outputAmount;
629
+ if (tradeType === _internalConstants.TradeType.EXACT_INPUT) {
630
+ amounts[0] = splitAmount;
631
+ for (let i = 0; i < route.tokenPath.length - 1; i++) {
632
+ amounts[i + 1] = route.pools[i].getOutputAmount(amounts[i]);
633
+ }
634
+ inputAmount = splitAmount;
635
+ outputAmount = amounts[amounts.length - 1];
636
+ } else {
637
+ amounts[amounts.length - 1] = splitAmount;
638
+ for (let i = route.tokenPath.length - 1; i > 0; i--) {
639
+ amounts[i - 1] = route.pools[i - 1].getInputAmount(amounts[i]);
640
+ }
641
+ inputAmount = amounts[0];
642
+ outputAmount = splitAmount;
643
+ }
644
+ if (!outputAmount.greaterThan(0)) {
645
+ return null;
646
+ }
647
+ return {
648
+ percent,
649
+ route: route,
650
+ inputAmount,
651
+ outputAmount
652
+ };
653
+ };
654
+ const percentToQuotes = {};
610
655
  for (const percent of percents) {
611
- percentToTrades.set(percent, []);
656
+ percentToQuotes[percent] = [];
612
657
  }
613
-
614
- // Оптимизируем внутренний цикл - группируем вычисления по маршрутам
615
658
  for (const route of validRoutes) {
616
659
  for (const percent of percents) {
617
660
  const splitAmount = percentToAmount.get(percent);
618
661
  try {
619
- const trade = Trade.fromRoute(route, splitAmount, tradeType, percent);
620
-
621
- // Only check outputAmount > 0, skip expensive priceImpact calculation
622
- if (trade.outputAmount.greaterThan(0)) {
623
- percentToTrades.get(percent).push(trade);
662
+ const quote = quoteRoute(route, splitAmount, percent);
663
+ if (quote) {
664
+ percentToQuotes[percent].push(quote);
624
665
  }
625
666
  } catch (error) {
626
667
  if (error.isInsufficientReservesError || error.isInsufficientInputAmountError) {
@@ -630,25 +671,19 @@ let Trade = exports.Trade = /*#__PURE__*/function () {
630
671
  }
631
672
  }
632
673
  }
633
-
634
- // Преобразуем Map обратно в объект для совместимости с getBestSwapRoute
635
- const percentToTradesObj = {};
636
- percentToTrades.forEach((trades, percent) => {
637
- percentToTradesObj[percent] = trades;
638
- });
639
- const bestTrades = (0, _getBestSwapRoute.getBestSwapRoute)(tradeType, percentToTradesObj, percents, swapConfig);
640
- if (!bestTrades) return null;
641
- const routes = bestTrades.map(({
674
+ const bestQuotes = (0, _getBestSwapRoute.getBestSwapRoute)(tradeType, percentToQuotes, percents, swapConfig);
675
+ if (!bestQuotes) return null;
676
+ const routes = bestQuotes.map(({
642
677
  inputAmount,
643
678
  outputAmount,
644
679
  route,
645
- swaps
680
+ percent
646
681
  }) => {
647
682
  return {
648
683
  inputAmount,
649
684
  outputAmount,
650
685
  route,
651
- percent: swaps[0].percent
686
+ percent
652
687
  };
653
688
  });
654
689
 
@@ -431,8 +431,14 @@ export let Trade = /*#__PURE__*/function () {
431
431
  value: function bestTradeExactIn(routes, currencyAmountIn, maxNumResults = 1) {
432
432
  invariant(routes.length > 0, 'ROUTES');
433
433
 
434
- // Pre-filter: remove routes with zero-liquidity pools
435
- const validRoutes = routes.filter(route => route.pools.every(pool => pool.active && pool.liquidity > ZERO));
434
+ // Pre-filter: drop routes touching empty pools (no positions at all).
435
+ // Use ticks presence, NOT current in-range liquidity: a pool can have
436
+ // liquidity === 0 at the current tick while all positions are out of range,
437
+ // yet still be tradeable (the swap crosses the gap into a position).
438
+ const validRoutes = routes.filter(route => route.pools.every(pool => {
439
+ var _ticks$length, _pool$tickDataProvide;
440
+ return pool.active && ((_ticks$length = (_pool$tickDataProvide = pool.tickDataProvider) === null || _pool$tickDataProvide === void 0 || (_pool$tickDataProvide = _pool$tickDataProvide.ticks) === null || _pool$tickDataProvide === void 0 ? void 0 : _pool$tickDataProvide.length) !== null && _ticks$length !== void 0 ? _ticks$length : 0) > 0;
441
+ }));
436
442
 
437
443
  // Helper: compute min liquidity (no overflow)
438
444
  const getMinLiquidity = route => {
@@ -484,8 +490,14 @@ export let Trade = /*#__PURE__*/function () {
484
490
  value: function bestTradeExactOut(routes, currencyAmountOut, maxNumResults = 1) {
485
491
  invariant(routes.length > 0, 'ROUTES');
486
492
 
487
- // Pre-filter: remove routes with zero-liquidity pools
488
- const validRoutes = routes.filter(route => route.pools.every(pool => pool.active && pool.liquidity > ZERO));
493
+ // Pre-filter: drop routes touching empty pools (no positions at all).
494
+ // Use ticks presence, NOT current in-range liquidity: a pool can have
495
+ // liquidity === 0 at the current tick while all positions are out of range,
496
+ // yet still be tradeable (the swap crosses the gap into a position).
497
+ const validRoutes = routes.filter(route => route.pools.every(pool => {
498
+ var _ticks$length2, _pool$tickDataProvide2;
499
+ return pool.active && ((_ticks$length2 = (_pool$tickDataProvide2 = pool.tickDataProvider) === null || _pool$tickDataProvide2 === void 0 || (_pool$tickDataProvide2 = _pool$tickDataProvide2.ticks) === null || _pool$tickDataProvide2 === void 0 ? void 0 : _pool$tickDataProvide2.length) !== null && _ticks$length2 !== void 0 ? _ticks$length2 : 0) > 0;
500
+ }));
489
501
 
490
502
  // Helper: compute min liquidity (no overflow)
491
503
  const getMinLiquidity = route => {
@@ -561,8 +573,14 @@ export let Trade = /*#__PURE__*/function () {
561
573
  invariant(_routes.length > 0, 'ROUTES');
562
574
  invariant(percents.length > 0, 'PERCENTS');
563
575
 
564
- // Pre-filter: remove routes with zero-liquidity or inactive pools
565
- const validRoutes = _routes.filter(route => route.pools.every(pool => pool.active && pool.liquidity > ZERO));
576
+ // Pre-filter: drop routes touching empty pools (no positions at all).
577
+ // Use ticks presence, NOT current in-range liquidity: a pool can have
578
+ // liquidity === 0 at the current tick while all positions are out of range,
579
+ // yet still be tradeable (the swap crosses the gap into a position).
580
+ const validRoutes = _routes.filter(route => route.pools.every(pool => {
581
+ var _ticks$length3, _pool$tickDataProvide3;
582
+ return pool.active && ((_ticks$length3 = (_pool$tickDataProvide3 = pool.tickDataProvider) === null || _pool$tickDataProvide3 === void 0 || (_pool$tickDataProvide3 = _pool$tickDataProvide3.ticks) === null || _pool$tickDataProvide3 === void 0 ? void 0 : _pool$tickDataProvide3.length) !== null && _ticks$length3 !== void 0 ? _ticks$length3 : 0) > 0;
583
+ }));
566
584
 
567
585
  // Helper: compute min liquidity for a route (no overflow)
568
586
  const getMinLiquidity = route => {
@@ -595,23 +613,46 @@ export let Trade = /*#__PURE__*/function () {
595
613
  for (const percent of percents) {
596
614
  percentToAmount.set(percent, amount.multiply(percent).divide(100));
597
615
  }
598
-
599
- // Используем Map вместо объекта для лучшей производительности
600
- const percentToTrades = new Map();
616
+ const quoteRoute = (route, splitAmount, percent) => {
617
+ const amounts = new Array(route.tokenPath.length);
618
+ let inputAmount;
619
+ let outputAmount;
620
+ if (tradeType === TradeType.EXACT_INPUT) {
621
+ amounts[0] = splitAmount;
622
+ for (let i = 0; i < route.tokenPath.length - 1; i++) {
623
+ amounts[i + 1] = route.pools[i].getOutputAmount(amounts[i]);
624
+ }
625
+ inputAmount = splitAmount;
626
+ outputAmount = amounts[amounts.length - 1];
627
+ } else {
628
+ amounts[amounts.length - 1] = splitAmount;
629
+ for (let i = route.tokenPath.length - 1; i > 0; i--) {
630
+ amounts[i - 1] = route.pools[i - 1].getInputAmount(amounts[i]);
631
+ }
632
+ inputAmount = amounts[0];
633
+ outputAmount = splitAmount;
634
+ }
635
+ if (!outputAmount.greaterThan(0)) {
636
+ return null;
637
+ }
638
+ return {
639
+ percent,
640
+ route: route,
641
+ inputAmount,
642
+ outputAmount
643
+ };
644
+ };
645
+ const percentToQuotes = {};
601
646
  for (const percent of percents) {
602
- percentToTrades.set(percent, []);
647
+ percentToQuotes[percent] = [];
603
648
  }
604
-
605
- // Оптимизируем внутренний цикл - группируем вычисления по маршрутам
606
649
  for (const route of validRoutes) {
607
650
  for (const percent of percents) {
608
651
  const splitAmount = percentToAmount.get(percent);
609
652
  try {
610
- const trade = Trade.fromRoute(route, splitAmount, tradeType, percent);
611
-
612
- // Only check outputAmount > 0, skip expensive priceImpact calculation
613
- if (trade.outputAmount.greaterThan(0)) {
614
- percentToTrades.get(percent).push(trade);
653
+ const quote = quoteRoute(route, splitAmount, percent);
654
+ if (quote) {
655
+ percentToQuotes[percent].push(quote);
615
656
  }
616
657
  } catch (error) {
617
658
  if (error.isInsufficientReservesError || error.isInsufficientInputAmountError) {
@@ -621,25 +662,19 @@ export let Trade = /*#__PURE__*/function () {
621
662
  }
622
663
  }
623
664
  }
624
-
625
- // Преобразуем Map обратно в объект для совместимости с getBestSwapRoute
626
- const percentToTradesObj = {};
627
- percentToTrades.forEach((trades, percent) => {
628
- percentToTradesObj[percent] = trades;
629
- });
630
- const bestTrades = getBestSwapRoute(tradeType, percentToTradesObj, percents, swapConfig);
631
- if (!bestTrades) return null;
632
- const routes = bestTrades.map(({
665
+ const bestQuotes = getBestSwapRoute(tradeType, percentToQuotes, percents, swapConfig);
666
+ if (!bestQuotes) return null;
667
+ const routes = bestQuotes.map(({
633
668
  inputAmount,
634
669
  outputAmount,
635
670
  route,
636
- swaps
671
+ percent
637
672
  }) => {
638
673
  return {
639
674
  inputAmount,
640
675
  outputAmount,
641
676
  route,
642
- percent: swaps[0].percent
677
+ percent
643
678
  };
644
679
  });
645
680
 
@@ -1,106 +1,78 @@
1
1
  import Queue from 'mnemonist/queue';
2
- import FixedReverseHeap from 'mnemonist/fixed-reverse-heap';
3
2
  import { TradeType } from '../internalConstants';
4
3
  export function getBestSwapRoute(routeType, percentToQuotes, percents, swapRouteConfig = {
5
4
  minSplits: 1,
6
5
  maxSplits: 8
7
6
  }) {
8
- // Извлекаем уникальные пулы из всех маршрутов
9
- const allPools = [...new Set(Object.values(percentToQuotes).flatMap(routes => routes.flatMap(r => r.route.pools)))];
7
+ const {
8
+ minSplits,
9
+ maxSplits,
10
+ branchFactor = 1,
11
+ candidateLimit = 0
12
+ } = swapRouteConfig;
13
+ const branchWidth = Math.max(1, branchFactor);
14
+ const quoteCompFn = routeType === TradeType.EXACT_INPUT ? (a, b) => a.greaterThan(b) : (a, b) => a.lessThan(b);
15
+ const quoteOf = routeType === TradeType.EXACT_INPUT ? q => q.outputAmount : q => q.inputAmount;
10
16
 
11
- // Создаем битовую карту для пулов (bigint для поддержки >31 пулов)
17
+ // Build pool bit ids without intermediate arrays.
12
18
  const poolToBit = new Map();
13
19
  let bitCounter = BigInt(0);
14
- for (const pool of allPools) {
15
- poolToBit.set(pool.id, BigInt(1) << bitCounter++);
20
+ for (const quotes of Object.values(percentToQuotes)) {
21
+ for (const quote of quotes) {
22
+ for (const pool of quote.route.pools) {
23
+ if (!poolToBit.has(pool.id)) {
24
+ poolToBit.set(pool.id, BigInt(1) << bitCounter);
25
+ bitCounter += BigInt(1);
26
+ }
27
+ }
28
+ }
16
29
  }
17
-
18
- // Предвычисляем маски для всех маршрутов
19
30
  const routeToMask = new Map();
20
- for (const routes of Object.values(percentToQuotes)) {
21
- for (const route of routes) {
31
+ for (const quotes of Object.values(percentToQuotes)) {
32
+ for (const quote of quotes) {
22
33
  let mask = BigInt(0);
23
- for (const pool of route.route.pools) {
34
+ for (const pool of quote.route.pools) {
24
35
  mask |= poolToBit.get(pool.id);
25
36
  }
26
- routeToMask.set(route, mask);
37
+ routeToMask.set(quote, mask);
27
38
  }
28
39
  }
29
-
30
- // Сортируем маршруты для каждого процента
31
40
  const percentToSortedQuotes = {};
32
41
  for (const percent in percentToQuotes) {
33
- percentToSortedQuotes[percent] = percentToQuotes[percent].sort((a, b) => routeType === TradeType.EXACT_INPUT ? a.outputAmount.greaterThan(b.outputAmount) ? -1 : 1 : a.inputAmount.lessThan(b.inputAmount) ? -1 : 1);
42
+ const sorted = percentToQuotes[percent].sort((a, b) => {
43
+ const qa = quoteOf(a);
44
+ const qb = quoteOf(b);
45
+ return quoteCompFn(qa, qb) ? -1 : 1;
46
+ });
47
+ percentToSortedQuotes[percent] = candidateLimit > 0 && sorted.length > candidateLimit ? sorted.slice(0, candidateLimit) : sorted;
34
48
  }
35
-
36
- // Функция сравнения для типа торговли
37
- const quoteCompFn = routeType === TradeType.EXACT_INPUT ? (a, b) => a.greaterThan(b) : (a, b) => a.lessThan(b);
38
-
39
- // Функция суммирования CurrencyAmount
40
- const sumFn = currencyAmounts => {
41
- let sum = currencyAmounts[0];
42
- for (let i = 1; i < currencyAmounts.length; i++) {
43
- sum = sum.add(currencyAmounts[i]);
44
- }
45
- return sum;
46
- };
47
49
  let bestQuote;
48
50
  let bestSwap;
49
-
50
- // Храним лучшие маршруты для каждого уровня разбиения (максимум 3)
51
- const bestSwapsPerSplit = new FixedReverseHeap(Array, (a, b) => quoteCompFn(a.quote, b.quote) ? -1 : 1, 3);
52
- const {
53
- minSplits,
54
- maxSplits
55
- } = swapRouteConfig;
56
-
57
- // Проверяем наличие маршрута для 100% и инициализируем начальные данные
58
- if (!percentToSortedQuotes[100] || percentToSortedQuotes[100].length === 0 || minSplits > 1) {
51
+ if ((!percentToSortedQuotes[100] || percentToSortedQuotes[100].length === 0) && minSplits <= 1) {
59
52
  console.log('Did not find a valid route without any splits. Continuing search anyway.');
60
- } else {
61
- bestQuote = percentToSortedQuotes[100][0].outputAmount;
53
+ } else if (minSplits <= 1 && percentToSortedQuotes[100] && percentToSortedQuotes[100][0]) {
62
54
  bestSwap = [percentToSortedQuotes[100][0]];
63
- for (const routeWithQuote of percentToSortedQuotes[100].slice(0, 5)) {
64
- bestSwapsPerSplit.push({
65
- quote: routeWithQuote.outputAmount,
66
- routes: [routeWithQuote]
67
- });
68
- }
55
+ bestQuote = quoteOf(percentToSortedQuotes[100][0]);
69
56
  }
70
-
71
- // Очередь для обработки комбинаций маршрутов (с usedMask для быстрой проверки overlap)
72
57
  const queue = new Queue();
73
58
  if (percents.length === 0) return null;
74
-
75
- // Инициализируем очередь с топ-2 маршрутами для каждого процента
76
59
  for (let i = percents.length - 1; i >= 0; i--) {
77
60
  const percent = percents[i];
78
- if (!percentToSortedQuotes[percent] || percentToSortedQuotes[percent].length === 0) continue;
79
- const topRoutes = percentToSortedQuotes[percent].slice(0, 2);
80
- if (topRoutes[0]) {
61
+ const candidates = percentToSortedQuotes[percent];
62
+ if (!candidates || candidates.length === 0) continue;
63
+ const seeds = candidates.slice(0, branchWidth);
64
+ for (const seed of seeds) {
81
65
  queue.enqueue({
82
- curRoutes: [topRoutes[0]],
66
+ curRoutes: [seed],
83
67
  percentIndex: i,
84
68
  remainingPercent: 100 - percent,
85
- usedMask: routeToMask.get(topRoutes[0]),
86
- special: false
87
- });
88
- }
89
- if (topRoutes[1]) {
90
- queue.enqueue({
91
- curRoutes: [topRoutes[1]],
92
- percentIndex: i,
93
- remainingPercent: 100 - percent,
94
- usedMask: routeToMask.get(topRoutes[1]),
95
- special: true
69
+ usedMask: routeToMask.get(seed),
70
+ quoteSoFar: quoteOf(seed)
96
71
  });
97
72
  }
98
73
  }
99
74
  let splits = 1;
100
-
101
- // Основной цикл поиска лучших маршрутов
102
75
  while (queue.size > 0) {
103
- bestSwapsPerSplit.clear();
104
76
  let layer = queue.size;
105
77
  splits++;
106
78
  if (splits >= 3 && bestSwap && bestSwap.length < splits - 1) {
@@ -116,38 +88,37 @@ export function getBestSwapRoute(routeType, percentToQuotes, percents, swapRoute
116
88
  curRoutes,
117
89
  percentIndex,
118
90
  usedMask,
119
- special
91
+ quoteSoFar
120
92
  } = queue.dequeue();
121
93
  for (let i = percentIndex; i >= 0; i--) {
122
- const percentA = percents[i];
123
- if (percentA > remainingPercent) continue;
124
- if (!percentToSortedQuotes[percentA] || percentToSortedQuotes[percentA].length === 0) continue;
125
- const candidateRoutesA = percentToSortedQuotes[percentA];
126
- const routeWithQuoteA = findFirstRouteNotUsingUsedPools(usedMask, candidateRoutesA, routeToMask);
127
- if (!routeWithQuoteA) continue;
128
- const remainingPercentNew = remainingPercent - percentA;
129
- const curRoutesNew = curRoutes.slice();
130
- curRoutesNew.push(routeWithQuoteA);
131
- const usedMaskNew = usedMask | routeToMask.get(routeWithQuoteA);
132
- if (remainingPercentNew === 0 && splits >= minSplits) {
133
- const quotesNew = curRoutesNew.map(r => r.outputAmount);
134
- const quoteNew = sumFn(quotesNew);
135
- bestSwapsPerSplit.push({
136
- quote: quoteNew,
137
- routes: curRoutesNew
138
- });
139
- if (!bestQuote || quoteCompFn(quoteNew, bestQuote)) {
140
- bestQuote = quoteNew;
141
- bestSwap = curRoutesNew;
94
+ const percent = percents[i];
95
+ if (percent > remainingPercent) continue;
96
+ const candidates = percentToSortedQuotes[percent];
97
+ if (!candidates || candidates.length === 0) continue;
98
+ const routeCandidates = findRoutesNotUsingUsedPools(usedMask, candidates, routeToMask, branchWidth);
99
+ if (routeCandidates.length === 0) continue;
100
+ for (const candidate of routeCandidates) {
101
+ const remainingPercentNew = remainingPercent - percent;
102
+ const usedMaskNew = usedMask | routeToMask.get(candidate);
103
+ const quoteNew = quoteSoFar.add(quoteOf(candidate));
104
+ if (remainingPercentNew === 0 && splits >= minSplits) {
105
+ const curRoutesNew = curRoutes.slice();
106
+ curRoutesNew.push(candidate);
107
+ if (!bestQuote || quoteCompFn(quoteNew, bestQuote)) {
108
+ bestQuote = quoteNew;
109
+ bestSwap = curRoutesNew;
110
+ }
111
+ } else {
112
+ const curRoutesNew = curRoutes.slice();
113
+ curRoutesNew.push(candidate);
114
+ queue.enqueue({
115
+ curRoutes: curRoutesNew,
116
+ remainingPercent: remainingPercentNew,
117
+ percentIndex: i,
118
+ usedMask: usedMaskNew,
119
+ quoteSoFar: quoteNew
120
+ });
142
121
  }
143
- } else {
144
- queue.enqueue({
145
- curRoutes: curRoutesNew,
146
- remainingPercent: remainingPercentNew,
147
- percentIndex: i,
148
- usedMask: usedMaskNew,
149
- special
150
- });
151
122
  }
152
123
  }
153
124
  }
@@ -156,18 +127,16 @@ export function getBestSwapRoute(routeType, percentToQuotes, percents, swapRoute
156
127
  console.log('Could not find a valid swap');
157
128
  return null;
158
129
  }
159
- const quote = sumFn(bestSwap.map(routeWithValidQuote => routeWithValidQuote.outputAmount));
160
- const routeWithQuotes = bestSwap.sort((routeAmountA, routeAmountB) => routeAmountB.outputAmount.greaterThan(routeAmountA.outputAmount) ? 1 : -1);
161
- return routeWithQuotes;
130
+ return bestSwap;
162
131
  }
163
-
164
- // Вспомогательная функция для поиска маршрута без пересекающихся пулов
165
- const findFirstRouteNotUsingUsedPools = (usedMask, candidateRoutes, routeToMask) => {
132
+ const findRoutesNotUsingUsedPools = (usedMask, candidateRoutes, routeToMask, limit) => {
133
+ const result = [];
166
134
  for (const candidate of candidateRoutes) {
167
135
  const candidateMask = routeToMask.get(candidate);
168
136
  if ((candidateMask & usedMask) === BigInt(0)) {
169
- return candidate;
137
+ result.push(candidate);
138
+ if (result.length >= limit) return result;
170
139
  }
171
140
  }
172
- return null;
141
+ return result;
173
142
  };
@@ -7,6 +7,7 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
7
7
  import { Trade } from '../entities';
8
8
  import { CurrencyAmount } from '../entities/fractions';
9
9
  import { TradeType } from '../internalConstants';
10
+ import { getBestSwapRoute } from './getBestSwapRoute';
10
11
 
11
12
  // WASM module - lazy loaded
12
13
  let wasmModule = null;
@@ -146,6 +147,8 @@ export let WASMTradeCalculator = /*#__PURE__*/function () {
146
147
  amountOut: CurrencyAmount.fromRawAmount(route.output, BigInt(result.amountOut)),
147
148
  priceImpact: result.priceImpact
148
149
  });
150
+ } else {
151
+ trades.push(null);
149
152
  }
150
153
  }
151
154
  }
@@ -212,37 +215,34 @@ export async function bestTradeWithSplitWASM(routes, amount, percents, tradeType
212
215
  const allTrades = calculator.calculateTradesBatch(routes, splitAmounts);
213
216
 
214
217
  // Group trades by percent
215
- const tradesByPercent = {};
216
- let tradeIdx = 0;
218
+ const quotesByPercent = {};
217
219
  for (let i = 0; i < percents.length; i++) {
218
220
  const percent = percents[i];
219
- tradesByPercent[percent] = [];
220
- for (const route of routes) {
221
- const tradeDat = allTrades[tradeIdx++];
221
+ quotesByPercent[percent] = [];
222
+ }
223
+ const amountsPerRoute = splitAmounts.length;
224
+ for (let routeIndex = 0; routeIndex < routes.length; routeIndex++) {
225
+ for (let percentIndex = 0; percentIndex < percents.length; percentIndex++) {
226
+ const flatIndex = routeIndex * amountsPerRoute + percentIndex;
227
+ const tradeDat = allTrades[flatIndex];
222
228
  if (tradeDat) {
223
- const trade = Trade.createUncheckedTrade({
229
+ const percent = percents[percentIndex];
230
+ quotesByPercent[percent].push({
224
231
  route: tradeDat.route,
225
232
  inputAmount: tradeDat.amountIn,
226
233
  outputAmount: tradeDat.amountOut,
227
- tradeType,
228
234
  percent
229
235
  });
230
- tradesByPercent[percent].push(trade);
231
236
  }
232
237
  }
233
238
  }
234
-
235
- // Use existing getBestSwapRoute logic
236
- const {
237
- getBestSwapRoute
238
- } = require('./getBestSwapRoute');
239
- const bestTrades = getBestSwapRoute(tradeType, tradesByPercent, percents, swapConfig);
239
+ const bestTrades = getBestSwapRoute(tradeType, quotesByPercent, percents, swapConfig);
240
240
  if (!bestTrades) return null;
241
241
  const routeData = bestTrades.map(trade => ({
242
242
  inputAmount: trade.inputAmount,
243
243
  outputAmount: trade.outputAmount,
244
244
  route: trade.route,
245
- percent: trade.swaps[0].percent
245
+ percent: trade.percent
246
246
  }));
247
247
  return Trade.createUncheckedTradeWithMultipleRoutes({
248
248
  routes: routeData,
@@ -1,9 +1,20 @@
1
1
  import { Currency } from '../entities/currency';
2
- import { Trade } from '../entities/trade';
2
+ import { Route } from '../entities/route';
3
+ import { CurrencyAmount } from '../entities/fractions';
3
4
  import { TradeType } from '../internalConstants';
4
- export declare function getBestSwapRoute(routeType: TradeType, percentToQuotes: {
5
- [percent: number]: Trade<Currency, Currency, TradeType>[];
6
- }, percents: number[], swapRouteConfig?: {
5
+ export interface SplitRouteQuote {
6
+ percent: number;
7
+ route: Route<Currency, Currency>;
8
+ inputAmount: CurrencyAmount<Currency>;
9
+ outputAmount: CurrencyAmount<Currency>;
10
+ }
11
+ interface SwapRouteConfig {
7
12
  minSplits: number;
8
13
  maxSplits: number;
9
- }): Trade<Currency, Currency, TradeType>[] | null;
14
+ branchFactor?: number;
15
+ candidateLimit?: number;
16
+ }
17
+ export declare function getBestSwapRoute(routeType: TradeType, percentToQuotes: {
18
+ [percent: number]: SplitRouteQuote[];
19
+ }, percents: number[], swapRouteConfig?: SwapRouteConfig): SplitRouteQuote[] | null;
20
+ export {};
@@ -5,109 +5,81 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.getBestSwapRoute = getBestSwapRoute;
7
7
  var _queue = _interopRequireDefault(require("mnemonist/queue"));
8
- var _fixedReverseHeap = _interopRequireDefault(require("mnemonist/fixed-reverse-heap"));
9
8
  var _internalConstants = require("../internalConstants");
10
9
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
10
  function getBestSwapRoute(routeType, percentToQuotes, percents, swapRouteConfig = {
12
11
  minSplits: 1,
13
12
  maxSplits: 8
14
13
  }) {
15
- // Извлекаем уникальные пулы из всех маршрутов
16
- const allPools = [...new Set(Object.values(percentToQuotes).flatMap(routes => routes.flatMap(r => r.route.pools)))];
14
+ const {
15
+ minSplits,
16
+ maxSplits,
17
+ branchFactor = 1,
18
+ candidateLimit = 0
19
+ } = swapRouteConfig;
20
+ const branchWidth = Math.max(1, branchFactor);
21
+ const quoteCompFn = routeType === _internalConstants.TradeType.EXACT_INPUT ? (a, b) => a.greaterThan(b) : (a, b) => a.lessThan(b);
22
+ const quoteOf = routeType === _internalConstants.TradeType.EXACT_INPUT ? q => q.outputAmount : q => q.inputAmount;
17
23
 
18
- // Создаем битовую карту для пулов (bigint для поддержки >31 пулов)
24
+ // Build pool bit ids without intermediate arrays.
19
25
  const poolToBit = new Map();
20
26
  let bitCounter = BigInt(0);
21
- for (const pool of allPools) {
22
- poolToBit.set(pool.id, BigInt(1) << bitCounter++);
27
+ for (const quotes of Object.values(percentToQuotes)) {
28
+ for (const quote of quotes) {
29
+ for (const pool of quote.route.pools) {
30
+ if (!poolToBit.has(pool.id)) {
31
+ poolToBit.set(pool.id, BigInt(1) << bitCounter);
32
+ bitCounter += BigInt(1);
33
+ }
34
+ }
35
+ }
23
36
  }
24
-
25
- // Предвычисляем маски для всех маршрутов
26
37
  const routeToMask = new Map();
27
- for (const routes of Object.values(percentToQuotes)) {
28
- for (const route of routes) {
38
+ for (const quotes of Object.values(percentToQuotes)) {
39
+ for (const quote of quotes) {
29
40
  let mask = BigInt(0);
30
- for (const pool of route.route.pools) {
41
+ for (const pool of quote.route.pools) {
31
42
  mask |= poolToBit.get(pool.id);
32
43
  }
33
- routeToMask.set(route, mask);
44
+ routeToMask.set(quote, mask);
34
45
  }
35
46
  }
36
-
37
- // Сортируем маршруты для каждого процента
38
47
  const percentToSortedQuotes = {};
39
48
  for (const percent in percentToQuotes) {
40
- percentToSortedQuotes[percent] = percentToQuotes[percent].sort((a, b) => routeType === _internalConstants.TradeType.EXACT_INPUT ? a.outputAmount.greaterThan(b.outputAmount) ? -1 : 1 : a.inputAmount.lessThan(b.inputAmount) ? -1 : 1);
49
+ const sorted = percentToQuotes[percent].sort((a, b) => {
50
+ const qa = quoteOf(a);
51
+ const qb = quoteOf(b);
52
+ return quoteCompFn(qa, qb) ? -1 : 1;
53
+ });
54
+ percentToSortedQuotes[percent] = candidateLimit > 0 && sorted.length > candidateLimit ? sorted.slice(0, candidateLimit) : sorted;
41
55
  }
42
-
43
- // Функция сравнения для типа торговли
44
- const quoteCompFn = routeType === _internalConstants.TradeType.EXACT_INPUT ? (a, b) => a.greaterThan(b) : (a, b) => a.lessThan(b);
45
-
46
- // Функция суммирования CurrencyAmount
47
- const sumFn = currencyAmounts => {
48
- let sum = currencyAmounts[0];
49
- for (let i = 1; i < currencyAmounts.length; i++) {
50
- sum = sum.add(currencyAmounts[i]);
51
- }
52
- return sum;
53
- };
54
56
  let bestQuote;
55
57
  let bestSwap;
56
-
57
- // Храним лучшие маршруты для каждого уровня разбиения (максимум 3)
58
- const bestSwapsPerSplit = new _fixedReverseHeap.default(Array, (a, b) => quoteCompFn(a.quote, b.quote) ? -1 : 1, 3);
59
- const {
60
- minSplits,
61
- maxSplits
62
- } = swapRouteConfig;
63
-
64
- // Проверяем наличие маршрута для 100% и инициализируем начальные данные
65
- if (!percentToSortedQuotes[100] || percentToSortedQuotes[100].length === 0 || minSplits > 1) {
58
+ if ((!percentToSortedQuotes[100] || percentToSortedQuotes[100].length === 0) && minSplits <= 1) {
66
59
  console.log('Did not find a valid route without any splits. Continuing search anyway.');
67
- } else {
68
- bestQuote = percentToSortedQuotes[100][0].outputAmount;
60
+ } else if (minSplits <= 1 && percentToSortedQuotes[100] && percentToSortedQuotes[100][0]) {
69
61
  bestSwap = [percentToSortedQuotes[100][0]];
70
- for (const routeWithQuote of percentToSortedQuotes[100].slice(0, 5)) {
71
- bestSwapsPerSplit.push({
72
- quote: routeWithQuote.outputAmount,
73
- routes: [routeWithQuote]
74
- });
75
- }
62
+ bestQuote = quoteOf(percentToSortedQuotes[100][0]);
76
63
  }
77
-
78
- // Очередь для обработки комбинаций маршрутов (с usedMask для быстрой проверки overlap)
79
64
  const queue = new _queue.default();
80
65
  if (percents.length === 0) return null;
81
-
82
- // Инициализируем очередь с топ-2 маршрутами для каждого процента
83
66
  for (let i = percents.length - 1; i >= 0; i--) {
84
67
  const percent = percents[i];
85
- if (!percentToSortedQuotes[percent] || percentToSortedQuotes[percent].length === 0) continue;
86
- const topRoutes = percentToSortedQuotes[percent].slice(0, 2);
87
- if (topRoutes[0]) {
68
+ const candidates = percentToSortedQuotes[percent];
69
+ if (!candidates || candidates.length === 0) continue;
70
+ const seeds = candidates.slice(0, branchWidth);
71
+ for (const seed of seeds) {
88
72
  queue.enqueue({
89
- curRoutes: [topRoutes[0]],
73
+ curRoutes: [seed],
90
74
  percentIndex: i,
91
75
  remainingPercent: 100 - percent,
92
- usedMask: routeToMask.get(topRoutes[0]),
93
- special: false
94
- });
95
- }
96
- if (topRoutes[1]) {
97
- queue.enqueue({
98
- curRoutes: [topRoutes[1]],
99
- percentIndex: i,
100
- remainingPercent: 100 - percent,
101
- usedMask: routeToMask.get(topRoutes[1]),
102
- special: true
76
+ usedMask: routeToMask.get(seed),
77
+ quoteSoFar: quoteOf(seed)
103
78
  });
104
79
  }
105
80
  }
106
81
  let splits = 1;
107
-
108
- // Основной цикл поиска лучших маршрутов
109
82
  while (queue.size > 0) {
110
- bestSwapsPerSplit.clear();
111
83
  let layer = queue.size;
112
84
  splits++;
113
85
  if (splits >= 3 && bestSwap && bestSwap.length < splits - 1) {
@@ -123,38 +95,37 @@ function getBestSwapRoute(routeType, percentToQuotes, percents, swapRouteConfig
123
95
  curRoutes,
124
96
  percentIndex,
125
97
  usedMask,
126
- special
98
+ quoteSoFar
127
99
  } = queue.dequeue();
128
100
  for (let i = percentIndex; i >= 0; i--) {
129
- const percentA = percents[i];
130
- if (percentA > remainingPercent) continue;
131
- if (!percentToSortedQuotes[percentA] || percentToSortedQuotes[percentA].length === 0) continue;
132
- const candidateRoutesA = percentToSortedQuotes[percentA];
133
- const routeWithQuoteA = findFirstRouteNotUsingUsedPools(usedMask, candidateRoutesA, routeToMask);
134
- if (!routeWithQuoteA) continue;
135
- const remainingPercentNew = remainingPercent - percentA;
136
- const curRoutesNew = curRoutes.slice();
137
- curRoutesNew.push(routeWithQuoteA);
138
- const usedMaskNew = usedMask | routeToMask.get(routeWithQuoteA);
139
- if (remainingPercentNew === 0 && splits >= minSplits) {
140
- const quotesNew = curRoutesNew.map(r => r.outputAmount);
141
- const quoteNew = sumFn(quotesNew);
142
- bestSwapsPerSplit.push({
143
- quote: quoteNew,
144
- routes: curRoutesNew
145
- });
146
- if (!bestQuote || quoteCompFn(quoteNew, bestQuote)) {
147
- bestQuote = quoteNew;
148
- bestSwap = curRoutesNew;
101
+ const percent = percents[i];
102
+ if (percent > remainingPercent) continue;
103
+ const candidates = percentToSortedQuotes[percent];
104
+ if (!candidates || candidates.length === 0) continue;
105
+ const routeCandidates = findRoutesNotUsingUsedPools(usedMask, candidates, routeToMask, branchWidth);
106
+ if (routeCandidates.length === 0) continue;
107
+ for (const candidate of routeCandidates) {
108
+ const remainingPercentNew = remainingPercent - percent;
109
+ const usedMaskNew = usedMask | routeToMask.get(candidate);
110
+ const quoteNew = quoteSoFar.add(quoteOf(candidate));
111
+ if (remainingPercentNew === 0 && splits >= minSplits) {
112
+ const curRoutesNew = curRoutes.slice();
113
+ curRoutesNew.push(candidate);
114
+ if (!bestQuote || quoteCompFn(quoteNew, bestQuote)) {
115
+ bestQuote = quoteNew;
116
+ bestSwap = curRoutesNew;
117
+ }
118
+ } else {
119
+ const curRoutesNew = curRoutes.slice();
120
+ curRoutesNew.push(candidate);
121
+ queue.enqueue({
122
+ curRoutes: curRoutesNew,
123
+ remainingPercent: remainingPercentNew,
124
+ percentIndex: i,
125
+ usedMask: usedMaskNew,
126
+ quoteSoFar: quoteNew
127
+ });
149
128
  }
150
- } else {
151
- queue.enqueue({
152
- curRoutes: curRoutesNew,
153
- remainingPercent: remainingPercentNew,
154
- percentIndex: i,
155
- usedMask: usedMaskNew,
156
- special
157
- });
158
129
  }
159
130
  }
160
131
  }
@@ -163,18 +134,16 @@ function getBestSwapRoute(routeType, percentToQuotes, percents, swapRouteConfig
163
134
  console.log('Could not find a valid swap');
164
135
  return null;
165
136
  }
166
- const quote = sumFn(bestSwap.map(routeWithValidQuote => routeWithValidQuote.outputAmount));
167
- const routeWithQuotes = bestSwap.sort((routeAmountA, routeAmountB) => routeAmountB.outputAmount.greaterThan(routeAmountA.outputAmount) ? 1 : -1);
168
- return routeWithQuotes;
137
+ return bestSwap;
169
138
  }
170
-
171
- // Вспомогательная функция для поиска маршрута без пересекающихся пулов
172
- const findFirstRouteNotUsingUsedPools = (usedMask, candidateRoutes, routeToMask) => {
139
+ const findRoutesNotUsingUsedPools = (usedMask, candidateRoutes, routeToMask, limit) => {
140
+ const result = [];
173
141
  for (const candidate of candidateRoutes) {
174
142
  const candidateMask = routeToMask.get(candidate);
175
143
  if ((candidateMask & usedMask) === BigInt(0)) {
176
- return candidate;
144
+ result.push(candidate);
145
+ if (result.length >= limit) return result;
177
146
  }
178
147
  }
179
- return null;
148
+ return result;
180
149
  };
@@ -26,7 +26,7 @@ export declare class WASMTradeCalculator {
26
26
  amountIn: CurrencyAmount<Token>;
27
27
  amountOut: CurrencyAmount<Token>;
28
28
  priceImpact: number;
29
- }>;
29
+ } | null>;
30
30
  /**
31
31
  * Clear all pools from memory
32
32
  */
@@ -9,6 +9,7 @@ exports.createTradeFromRouteWASM = createTradeFromRouteWASM;
9
9
  var _entities = require("../entities");
10
10
  var _fractions = require("../entities/fractions");
11
11
  var _internalConstants = require("../internalConstants");
12
+ var _getBestSwapRoute = require("./getBestSwapRoute");
12
13
  function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
13
14
  function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
14
15
  function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
@@ -153,6 +154,8 @@ let WASMTradeCalculator = exports.WASMTradeCalculator = /*#__PURE__*/function ()
153
154
  amountOut: _fractions.CurrencyAmount.fromRawAmount(route.output, BigInt(result.amountOut)),
154
155
  priceImpact: result.priceImpact
155
156
  });
157
+ } else {
158
+ trades.push(null);
156
159
  }
157
160
  }
158
161
  }
@@ -218,37 +221,34 @@ async function bestTradeWithSplitWASM(routes, amount, percents, tradeType, pools
218
221
  const allTrades = calculator.calculateTradesBatch(routes, splitAmounts);
219
222
 
220
223
  // Group trades by percent
221
- const tradesByPercent = {};
222
- let tradeIdx = 0;
224
+ const quotesByPercent = {};
223
225
  for (let i = 0; i < percents.length; i++) {
224
226
  const percent = percents[i];
225
- tradesByPercent[percent] = [];
226
- for (const route of routes) {
227
- const tradeDat = allTrades[tradeIdx++];
227
+ quotesByPercent[percent] = [];
228
+ }
229
+ const amountsPerRoute = splitAmounts.length;
230
+ for (let routeIndex = 0; routeIndex < routes.length; routeIndex++) {
231
+ for (let percentIndex = 0; percentIndex < percents.length; percentIndex++) {
232
+ const flatIndex = routeIndex * amountsPerRoute + percentIndex;
233
+ const tradeDat = allTrades[flatIndex];
228
234
  if (tradeDat) {
229
- const trade = _entities.Trade.createUncheckedTrade({
235
+ const percent = percents[percentIndex];
236
+ quotesByPercent[percent].push({
230
237
  route: tradeDat.route,
231
238
  inputAmount: tradeDat.amountIn,
232
239
  outputAmount: tradeDat.amountOut,
233
- tradeType,
234
240
  percent
235
241
  });
236
- tradesByPercent[percent].push(trade);
237
242
  }
238
243
  }
239
244
  }
240
-
241
- // Use existing getBestSwapRoute logic
242
- const {
243
- getBestSwapRoute
244
- } = require('./getBestSwapRoute');
245
- const bestTrades = getBestSwapRoute(tradeType, tradesByPercent, percents, swapConfig);
245
+ const bestTrades = (0, _getBestSwapRoute.getBestSwapRoute)(tradeType, quotesByPercent, percents, swapConfig);
246
246
  if (!bestTrades) return null;
247
247
  const routeData = bestTrades.map(trade => ({
248
248
  inputAmount: trade.inputAmount,
249
249
  outputAmount: trade.outputAmount,
250
250
  route: trade.route,
251
- percent: trade.swaps[0].percent
251
+ percent: trade.percent
252
252
  }));
253
253
  return _entities.Trade.createUncheckedTradeWithMultipleRoutes({
254
254
  routes: routeData,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alcorexchange/alcor-swap-sdk",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "module": "build/esm/index.js",