UNPKG

@alcorexchange/alcor-swap-sdk

Version:

## Installation ​​ **npm** ``` npm i @alcorexchange/alcor-swap-sdk ``` **yarn** ``` yarn add @alcorexchange/alcor-swap-sdk ``` ## Usage ### Import:

219 lines (183 loc) 7.52 kB
import Queue from 'mnemonist/queue'; import FixedReverseHeap from 'mnemonist/fixed-reverse-heap'; import { Currency } from '../entities/currency'; import { Trade } from '../entities/trade'; import { CurrencyAmount } from '../entities/fractions'; import { TradeType } from '../internalConstants'; export function getBestSwapRoute( routeType: TradeType, percentToQuotes: { [percent: number]: Trade<Currency, Currency, TradeType>[] }, percents: number[], swapRouteConfig = { minSplits: 1, maxSplits: 8 } ): Trade<Currency, Currency, TradeType>[] | null { // Извлекаем уникальные пулы из всех маршрутов const allPools = [...new Set(Object.values(percentToQuotes).flatMap(routes => routes.flatMap(r => r.route.pools)))]; // Создаем битовую карту для пулов const poolToBit = new Map<number, number>(); let bitCounter = 0; for (const pool of allPools) { poolToBit.set(pool.id, 1 << bitCounter++); } // Предвычисляем маски для всех маршрутов const routeToMask = new Map<Trade<Currency, Currency, TradeType>, number>(); for (const routes of Object.values(percentToQuotes)) { for (const route of routes) { let mask = 0; for (const pool of route.route.pools) { mask |= poolToBit.get(pool.id)!; } routeToMask.set(route, mask); } } // Сортируем маршруты для каждого процента const percentToSortedQuotes: { [percent: number]: Trade<Currency, Currency, TradeType>[] } = {}; for (const percent in percentToQuotes) { 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 ); } // Функция сравнения для типа торговли const quoteCompFn = routeType === TradeType.EXACT_INPUT ? (a: CurrencyAmount<Currency>, b: CurrencyAmount<Currency>) => a.greaterThan(b) : (a: CurrencyAmount<Currency>, b: CurrencyAmount<Currency>) => a.lessThan(b); // Функция суммирования CurrencyAmount const sumFn = (currencyAmounts: CurrencyAmount<Currency>[]): CurrencyAmount<Currency> => { let sum = currencyAmounts[0]!; for (let i = 1; i < currencyAmounts.length; i++) { sum = sum.add(currencyAmounts[i]!); } return sum; }; let bestQuote: CurrencyAmount<Currency> | undefined; let bestSwap: Trade<Currency, Currency, TradeType>[] | undefined; // Храним лучшие маршруты для каждого уровня разбиения (максимум 3) const bestSwapsPerSplit = new FixedReverseHeap<{ quote: CurrencyAmount<Currency>; routes: Trade<Currency, Currency, TradeType>[]; }>( Array, (a, b) => (quoteCompFn(a.quote, b.quote) ? -1 : 1), 3 ); const { minSplits, maxSplits } = swapRouteConfig; // Проверяем наличие маршрута для 100% и инициализируем начальные данные if (!percentToSortedQuotes[100] || percentToSortedQuotes[100].length === 0 || minSplits > 1) { console.log('Did not find a valid route without any splits. Continuing search anyway.'); } else { bestQuote = percentToSortedQuotes[100][0].outputAmount; bestSwap = [percentToSortedQuotes[100][0]]; for (const routeWithQuote of percentToSortedQuotes[100].slice(0, 5)) { bestSwapsPerSplit.push({ quote: routeWithQuote.outputAmount, routes: [routeWithQuote], }); } } // Очередь для обработки комбинаций маршрутов const queue = new Queue<{ percentIndex: number; curRoutes: Trade<Currency, Currency, TradeType>[]; remainingPercent: number; special: boolean; }>(); if (percents.length === 0) return null; // Инициализируем очередь с топ-2 маршрутами для каждого процента for (let i = percents.length - 1; i >= 0; i--) { const percent = percents[i]; if (!percentToSortedQuotes[percent] || percentToSortedQuotes[percent].length === 0) continue; const topRoutes = percentToSortedQuotes[percent].slice(0, 2); if (topRoutes[0]) { queue.enqueue({ curRoutes: [topRoutes[0]], percentIndex: i, remainingPercent: 100 - percent, special: false, }); } if (topRoutes[1]) { queue.enqueue({ curRoutes: [topRoutes[1]], percentIndex: i, remainingPercent: 100 - percent, special: true, }); } } let splits = 1; // Основной цикл поиска лучших маршрутов while (queue.size > 0) { bestSwapsPerSplit.clear(); let layer = queue.size; splits++; if (splits >= 3 && bestSwap && bestSwap.length < splits - 1) { break; } if (splits > maxSplits) { break; } while (layer > 0) { layer--; const { remainingPercent, curRoutes, percentIndex, special } = queue.dequeue()!; for (let i = percentIndex; i >= 0; i--) { const percentA = percents[i]; if (percentA > remainingPercent) continue; if (!percentToSortedQuotes[percentA] || percentToSortedQuotes[percentA].length === 0) continue; const candidateRoutesA = percentToSortedQuotes[percentA]; const routeWithQuoteA = findFirstRouteNotUsingUsedPools(curRoutes, candidateRoutesA, routeToMask); if (!routeWithQuoteA) continue; const remainingPercentNew = remainingPercent - percentA; const curRoutesNew = curRoutes.slice(); curRoutesNew.push(routeWithQuoteA); if (remainingPercentNew === 0 && splits >= minSplits) { const quotesNew = curRoutesNew.map(r => r.outputAmount); const quoteNew = sumFn(quotesNew); bestSwapsPerSplit.push({ quote: quoteNew, routes: curRoutesNew, }); if (!bestQuote || quoteCompFn(quoteNew, bestQuote)) { bestQuote = quoteNew; bestSwap = curRoutesNew; } } else { queue.enqueue({ curRoutes: curRoutesNew, remainingPercent: remainingPercentNew, percentIndex: i, special, }); } } } } if (!bestSwap) { console.log('Could not find a valid swap'); return null; } const quote = sumFn(bestSwap.map(routeWithValidQuote => routeWithValidQuote.outputAmount)); const routeWithQuotes = bestSwap.sort((routeAmountA, routeAmountB) => routeAmountB.outputAmount.greaterThan(routeAmountA.outputAmount) ? 1 : -1 ); return routeWithQuotes; } // Вспомогательная функция для поиска маршрута без пересекающихся пулов const findFirstRouteNotUsingUsedPools = ( usedRoutes: Trade<Currency, Currency, TradeType>[], candidateRoutes: Trade<Currency, Currency, TradeType>[], routeToMask: Map<Trade<Currency, Currency, TradeType>, number> ): Trade<Currency, Currency, TradeType> | null => { let usedMask = 0; for (const route of usedRoutes) { usedMask |= routeToMask.get(route)!; } for (const candidate of candidateRoutes) { const candidateMask = routeToMask.get(candidate)!; if ((candidateMask & usedMask) === 0) { return candidate; } } return null; };