@pontem/liquidswap-widget
Version:
Liquidswap widget as custom web component
367 lines (319 loc) • 11.3 kB
text/typescript
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,
};
});