diff --git a/.env.mainnet.prod b/.env.mainnet.prod index 9f439f34c..3fe9acc07 100644 --- a/.env.mainnet.prod +++ b/.env.mainnet.prod @@ -37,6 +37,9 @@ REACT_APP_POOLS_URL=wss://dexes-api-mainnet.prod.templewallet.com/ REACT_APP_FARMING_API_URL=https://staking-api-mainnet.prod.quipuswap.com REACT_APP_STABLESWAP_API_URL=https://stableswap-api-mainnet.prod.quipuswap.com REACT_APP_LIQUIDITY_API_URL=https://liquidity-api-mainnet.prod.quipuswap.com +REACT_APP_THREE_ROUTE_API_URL=https://quipuswap.3route.io +REACT_APP_THREE_ROUTE_API_AUTH_TOKEN=***REMOVED*** +REACT_APP_THREE_ROUTE_CONTRACT_ADDRESS=KT1Tuta6vbpHhZ15ixsYD3qJdhnpEAuogLQ9 #sentry REACT_APP_SENTRY_DSN=***REMOVED*** diff --git a/.env.mainnet.stage b/.env.mainnet.stage index 224274bff..cef8c79b4 100644 --- a/.env.mainnet.stage +++ b/.env.mainnet.stage @@ -37,6 +37,9 @@ REACT_APP_POOLS_URL=wss://dexes-api-mainnet.stage.madfish.xyz/ REACT_APP_FARMING_API_URL=https://staking-api-mainnet.stage.madfish.xyz REACT_APP_STABLESWAP_API_URL=https://stableswap-api-mainnet.stage.madfish.xyz REACT_APP_LIQUIDITY_API_URL=https://liquidity-api-mainnet.stage.madfish.xyz +REACT_APP_THREE_ROUTE_API_URL=https://quipuswap.3route.io +REACT_APP_THREE_ROUTE_API_AUTH_TOKEN=***REMOVED*** +REACT_APP_THREE_ROUTE_CONTRACT_ADDRESS=KT1Tuta6vbpHhZ15ixsYD3qJdhnpEAuogLQ9 #TZKT REACT_APP_TZKT_API=https://api.ghostnet.tzkt.io/v1 diff --git a/package.json b/package.json index f12f2c3a3..99ad7a4c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "quipuswap-webapp-2", - "version": "3.3.5", + "version": "3.3.6", "private": true, "scripts": { "pre-build": "node -v && npm -v && yarn -v && node ./scripts/build.js", diff --git a/src/config/config.ts b/src/config/config.ts index 8c2443223..c28cf0bf7 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -131,3 +131,6 @@ export const HOT_POOLS: Array<{ id: string; type: PoolType }> = [ // Coinflip export const COINFLIP_CONTRACT_DECIMALS = 18; export const COINFLIP_TOKEN_DECIMALS = 6; + +// 3route +export const THREE_ROUTE_APP_ID = 3; diff --git a/src/config/constants.ts b/src/config/constants.ts index 15286f84f..a9ce3adec 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -138,6 +138,9 @@ export const USD_DECIMALS = 2; export const MAX_ITEMS_PER_PAGE = 5; export const MAX_HOPS_COUNT = 3; +export const PRICE_IMPACT_WARNING_THRESHOLD = 10; + +export const DATA_TEST_ID_PROP_NAME = 'data-test-id'; export const NO_WITHDRAWAL_FEE_VALUE = 0; diff --git a/src/config/environment.ts b/src/config/environment.ts index 10c89c01f..714682464 100644 --- a/src/config/environment.ts +++ b/src/config/environment.ts @@ -35,6 +35,9 @@ export const STABLESWAP_FACTORY_CONTRACT_ADDRESS = process.env.REACT_APP_STABLES export const TEMPLEWALLET_API_URL = process.env.REACT_APP_TEMPLEWALLET_API_URL!; export const METADATA_API = process.env.REACT_APP_METADATA_API_URL!; export const DEX_POOL_URL = process.env.REACT_APP_POOLS_URL!; +export const THREE_ROUTE_API_URL = process.env.REACT_APP_THREE_ROUTE_API_URL; +export const THREE_ROUTE_API_AUTH_TOKEN = process.env.REACT_APP_THREE_ROUTE_API_AUTH_TOKEN; +export const THREE_ROUTE_CONTRACT_ADDRESS = process.env.REACT_APP_THREE_ROUTE_CONTRACT_ADDRESS; export const FARMING_API_URL = process.env.REACT_APP_FARMING_API_URL!; export const STABLESWAP_API_URL = process.env.REACT_APP_STABLESWAP_API_URL!; diff --git a/src/modules/farming/helpers/helpers.ts b/src/modules/farming/helpers/helpers.ts index 511559874..0eb23389a 100644 --- a/src/modules/farming/helpers/helpers.ts +++ b/src/modules/farming/helpers/helpers.ts @@ -2,7 +2,15 @@ import { TezosToolkit } from '@taquito/taquito'; import { BigNumber } from 'bignumber.js'; import { getUserTokenBalance } from '@blockchain'; -import { PRECISION_FACTOR, PRECISION_FACTOR_STABLESWAP_LP, SECONDS_IN_DAY, ZERO_AMOUNT_BN } from '@config/constants'; +import { + HOURS_IN_DAY, + PRECISION_FACTOR, + PRECISION_FACTOR_STABLESWAP_LP, + SECONDS_IN_DAY, + SECONDS_IN_MINUTE, + MINUTES_IN_HOUR, + ZERO_AMOUNT_BN +} from '@config/constants'; import { FARMING_CONTRACT_ADDRESS } from '@config/environment'; import { Tzkt } from '@shared/api'; import { getContract, getStorageInfo } from '@shared/dapp'; @@ -200,7 +208,8 @@ export const calculateYouvesFarmingRewards = ( farmVersion: FarmVersion, farmRewardTokenBalance: BigNumber, stake: Optional, - timestampMs: number + timestampMs: number, + rewardsPerDay: BigNumber ) => { if (!isExist(stake)) { return { @@ -221,10 +230,21 @@ export const calculateYouvesFarmingRewards = ( calculateTimeDiffInSeconds(new Date(ageTimestamp), new Date(timestampMs)), vestingPeriodSeconds ); + + const futureFarmRewardTokenBalance = farmRewardTokenBalance.plus( + vestingPeriodSeconds + .minus(stakeAge) + .dividedToIntegerBy(SECONDS_IN_MINUTE * MINUTES_IN_HOUR) + .times(rewardsPerDay.dividedToIntegerBy(HOURS_IN_DAY)) + ); + const futureReward = futureFarmRewardTokenBalance.minus(lastRewards); + const futureNewDiscFactor = discFactor.plus(futureReward.multipliedBy(precision).dividedToIntegerBy(totalStaked)); + const fullReward = stakeAmount.times(newDiscFactor.minus(userDiscFactor)).dividedToIntegerBy(precision); const claimableReward = fullReward.times(stakeAge).dividedToIntegerBy(vestingPeriodSeconds); + const futureFullReward = stakeAmount.times(futureNewDiscFactor.minus(userDiscFactor)).dividedToIntegerBy(precision); - return { claimableReward, fullReward }; + return { claimableReward, fullReward: futureFullReward }; }; const TOKEN_BALANCE_RETRY_TIMES = 3; @@ -248,14 +268,16 @@ export const getUserYouvesFarmingBalances = async ( last_rewards: lastRewards, disc_factor: discFactor, max_release_period: vestingPeriodSeconds, - total_stake: staked + total_stake: staked, + expected_rewards: rewardsPerDay } = await getStorageInfo(tezos, farmAddress); const { claimableReward, fullReward } = calculateYouvesFarmingRewards( { lastRewards: lastRewards.toFixed(), discFactor, vestingPeriodSeconds, staked }, farming.version, farmRewardTokenBalance, stake, - timestampMs + timestampMs, + rewardsPerDay ); return { diff --git a/src/modules/farming/hooks/blockchain/use-do-harvest-all.ts b/src/modules/farming/hooks/blockchain/use-do-harvest-all.ts index 58d0ce20d..74712c962 100644 --- a/src/modules/farming/hooks/blockchain/use-do-harvest-all.ts +++ b/src/modules/farming/hooks/blockchain/use-do-harvest-all.ts @@ -35,7 +35,7 @@ export const useDoHarvestAll = () => { defined(rootStore.authStore.accountPkh) ); - await confirmOperation(operation.opHash, { message: t('farm|Stake successful') }); + await confirmOperation(operation.opHash, { message: t('farm|Harvest successful') }); amplitudeService.logEvent('HARVEST_ALL_SUCCESS', logData); } catch (error) { showErrorToast(error as Error); diff --git a/src/modules/farming/pages/youves-item/api/types.ts b/src/modules/farming/pages/youves-item/api/types.ts index 55400e090..1e115462b 100644 --- a/src/modules/farming/pages/youves-item/api/types.ts +++ b/src/modules/farming/pages/youves-item/api/types.ts @@ -23,6 +23,7 @@ export interface YouvesFarmStorage { stakes: BigMap; stakes_owner_lookup: BigMap; total_stake: BigNumber; + expected_rewards: BigNumber; } export interface YouvesFarmStakes { diff --git a/src/modules/farming/pages/youves-item/components/youves-reward-info/youves-reward-info.view.tsx b/src/modules/farming/pages/youves-item/components/youves-reward-info/youves-reward-info.view.tsx index ddc8da08c..9e9657af9 100644 --- a/src/modules/farming/pages/youves-item/components/youves-reward-info/youves-reward-info.view.tsx +++ b/src/modules/farming/pages/youves-item/components/youves-reward-info/youves-reward-info.view.tsx @@ -7,7 +7,7 @@ import { Link } from 'react-router-dom'; import { AppRootRoutes } from '@app.router'; import { ColorModes, ColorThemeContext } from '@providers/color-theme-context'; -import { StateCurrencyAmount, BackToListRewardHeader } from '@shared/components'; +import { StateCurrencyAmount, CardHeaderWithBackButton } from '@shared/components'; import { Nullable } from '@shared/types'; import { useTranslation } from '@translation'; @@ -79,7 +79,7 @@ export const YouvesRewardInfoView: FC = observer( longTermRewardsLoading={longTermRewardsLoading} className={cx(styles.rewardInfo, modeClass[colorThemeMode])} header={{ - content: , + content: , className: styles.rewardHeader }} footer={} diff --git a/src/modules/farming/store/farming-youves-item.store.ts b/src/modules/farming/store/farming-youves-item.store.ts index d07475f10..dc77ab288 100644 --- a/src/modules/farming/store/farming-youves-item.store.ts +++ b/src/modules/farming/store/farming-youves-item.store.ts @@ -9,7 +9,7 @@ import { ZERO_AMOUNT_BN } from '@config/constants'; import { DexLink } from '@modules/liquidity/helpers'; -import { getLastElement, isExist, isNull, MakeInterval, toReal } from '@shared/helpers'; +import { getLastElement, isExist, isNull, MakeInterval, toAtomic, toReal, toRealIfPossible } from '@shared/helpers'; import { Led, ModelBuilder } from '@shared/model-builder'; import { LoadingErrorData, RootStore } from '@shared/store'; import { Nullable, Token } from '@shared/types'; @@ -50,7 +50,16 @@ export class FarmingYouvesItemStore { readonly itemStore: LoadingErrorData; get item() { - return this.itemStore.model.item; + const { item } = this.itemStore.model; + const currentStakeRealBalance = toRealIfPossible(this.currentStakeBalance, item?.stakedToken); + + return ( + item && { + ...item, + tvlInStakedToken: BigNumber.maximum(item.tvlInStakedToken, currentStakeRealBalance ?? ZERO_AMOUNT_BN), + staked: BigNumber.maximum(item.staked, this.currentStakeBalance ?? ZERO_AMOUNT_BN) + } + ); } get investHref() { @@ -140,7 +149,8 @@ export class FarmingYouvesItemStore { this.version, this.contractBalance, this.currentStake, - Date.now() + Date.now(), + toAtomic(this.item.dailyDistribution, this.item.rewardToken) ); this.claimableRewards = toReal(claimableReward, this.item.rewardToken); diff --git a/src/modules/liquidity/pages/v3-item-page/components/create-pool-form/create-pool-form.tsx b/src/modules/liquidity/pages/v3-item-page/components/create-pool-form/create-pool-form.tsx index 8fbfe236f..9f5b73585 100644 --- a/src/modules/liquidity/pages/v3-item-page/components/create-pool-form/create-pool-form.tsx +++ b/src/modules/liquidity/pages/v3-item-page/components/create-pool-form/create-pool-form.tsx @@ -1,7 +1,9 @@ +import { AppRootRoutes } from '@app.router'; import { AlarmMessage, Button, Card, + CardHeaderWithBackButton, ConnectWalletOrDoSomething, Iterator, RadioButton, @@ -22,23 +24,31 @@ export const CreatePoolForm = () => { alarmMessageInfo, translation, tokensSelectData, - tokens, + quoteToken, radioButtonParams, disabled, isSubmitting, + initialPriceLabel, initialPriceValue, initialPriceError, setInitialPriceValue, onSubmit, warningMessage, - errorMessage + errorMessage, + tokensSwitcher } = useCreatePoolFormViewModel(); - const { create, initialPrice, feeRates } = translation; + const { create, feeRates } = translation; const { poolExists, poolLink } = alarmMessageInfo; return ( - + , + button: tokensSwitcher + }} + >
} /> @@ -46,10 +56,10 @@ export const CreatePoolForm = () => { diff --git a/src/modules/liquidity/pages/v3-item-page/components/create-pool-form/create-pool-form.vm.ts b/src/modules/liquidity/pages/v3-item-page/components/create-pool-form/create-pool-form.vm.tsx similarity index 64% rename from src/modules/liquidity/pages/v3-item-page/components/create-pool-form/create-pool-form.vm.ts rename to src/modules/liquidity/pages/v3-item-page/components/create-pool-form/create-pool-form.vm.tsx index 598b88f39..a4ab34739 100644 --- a/src/modules/liquidity/pages/v3-item-page/components/create-pool-form/create-pool-form.vm.ts +++ b/src/modules/liquidity/pages/v3-item-page/components/create-pool-form/create-pool-form.vm.tsx @@ -1,48 +1,45 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; -import { BigNumber } from 'bignumber.js'; -import { FormikHelpers, useFormik } from 'formik'; -import { useNavigate } from 'react-router-dom'; +import BigNumber from 'bignumber.js'; +import { useFormik } from 'formik'; import * as yup from 'yup'; -import { AppRootRoutes } from '@app.router'; -import { DELAY_BEFORE_DATA_UPDATE, EMPTY_STRING, FEE_BASE_POINTS_PRECISION, SLASH } from '@config/constants'; -import { WTEZ_TOKEN } from '@config/tokens'; +import { EMPTY_STRING, FIRST_INDEX, SECOND_TUPLE_INDEX } from '@config/constants'; +import { TEZOS_TOKEN_DECIMALS, WTEZ_TOKEN } from '@config/tokens'; import { useGetLiquidityList } from '@modules/liquidity/hooks'; -import { LiquidityRoutes } from '@modules/liquidity/liquidity-routes.enum'; -import { useTezos } from '@providers/use-dapp'; +import { AssetSwitcher } from '@shared/components'; import { TokenSelectProps } from '@shared/components/token-select'; import { - getFirstElement, getFormikError, + getInvertedValue, + getTokenSymbol, isExist, isNull, operationAmountSchema, - toFraction, - sleep + stringToBigNumber } from '@shared/helpers'; import { noopMap } from '@shared/mapping'; import { Token } from '@shared/types'; import { i18n, useTranslation } from '@translation'; -import { tezosTokenIsIncluded, tokenIsIncluded, findPool, sortTokens } from '../../helpers'; -import { useDoCreateV3Pool } from '../../use-create-new-pool-page.vm'; +import { tezosTokenIsIncluded, tokenIsIncluded } from '../../helpers'; import styles from './create-pool-form.module.scss'; +import { useHandleSubmit } from './use-handle-submit'; enum eCreatePoolValues { feeRate = 'feeRate', initialPrice = 'initialPrice', - tokens = 'tokens' + tokens = 'tokens', + activeAssetIndex = 'activeAssetIndex' } interface CreatePoolValues { [eCreatePoolValues.feeRate]: number; [eCreatePoolValues.initialPrice]: string; [eCreatePoolValues.tokens]: Array; + [eCreatePoolValues.activeAssetIndex]: number; } -const INVERSION_DIVIDEND = 1; - export const feeRateRadioButtonOptions = [ { radioName: eCreatePoolValues.feeRate, @@ -87,7 +84,7 @@ const validationSchema = yup.object().shape({ [eCreatePoolValues.initialPrice]: operationAmountSchema( null, true, - 6, + TEZOS_TOKEN_DECIMALS, 'The value should be less than 6 decimals.' ).required(), [eCreatePoolValues.tokens]: yup @@ -101,64 +98,23 @@ const validationSchema = yup.object().shape({ const [firstToken, secondToken] = tokens; return isExist(firstToken) && isExist(secondToken); - }) + }), + [eCreatePoolValues.activeAssetIndex]: yup.number().required() }); const initialValues: CreatePoolValues = { [eCreatePoolValues.feeRate]: feeRateRadioButtonOptions[0].value, [eCreatePoolValues.initialPrice]: '', - [eCreatePoolValues.tokens]: [] + [eCreatePoolValues.tokens]: [], + [eCreatePoolValues.activeAssetIndex]: FIRST_INDEX }; export const useCreatePoolFormViewModel = () => { const { t } = useTranslation(); const { getLiquidityList } = useGetLiquidityList(); - const tezos = useTezos(); - const { doCreatePool } = useDoCreateV3Pool(); const [alarmMessageInfo, setAlarmMessageInfo] = useState(DEFAULT_ALARM_MESSAGE_INFO); - const navigate = useNavigate(); - - const handleSubmit = useCallback( - async (values: CreatePoolValues, actions: FormikHelpers) => { - actions.setSubmitting(true); - const feeRate = new BigNumber(values[eCreatePoolValues.feeRate]); - const tokens = values[eCreatePoolValues.tokens]; - const sortedTokens = Array.from(values[eCreatePoolValues.tokens]).sort(sortTokens); - const [token0, token1] = sortedTokens; - const tokensAreSwapped = token0 !== getFirstElement(tokens); - const rawInitialPrice = values[eCreatePoolValues.initialPrice]; - const initialPrice = tokensAreSwapped - ? new BigNumber(rawInitialPrice) - : new BigNumber(INVERSION_DIVIDEND).div(rawInitialPrice); - const poolId = await findPool(tezos, toFraction(feeRate).multipliedBy(FEE_BASE_POINTS_PRECISION), sortedTokens); - - if (isExist(poolId)) { - setAlarmMessageInfo({ - poolExists: true, - poolLink: `${AppRootRoutes.Liquidity}${LiquidityRoutes.v3}${SLASH}${poolId}` - }); - - return; - } - - try { - await doCreatePool(feeRate, token0, token1, initialPrice); - await sleep(DELAY_BEFORE_DATA_UPDATE); - - const newPoolId = await findPool( - tezos, - toFraction(feeRate).multipliedBy(FEE_BASE_POINTS_PRECISION), - sortedTokens - ); - navigate(`${AppRootRoutes.Liquidity}${LiquidityRoutes.v3}${SLASH}${newPoolId}${LiquidityRoutes.create}`); - } finally { - actions.resetForm(); - actions.setSubmitting(false); - } - }, - [tezos, doCreatePool, navigate] - ); + const handleSubmit = useHandleSubmit(setAlarmMessageInfo); const formik = useFormik({ validationSchema, @@ -179,8 +135,22 @@ export const useCreatePoolFormViewModel = () => { formik.setFieldValue(eCreatePoolValues.initialPrice, value); }; - const token0 = formik.values[eCreatePoolValues.tokens][0]; - const token1 = formik.values[eCreatePoolValues.tokens][1]; + const handleAssetSwitch = async (index: number) => + formik.setValues(prevValues => { + const prevInitialPrice = stringToBigNumber(prevValues[eCreatePoolValues.initialPrice]); + + return { + ...prevValues, + [eCreatePoolValues.activeAssetIndex]: index, + [eCreatePoolValues.initialPrice]: + prevInitialPrice.isZero() || !prevInitialPrice.isFinite() + ? EMPTY_STRING + : getInvertedValue(prevInitialPrice).decimalPlaces(TEZOS_TOKEN_DECIMALS, BigNumber.ROUND_DOWN).toFixed() + }; + }); + + const tokens = formik.values[eCreatePoolValues.tokens]; + const [token0, token1] = tokens; const tokensSelectData: Array = [ { @@ -192,7 +162,7 @@ export const useCreatePoolFormViewModel = () => { }, // eslint-disable-next-line @typescript-eslint/no-explicit-any error: !token0 ? getFormikError(formik as any, eCreatePoolValues.tokens) : undefined, - blackListedTokens: token1 ? [token1] : [] + blackListedTokens: [token1].filter(isExist) }, { ...standardTokenSelectProps, @@ -203,18 +173,16 @@ export const useCreatePoolFormViewModel = () => { }, // eslint-disable-next-line @typescript-eslint/no-explicit-any error: !token1 ? getFormikError(formik as any, eCreatePoolValues.tokens) : undefined, - blackListedTokens: token0 ? [token0] : [] + blackListedTokens: [token0].filter(isExist) } ]; const translation = { tokenSelect: t('common|Input'), - initialPrice: t('liquidity|initialPrice'), feeRates: t('liquidity|feeRates'), create: t('common|Create') }; - const tokens = formik.values[eCreatePoolValues.tokens]; const errorMessage = tezosTokenIsIncluded([token0, token1]) && tokenIsIncluded([token0, token1], WTEZ_TOKEN) ? t('liquidity|cannotCreatePoolError') @@ -226,19 +194,39 @@ export const useCreatePoolFormViewModel = () => { void getLiquidityList(); }, [getLiquidityList]); + const activeAssetIndex = formik.values[eCreatePoolValues.activeAssetIndex]; + const initialPriceLabel = useMemo(() => { + const baseToken = tokens[SECOND_TUPLE_INDEX - activeAssetIndex]; + if (isExist(baseToken)) { + return `${t('liquidity|initialPrice')}: 1 ${getTokenSymbol(baseToken)} =`; + } + + return t('liquidity|initialPrice'); + }, [t, tokens, activeAssetIndex]); + + const tokensSwitcher = isExist(token0) && isExist(token1) && ( + + ); + return { alarmMessageInfo, translation, radioButtonParams, tokensSelectData, - tokens, + quoteToken: tokens[activeAssetIndex], disabled: !tokens || formik.isSubmitting || isExist(errorMessage) || alarmMessageInfo.poolExists, isSubmitting: formik.isSubmitting, + initialPriceLabel, initialPriceValue: formik.values[eCreatePoolValues.initialPrice], // eslint-disable-next-line @typescript-eslint/no-explicit-any initialPriceError: getFormikError(formik as any, eCreatePoolValues.initialPrice), setInitialPriceValue, onSubmit: formik.handleSubmit, + tokensSwitcher, warningMessage, errorMessage }; diff --git a/src/modules/liquidity/pages/v3-item-page/components/create-pool-form/use-handle-submit.ts b/src/modules/liquidity/pages/v3-item-page/components/create-pool-form/use-handle-submit.ts new file mode 100644 index 000000000..f94f2750c --- /dev/null +++ b/src/modules/liquidity/pages/v3-item-page/components/create-pool-form/use-handle-submit.ts @@ -0,0 +1,67 @@ +import { Dispatch, useCallback, SetStateAction } from 'react'; + +import { BigNumber } from 'bignumber.js'; +import { FormikHelpers } from 'formik'; +import { useNavigate } from 'react-router-dom'; + +import { AppRootRoutes } from '@app.router'; +import { DELAY_BEFORE_DATA_UPDATE, FEE_BASE_POINTS_PRECISION, FIRST_INDEX, SLASH } from '@config/constants'; +import { LiquidityRoutes } from '@modules/liquidity/liquidity-routes.enum'; +import { useTezos } from '@providers/use-dapp'; +import { getFirstElement, isExist, toFraction, sleep, getInvertedValue } from '@shared/helpers'; + +import { findPool, sortTokens } from '../../helpers'; +import { CreatePoolValues, eCreatePoolValues } from '../../types'; +import { useDoCreateV3Pool } from '../../use-create-new-pool-page.vm'; + +export const useHandleSubmit = ( + setAlarmMessageInfo: Dispatch> +) => { + const tezos = useTezos(); + const { doCreatePool } = useDoCreateV3Pool(); + const navigate = useNavigate(); + + return useCallback( + async (values: CreatePoolValues, actions: FormikHelpers) => { + actions.setSubmitting(true); + const feeRate = new BigNumber(values[eCreatePoolValues.feeRate]); + const tokens = values[eCreatePoolValues.tokens]; + const sortedTokens = Array.from(values[eCreatePoolValues.tokens]).sort(sortTokens); + const activeAssetIndex = values[eCreatePoolValues.activeAssetIndex]; + const [token0, token1] = sortedTokens; + const tokensAreSwapped = token0 !== getFirstElement(tokens); + const shouldInvertPrice = (activeAssetIndex === FIRST_INDEX) === tokensAreSwapped; + const rawInitialPrice = values[eCreatePoolValues.initialPrice]; + const initialPrice = shouldInvertPrice + ? new BigNumber(rawInitialPrice) + : getInvertedValue(new BigNumber(rawInitialPrice)); + const poolId = await findPool(tezos, toFraction(feeRate).multipliedBy(FEE_BASE_POINTS_PRECISION), sortedTokens); + + if (isExist(poolId)) { + setAlarmMessageInfo({ + poolExists: true, + poolLink: `${AppRootRoutes.Liquidity}${LiquidityRoutes.v3}${SLASH}${poolId}` + }); + + return; + } + + try { + await doCreatePool(feeRate, token0, token1, initialPrice); + await sleep(DELAY_BEFORE_DATA_UPDATE); + + const newPoolId = await findPool( + tezos, + toFraction(feeRate).multipliedBy(FEE_BASE_POINTS_PRECISION), + sortedTokens + ); + + navigate(`${AppRootRoutes.Liquidity}${LiquidityRoutes.v3}${SLASH}${newPoolId}${LiquidityRoutes.create}`); + } finally { + actions.resetForm(); + actions.setSubmitting(false); + } + }, + [tezos, setAlarmMessageInfo, doCreatePool, navigate] + ); +}; diff --git a/src/modules/liquidity/pages/v3-item-page/components/fees-list/fees-list.tsx b/src/modules/liquidity/pages/v3-item-page/components/fees-list/fees-list.tsx index 319ae9dcd..a0b92f01d 100644 --- a/src/modules/liquidity/pages/v3-item-page/components/fees-list/fees-list.tsx +++ b/src/modules/liquidity/pages/v3-item-page/components/fees-list/fees-list.tsx @@ -3,7 +3,7 @@ import { FC, ReactNode } from 'react'; import { BigNumber } from 'bignumber.js'; import { DOLLAR, USD_DECIMALS } from '@config/constants'; -import { BackToListRewardHeader, DetailsCardCell, StateCurrencyAmount } from '@shared/components'; +import { CardHeaderWithBackButton, DetailsCardCell, StateCurrencyAmount } from '@shared/components'; import { RewardInfo } from '@shared/structures'; import { Nullable } from '@shared/types'; @@ -40,6 +40,7 @@ export const FeesList: FC = ({ }) => { const { rewardsTooltipTranslation, + backToTheListTranslation, claimFeeTranslation, totalFeesTranslation, totalDepositTranslation, @@ -67,7 +68,7 @@ export const FeesList: FC = ({ header={ backHref ? { - content: , + content: , className: styles.rewardHeader } : undefined diff --git a/src/modules/liquidity/pages/v3-item-page/components/position-details/position-details.tsx b/src/modules/liquidity/pages/v3-item-page/components/position-details/position-details.tsx index 5487a7315..83cb8f83c 100644 --- a/src/modules/liquidity/pages/v3-item-page/components/position-details/position-details.tsx +++ b/src/modules/liquidity/pages/v3-item-page/components/position-details/position-details.tsx @@ -4,32 +4,23 @@ import { observer } from 'mobx-react-lite'; import { PERCENT } from '@config/constants'; import { LiquidityLabels } from '@modules/liquidity/components'; -import { Button, Card, DetailsCardCell, StateCurrencyAmount, AssetSwitcher } from '@shared/components'; +import { Button, Card, DetailsCardCell, StateCurrencyAmount } from '@shared/components'; import { ExternalLink } from '@shared/svg'; import commonContainerStyles from '@styles/CommonContainer.module.scss'; import { useTranslation } from '@translation'; +import { useShouldShowTokenXToYPrice } from '../../hooks'; import { PositionStatus } from '../position-status'; +import { PriceView } from '../price-view'; +import { TokensOrderSwitcher } from '../tokens-order-switcher'; import styles from './position-details.module.scss'; import { usePositionDetailsViewModel } from './use-position-details.vm'; export const PositionDetails: FC = observer(() => { const { t } = useTranslation(); - const { - poolContractUrl, - id, - feeBps, - currentPrice, - tokensSymbols, - tokenXSymbol, - tokenYSymbol, - tokenActiveIndex, - handleButtonClick, - minPrice, - maxPrice, - isInRange, - categories - } = usePositionDetailsViewModel(); + const { poolContractUrl, id, feeBps, currentPrice, minPrice, maxPrice, isInRange, categories } = + usePositionDetailsViewModel(); + const shouldShowTokenXToYPrice = useShouldShowTokenXToYPrice(); return ( { content: (
{t('liquidity|positionDetails')} - +
), className: styles.header @@ -67,16 +53,16 @@ export const PositionDetails: FC = observer(() => { - + - + - +
- {t('common|Back to the list')} + {text ?? t('common|Back')}
); }; diff --git a/src/shared/components/card-header-with-back-button/index.ts b/src/shared/components/card-header-with-back-button/index.ts new file mode 100644 index 000000000..968a2413c --- /dev/null +++ b/src/shared/components/card-header-with-back-button/index.ts @@ -0,0 +1 @@ +export * from './card-header-with-back-button'; diff --git a/src/shared/components/complex-error/complex-error.tsx b/src/shared/components/complex-error/complex-error.tsx index e8c4ac454..2798ebbed 100644 --- a/src/shared/components/complex-error/complex-error.tsx +++ b/src/shared/components/complex-error/complex-error.tsx @@ -1,12 +1,15 @@ import { FC } from 'react'; +import { DATA_TEST_ID_PROP_NAME } from '@config/constants'; + import styles from './complex-error.module.scss'; -interface Props { +export interface ComplexErrorProps { error?: string; + [DATA_TEST_ID_PROP_NAME]?: string; } -export const ComplexError: FC = ({ error, ...props }) => ( +export const ComplexError: FC = ({ error, ...props }) => (

{error} diff --git a/src/shared/components/index.ts b/src/shared/components/index.ts index 5e28195e2..c83fa2947 100644 --- a/src/shared/components/index.ts +++ b/src/shared/components/index.ts @@ -73,4 +73,4 @@ export * from './virtual-list'; export * from './error-fallback'; export * from './loader-fallback'; export * from './asset-switcher'; -export * from './back-to-list-reward-header'; +export * from './card-header-with-back-button'; diff --git a/src/shared/helpers/can-use-three-route-api.ts b/src/shared/helpers/can-use-three-route-api.ts new file mode 100644 index 000000000..3f3560f9f --- /dev/null +++ b/src/shared/helpers/can-use-three-route-api.ts @@ -0,0 +1,5 @@ +import { THREE_ROUTE_API_URL } from '@config/environment'; + +import { isExist } from './type-checks'; + +export const canUseThreeRouteApi = () => isExist(THREE_ROUTE_API_URL); diff --git a/src/shared/helpers/formaters/format-balance.ts b/src/shared/helpers/formaters/format-balance.ts index 6cf0dd918..ddc31d365 100644 --- a/src/shared/helpers/formaters/format-balance.ts +++ b/src/shared/helpers/formaters/format-balance.ts @@ -79,6 +79,11 @@ export const formatValueBalance = ( maxAmountWithoutLetters = MAX_AMOUNT_WITHOUT_LETTERS ): string => { const bn = new BigNumber(amount); + + if (!bn.isFinite()) { + return bn.toString(); + } + if (bn.gte(maxAmountWithoutLetters)) { return shortNumberWithLetters(amount, amountDecimals); } diff --git a/src/shared/helpers/index.ts b/src/shared/helpers/index.ts index 4517f93c3..eb698cdee 100644 --- a/src/shared/helpers/index.ts +++ b/src/shared/helpers/index.ts @@ -31,7 +31,6 @@ export * from './readonly-signer'; export * from './shortize'; export * from './sleep'; export * from './strings'; -export * from './swap'; export * from './taquito-fast-rpc'; export * from './type-checks'; export * from './parse-decimals'; @@ -72,3 +71,4 @@ export * from './tokens-value'; export * from './math'; export * from './get-precentage-from-number'; export * from './set-caret-position'; +export * from './can-use-three-route-api'; diff --git a/src/shared/helpers/swap.ts b/src/shared/helpers/swap.ts deleted file mode 100644 index c93b3abeb..000000000 --- a/src/shared/helpers/swap.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { TezosToolkit } from '@taquito/taquito'; -import { BigNumber } from 'bignumber.js'; -import { getTradeOpParams, parseTransferParamsToParamsWithKind, Trade } from 'swap-router-sdk'; - -import { STABLESWAP_REFERRAL } from '@config/config'; -import { QUIPUSWAP_REFERRAL_CODE } from '@config/constants'; -import { DexPool } from '@modules/swap/types'; -import { TokenId } from '@shared/types'; - -export interface SwapParams { - deadlineTimespan?: number; - inputToken: TokenId; - inputAmount: BigNumber; - dexChain: DexPool[]; - trade: Trade; - slippageTolerance?: BigNumber; - ttDexAddress?: string; - recipient?: string; -} - -// TODO: use recipient PKH -export const estimateSwapFee = async ( - tezos: TezosToolkit, - accountPkh: string, - trade: Trade, - recipientPkh = accountPkh, - deadlineTimespan?: BigNumber -) => { - const tradeTransferParams = await getTradeOpParams( - trade, - accountPkh, - tezos, - STABLESWAP_REFERRAL, - recipientPkh, - deadlineTimespan?.toNumber(), - QUIPUSWAP_REFERRAL_CODE.toNumber() - ); - - const estimations = await tezos.estimate.batch( - tradeTransferParams.map(params => ({ - ...parseTransferParamsToParamsWithKind(params), - source: accountPkh - })) - ); - - return estimations.reduce( - ( - acc, - { - storageLimit, - suggestedFeeMutez, - // @ts-ignore - minimalFeePerStorageByteMutez - } - ) => acc.plus(suggestedFeeMutez).plus(storageLimit * minimalFeePerStorageByteMutez), - new BigNumber(0) - ); -}; diff --git a/src/shared/store/root.store.ts b/src/shared/store/root.store.ts index 160ddd6d1..4bbf570ce 100644 --- a/src/shared/store/root.store.ts +++ b/src/shared/store/root.store.ts @@ -25,6 +25,7 @@ import { StableDividendsFilterStore as IStableDividendsFilterStore, StableDividendsItemStore as IStableDividendsItemStore } from '@modules/stableswap/store'; +import { SwapStore as ISwapStore } from '@modules/swap/store'; import { TokensModalStore } from '@shared/modals/tokens-modal/tokens-modal.store'; import { isExist, isNull } from '../helpers'; @@ -68,6 +69,8 @@ export class RootStore { coinflipStore: Nullable = null; + swapStore: Nullable = null; + tezos: Nullable = null; constructor() { @@ -99,6 +102,8 @@ export class RootStore { coinflipStore: observable, + swapStore: observable, + setTezos: action, createFarmingListStatsStore: action, createFarmingListStore: action, @@ -118,7 +123,9 @@ export class RootStore { createLiquidityV3PoolStore: action, createLiquidityV3PositionStore: action, createLiquidityV3PositionsStore: action, - createLiquidityListFiltersStore: action + createLiquidityListFiltersStore: action, + + createSwapStore: action }); } @@ -126,6 +133,13 @@ export class RootStore { this.tezos = tezos; } + async createSwapStore() { + if (isNull(this.swapStore)) { + const { SwapStore } = await import('@modules/swap/store'); + this.swapStore = new SwapStore(); + } + } + async createLiquidityListStore() { if (isNull(this.liquidityListStore)) { const { LiquidityListStore } = await import('@modules/liquidity/store/liquidity-list.store'); diff --git a/src/shared/utils/toasts/use-toasts.ts b/src/shared/utils/toasts/use-toasts.ts index 371147f85..af8dbd07d 100644 --- a/src/shared/utils/toasts/use-toasts.ts +++ b/src/shared/utils/toasts/use-toasts.ts @@ -1,10 +1,9 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { captureException } from '@sentry/react'; import { ToastContent, UpdateOptions } from 'react-toastify'; -import { SERVER_UNAVAILABLE_ERROR_MESSAGE, SERVER_UNAVAILABLE_MESSAGE } from '@config/constants'; -import { useTranslation } from '@translation'; +import { i18n } from '@translation'; import { useUpdateToast } from './use-update-toast'; @@ -16,55 +15,44 @@ export interface UseToasts { showRichTextErrorToast: (error: Error | string, render: ToastContent) => void; } -export const useToasts = (): UseToasts => { - const updateToast = useUpdateToast(); - - const { t } = useTranslation(); - const knownErrorsMessages = useMemo>( - () => ({ - 'Dex/high-min-out': t('common|highMinOutError') - }), - [t] - ); - - const showErrorToast = useCallback( - (error: Error | string) => { - if (typeof error === 'string') { - captureException(new Error(error)); +const knownErrorsMessages = { + 'Dex/high-min-out': i18n.t('common|highMinOutError'), + '503 Service Temporarily Unavailable': 'The server is temporarily unavailable.', + 'Permission Not Granted': 'You rejected the operation.' +}; - updateToast({ - type: 'error', - render: error - }); +const getErrorMessage = (error: Error | object | string) => { + const errorMessage = typeof error === 'string' ? error : `${JSON.stringify(error)}`; - return; - } + const foundKey = Object.keys(knownErrorsMessages).find(key => + errorMessage.includes(key) + ) as keyof typeof knownErrorsMessages; - if (error instanceof Error) { - captureException(error); + if (foundKey) { + return knownErrorsMessages[foundKey]; + } + if (error instanceof Error) { + return `${error.name}: ${error.message}`; + } - let knownErrorMessage = knownErrorsMessages[error.message]; + return errorMessage; +}; - if (error.message.includes(SERVER_UNAVAILABLE_ERROR_MESSAGE)) { - knownErrorMessage = SERVER_UNAVAILABLE_MESSAGE; - } +export const useToasts = (): UseToasts => { + const updateToast = useUpdateToast(); - updateToast({ - type: 'error', - render: knownErrorMessage ?? `${error.name}: ${error.message}` - }); + const showErrorToast = useCallback( + (error: Error | string) => { + const errorMessage = getErrorMessage(error); - return; - } + captureException(error instanceof Error ? error : new Error(errorMessage)); - const errorMessage = `${JSON.stringify(error)}`; - captureException(new Error(errorMessage)); updateToast({ type: 'error', render: errorMessage }); }, - [updateToast, knownErrorsMessages] + [updateToast] ); const showRichTextErrorToast = useCallback( diff --git a/src/translation/locales/en/swap.ts b/src/translation/locales/en/swap.ts index 15e8932b0..c8230f143 100644 --- a/src/translation/locales/en/swap.ts +++ b/src/translation/locales/en/swap.ts @@ -17,5 +17,7 @@ export const swap = { priceImpactWarning: 'Note! Your price impact is {{priceImpact}}%. Double check the output amount and/or try to split the exchange to the smaller trades.', exchangeDetails: 'Exchange Details', - inputAmountIsTooBig: 'Input amount is too big, try a smaller one' + inputAmountIsTooBig: 'Input amount is too big, try a smaller one', + tryChangingAmount: 'Failed to find a route, try changing the amount', + failedToFindNotEmptyPoolsForTokens: 'Failed to find not empty pools for the specified tokens' } as const;