@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
JavaScript
'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