UNPKG

@pontem/liquidswap-widget

Version:
367 lines (319 loc) 11.3 kB
import isFinite from 'lodash/isFinite'; import { defineStore } from 'pinia'; import { reactive, watch, computed, ref, onMounted } from 'vue'; import { watchDebounced, useStorage } from '@vueuse/core'; import { getPoolStr } from '@/utils/pools'; import { useStore } from '@/store/useStore'; import { is_sorted, d, decimalsMultiplier } from '@/utils/utils'; import { usePoolsStore, useTokensStore } from '@/store'; import { getFromCache } from '@/utils/cache'; import { DENOMINATOR, VERSION_0_5, VERSION_0 } from '@/constants/constants'; import { usePoolExistence } from '@/composables/usePoolExistence'; import { useContractVersion } from '@/composables/useContractVersion'; import { IStoredToken, TVersionType } from '@/types'; import { getCurve, getResourcesAccount, getShortCurveFromFull } from '@/utils/contracts'; import { PontemWalletName } from '@pontem/aptos-wallet-adapter'; const DEFAULT_SLIPPAGE = 0.005; export const useSwapStore = defineStore('swapStore', () => { const from = reactive<IStoredToken>({ token: undefined, amount: undefined, reserve: 0, usdEquivalent: undefined, }); const to = reactive<IStoredToken>({ token: undefined, amount: undefined, reserve: 0, usdEquivalent: undefined, }); const { version } = useContractVersion(); const poolsStore = usePoolsStore(); const mainStore = useStore(); const { sdk } = mainStore; const curve = ref<string>('stable'); const stableSwapType = ref<'high' | 'normal'>('high'); const poolExistence = usePoolExistence(); const slippageIsDefault = ref(true); const slippage = ref(DEFAULT_SLIPPAGE); const convertRate = ref(0); const fee = ref(0); // integer const convertFee = ref(0); // float (in percents) const convertFeeAmount = ref(0); // float in from currency const interactiveField = ref<'from' | 'to'>('from'); const lastInteractiveField = ref<'from' | 'to'>('from'); const convertError = ref<string>(); const isUpdatingRate = ref(false); const isFrontrunEnable = useStorage('is-frontrun-enable', false); const tokensStore = useTokensStore(); const aptos = mainStore.client; const feeBasisPoint = computed(() => (mainStore.feeData.value?.feeBasisPoint)); const networkId = computed(() => mainStore.networkId); const predefinedCurve = computed(() => { return poolsStore.getCurveType(from.token, to.token, version.value as unknown as TVersionType); }); watch(slippageIsDefault, (value) => { if (value) { slippage.value = DEFAULT_SLIPPAGE; } }); watch([tokensStore.tokens], () => { from.token = tokensStore.getToken(from.token)?.type || mainStore.defaultToken.value; to.token = from.token !== tokensStore.getToken(to.token)?.type ? tokensStore.getToken(to.token)?.type : undefined; }); onMounted(() => resetState()); function resetState() { version.value = VERSION_0; from.token = mainStore.defaultToken.value; to.token = undefined; from.amount = undefined; from.reserve = 0; to.amount = undefined; to.reserve = 0; convertError.value = undefined; from.usdEquivalent = undefined; to.usdEquivalent = undefined; } function convertToDecimals(amt?: number, fromToken?: string, toToken?: string) { if (!amt || !fromToken || !toToken) { return amt; } const fromDecimals = tokensStore.tokens[fromToken].decimals; const toDecimals = tokensStore.tokens[toToken].decimals; const diff = toDecimals - fromDecimals; if (diff > 0) { return +d(amt).div(decimalsMultiplier(diff)).toFixed(0); } else if (diff < 0) { return +d(amt).mul(decimalsMultiplier(diff)).toFixed(0); } return amt; } function toggleCurrencies() { const _from = { ...from }; from.token = to.token; from.amount = convertToDecimals(to.amount, _from.token, to.token); to.amount = convertToDecimals(_from.amount, to.token, _from.token); to.token = _from.token; interactiveField.value = interactiveField.value === 'from' ? 'to' : 'from'; } async function refetchRates(silent = false): Promise<void> { if (from.token && to.token && curve.value) { lastInteractiveField.value = interactiveField.value; if (!silent) { isUpdatingRate.value = true; } const mode = lastInteractiveField.value; const isSorted = is_sorted(from.token, to.token); const [fromToken, toToken] = isSorted ? [from.token, to.token] : [to.token, from.token]; const resourceType = getPoolStr(fromToken, toToken, curve.value, version.value as TVersionType); const resourcesAccount = getResourcesAccount(version.value); try { const response = await getFromCache( ['calc', resourcesAccount, resourceType].join('-'), () => aptos.getAccountResource(resourcesAccount, resourceType), { time: 2000 }, ); const coinXReserve = +response.data.coin_x_reserve.value; const coinYReserve = +response.data.coin_y_reserve.value; fee.value = response.data.fee; if (feeBasisPoint.value) { fee.value = d(response.data.fee).plus(feeBasisPoint.value).toNumber(); } from.reserve = isSorted ? coinXReserve : coinYReserve; to.reserve = isSorted ? coinYReserve : coinXReserve; } catch (e) { from.reserve = 0; to.reserve = 0; fee.value = 0; isUpdatingRate.value = false; return; } let rate; if (!from.amount && !to.amount) { if (!silent) { isUpdatingRate.value = false; } return; } if (feeBasisPoint.value) { try { rate = await sdk.value.Swap.calculateRates({ fromToken: from.token, toToken: to.token, interactiveToken: mode, curveType: getShortCurveFromFull(curve.value) as 'stable' | 'uncorrelated', amount: mode === 'from' ? from.amount! : to.amount!, version: version.value as unknown as TVersionType, additionalFee: +feeBasisPoint.value, }); } catch(_e) {} } else { try { rate = await sdk.value.Swap.calculateRates({ fromToken: from.token, toToken: to.token, interactiveToken: mode, curveType: getShortCurveFromFull(curve.value) as 'stable' | 'uncorrelated', amount: mode === 'from' ? from.amount! : to.amount!, version: version.value as unknown as TVersionType, }); } catch (_e) {} } convertError.value = to.amount && to.reserve < to.amount ? 'Insufficient funds in Liquidity Pool' : undefined; if (d(rate).lessThanOrEqualTo(0) || !isFinite(Number(rate))) { if (!silent) { isUpdatingRate.value = false; } return; } if (mode === 'from') { to.amount = +Number(Number(rate).toFixed(0)); convertRate.value = (from.amount as number) / (to.amount as number); } else { from.amount = +Number(Number(rate).toFixed(0)); } const toDec = d(to.amount).div(decimalsMultiplier(tokensStore.tokens[to.token].decimals)); const fromDec = d(from.amount).div(decimalsMultiplier(tokensStore.tokens[from.token].decimals)); convertRate.value = +Number( toDec.div(fromDec).mul(decimalsMultiplier(tokensStore.tokens[to.token].decimals)).toFixed(0), ); convertFee.value = (fee.value * 100) / DENOMINATOR; convertFeeAmount.value = from?.amount && from.amount > 0 ? from.amount * convertFee.value * 0.01 : 0; if (!silent) { isUpdatingRate.value = false; } } else { from.reserve = 0; to.reserve = 0; } } async function check() { if (!from?.token || !to?.token || !curve.value) return; await poolExistence.check( { fromCoin: from.token, toCoin: to.token, curve: curve.value, }, version.value, ); } watchDebounced( () => [version, from.amount, from.reserve, from.token, to.amount, to.reserve, to.token, curve], async (_changed) => { await check(); refetchRates(false); }, { debounce: 500, maxWait: undefined, deep: true, immediate: true, }, ); const slippageAmount = computed(() => { const MULTIPLY = 10000; const slippagePercent = slippage.value * MULTIPLY; if (lastInteractiveField.value === 'from' && to.amount !== undefined) { return version.value === VERSION_0_5 || curve.value === getCurve('unstable', version.value) ? to.amount - (to.amount * slippagePercent) / MULTIPLY : to.amount - 1; } else if (lastInteractiveField.value === 'to' && from.amount !== undefined) { return version.value === VERSION_0_5 || curve.value === getCurve('unstable', version.value) ? from.amount + (from.amount * slippagePercent) / MULTIPLY : from.amount; } return 0; }); /** * Pool is absence */ const isPoolAbsence = computed( (): boolean => !!from.token && !!to.token && !poolExistence.isFetching( { fromCoin: from.token, toCoin: to.token, curve: curve.value ?? '', }, version.value, ) && !poolExistence.poolExists( { fromCoin: from.token, toCoin: to.token, curve: curve.value ?? '', }, version.value, ), ); const isBusy = computed(() => poolExistence.isFetching || isUpdatingRate.value); const priceImpact = computed(() => { if (!from?.amount || !from.reserve || !to.reserve) return 0; const constantProduct = from.reserve * to.reserve; const reserveToAfter = constantProduct / (from.reserve + from.amount); const amountOut = to.reserve - reserveToAfter; const marketPrice = from.amount / amountOut; const midPrice = from.reserve / to.reserve; const pImpact = (1 - midPrice / marketPrice) * 100; return pImpact; }); const formatter = Intl.NumberFormat('en', { notation: 'compact', minimumFractionDigits: 2, maximumFractionDigits: 2, }); const priceImpactFormatted = computed(() => formatter.format(priceImpact.value)); const priceImpactState = computed(() => { if (+priceImpactFormatted.value <= 10) return 'normal'; if (+priceImpactFormatted.value >= 10 && +priceImpactFormatted.value < 20) return 'warning'; return 'alert'; }); watch( () => mainStore.walletName.value, () => { if (mainStore.walletName.value !== PontemWalletName) { isFrontrunEnable.value = false; } }, { immediate: true, }, ); return { check, isBusy, isPoolAbsence, convertRate, convertFee, convertFeeAmount, fromCurrency: from, interactiveField, isUpdatingRate, lastInteractiveField, networkId, slippage, slippageAmount, slippageIsDefault, toCurrency: to, curve, toggleCurrencies, refetchRates, poolExists: poolExistence.poolExists, convertError, stableSwapType, priceImpact, priceImpactFormatted, priceImpactState, version, predefinedCurve, isFrontrunEnable, }; });