UNPKG

@perk.money/perk-swap-core

Version:

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

1,657 lines (1,499 loc) 93.1 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 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 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'; // TODO: change 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 = 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(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 }), memo: MEMO }, gas: DEFAULT_GAS, deposit: '1' } }] }); return transactions; } const FEE_DIVISOR$1 = /*#__PURE__*/JSBI__default["default"].BigInt(10000); // From ref.finance/jumbo specs function getOutputAmount({ 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$1); 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({ 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$1); 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 = 10000; const tradeFee = (amount, trade_fee) => { return JSBI__default["default"].divide(JSBI__default["default"].multiply(amount, trade_fee), JSBI__default["default"].BigInt(FEE_DIVISOR)); }; 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({ 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({ 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; } } 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({ 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({ 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', `tokenId: ${tokenId}`, `tokenOwnerId: ${tokenOwnerId}`); return { tokenId, existing: false, storageBalance: null }; } const res = await provider.query({ request_type: 'call_function', account_id: tokenId, method_name: 'storage_balance_of', args_base64: Buffer.from(JSON.stringify({ account_id: tokenOwnerId })).toString('base64'), finality: 'optimistic' }).then(res => JSON.parse(Buffer.from(res.result).toString())); return { tokenId, existing: res !== null, storageBalance: res }; }; async function registerToken(provider, tokenId, user) { const tokenOutActions = new Array(); const { storageBalance: tokenRegistered } = await loadAccountStorageBalance({ provider, tokenId, tokenOwnerId: user }); if (tokenRegistered) { return undefined; } tokenOutActions.push({ type: 'FunctionCall', params: { methodName: 'storage_deposit', args: { registration_only: true, account_id: user }, gas: DEFAULT_GAS, deposit: STORAGE_TO_REGISTER_WITH_MFT } }); return { receiverId: tokenId, signerId: user, actions: tokenOutActions }; } const builDepositNearActions = async (account, amount) => { const actions = new Array(); const registerTokenTx = await registerToken(account.connection.provider, WRAPPED_NEAR_ID, account.accountId); let deposit = amount; if (registerTokenTx) { actions.push(...registerTokenTx.actions); } else { const balance = await account.viewFunction(WRAPPED_NEAR_ID, 'ft_balance_of', { account_id: account.accountId }); if (JSBI__default["default"].greaterThanOrEqual(JSBI__default["default"].BigInt(balance), deposit)) { return []; } console.log('NEAR account found, balance:', balance); deposit = JSBI__default["default"].subtract(amount, JSBI__default["default"].BigInt(balance)); } actions.push({ type: 'FunctionCall', params: { methodName: 'near_deposit', args: {}, gas: DEFAULT_GAS, deposit: deposit.toString() } }); return actions; }; const buildDepositNearTransaction = async (account, amount) => { const transaction = { receiverId: WRAPPED_NEAR_ID, signerId: account.accountId, actions: await builDepositNearActions(account, amount) }; return transaction; }; const builWithdrawNearActions = async amount => { const actions = new Array(); const withdrawAmount = amount.toString(); actions.push({ type: 'FunctionCall', params: { methodName: 'near_withdraw', args: { amount: withdrawAmount }, gas: DEFAULT_GAS, deposit: '1' } }); // TBD: close wrapped account/return deposit return actions; }; const buildWithdrawNearTransaction = async (signerId, amount) => { const transaction = { receiverId: WRAPPED_NEAR_ID, signerId, actions: await builWithdrawNearActions(amount) }; return transaction; }; const callFunction = async params => { const { provider, accountId, method, args = {} } = params; const response = await provider.query({ request_type: 'call_function', account_id: accountId, method_name: method, args_base64: Buffer.from(JSON.stringify(args)).toString('base64'), finality: 'optimistic' }); const parsedResponse = JSON.parse(Buffer.from(response.result).toString()); return parsedResponse; }; const SPIN_CONTRACT_ADDRESS = 'spot.spin-fi.near'; const NATIVE_NEAR_ADDRESS = 'near.near'; class SpinFinanceMarket { constructor(rawMarket) { this.rawMarket = void 0; this.label = 'Spin Finance'; this.id = void 0; this.reserveTokenMints = void 0; this.contractId = SPIN_CONTRACT_ADDRESS; this.instanceId = void 0; this.orderBook = undefined; this.rawMarket = rawMarket; this.id = `${rawMarket.id}`; this.instanceId = rawMarket.id; // Pass wrapped near to the router this.reserveTokenMints = [rawMarket.base.address, rawMarket.quote.address].map(_ => _ === NATIVE_NEAR_ADDRESS ? WRAPPED_NEAR_ID : _); } static async loadUserDeposits({ user, provider }) { const deposits = await callFunction({ provider, accountId: SPIN_CONTRACT_ADDRESS, method: 'get_deposits', args: { account_id: user } }); return deposits; } static createWithdrawFromDepositsTransaction({ user, token, amount }) { const isNearToken = token === NATIVE_NEAR_ADDRESS; const transaction = { receiverId: SPIN_CONTRACT_ADDRESS, signerId: user, actions: [{ type: 'FunctionCall', params: { methodName: 'withdraw', args: { token: isNearToken ? NATIVE_NEAR_ADDRESS : token, amount }, gas: DEFAULT_GAS, deposit: isNearToken ? '0' : '1' } }] }; // @ts-ignore return transaction; } async createSwapInstructions(swapParams) { const { user, swapStep, provider } = swapParams; const transactions = []; const deposits = await SpinFinanceMarket.loadUserDeposits({ user, provider }); const inputDepositToken = swapStep.inputMint === WRAPPED_NEAR_ID ? NATIVE_NEAR_ADDRESS : swapStep.inputMint; const existingAmount = JSBI__default["default"].BigInt(deposits[inputDepositToken] || '0'); const { address: baseAddress } = this.rawMarket.base; const { address: quoteAddress } = this.rawMarket.quote; const isSellBase = baseAddress === swapStep.inputMint || baseAddress === NATIVE_NEAR_ADDRESS && swapStep.inputMint === WRAPPED_NEAR_ID; const isNativeMarket = baseAddress === NATIVE_NEAR_ADDRESS || quoteAddress === NATIVE_NEAR_ADDRESS; // Deposit if not enough if (JSBI__default["default"].lessThan(existingAmount, swapStep.amountIn)) { const amountToAdd = JSBI__default["default"].subtract(swapStep.amountIn, existingAmount); // Deposit native NEAR if (isNativeMarket && swapStep.inputMint === WRAPPED_NEAR_ID) { // Unwrap wrapped NEAR transactions.push(await buildWithdrawNearTransaction(user, amountToAdd)); // Deposit near transactions.push({ receiverId: SPIN_CONTRACT_ADDRESS, signerId: user, actions: [{ type: 'FunctionCall', params: { methodName: 'deposit_near', args: {}, gas: DEFAULT_GAS, deposit: amountToAdd.toString() } }] }); } else { // Deposit FT transactions.push({ receiverId: swapStep.inputMint, signerId: user, actions: [{ type: 'FunctionCall', params: { methodName: 'ft_transfer_call', args: { receiver_id: SPIN_CONTRACT_ADDRESS, amount: amountToAdd.toString(), msg: '' }, gas: DEFAULT_GAS, deposit: '1' } }] }); } } const methodName = isSellBase ? 'place_ask' : 'place_bid'; const quoteNominator = 10 ** this.rawMarket.quote.decimal; const orderStep = new Decimal__default["default"](this.rawMarket.limits.step_size); let quantity = new Decimal__default["default"]((isSellBase ? swapStep.amountIn : swapStep.minAmountOut).toString()).div(orderStep).floor().mul(orderStep); // if we buy base then quantity is what we'll receive, but spin fi // charge fee from amount out, that's why we need to add it here if (!isSellBase) { quantity = quantity.add(swapStep.feeAmount.toString()).div(orderStep).ceil().mul(orderStep); } const quoteAmount = new Decimal__default["default"]((isSellBase ? swapStep.minAmountOut : swapStep.amountIn).toString()).div(quoteNominator); const tickSize = new Decimal__default["default"](this.rawMarket.limits.tick_size); const price = quoteAmount.mul(quoteNominator).div(tickSize).div(quantity.div(10 ** this.rawMarket.base.decimal)).floor().mul(tickSize); // Place market order transactions.push({ receiverId: SPIN_CONTRACT_ADDRESS, signerId: user, actions: [{ type: 'FunctionCall', params: { methodName, args: { market_id: this.rawMarket.id, price: price.ceil().toFixed(0), quantity: quantity.toFixed(0), market_order: true, memo: MEMO }, gas: DEFAULT_GAS, deposit: '0' } }] }); const withdrawAmount = swapStep.minAmountOut.toString(); const withdrawToken = swapStep.outputMint === WRAPPED_NEAR_ID ? NATIVE_NEAR_ADDRESS : swapStep.outputMint; // Withdraw transactions.push(SpinFinanceMarket.createWithdrawFromDepositsTransaction({ user, token: withdrawToken, amount: withdrawAmount })); if (isNativeMarket && swapStep.outputMint === WRAPPED_NEAR_ID) { // Wrap NEAR back transactions.push({ receiverId: WRAPPED_NEAR_ID, signerId: user, actions: [{ type: 'FunctionCall', params: { methodName: 'near_deposit', args: {}, gas: DEFAULT_GAS, deposit: withdrawAmount } }] }); } return transactions; } async createSwapRouteInstructions(swapParams) { const { provider, user, swapRoute } = swapParams; const transactions = []; for (let routeStep of swapRoute.steps) { const stepTransactions = await routeStep.amm.createSwapInstructions({ provider, user, swapStep: routeStep }); transactions.push(...stepTransactions); } return transactions; } async getPromiseForUpdate({ provider }) { try { const orderBook = await callFunction({ provider, accountId: SPIN_CONTRACT_ADDRESS, method: 'get_orderbook', args: { market_id: this.rawMarket.id, limit: 100 } }); this.orderBook = orderBook; } catch (e) { console.log('error getPromiseForUpdate spin for market', this.rawMarket.id); } } getQuote(quoteParams) { if (!this.orderBook) { throw new Error(`Order book for market ${this.rawMarket.ticker} is not loaded`); } const isSellBase = quoteParams.inputMint === this.rawMarket.base.address || quoteParams.inputMint === WRAPPED_NEAR_ID && this.rawMarket.base.address === NATIVE_NEAR_ADDRESS; const side = isSellBase ? this.orderBook.bid_orders : this.orderBook.ask_orders; const isAmountInBase = isSellBase && quoteParams.swapMode === exports.SwapMode.ExactIn || !isSellBase && quoteParams.swapMode === exports.SwapMode.ExactOut; let restUserAmount = new Decimal__default["default"](quoteParams.amount.toString()); let amountIn = new Decimal__default["default"](0); let amountOut = new Decimal__default["default"](0); const baseMultiplier = new Decimal__default["default"](10).pow(this.rawMarket.base.decimal); const stepSize = new Decimal__default["default"](this.rawMarket.limits.step_size); const fee = new Decimal__default["default"](this.rawMarket.fees.taker_fee).div(10 ** this.rawMarket.fees.decimals); for (const order of side) { const price = new Decimal__default["default"](order.price); const restAmountInBase = isAmountInBase ? restUserAmount.div(stepSize).floor().mul(stepSize) : restUserAmount.mul(baseMultiplier).div(price).div(stepSize).floor().mul(stepSize); const orderAmount = new Decimal__default["default"](order.quantity); const takeFromLevelInBase = Decimal__default["default"].min(restAmountInBase, orderAmount); const takeFromLevelInQuote = takeFromLevelInBase.mul(price).div(baseMultiplier); amountIn = isSellBase ? amountIn.add(takeFromLevelInBase) : amountIn.add(takeFromLevelInQuote); amountOut = isSellBase ? amountOut.add(takeFromLevelInQuote) : amountOut.add(takeFromLevelInBase); const feeAmount = amountOut.mul(fee); const amountOutLessFee = amountOut.sub(feeAmount).floor(); let minAmountOut = new Decimal__default["default"](quoteParams.slippage.getFor(JSBI__default["default"].BigInt(amountOutLessFee.floor().toString())).toString()); // after removing slippage part requires to be stripped if (!isSellBase) { minAmountOut = minAmountOut.div(stepSize).floor().mul(stepSize); } restUserAmount = restUserAmount.sub(isAmountInBase ? takeFromLevelInBase : takeFromLevelInQuote); if (takeFromLevelInBase.lessThanOrEqualTo(0)) { const amountInBase = isSellBase ? amountIn : minAmountOut; const amountInQuote = isSellBase ? minAmountOut : amountIn; // check market limits for base token const isAmountInBaseGreaterMinLimit = amountInBase.greaterThanOrEqualTo(new Decimal__default["default"](this.rawMarket.limits.min_base_quantity)); const isAmountInBaseLessMaxLimit = amountInBase.lessThanOrEqualTo(new Decimal__default["default"](this.rawMarket.limits.max_base_quantity)); // check market limits for quote token const isAmountInQuoteGreaterMinLimit = amountInQuote.greaterThanOrEqualTo(new Decimal__default["default"](this.rawMarket.limits.min_quote_quantity)); const isAmountInQuoteLessMaxLimit = amountInQuote.lessThanOrEqualTo(new Decimal__default["default"](this.rawMarket.limits.max_quote_quantity)); return { amountIn: JSBI__default["default"].BigInt(amountIn.floor().toString()), amountOut: JSBI__default["default"].BigInt(amountOutLessFee.floor().toString()), minAmountOut: JSBI__default["default"].BigInt(minAmountOut.floor().toString()), feeAmount: JSBI__default["default"].BigInt(feeAmount.floor().toString()), feeMint: quoteParams.outputMint, feePct: parseFloat(this.rawMarket.fees.taker_fee) / 10 ** this.rawMarket.base.decimal, notEnoughLiquidity: !(isAmountInBaseGreaterMinLimit && isAmountInBaseLessMaxLimit && isAmountInQuoteGreaterMinLimit && isAmountInQuoteLessMaxLimit), priceImpact: 0 }; } } const feeAmount = amountOut.mul(fee); return { amountIn: JSBI__default["default"].BigInt(amountIn.floor().toString()), amountOut: JSBI__default["default"].BigInt(amountOut.sub(feeAmount).floor().toString()), minAmountOut: quoteParams.slippage.getFor(JSBI__default["default"].BigInt(amountOut.floor().toString())), feeAmount: JSBI__default["default"].BigInt(feeAmount.floor().toString()), feeMint: quoteParams.inputMint, feePct: parseFloat(this.rawMarket.fees.taker_fee) / 10 ** this.rawMarket.base.decimal, notEnoughLiquidity: true, priceImpact: 0 }; } static async loadMarkets(params) { const { provider } = params; const markets = await callFunction({ accountId: SPIN_CONTRACT_ADDRESS, method: 'get_markets', provider }); return markets.map(market => new SpinFinanceMarket(market)); } } const TONIC_FEE_DIVISOR = 10000; // By design const roundForStep = (n, step, up = false) => { JSBI__default["default"].BigInt(step); if (up) { return n.div(step).ceil().mul(step); } return n.div(step).floor().mul