@syncswap/sdk
Version:
SyncSwap TypeScript SDK for building DeFi applications
369 lines • 15.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SwapRouter = void 0;
exports.getRoutePools = getRoutePools;
exports.findAllPossiblePaths = findAllPossiblePaths;
exports.filterPathsToUseExactIn = filterPathsToUseExactIn;
exports.findBestAmountsForPathsExactIn = findBestAmountsForPathsExactIn;
const fetchRoutePools_1 = __importDefault(require("./fetchRoutePools"));
const types_1 = require("./types");
const sdkHelper_1 = require("../sdkHelper");
const address_1 = require("../utils/address");
const constants_1 = require("../utils/constants");
const tokenRegistry_1 = require("../tokens/tokenRegistry");
const numbers_1 = require("../utils/numbers");
const statestore_1 = require("../statestore/statestore");
function getRoutePools(userAccount, // using 0x1 by default
tokenA, tokenB, routerCommonBasesTokens) {
const route = (0, fetchRoutePools_1.default)(userAccount, tokenA, tokenB, routerCommonBasesTokens);
return route;
}
class SwapRouter {
static routeTimestamp() {
return this.latestRouteTimestamp;
}
}
exports.SwapRouter = SwapRouter;
SwapRouter.latestRouteTimestamp = 0;
const blacklistedPools = new Set(Array.from([], (item) => address_1.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.
function findAllPossiblePaths(tokenIn, ts, amountIn) {
const paths = []; // all viable paths for swapping
//console.log('findAllPossiblePaths routePools', routePools);
const routePools = (0, statestore_1.stateStore)().currentRoutePools;
if (!routePools) {
throw Error('Route pools not found');
}
// collect paths: direct
for (const pool of routePools.pools.poolsDirect) {
const allowed = (0, statestore_1.stateStore)().aquaPoolOnly ? (pool && ((0, statestore_1.stateStore)().allowUniswapV3Pools ? pool.poolType == types_1.PoolTypes.UNISWAP_V3 : pool.poolType == types_1.PoolTypes.AQUA)) : pool;
if (allowed && (0, sdkHelper_1.hasLiquidity)(pool, tokenIn) && !blacklistedPools.has(pool.pool.toLowerCase())) {
const path = (0, sdkHelper_1.createPath)([{
pool: pool,
tokenIn: tokenIn,
}]);
paths.push(path);
}
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
// exit if hops are disabled
if (!(0, statestore_1.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 (!(0, statestore_1.stateStore)().aquaPoolOnly || ((0, statestore_1.stateStore)().allowUniswapV3Pools ? pool.poolType == types_1.PoolTypes.UNISWAP_V3 : pool.poolType == types_1.PoolTypes.AQUA)) {
if (!blacklistedPools.has(pool.pool.toLowerCase())) {
const poolKey = (0, sdkHelper_1.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 ((0, sdkHelper_1.hasLiquidity)(pool, tokenIn, amountIn)) {
addPool(pool);
}
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
for (const pool of routePools.pools.poolsB) {
if ((0, sdkHelper_1.hasLiquidity)(pool)) {
addPool(pool);
}
}
if (ts !== SwapRouter.routeTimestamp()) {
throw Error('expired');
}
for (const pool of routePools?.pools.poolsBase) {
if ((0, sdkHelper_1.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((0, sdkHelper_1.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((0, sdkHelper_1.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 = (0, sdkHelper_1.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((0, sdkHelper_1.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((0, sdkHelper_1.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((0, sdkHelper_1.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 = (0, sdkHelper_1.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
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 (0, sdkHelper_1.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.
async function findBestAmountsForPathsExactIn(paths, amountIn, ts, tokenOut) {
const pathAmounts = await (0, sdkHelper_1.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) => {
(0, sdkHelper_1.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 = (0, sdkHelper_1.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 = constants_1.ZERO;
let bestQuoteOut = constants_1.ZERO;
let bestPriceImpact = null;
let bestVal = null;
const tokenOutData = tokenRegistry_1.TokenRegistry.getTokenByAddress(tokenOut);
const testAmountOut = tokenOutData ? numbers_1.Numbers.pow(tokenOutData.decimals) : constants_1.ETHER; // one token
const gasGranularity = constants_1.DECIMALS_6; // value for test gas price
// const network = ContractRegistry.getNetworkName();
const [tokenOutTestPrice, pathGasPrice] = [constants_1.ZERO, constants_1.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(constants_1.ETHER).div(group.quoteOut);
if (bestPriceImpact === null || groupPriceImpact.lt(bestPriceImpact)) {
bestPriceImpact = groupPriceImpact;
}
}
let bestTempVal = constants_1.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(constants_1.ETHER).div(group.quoteOut);
bestPriceImpact = groupPriceImpact;
}
else {
bestPriceImpact = constants_1.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 = (0, sdkHelper_1.computeGasFee)(bestPathsWithAmounts);
}
else {
gas = constants_1.ZERO;
}
return {
found: found,
direction: types_1.RouteDirection.EXACT_IN,
pathsWithAmounts: bestPathsWithAmounts,
amountIn: amountIn,
amountOut: bestAmountOut,
quote: bestQuoteOut,
bestPriceImpact: bestPriceImpact,
gas: gas,
};
}
//# sourceMappingURL=index.js.map