@syncswap/sdk
Version:
SyncSwap TypeScript SDK for building DeFi applications
358 lines • 14.5 kB
JavaScript
import fetchRoutePools from "./fetchRoutePools.js";
import { PoolTypes, RouteDirection, } from "./types.js";
import { calculateGroupAmounts, calculatePathAmountsByInput, computeGasFee, createPath, getPoolKey, hasLiquidity, splitAmount } from "../sdkHelper.js";
import { Address } from "../utils/address.js";
import { DECIMALS_6, ETHER, ZERO } from "../utils/constants.js";
import { TokenRegistry } from "../tokens/tokenRegistry.js";
import { Numbers } from "../utils/numbers.js";
import { stateStore } from "../statestore/statestore.js";
export function getRoutePools(userAccount, // using 0x1 by default
tokenA, tokenB, routerCommonBasesTokens) {
const route = fetchRoutePools(userAccount, tokenA, tokenB, routerCommonBasesTokens);
return route;
}
export class SwapRouter {
static routeTimestamp() {
return this.latestRouteTimestamp;
}
}
SwapRouter.latestRouteTimestamp = 0;
const blacklistedPools = new Set(Array.from([], (item) => Address.normalizeAddress(item)));
// find all possible paths from token in to token out with route pools.
// this is the first step of processing route pools after fetched.
export function findAllPossiblePaths(tokenIn, ts, amountIn) {
const paths = []; // all viable paths for swapping
//console.log('findAllPossiblePaths routePools', routePools);
const routePools = stateStore().currentRoutePools;
if (!routePools) {
throw Error('Route pools not found');
}
// collect paths: direct
for (const pool of routePools.pools.poolsDirect) {
const allowed = stateStore().aquaPoolOnly ? (pool && (stateStore().allowUniswapV3Pools ? pool.poolType == PoolTypes.UNISWAP_V3 : pool.poolType == PoolTypes.AQUA)) : pool;
if (allowed && hasLiquidity(pool, tokenIn) && !blacklistedPools.has(pool.pool.toLowerCase())) {
const path = createPath([{
pool: pool,
tokenIn: tokenIn,
}]);
paths.push(path);
}
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
// exit if hops are disabled
if (!stateStore().enableHops) {
//console.log('[findAllPossiblePaths] Multi-hop is disabled, paths', paths);
return paths;
}
// put pools to map for search
const poolByTokens = new Map();
const addPool = (pool) => {
if (!stateStore().aquaPoolOnly || (stateStore().allowUniswapV3Pools ? pool.poolType == PoolTypes.UNISWAP_V3 : pool.poolType == PoolTypes.AQUA)) {
if (!blacklistedPools.has(pool.pool.toLowerCase())) {
const poolKey = getPoolKey(pool.tokenA, pool.tokenB);
let pools = poolByTokens.get(poolKey);
if (!pools) {
poolByTokens.set(poolKey, [pool]);
}
else {
pools.push(pool);
}
}
}
};
for (const pool of routePools.pools.poolsA) {
if (hasLiquidity(pool, tokenIn, amountIn)) {
addPool(pool);
}
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
for (const pool of routePools.pools.poolsB) {
if (hasLiquidity(pool)) {
addPool(pool);
}
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
for (const pool of routePools?.pools.poolsBase) {
if (hasLiquidity(pool)) {
addPool(pool);
}
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
//console.log('[findAllPossiblePaths] poolByTokens', poolByTokens);
// multi-step paths with hops
const length = routePools.routeTokens.length;
const tokenOut = (tokenIn === routePools.tokenA ? routePools.tokenB : routePools.tokenA).toLowerCase();
// collect paths: 1 hop
for (let i = 0; i < length; i++) {
const baseToken = routePools.routeTokens[i].toLowerCase();
// skip invalid paths
if (baseToken === tokenIn || baseToken === tokenOut) {
continue;
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
const poolsA = poolByTokens.get(getPoolKey(tokenIn, baseToken));
//console.log('[findAllPossiblePaths] hop 1 search poolA', [tokenIn, baseToken], poolA);
// skip if pool A has no liquidity
if (!poolsA || poolsA.length === 0) {
continue;
}
const poolsB = poolByTokens.get(getPoolKey(baseToken, tokenOut));
//console.log('[findAllPossiblePaths] hop 1 search poolA', [baseToken, tokenOut], poolB);
// skip if pool B has no liquidity
if (!poolsB || poolsB.length === 0) {
continue;
}
function createPaths(poolA, poolB) {
if (poolA && poolB) {
const path = createPath([{
pool: poolA,
tokenIn: tokenIn,
}, {
pool: poolB,
tokenIn: baseToken,
}]);
paths.push(path);
}
}
// A1 - B1, A1 - B2, A1 - B3, A2 - B1, A2 - B2, A2 - B3, A3 - B1, A3 - B2, A3 - B3
for (const poolA of poolsA) {
for (const poolB of poolsB) {
createPaths(poolA, poolB);
}
}
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
// collect paths: 2 hop
for (let i = 0; i < length; i++) { // base token 1
const baseToken1 = routePools.routeTokens[i].toLowerCase();
// skip invalid paths
if (baseToken1 === tokenIn || baseToken1 === tokenOut) {
continue;
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
const poolsA = poolByTokens.get(getPoolKey(tokenIn, baseToken1));
// skip if pool A has no liquidity
if (!poolsA || poolsA.length === 0) {
continue;
}
for (let j = i + 1; j < length; j++) { // base token 2
// skip identical bases
if (i === j) {
continue;
}
const baseToken2 = routePools.routeTokens[j].toLowerCase();
// skip invalid paths
if (baseToken2 === tokenIn || baseToken2 === tokenOut || baseToken2 === baseToken1) {
continue;
}
const poolsBase = poolByTokens.get(getPoolKey(baseToken1, baseToken2));
// skip if pool Base has no liquidity
if (!poolsBase || poolsBase.length === 0) {
continue;
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
const poolsB = poolByTokens.get(getPoolKey(baseToken2, tokenOut));
// skip if pool B has no liquidity
if (!poolsB || poolsB.length === 0) {
continue;
}
function createPaths(poolA, poolBase, poolB) {
if (poolA && poolBase && poolB) {
const path = createPath([{
pool: poolA,
tokenIn: tokenIn,
}, {
pool: poolBase,
tokenIn: baseToken1,
}, {
pool: poolB,
tokenIn: baseToken2,
}]);
paths.push(path);
}
}
for (const poolA of poolsA) {
for (const poolBase of poolsBase) {
for (const poolB of poolsB) {
createPaths(poolA, poolBase, poolB);
}
}
}
}
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
return paths;
}
const MAX_PATHS = 2; // max path count to use
export async function filterPathsToUseExactIn(paths, tokenIn, amountIn, ts, enableSplits = true) {
if (paths.length <= MAX_PATHS) {
return paths;
}
const availablePaths = [];
const availablePathAmounts = new Map();
// filter available paths with output amount
const sessionId = Date.now();
for (const path of paths) {
const amounts = await calculatePathAmountsByInput(path, amountIn, sessionId);
// skip failed paths
if (amounts !== null) {
availablePaths.push(path);
availablePathAmounts.set(path, amounts);
}
}
if (availablePaths.length <= MAX_PATHS) {
return availablePaths;
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
// sort available paths by output amount
availablePaths.sort((pathA, pathB) => {
const amountsA = availablePathAmounts.get(pathA);
const amountsB = availablePathAmounts.get(pathB);
//invariant(amountsA && amountsB, 'No amounts for available path');
return amountsA.amountOut.gt(amountsB.amountOut) ? -1 : 1;
});
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
const pathsToUse = availablePaths.slice(0, enableSplits ? MAX_PATHS : 1 // split count
);
return pathsToUse;
}
// find the best amounts for given paths with all possible input amount groups.
// this is the final step of processing route pools, after paths filtered.
export async function findBestAmountsForPathsExactIn(paths, amountIn, ts, tokenOut) {
const pathAmounts = await splitAmount(amountIn, paths.length);
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
const groups = [];
const groupPromises = [];
for (const amounts of pathAmounts) {
const promise = new Promise((resolve, reject) => {
calculateGroupAmounts(paths, amounts).then(group => {
if (group === null) {
reject('expired');
}
else {
//groups.push(group);
const { amountOut, quoteOut, pathsWithAmounts } = group;
if (!amountOut.isZero() && !quoteOut.isZero()) {
group.gas = computeGasFee(pathsWithAmounts);
//amountGranularity = amountGranularity.add(amountOut);
//gasGranularity = gasGranularity.add(group.gas);
groups.push(group);
}
resolve(true);
}
});
});
groupPromises.push(promise);
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
await Promise.all(groupPromises);
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
let bestPathsWithAmounts = [];
let bestAmountOut = ZERO;
let bestQuoteOut = ZERO;
let bestPriceImpact = null;
let bestVal = null;
const tokenOutData = TokenRegistry.getTokenByAddress(tokenOut);
const testAmountOut = tokenOutData ? Numbers.pow(tokenOutData.decimals) : ETHER; // one token
const gasGranularity = DECIMALS_6; // value for test gas price
// const network = ContractRegistry.getNetworkName();
const [tokenOutTestPrice, pathGasPrice] = [ZERO, ZERO];
const computeWithoutGasPrice = pathGasPrice.isZero() || tokenOutTestPrice.isZero();
for (const group of groups) {
const groupAmountOut = group.amountOut;
if (!groupAmountOut.isZero()) {
// Check and update price impact
if (!group.quoteOut.isZero()) {
const amountLoss = group.quoteOut.sub(groupAmountOut);
const groupPriceImpact = amountLoss.mul(ETHER).div(group.quoteOut);
if (bestPriceImpact === null || groupPriceImpact.lt(bestPriceImpact)) {
bestPriceImpact = groupPriceImpact;
}
}
let bestTempVal = ZERO;
if (computeWithoutGasPrice || !group.gas) {
bestTempVal = groupAmountOut;
}
else {
const groupAmountOutPrice = groupAmountOut.mul(tokenOutTestPrice).div(testAmountOut);
//console.log('groupAmountOutPrice', groupAmountOutPrice.toString(), 'amountGranularity', amountGranularity.toString(), 'tokenOutTestPrice', tokenOutTestPrice.toString());
const groupPathGasPrice = group.gas.mul(pathGasPrice).div(gasGranularity);
const finallyPrice = groupAmountOutPrice.sub(groupPathGasPrice);
bestTempVal = finallyPrice;
}
// Check and update amount out
//if (groupAmountOut.gt(bestAmountOut)) {
const hasBetterValue = !bestVal || bestTempVal.gt(bestVal);
//const hasBetterAmountOut = groupAmountOut.gt(bestAmountOut);
if (hasBetterValue) {
// set as best if has more output,
// there is not need to check path length as the case is rare
bestPathsWithAmounts = group.pathsWithAmounts;
bestAmountOut = groupAmountOut;
bestQuoteOut = group.quoteOut;
bestVal = bestTempVal;
// Check and update price impact
if (!group.quoteOut.isZero()) {
const amountLoss = group.quoteOut.sub(groupAmountOut);
const groupPriceImpact = amountLoss.mul(ETHER).div(group.quoteOut);
bestPriceImpact = groupPriceImpact;
}
else {
bestPriceImpact = ZERO;
}
}
}
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
const found = bestAmountOut !== null; // && !bestAmountOut.isZero() && bestPathsWithAmounts.length !== 0;
// estimate gas
//startTime = Date.now();
let gas;
if (found) {
gas = computeGasFee(bestPathsWithAmounts);
}
else {
gas = ZERO;
}
return {
found: found,
direction: RouteDirection.EXACT_IN,
pathsWithAmounts: bestPathsWithAmounts,
amountIn: amountIn,
amountOut: bestAmountOut,
quote: bestQuoteOut,
bestPriceImpact: bestPriceImpact,
gas: gas,
};
}
//# sourceMappingURL=index.js.map