UNPKG

@xzwiex/perk-swap-core

Version:

This npm package contains core logic of Perk Aggregator build on top of NEAR blockchain

1,651 lines (1,495 loc) 109 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var tokenList = require('@tonic-foundation/token-list'); var JSBI = require('jsbi'); var BN$1 = require('bn.js'); var nearApiJs = require('near-api-js'); var concentratedLiquidity = require('@perk/concentrated-liquidity'); var Decimal = require('decimal.js'); var jsonRpcProvider = require('near-api-js/lib/providers/json-rpc-provider'); var nearUnits = require('near-units'); var transaction = require('near-api-js/lib/transaction'); var rpc_errors = require('near-api-js/lib/utils/rpc_errors'); var utils = require('near-api-js/lib/utils'); var key_stores = require('near-api-js/lib/key_stores'); var provider = require('near-api-js/lib/providers/provider'); var constants = require('near-api-js/lib/constants'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var JSBI__default = /*#__PURE__*/_interopDefaultLegacy(JSBI); var BN__default = /*#__PURE__*/_interopDefaultLegacy(BN$1); var Decimal__default = /*#__PURE__*/_interopDefaultLegacy(Decimal); const ZERO = /*#__PURE__*/JSBI__default["default"].BigInt(0); const STORAGE_TO_REGISTER_WITH_MFT = '100000000000000000000000'; const PERK_LIQUIDITY_ID = 'dev-perk-conliq2.near'; const REF_FINANCE_ID = 'v2.ref-finance.near'; const JUMBO_ID = 'v1.jumbo_exchange.near'; const TONIC_ID = 'v1.orderbook.near'; const WRAPPED_NEAR_ID = 'wrap.near'; const MEMO = 'perk'; const REFERRAL_ID = '95c9554eab0003879a571bc4c24f1ab8e30e1f698929c1be61e6a66770358b0f'; const DEFAULT_GAS = '150000000000000'; exports.SwapMode = void 0; (function (SwapMode) { SwapMode["ExactIn"] = "ExactIn"; SwapMode["ExactOut"] = "ExactOut"; })(exports.SwapMode || (exports.SwapMode = {})); const decimalsExponentiateBase = /*#__PURE__*/JSBI__default["default"].BigInt(10); const addDecimals = (number, decimals) => { const decimalsJSBI = decimals instanceof JSBI__default["default"] ? decimals : JSBI__default["default"].BigInt(decimals); const multiplier = JSBI__default["default"].exponentiate(decimalsExponentiateBase, decimalsJSBI); return JSBI__default["default"].multiply(number, multiplier); }; const removeDecimals = (number, decimals) => { const decimalsJSBI = decimals instanceof JSBI__default["default"] ? decimals : JSBI__default["default"].BigInt(decimals); const multiplier = JSBI__default["default"].exponentiate(decimalsExponentiateBase, decimalsJSBI); return JSBI__default["default"].divide(number, multiplier); }; const changeDecimals = (number, fromDecimals, toDecimals) => { const numbweWithNewDecimals = removeDecimals(addDecimals(number, toDecimals), fromDecimals); return numbweWithNewDecimals; }; const filterEmptyPools = pools => pools.filter(pool => pool.shares_total_supply !== '0'); const createCorrelation = (arr1, arr2) => arr1.reduce((acc, curr, index) => acc.set(curr, arr2[index]), new Map()); const createReserves = pool => createCorrelation(pool.token_account_ids, pool.amounts.map(a => JSBI__default["default"].BigInt(a))); const createCAmountReserves = pool => createCorrelation(pool.token_account_ids, pool.c_amounts.map(a => JSBI__default["default"].BigInt(a))); const getMostLiquidPools$1 = pools => { const poolsMap = pools.reduce((acc, pool) => { const tokenAccountsIds = pool.token_account_ids.sort(); const id = `${tokenAccountsIds[0]}_${tokenAccountsIds[1]}`; if (acc.has(id)) { const savedPools = acc.get(id) || []; acc.set(id, [...savedPools, pool]); } else { acc.set(id, [pool]); } return acc; }, new Map()); const uniqPoolsByAmounts = Array.from(poolsMap.values()).flatMap(poolsByTokens => { return poolsByTokens.sort((poolA, poolB) => { const [tokenA, tokenB] = poolA.token_account_ids; return JSBI__default["default"].greaterThan(JSBI__default["default"].BigInt(poolB.reserves.get(tokenA) || ZERO), JSBI__default["default"].BigInt(poolA.reserves.get(tokenA) || ZERO)) && JSBI__default["default"].greaterThan(JSBI__default["default"].BigInt(poolB.reserves.get(tokenB) || ZERO), JSBI__default["default"].BigInt(poolA.reserves.get(tokenB) || ZERO)) ? 1 : -1; })[0]; }); // console.log('uniqPoolsByAmounts', uniqPoolsByAmounts); return uniqPoolsByAmounts; }; const filterMostLiquidUniqPools = pools => { const notEmptyPools = filterEmptyPools(pools); const simplePools = notEmptyPools.filter(pool => pool.pool_kind === 'SIMPLE_POOL'); const mostLiquidPools = getMostLiquidPools$1(simplePools); return mostLiquidPools; }; const getNumberOfPools = async ({ provider, ammId }) => { const poolsNumber = await provider.query({ request_type: 'call_function', account_id: ammId, method_name: 'get_number_of_pools', args_base64: Buffer.from(JSON.stringify({})).toString('base64'), finality: 'optimistic' }).then(res => JSON.parse(Buffer.from(res.result).toString())); return poolsNumber; }; const parseSimplePool = (pool, id) => { return { ...pool, id, reserves: createReserves(pool) }; }; const loadSimplePool = async ({ provider, ammId, poolId }) => { const pool = await provider.query({ request_type: 'call_function', account_id: ammId, method_name: 'get_pool', args_base64: Buffer.from(JSON.stringify({ pool_id: poolId })).toString('base64'), finality: 'optimistic' }).then(res => JSON.parse(Buffer.from(res.result).toString())); const parsedPool = parseSimplePool(pool, poolId); return parsedPool; }; const loadPool = async ({ provider, ammId, poolId, poolKind }) => { if (poolKind) { switch (poolKind) { case 'STABLE_SWAP': return await getStablePool(provider, ammId, poolId); case 'RATED_SWAP': return await getRatedPool(provider, ammId, poolId); default: // @ts-ignore return await loadSimplePool({ provider, ammId, poolId }); } } else { // @ts-ignore return await loadSimplePool({ provider, ammId, poolId }); } }; const loadPools = async ({ provider, ammId, index = 0, limit }) => { const pools = await provider.query({ request_type: 'call_function', account_id: ammId, method_name: 'get_pools', args_base64: Buffer.from(JSON.stringify({ from_index: index, limit })).toString('base64'), finality: 'optimistic' }).then(res => JSON.parse(Buffer.from(res.result).toString())); return pools; }; const parseStableSwapPool = (pool, id) => { const STABLE_SWAP_LP_DECIMALS = 18; return { ...pool, id, reserves: createReserves(pool), cAmountReserves: createCAmountReserves(pool), decimals: createCorrelation(pool.token_account_ids, pool.decimals), // @ts-ignore rates: createCorrelation(pool.token_account_ids, pool.c_amounts.map(() => addDecimals(JSBI__default["default"].BigInt(1), STABLE_SWAP_LP_DECIMALS))), pool_kind: 'STABLE_SWAP' }; }; async function getStablePool(provider, exchange, poolId) { const pool = await provider.query({ request_type: 'call_function', account_id: exchange, method_name: 'get_stable_pool', args_base64: Buffer.from(JSON.stringify({ pool_id: poolId })).toString('base64'), finality: 'optimistic' }).then(res => JSON.parse(Buffer.from(res.result).toString())); return parseStableSwapPool(pool, poolId); } const parseRatedPool = (pool, poolId) => { return { ...pool, id: poolId, reserves: createReserves(pool), cAmountReserves: createCAmountReserves(pool), decimals: createCorrelation(pool.token_account_ids, pool.decimals), rates: createCorrelation(pool.token_account_ids, pool.rates.map(r => JSBI__default["default"].BigInt(r))), pool_kind: 'RATED_SWAP' }; }; /** * Fetch a rated pool. Rated pools are an improved version of stable pools. * @param provider * @param poolId * @returns */ async function getRatedPool(provider, exchange, poolId) { const pool = await provider.query({ request_type: 'call_function', account_id: exchange, method_name: 'get_rated_pool', args_base64: Buffer.from(JSON.stringify({ pool_id: poolId })).toString('base64'), finality: 'optimistic' }).then(res => JSON.parse(Buffer.from(res.result).toString())); return parseRatedPool(pool, poolId); } const loadAllPools = async ({ provider, ammId }) => { const numberOfPools = await getNumberOfPools({ provider, ammId }); let poolsCounter = 0; let poolsPromises = []; const poolsBatchToLoad = 500; while (numberOfPools > poolsCounter) { const poolsBatchPromise = loadPools({ provider, ammId, index: poolsCounter, limit: poolsBatchToLoad }); poolsPromises.push(poolsBatchPromise); poolsCounter += poolsBatchToLoad; } const pools = (await Promise.all(poolsPromises)).flat(); const poolsWithIndexes = pools.map((pool, index) => parseSimplePool(pool, index)); // @ts-ignore const simplePools = poolsWithIndexes.filter(pool => pool.pool_kind === 'SIMPLE_POOL'); const rawStablePools = poolsWithIndexes.filter(pool => pool.pool_kind !== 'SIMPLE_POOL'); const stablePools = await Promise.all(rawStablePools.map(pool => { if (pool.pool_kind === 'STABLE_SWAP') { return getStablePool(provider, ammId, pool.id); } else { return getRatedPool(provider, ammId, pool.id); } })); return { simplePools, stablePools }; }; // will test later async function createRefTransactions({ user, swapSteps }) { const transactions = []; // works only for 1 and 2 steps route, if there will be 3 and more steps // in future - we'll need to update it const actionsList = swapSteps.map((swapStep, index, arr) => { const { amm, inputMint, outputMint, amountIn, minAmountOut } = swapStep; const commonActionInfo = { pool_id: amm.instanceId, token_in: inputMint, token_out: outputMint }; const isFirstSwapStep = index === 0; const isMultiHop = arr.length > 1; // for direct route specify both amount in & out if (!isMultiHop) { return { ...commonActionInfo, amount_in: amountIn.toString(), min_amount_out: minAmountOut.toString() }; } // for first step in multi hop we skip min amount out and specify iy only // on last step of swap route if (isFirstSwapStep) { return { ...commonActionInfo, amount_in: amountIn.toString(), min_amount_out: '0' }; } // for last step we specify only min amount out - so we take all amount out from // prev step as amount in, and fail automatically if it's less then needed for min_amount_out return { pool_id: amm.instanceId, token_in: inputMint, token_out: outputMint, min_amount_out: minAmountOut.toString() }; }); const { amm: commonAMM, inputMint, amountIn } = swapSteps[0]; transactions.push({ receiverId: inputMint, signerId: user, actions: [{ type: 'FunctionCall', params: { methodName: 'ft_transfer_call', args: { receiver_id: commonAMM.contractId, amount: amountIn.toString(), msg: JSON.stringify({ force: 0, actions: actionsList, referral_id: REFERRAL_ID }), memo: MEMO }, gas: DEFAULT_GAS, deposit: '1' } }] }); return transactions; } const FEE_DIVISOR$2 = /*#__PURE__*/JSBI__default["default"].BigInt(10000); // From ref.finance/jumbo specs function getOutputAmount$1({ reserves, poolFee = 0, slippage, inputMint, outputMint, inputAmount }) { const inputPoolBalance = reserves.get(inputMint) || ZERO; const outputPoolBalance = reserves.get(outputMint) || ZERO; if (JSBI__default["default"].equal(inputPoolBalance, ZERO) || JSBI__default["default"].equal(outputPoolBalance, ZERO)) { return { amountIn: inputAmount, amountOut: ZERO, minAmountOut: ZERO, feeAmount: ZERO, notEnoughLiquidity: true, priceImpact: 0 }; } const feeAmount = JSBI__default["default"].divide(JSBI__default["default"].multiply(JSBI__default["default"].BigInt(poolFee), inputAmount), FEE_DIVISOR$2); const inputAmountLessFees = JSBI__default["default"].subtract(inputAmount, feeAmount); const numerator = JSBI__default["default"].multiply(inputAmountLessFees, outputPoolBalance); const denominator = JSBI__default["default"].add(inputPoolBalance, inputAmountLessFees); const amountOut = JSBI__default["default"].divide(numerator, denominator); const amountOutLessSlippage = slippage.getFor(amountOut); const finalPrice = +inputPoolBalance.toString() / +outputPoolBalance.toString(); const newPrice = +inputAmount.toString() / +amountOut.toString(); const priceImpact = (newPrice - finalPrice) / newPrice; return { notEnoughLiquidity: false, amountIn: inputAmount, amountOut, minAmountOut: amountOutLessSlippage, feeAmount, priceImpact }; } function getInputAmount$1({ reserves, poolFee = 0, slippage, inputMint, outputMint, outputAmount }) { const inputPoolBalance = reserves.get(inputMint) || ZERO; const outputPoolBalance = reserves.get(outputMint) || ZERO; if (JSBI__default["default"].equal(inputPoolBalance, ZERO) || JSBI__default["default"].equal(outputPoolBalance, ZERO)) { return { amountIn: ZERO, amountOut: outputAmount, minAmountOut: outputAmount, feeAmount: ZERO, notEnoughLiquidity: true, priceImpact: 0 }; } const numerator = JSBI__default["default"].multiply(outputAmount, inputPoolBalance); const denominator = JSBI__default["default"].subtract(outputPoolBalance, outputAmount); const amountIn = JSBI__default["default"].divide(numerator, denominator); const feeAmount = JSBI__default["default"].divide(JSBI__default["default"].multiply(JSBI__default["default"].BigInt(poolFee), amountIn), FEE_DIVISOR$2); const amountInWithFees = JSBI__default["default"].add(amountIn, feeAmount); const slippageAmount = JSBI__default["default"].divide(JSBI__default["default"].multiply(outputAmount, slippage.numerator), slippage.denominator); const minAmountOut = JSBI__default["default"].subtract(outputAmount, slippageAmount); const finalPrice = +inputPoolBalance.toString() / +outputPoolBalance.toString(); const newPrice = +amountInWithFees.toString() / +outputAmount.toString(); const priceImpact = (newPrice - finalPrice) / newPrice; return { notEnoughLiquidity: false, amountIn: amountInWithFees, amountOut: outputAmount, minAmountOut, feeAmount, priceImpact }; } const calc_d = ({ pool }) => { const { amp, cAmountReserves } = pool; const token_num = cAmountReserves.size; const numReserves = Array.from(cAmountReserves.values()).map(v => +v.toString()); const sum_amounts = numReserves.reduce((acc, amount) => acc + amount, 0); let d_prev = 0; let d = sum_amounts; for (let i = 0; i < 256; i++) { let d_prod = d; for (const c_amount of numReserves) { d_prod = d_prod * d / (c_amount * token_num); } d_prev = d; const ann = amp * token_num ** token_num; const numerator = d_prev * (d_prod * token_num + ann * sum_amounts); const denominator = d_prev * (ann - 1) + d_prod * (token_num + 1); d = numerator / denominator; if (Math.abs(d - d_prev) <= 1) break; } return d; }; const calc_y = ({ pool, x_c_amount, tokenInId, tokenOutId }) => { const token_num = pool.cAmountReserves.size; const ann = pool.amp * token_num ** token_num; const d = calc_d({ pool }); let s = x_c_amount; let c = d * d / x_c_amount; // need for 3-token pools for (const [token, amount] of pool.cAmountReserves) { if (token !== tokenInId && token !== tokenOutId) { const numAmount = +amount.toString(); s += numAmount; c = c * d / numAmount; } } c = c * d / (ann * token_num ** token_num); const b = d / ann + s; let y_prev = 0; let y = d; for (let i = 0; i < 256; i++) { y_prev = y; const y_numerator = y ** 2 + c; const y_denominator = 2 * y + b - d; y = y_numerator / y_denominator; if (Math.abs(y - y_prev) <= 1) break; } return y; }; const FEE_DIVISOR$1 = 10000; const tradeFee = (amount, trade_fee) => { return JSBI__default["default"].divide(JSBI__default["default"].multiply(amount, trade_fee), JSBI__default["default"].BigInt(FEE_DIVISOR$1)); }; function calc_swap({ pool, tokenInId, tokenOutId, amountIn }) { const inputTokenBalance = pool.cAmountReserves.get(tokenInId) || '0'; const y = calc_y({ pool, x_c_amount: +JSBI__default["default"].add(amountIn, JSBI__default["default"].BigInt(inputTokenBalance)).toString(), tokenInId, tokenOutId }); const outputTokenBalance = pool.cAmountReserves.get(tokenOutId) || '0'; const amountOut = JSBI__default["default"].BigInt(+outputTokenBalance.toString() - y); const fee = tradeFee(amountOut, JSBI__default["default"].BigInt(pool.total_fee)); const amountOutLessFees = JSBI__default["default"].subtract(amountOut, fee); return [amountOutLessFees, fee]; } // TODO: add output types function getStableOutputAmount({ tokenInId, tokenOutId, slippage, amountIn, stablePool }) { // hardcoded decimals for tokens in pool const STABLE_LP_TOKEN_DECIMALS = stablePool.pool_kind === 'STABLE_SWAP' ? 18 : 24; const inputTokenDecimals = stablePool.decimals.get(tokenInId) || null; const outputTokenDecimals = stablePool.decimals.get(tokenOutId) || null; if (inputTokenDecimals === null || outputTokenDecimals === null) { console.error({ inputTokenDecimals, outputTokenDecimals, stablePool, tokenInId, tokenOutId }); throw new Error('No info about decimals of tokens'); } // amount * rate / stable lp const updatedCReservesNum = Array.from(stablePool.cAmountReserves.entries()).map(([tokenId, amount]) => { const rate = stablePool.rates.get(tokenId) || null; if (!rate) { throw new Error('No rate for token'); } return [tokenId, removeDecimals(JSBI__default["default"].multiply(amount, rate), STABLE_LP_TOKEN_DECIMALS)]; }); const updatedCReserves = updatedCReservesNum.reduce((acc, [tokenId, amount]) => acc.set(tokenId, amount), new Map()); const updatedStablePool = { ...stablePool, cAmountReserves: updatedCReserves }; // change decimals from one to another const amountInWithHardcodedDecimals = changeDecimals(amountIn, inputTokenDecimals, STABLE_LP_TOKEN_DECIMALS); const inputPoolBalance = updatedCReserves.get(tokenInId) || ZERO; const outputPoolBalance = updatedCReserves.get(tokenOutId) || ZERO; if (JSBI__default["default"].equal(inputPoolBalance, ZERO) || JSBI__default["default"].equal(outputPoolBalance, ZERO)) { return { amountIn, amountOut: ZERO, minAmountOut: ZERO, feeAmount: ZERO, notEnoughLiquidity: true, priceImpact: 0 }; } const [amount_swapped, fee] = calc_swap({ tokenInId, amountIn: amountInWithHardcodedDecimals, tokenOutId, pool: updatedStablePool }); const rateIn = stablePool.rates.get(tokenInId) || null; const rateOut = stablePool.rates.get(tokenOutId) || null; if (!rateIn || !rateOut) { throw new Error('No rate for in or out token'); } const amountOutWithRate = JSBI__default["default"].divide(addDecimals(amount_swapped, STABLE_LP_TOKEN_DECIMALS), rateOut); const feeWithRate = JSBI__default["default"].divide(addDecimals(fee, STABLE_LP_TOKEN_DECIMALS), rateOut); const amountOutWithTokenDecimals = changeDecimals(amountOutWithRate, STABLE_LP_TOKEN_DECIMALS, outputTokenDecimals); const feeAmountWithTokenDecimals = changeDecimals(feeWithRate, STABLE_LP_TOKEN_DECIMALS, outputTokenDecimals); const minAmountOut = slippage.getFor(amountOutWithTokenDecimals); const marketPrice = +rateOut.toString() / +rateIn.toString(); const newMarketPrice = +amountInWithHardcodedDecimals.toString() / +amount_swapped.toString(); const priceImpact = (newMarketPrice - marketPrice) / newMarketPrice; return { notEnoughLiquidity: false, amountIn, amountOut: amountOutWithTokenDecimals, minAmountOut, feeAmount: feeAmountWithTokenDecimals, priceImpact }; } // TODO: separate & pass swap func as param const createSwapOptions = params => { const { stablePool, tokenInId, tokenOutId, slippage, minAmount, maxAmount, numberOfSteps = 200 } = params; const stepSize = JSBI__default["default"].divide(JSBI__default["default"].subtract(maxAmount, minAmount), JSBI__default["default"].BigInt(numberOfSteps)); const swapOptions = Array.from(Array(numberOfSteps).keys()).map(v => v + 1).map(iteration => { const amountIn = JSBI__default["default"].add(minAmount, JSBI__default["default"].multiply(stepSize, JSBI__default["default"].BigInt(iteration))); const { notEnoughLiquidity, amountOut, minAmountOut, feeAmount, priceImpact } = getStableOutputAmount({ amountIn, tokenOutId, tokenInId, slippage, stablePool }); return { priceImpact, notEnoughLiquidity, amountIn, amountOut, minAmountOut, feeAmount }; }); return swapOptions; }; const findBestOption = params => { const { stablePool, tokenInId, tokenOutId, outputAmount, slippage, appproxInputAmount, minAmountMultiplier = 0, maxAmountMultiplier = 2, numberOfSteps = 200 } = params; const minAmount = removeDecimals(JSBI__default["default"].multiply(appproxInputAmount, JSBI__default["default"].BigInt(minAmountMultiplier * 10 ** 3)), 3); const maxAmount = removeDecimals(JSBI__default["default"].multiply(appproxInputAmount, JSBI__default["default"].BigInt(maxAmountMultiplier * 10 ** 3)), 3); // create array of results swapping tokenA-tokenB and tokenB-tokenA const swapResults = createSwapOptions({ stablePool, tokenInId, tokenOutId, slippage, minAmount, maxAmount, numberOfSteps }); // take the closes one to pool amount ratio - user amount ratio const swapAmounts = swapResults.map(ratios => { const { amountOut } = ratios; const swapAmountOutDiff = JSBI__default["default"].subtract(amountOut, outputAmount); const absDiff = JSBI__default["default"].lessThan(swapAmountOutDiff, ZERO) ? JSBI__default["default"].unaryMinus(swapAmountOutDiff) : swapAmountOutDiff; return { ...ratios, swapAmountOutDiff: absDiff }; }).sort((ratioA, ratioB) => JSBI__default["default"].equal(ratioA.swapAmountOutDiff, ratioB.swapAmountOutDiff) ? 0 : JSBI__default["default"].lessThan(ratioA.swapAmountOutDiff, ratioB.swapAmountOutDiff) ? -1 : 1); return swapAmounts[0]; }; // TODO: update this method to do it by calculations, not searching the best match const getStableInputAmount = ({ tokenInId, tokenOutId, amountOut, slippage, stablePool }) => { // we expect that it's almost the same const appproxInputAmount = amountOut; let bestOption = null; const numberOfIterations = 2; for (let i = 0; i < numberOfIterations; i += 1) { if (!bestOption) { bestOption = findBestOption({ stablePool, tokenInId, tokenOutId, slippage, outputAmount: amountOut, appproxInputAmount, numberOfSteps: 200 }); } else { bestOption = findBestOption({ stablePool, tokenInId, tokenOutId, slippage, outputAmount: amountOut, appproxInputAmount: bestOption.amountIn, minAmountMultiplier: 0.95, maxAmountMultiplier: 1.05, numberOfSteps: 200 }); } } if (!bestOption) { throw new Error('No best option for getInputAmount'); } return bestOption; }; class Jumbo { constructor(pool) { this.id = void 0; this.contractId = void 0; this.instanceId = void 0; this.label = void 0; this.pool = void 0; this.isSimplePool = void 0; this.reserves = void 0; this.label = 'Jumbo'; this.id = `${pool.id}`; this.contractId = JUMBO_ID; this.instanceId = pool.id; this.pool = pool; this.isSimplePool = pool.pool_kind === 'SIMPLE_POOL'; this.reserves = pool.reserves; } static async loadPools({ provider }) { try { const { simplePools, stablePools } = await loadAllPools({ provider, ammId: JUMBO_ID }); const mostLiquidPools = filterMostLiquidUniqPools(simplePools); return [...mostLiquidPools, ...stablePools].map(pool => new Jumbo(pool)); } catch (e) { console.log('Error loading pools for Ref Finance', e); } return []; } getQuote(quoteParams) { const { inputMint, outputMint, amount, slippage, swapMode } = quoteParams; const feePct = this.pool.total_fee / 10000; if (swapMode === exports.SwapMode.ExactIn) { if (this.isSimplePool) { const { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feeAmount, priceImpact } = getOutputAmount$1({ reserves: this.pool.reserves, poolFee: this.pool.total_fee, inputMint, outputMint, inputAmount: amount, slippage }); return { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feePct, feeMint: inputMint, feeAmount, priceImpact }; } else { const { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feeAmount, priceImpact } = getStableOutputAmount({ tokenInId: inputMint, tokenOutId: outputMint, slippage, // @ts-ignore stablePool: this.pool, amountIn: amount }); return { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feePct, feeMint: inputMint, feeAmount, priceImpact }; } } else if (swapMode === exports.SwapMode.ExactOut) { if (this.isSimplePool) { const { amountIn, amountOut, minAmountOut, feeAmount, priceImpact, notEnoughLiquidity } = getInputAmount$1({ reserves: this.pool.reserves, poolFee: this.pool.total_fee, inputMint, outputMint, outputAmount: amount, slippage }); return { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feePct, feeMint: inputMint, feeAmount, priceImpact }; } else { const { notEnoughLiquidity, feeAmount, amountIn, amountOut, minAmountOut, priceImpact } = getStableInputAmount({ tokenInId: inputMint, tokenOutId: outputMint, slippage, // @ts-ignore stablePool: this.pool, amountOut: amount }); return { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feePct, feeMint: inputMint, feeAmount, priceImpact }; } } throw new Error('Unknown swap mode type'); } async getPromiseForUpdate({ provider }) { return loadPool({ provider, ammId: this.contractId, poolId: this.instanceId, poolKind: this.pool.pool_kind }).then(pool => { this.pool = pool; }); } async createSwapInstructions(params) { const { user, swapStep } = params; const swapTransactions = await createRefTransactions({ user, swapSteps: [swapStep] }); console.log('swapTransactions', swapTransactions); return swapTransactions; } async createSwapRouteInstructions(params) { const { user, swapRoute } = params; const swapTransactions = await createRefTransactions({ user, swapSteps: swapRoute.steps }); console.log('swapTransactions', swapTransactions); return swapTransactions; } get reserveTokenMints() { return this.pool.token_account_ids; } } const transformPositionsToTicksData = ({ perkLiquidity, pool, positions }) => { const { spacing } = pool; const ticksMap = positions.reduce((acc, position) => { const { lower_index, upper_index, total_liquidity } = position; const numberOfTicks = Math.abs(upper_index - lower_index) / spacing; const liquidityForTick = total_liquidity; for (let i = 0; i <= numberOfTicks; i++) { const tick = lower_index + spacing * i; if (acc.has(tick)) { acc.set(tick, (acc.get(tick) || 0) + liquidityForTick); } else { acc.set(tick, liquidityForTick); } } return acc; }, new Map()); const ticks = Array.from(ticksMap.entries()).map(tickData => { const actualPrice = perkLiquidity.math.getPriceFromTick({ tick: tickData[0], spacing }); const liquidity = Math.floor(tickData[1]); return { tick: tickData[0], actualPrice, sqrtActualPrice: Math.sqrt(actualPrice), liquidity }; }).sort((a, b) => a.tick - b.tick); return ticks; }; const FEE_DIVISOR = 1000000; const FEE_DIVISOR_BI = /*#__PURE__*/JSBI__default["default"].BigInt(FEE_DIVISOR); function getOutputAmount({ positions, perkLiquiditySDK, pool, slippage, inputMint, outputMint, inputAmount }) { const ticks = transformPositionsToTicksData({ pool, positions, perkLiquidity: perkLiquiditySDK }); if (ticks.length === 0) { return { amountIn: inputAmount, amountOut: ZERO, minAmountOut: ZERO, feeAmount: ZERO, notEnoughLiquidity: true, priceImpact: 0 }; } const tickDirectionToPass = inputMint === pool.alpha_id ? 'below' : 'above'; // prepare ticks so we can go one by one in any case const filteredTicks = ticks.filter(tick => { if (tickDirectionToPass === 'below') { return tick.actualPrice < pool.current_price; } return tick.actualPrice > pool.current_price; }).sort((a, b) => { if (tickDirectionToPass === 'below') { return b.actualPrice - a.actualPrice; } return a.actualPrice - b.actualPrice; }); console.log('filteredTicks', filteredTicks); const feeAmount = JSBI__default["default"].divide(JSBI__default["default"].multiply(JSBI__default["default"].BigInt(pool.pool_fee_tier * FEE_DIVISOR), inputAmount), FEE_DIVISOR_BI); const inputAmountLessFees = JSBI__default["default"].subtract(inputAmount, feeAmount); let remainingInputAmount = inputAmountLessFees; let outputAmount = ZERO; let currentPrice = pool.current_price; for (const tickData of filteredTicks) { if (JSBI__default["default"].equal(remainingInputAmount, ZERO)) continue; if (inputMint === pool.alpha_id) { const betaAmountInTick = perkLiquiditySDK.math.getBetaAmountFromLiquidity({ liquidity: tickData.liquidity, currentPrice: currentPrice, lowerPrice: tickData.actualPrice }); const inputAmountToReduce = JSBI__default["default"].BigInt(new Decimal__default["default"](betaAmountInTick).div(tickData.actualPrice).floor()); console.log({ tickData, currentPrice, betaAmountInTick, remainingInputAmount: remainingInputAmount.toString(), inputAmountToReduce: inputAmountToReduce.toString() }); if (JSBI__default["default"].greaterThanOrEqual(inputAmountToReduce, remainingInputAmount)) { const outputAmountToAdd = JSBI__default["default"].BigInt(new Decimal__default["default"](remainingInputAmount.toString()).mul(tickData.actualPrice).floor()); remainingInputAmount = ZERO; outputAmount = JSBI__default["default"].add(outputAmount, outputAmountToAdd); } else { remainingInputAmount = JSBI__default["default"].subtract(remainingInputAmount, JSBI__default["default"].BigInt(inputAmountToReduce)); outputAmount = JSBI__default["default"].add(outputAmount, JSBI__default["default"].BigInt(new Decimal__default["default"](betaAmountInTick).floor())); } } else { const alphaAmountInTick = perkLiquiditySDK.math.getAlphaAmountFromLiquidity({ liquidity: tickData.liquidity, currentPrice: currentPrice, upperPrice: tickData.actualPrice }); const inputAmountToReduce = JSBI__default["default"].BigInt(new Decimal__default["default"](alphaAmountInTick).mul(tickData.actualPrice).floor()); if (JSBI__default["default"].greaterThanOrEqual(inputAmountToReduce, remainingInputAmount)) { const outputAmountToAdd = JSBI__default["default"].BigInt(new Decimal__default["default"](remainingInputAmount.toString()).div(tickData.actualPrice).floor()); remainingInputAmount = ZERO; outputAmount = JSBI__default["default"].add(outputAmount, outputAmountToAdd); } else { remainingInputAmount = JSBI__default["default"].subtract(remainingInputAmount, JSBI__default["default"].BigInt(inputAmountToReduce)); outputAmount = JSBI__default["default"].add(outputAmount, JSBI__default["default"].BigInt(new Decimal__default["default"](alphaAmountInTick).floor())); } } currentPrice = tickData.actualPrice; } const outputAmountLessSlippage = slippage.getFor(outputAmount); const newPrice = inputMint === pool.alpha_id ? +outputAmount.toString() / +inputAmount.toString() : +inputAmount.toString() / +outputAmount.toString(); // check const priceImpact = Math.abs(newPrice - pool.current_price) / newPrice; console.log({ remainingInputAmount: remainingInputAmount.toString(), inputAmount: inputAmount.toString(), outputAmount: outputAmount.toString(), newPrice, currentPrice, pool: pool.current_price, priceImpact }); return { notEnoughLiquidity: JSBI__default["default"].greaterThan(remainingInputAmount, ZERO), amountIn: inputAmount, amountOut: outputAmount, minAmountOut: outputAmountLessSlippage, feeAmount, priceImpact }; } function getInputAmount({ positions, perkLiquiditySDK, pool, slippage, inputMint, outputMint, outputAmount }) { const ticks = transformPositionsToTicksData({ pool, positions, perkLiquidity: perkLiquiditySDK }); if (ticks.length === 0) { return { amountIn: ZERO, amountOut: outputAmount, minAmountOut: outputAmount, feeAmount: ZERO, notEnoughLiquidity: true, priceImpact: 0 }; } const tickDirectionToPass = inputMint === pool.alpha_id ? 'below' : 'above'; // prepare ticks so we can go one by one in any case const filteredTicks = ticks.filter(tick => { if (tickDirectionToPass === 'below') { return tick.actualPrice < pool.current_price; } return tick.actualPrice > pool.current_price; }).sort((a, b) => { if (tickDirectionToPass === 'below') { return b.actualPrice - a.actualPrice; } return a.actualPrice - b.actualPrice; }); let inputAmount = ZERO; let remainingOutputAmount = outputAmount; let currentPrice = pool.current_price; for (const tickData of filteredTicks) { if (JSBI__default["default"].equal(remainingOutputAmount, ZERO)) continue; if (inputMint === pool.alpha_id) { const betaAmountInTick = perkLiquiditySDK.math.getBetaAmountFromLiquidity({ liquidity: tickData.liquidity, currentPrice: currentPrice, lowerPrice: tickData.actualPrice }); const betaAmountInTickJSBI = JSBI__default["default"].BigInt(new Decimal__default["default"](betaAmountInTick).floor()); const alphaAmountFromTick = JSBI__default["default"].BigInt(new Decimal__default["default"](betaAmountInTick).div(tickData.actualPrice).ceil()); if (JSBI__default["default"].greaterThanOrEqual(betaAmountInTickJSBI, remainingOutputAmount)) { const leftInputAmountToAdd = JSBI__default["default"].BigInt(new Decimal__default["default"](remainingOutputAmount.toString()).div(tickData.actualPrice).ceil()); remainingOutputAmount = ZERO; inputAmount = JSBI__default["default"].add(inputAmount, leftInputAmountToAdd); } else { remainingOutputAmount = JSBI__default["default"].subtract(remainingOutputAmount, betaAmountInTickJSBI); inputAmount = JSBI__default["default"].add(inputAmount, alphaAmountFromTick); } } else { const alphaAmountInTick = perkLiquiditySDK.math.getAlphaAmountFromLiquidity({ liquidity: tickData.liquidity, currentPrice: currentPrice, upperPrice: tickData.actualPrice }); const alphaAmountInTickJSBI = JSBI__default["default"].BigInt(new Decimal__default["default"](alphaAmountInTick).floor()); const betaAmountInTickJSBI = JSBI__default["default"].BigInt(new Decimal__default["default"](alphaAmountInTick).mul(tickData.actualPrice).ceil()); if (JSBI__default["default"].greaterThanOrEqual(alphaAmountInTickJSBI, remainingOutputAmount)) { const inputAmountToAdd = JSBI__default["default"].BigInt(new Decimal__default["default"](remainingOutputAmount.toString()).mul(tickData.actualPrice).ceil()); remainingOutputAmount = ZERO; inputAmount = JSBI__default["default"].add(inputAmount, inputAmountToAdd); } else { remainingOutputAmount = JSBI__default["default"].subtract(remainingOutputAmount, alphaAmountInTickJSBI); inputAmount = JSBI__default["default"].add(inputAmount, betaAmountInTickJSBI); } } currentPrice = tickData.actualPrice; } const feeAmount = JSBI__default["default"].divide(JSBI__default["default"].multiply(JSBI__default["default"].BigInt(pool.pool_fee_tier * FEE_DIVISOR), inputAmount), FEE_DIVISOR_BI); const inputAmountwithFees = JSBI__default["default"].add(inputAmount, feeAmount); const outputAmountLessSlippage = slippage.getFor(outputAmount); const newPrice = inputMint === pool.alpha_id ? +outputAmount.toString() / +inputAmount.toString() : +inputAmount.toString() / +outputAmount.toString(); // check const priceImpact = Math.abs(newPrice - pool.current_price) / newPrice; return { notEnoughLiquidity: JSBI__default["default"].greaterThan(remainingOutputAmount, ZERO), amountIn: inputAmountwithFees, amountOut: outputAmount, minAmountOut: outputAmountLessSlippage, feeAmount, priceImpact }; } const getMostLiquidPools = pools => { const poolsMap = pools.reduce((acc, pool) => { const tokenAccountsIds = [pool.alpha_id, pool.beta_id].sort(); const id = `${tokenAccountsIds[0]}_${tokenAccountsIds[1]}-${pool.pool_fee_tier}`; if (acc.has(id)) { const savedPools = acc.get(id) || []; acc.set(id, [...savedPools, pool]); } else { acc.set(id, [pool]); } return acc; }, new Map()); const uniqPoolsByAmounts = Array.from(poolsMap.values()).flatMap(poolsByTokens => { return poolsByTokens.sort((poolA, poolB) => { return JSBI__default["default"].greaterThan(JSBI__default["default"].BigInt(poolB.liquidity || ZERO), JSBI__default["default"].BigInt(poolA.liquidity || ZERO)) ? 1 : -1; })[0]; }); return uniqPoolsByAmounts; }; class PerkLiquidity { constructor(pool, perkLiquiditySDK) { this.id = void 0; this.contractId = void 0; this.instanceId = void 0; this.label = void 0; this.pool = void 0; this.positions = void 0; this.perkLiquiditySDK = void 0; this.label = 'Perk'; this.id = `${pool.pool_id}`; this.contractId = PERK_LIQUIDITY_ID; this.instanceId = pool.pool_id; this.pool = pool; this.positions = []; this.perkLiquiditySDK = perkLiquiditySDK; } static async loadPools({ provider }) { try { const perkLiquiditySDK = await concentratedLiquidity.PerkLiquidity.load({ provider }); const pools = await perkLiquiditySDK.fetchers.loadPools(); // removing duplicates here const mostLiquidPools = getMostLiquidPools(pools); return mostLiquidPools.map(pool => new PerkLiquidity(pool, perkLiquiditySDK)); } catch (e) { console.log('Error loading pools for Ref Finance', e); } return []; } getQuote(quoteParams) { const { inputMint, outputMint, amount, slippage, swapMode } = quoteParams; const feePct = this.pool.pool_fee_tier; if (swapMode === exports.SwapMode.ExactIn) { const { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feeAmount, priceImpact } = getOutputAmount({ positions: this.positions, pool: this.pool, perkLiquiditySDK: this.perkLiquiditySDK, inputMint, outputMint, inputAmount: amount, slippage }); return { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feePct, feeMint: inputMint, feeAmount, priceImpact }; } else if (swapMode === exports.SwapMode.ExactOut) { const { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feeAmount, priceImpact } = getInputAmount({ positions: this.positions, pool: this.pool, perkLiquiditySDK: this.perkLiquiditySDK, inputMint, outputMint, outputAmount: amount, slippage }); return { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feePct, feeMint: inputMint, feeAmount, priceImpact }; } throw new Error('Unknown swap mode type'); } async getPromiseForUpdate() { return Promise.all([this.perkLiquiditySDK.fetchers.loadPool(this.pool.pool_id).then(pool => { this.pool = pool; }), this.perkLiquiditySDK.fetchers.getPoolsPositions([this.pool.pool_id]).then(positions => { this.positions = Array.from(positions.values()).flat(); })]).then(() => {}); } async createSwapInstructions(params) { const { swapStep, user } = params; const swapTransactions = await this.perkLiquiditySDK.instructions.getSwapTransactions({ user, pool_id: swapStep.amm.instanceId, input_token: swapStep.inputMint, output_token: swapStep.outputMint, input_amount: swapStep.amountIn.toString(), output_amount: swapStep.amountOut.toString() }); console.log('swapTransactions', swapTransactions); return swapTransactions; } async createSwapRouteInstructions(params) { const { swapRoute, user } = params; const formattedSteps = swapRoute.steps.map(swapStep => ({ pool_id: swapStep.amm.instanceId, input_token: swapStep.inputMint, output_token: swapStep.outputMint, input_amount: swapStep.amountIn.toString(), output_amount: swapStep.amountOut.toString() })); const swapTransactions = await this.perkLiquiditySDK.instructions.getMultiSwapTransactions({ user, steps: formattedSteps }); console.log('swapTransactions', swapTransactions); return swapTransactions; } get reserveTokenMints() { return [this.pool.alpha_id, this.pool.beta_id]; } } class RefFinance { constructor(pool) { this.id = void 0; this.contractId = void 0; this.instanceId = void 0; this.pool = void 0; this.isSimplePool = void 0; this.label = void 0; this.label = 'Ref.Finance'; this.id = `${pool.id}`; this.contractId = REF_FINANCE_ID; this.instanceId = pool.id; this.pool = pool; this.isSimplePool = pool.pool_kind === 'SIMPLE_POOL'; } static async loadPools({ provider }) { try { const { simplePools, stablePools } = await loadAllPools({ provider, ammId: REF_FINANCE_ID }); const mostLiquidSimplePools = filterMostLiquidUniqPools(simplePools); return [...mostLiquidSimplePools, ...stablePools].map(pool => new RefFinance(pool)); } catch (e) { console.log('Error loading pools for Ref Finance', e); } return []; } getQuote(quoteParams) { const { inputMint, outputMint, amount, slippage, swapMode } = quoteParams; const feePct = this.pool.total_fee / 10000; if (swapMode === exports.SwapMode.ExactIn) { if (this.isSimplePool) { const { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feeAmount, priceImpact } = getOutputAmount$1({ reserves: this.pool.reserves, poolFee: this.pool.total_fee, inputMint, outputMint, inputAmount: amount, slippage }); return { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feePct, feeMint: inputMint, feeAmount, priceImpact }; } else { const { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feeAmount, priceImpact } = getStableOutputAmount({ tokenInId: inputMint, tokenOutId: outputMint, slippage, // @ts-ignore stablePool: this.pool, amountIn: amount }); return { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feePct, feeMint: inputMint, feeAmount, priceImpact }; } } else if (swapMode === exports.SwapMode.ExactOut) { if (this.isSimplePool) { const { amountIn, amountOut, minAmountOut, feeAmount, priceImpact, notEnoughLiquidity } = getInputAmount$1({ reserves: this.pool.reserves, poolFee: this.pool.total_fee, inputMint, outputMint, outputAmount: amount, slippage }); return { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feePct, feeMint: inputMint, feeAmount, priceImpact }; } else { const { notEnoughLiquidity, feeAmount, amountIn, amountOut, minAmountOut, priceImpact } = getStableInputAmount({ tokenInId: inputMint, tokenOutId: outputMint, slippage, // @ts-ignore stablePool: this.pool, amountOut: amount }); return { notEnoughLiquidity, amountIn, amountOut, minAmountOut, feePct, feeMint: inputMint, feeAmount, priceImpact }; } } throw new Error('Unknown swap mode type'); } async getPromiseForUpdate({ provider }) { return loadPool({ provider, ammId: this.contractId, poolId: this.instanceId, poolKind: this.pool.pool_kind }).then(pool => { this.pool = pool; }); } async createSwapInstructions(params) { const { user, swapStep } = params; const swapTransactions = await createRefTransactions({ user, swapSteps: [swapStep] }); console.log('swapTransactions', swapTransactions); return swapTransactions; } async createSwapRouteInstructions(params) { const { user, swapRoute } = params; const swapTransactions = await createRefTransactions({ user, swapSteps: swapRoute.steps }); console.log('swapTransactions', swapTransactions); return swapTransactions; } get reserveTokenMints() { return this.pool.token_account_ids; } } const SLIPPAGE_NUMERATOR = 10000; const SLIPPAGE_DENOMINATOR = 1000000; const FEE_BPS_DENOMINATOR = 100000; class Percentage { constructor(numerator, denominator) { this.numerator = void 0; this.denominator = void 0; this.toString = () => { return `${this.numerator.toString()}/${this.denominator.toString()}`; }; this.numerator = numerator; this.denominator = denominator; } static fromSlippageNumber(num) { return new Percentage(JSBI__default["default"].BigInt(num * SLIPPAGE_NUMERATOR), JSBI__default["default"].BigInt(SLIPPAGE_DENOMINATOR)); } static fromFeeBpsNumber(num) { return new Percentage(JSBI__default["default"].BigInt(num), JSBI__default["default"].BigInt(FEE_BPS_DENOMINATOR)); } getFor(amount) { const numerator = JSBI__default["default"].BigInt(this.numerator.toString()); const denominator = JSBI__default["default"].BigInt(this.denominator.toString()); return JSBI__default["default"].divide(JSBI__default["default"].multiply(amount, JSBI__default["default"].subtract(denominator, numerator)), denominator); } getFrom(amount) { const numerator = JSBI__default["default"].BigInt(this.numerator.toString()); const denominator = JSBI__default["default"].BigInt(this.denominator.toString()); return JSBI__default["default"].divide(JSBI__default["default"].multiply(amount, numerator), denominator); } } const loadAccountStorageBalance = async ({ provider, tokenId, tokenOwnerId }) => { if (!tokenId || !tokenOwnerId) { console.error('No token id or token owner id for loading account storage balance', `to