diff --git a/src/assets/translations/de/main.json b/src/assets/translations/de/main.json index 6b1602d1560..879f4ba2eef 100644 --- a/src/assets/translations/de/main.json +++ b/src/assets/translations/de/main.json @@ -408,7 +408,7 @@ "disabledDepositTitle": "Einzahlungen sind vorübergehend deaktiviert.", "haltedWithdrawTitle": "Auszahlungen sind vorübergehend gestoppt.", "disabledWithdrawTitle": "Auszahlungen sind vorübergehend deaktiviert.", - "runePoolWithdrawLockedTitle": "Sie müssen %{timeHuman} warten, um abzuheben.", + "withdrawLockedTitle": "Sie müssen %{timeHuman} warten, um abzuheben.", "haltedDescription": "Bitte twittern Sie an @THORChain, um die Obergrenzen zu erhöhen!", "protocolFee": "Protokollgebühr", "fee": "Gebühr", diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 13ddb65bc9c..4779b7b4a5e 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -409,7 +409,7 @@ "disabledDepositTitle": "Deposits are temporarily disabled.", "haltedWithdrawTitle": "Withdraws are temporarily halted.", "disabledWithdrawTitle": "Withdraws are temporarily disabled.", - "runePoolWithdrawLockedTitle": "You have to wait %{timeHuman} to withdraw.", + "withdrawLockedTitle": "You have to wait %{timeHuman} to withdraw.", "haltedDescription": "Please tweet at @THORChain to raise the caps!", "protocolFee": "Protocol Fee", "fee": "Fee", @@ -505,6 +505,8 @@ "myRewardsBody": "Your active positions with claimable rewards will appear here.", "viewAllPositions": "View All Positions", "liquidityPools": "Liquidity Pools", + "liquidityLockupWarning": "There is a lockup period of %{time}. During lockup you can't access your liquidity.", + "liquidityLocked": "Liquidity Locked (%{time})", "stakingVaults": "Staking & Vaults", "farming": "Farming", "totalEarningBalance": "Total Earning Balance", diff --git a/src/assets/translations/es/main.json b/src/assets/translations/es/main.json index c83617be4d3..4e8872f2255 100644 --- a/src/assets/translations/es/main.json +++ b/src/assets/translations/es/main.json @@ -408,7 +408,7 @@ "disabledDepositTitle": "Depósitos temporalmente deshabilitados.", "haltedWithdrawTitle": "Retiros suspendidos temporalmente.", "disabledWithdrawTitle": "Retiros temporalmente deshabilitados.", - "runePoolWithdrawLockedTitle": "Tienes que esperar %{timeHuman} para retirar.", + "withdrawLockedTitle": "Tienes que esperar %{timeHuman} para retirar.", "haltedDescription": "¡Tuitee a @THORChain para aumentar los límites!", "protocolFee": "Tarifa de Protocolo", "fee": "Comisión", diff --git a/src/assets/translations/fr/main.json b/src/assets/translations/fr/main.json index 43ce153a82f..89de141b2f5 100644 --- a/src/assets/translations/fr/main.json +++ b/src/assets/translations/fr/main.json @@ -408,7 +408,7 @@ "disabledDepositTitle": "Les dépôts sont temporairement désactivés.", "haltedWithdrawTitle": "Les retraits sont temporairement interrompus.", "disabledWithdrawTitle": "Les retraits sont temporairement désactivés.", - "runePoolWithdrawLockedTitle": "Vous devez attendre %{timeHuman} pour retirer.", + "withdrawLockedTitle": "Vous devez attendre %{timeHuman} pour retirer.", "haltedDescription": "Veuillez tweeter @THORChain pour augmenter les plafonds !", "protocolFee": "Frais de protocole", "fee": "Frais", diff --git a/src/assets/translations/ja/main.json b/src/assets/translations/ja/main.json index 9325c6b69e8..c512304c4c8 100644 --- a/src/assets/translations/ja/main.json +++ b/src/assets/translations/ja/main.json @@ -408,7 +408,7 @@ "disabledDepositTitle": "デポジットは一時的に無効になります。", "haltedWithdrawTitle": "出金を一時停止しております。", "disabledWithdrawTitle": "引き出しは一時的に無効になっています。", - "runePoolWithdrawLockedTitle": "引き出すには%{timeHuman}待つ必要があります。", + "withdrawLockedTitle": "引き出すには%{timeHuman}待つ必要があります。", "haltedDescription": "上限を増やすには、@THORChain でツイートしてください。", "protocolFee": "プロトコル手数料", "fee": "手数料", diff --git a/src/assets/translations/pt/main.json b/src/assets/translations/pt/main.json index 8147b1db31a..421f85c7d8e 100644 --- a/src/assets/translations/pt/main.json +++ b/src/assets/translations/pt/main.json @@ -408,7 +408,7 @@ "disabledDepositTitle": "Os depósitos estão temporariamente desativados.", "haltedWithdrawTitle": "As retiradas estão temporariamente suspensas.", "disabledWithdrawTitle": "Os saques estão temporariamente desativados.", - "runePoolWithdrawLockedTitle": "Você tem que esperar %{timeHuman} para sacar.", + "withdrawLockedTitle": "Você tem que esperar %{timeHuman} para sacar.", "haltedDescription": "Por favor, twitte em @THORChain para aumentar o limite!", "protocolFee": "Taxas do Protocolo", "fee": "Taxa", diff --git a/src/assets/translations/ru/main.json b/src/assets/translations/ru/main.json index 73749d94d8e..48faea47f0f 100644 --- a/src/assets/translations/ru/main.json +++ b/src/assets/translations/ru/main.json @@ -408,7 +408,7 @@ "disabledDepositTitle": "Депозиты временно отключены.", "haltedWithdrawTitle": "Вывод средств временно приостановлен.", "disabledWithdrawTitle": "Вывод средств временно отключен.", - "runePoolWithdrawLockedTitle": "Чтобы снять деньги, вам придется подождать %{timeHuman}.", + "withdrawLockedTitle": "Чтобы снять деньги, вам придется подождать %{timeHuman}.", "haltedDescription": "Пожалуйста, напишите в твиттере @THORChain, чтобы поднять капитализацию!", "protocolFee": "Плата за протокол", "fee": "Комиссия", diff --git a/src/assets/translations/tr/main.json b/src/assets/translations/tr/main.json index 80d8d3ddf56..1552ba2bc56 100644 --- a/src/assets/translations/tr/main.json +++ b/src/assets/translations/tr/main.json @@ -408,7 +408,7 @@ "disabledDepositTitle": "Para yatırma işlemleri geçici olarak devre dışı bırakıldı.", "haltedWithdrawTitle": "Para çekme işlemleri geçici olarak durdurulmuştur.", "disabledWithdrawTitle": "Para çekme işlemleri geçici olarak devre dışı bırakıldı.", - "runePoolWithdrawLockedTitle": "Para çekmek için %{timeHuman} beklemeniz gerekiyor.", + "withdrawLockedTitle": "Para çekmek için %{timeHuman} beklemeniz gerekiyor.", "haltedDescription": "Lütfen @THORChain adresine tweet atarak üst sınırınızı yükseltin!", "protocolFee": "Protokol Ücreti", "fee": "Ücret", diff --git a/src/assets/translations/uk/main.json b/src/assets/translations/uk/main.json index e1107c350f4..77802b4ac1c 100644 --- a/src/assets/translations/uk/main.json +++ b/src/assets/translations/uk/main.json @@ -408,7 +408,7 @@ "disabledDepositTitle": "Депозити тимчасово не працюють.", "haltedWithdrawTitle": "Виведення коштів тимчасово призупинено.", "disabledWithdrawTitle": "Виведення коштів тимчасово відключено.", - "runePoolWithdrawLockedTitle": "Ви повинні дочекатися %{timeHuman}, щоб вивести кошти.", + "withdrawLockedTitle": "Ви повинні дочекатися %{timeHuman}, щоб вивести кошти.", "haltedDescription": "Будь ласка, напишіть у твіттері @THORChain, щоб підняти наш рівень!", "protocolFee": "Плата за протокол", "fee": "Комісія", diff --git a/src/assets/translations/zh/main.json b/src/assets/translations/zh/main.json index 4b7a9a68546..f9e6240877f 100644 --- a/src/assets/translations/zh/main.json +++ b/src/assets/translations/zh/main.json @@ -408,7 +408,7 @@ "disabledDepositTitle": "暂时禁止存款。", "haltedWithdrawTitle": "提款暂时停止。", "disabledWithdrawTitle": "暂时禁止提款。", - "runePoolWithdrawLockedTitle": "您必须等待 %{timeHuman} 才能提款。", + "withdrawLockedTitle": "您必须等待 %{timeHuman} 才能提款。", "haltedDescription": "请 @THORChain 发推文以提高上限!", "protocolFee": "协议费", "fee": "费用", diff --git a/src/components/Acknowledgement/Acknowledgement.tsx b/src/components/Acknowledgement/Acknowledgement.tsx index 258c6cf27a1..6a27d22fb3e 100644 --- a/src/components/Acknowledgement/Acknowledgement.tsx +++ b/src/components/Acknowledgement/Acknowledgement.tsx @@ -1,5 +1,6 @@ -import type { ComponentWithAs, IconProps, ThemeTypings } from '@chakra-ui/react' +import type { ComponentWithAs, IconProps, ResponsiveValue, ThemeTypings } from '@chakra-ui/react' import { Box, Button, Checkbox, Link, useColorModeValue } from '@chakra-ui/react' +import type * as CSS from 'csstype' import type { AnimationDefinition, MotionStyle } from 'framer-motion' import { AnimatePresence, motion } from 'framer-motion' import type { InterpolationOptions } from 'node-polyglot' @@ -91,6 +92,7 @@ type AcknowledgementProps = { buttonTranslation?: string | [string, InterpolationOptions] icon?: ComponentWithAs<'svg', IconProps> disableButton?: boolean + position?: ResponsiveValue } type StreamingAcknowledgementProps = Omit & { @@ -113,6 +115,7 @@ export const Acknowledgement = ({ buttonTranslation, disableButton, icon: CustomIcon, + position = 'relative', }: AcknowledgementProps) => { const translate = useTranslate() const [isShowing, setIsShowing] = useState(false) @@ -149,7 +152,7 @@ export const Acknowledgement = ({ return ( = ({ null, ) const [daysToBreakEven, setDaysToBreakEven] = useState(null) + const [shouldShowInfoAcknowledgement, setShouldShowInfoAcknowledgement] = useState(false) + const [depositValues, setDepositValues] = useState() const [inputValues, setInputValues] = useState<{ fiatAmount: string cryptoAmount: string @@ -166,6 +171,9 @@ export const Deposit: React.FC = ({ selectPortfolioCryptoBalanceBaseUnitByFilter(state, balanceFilter), ) + const { data: thorchainMimirTimes, isLoading: isThorchainMimirTimesLoading } = + useThorchainMimirTimes() + const { data: thorchainSaversDepositQuote, isLoading: isThorchainSaversDepositQuoteLoading, @@ -816,60 +824,103 @@ export const Deposit: React.FC = ({ [validateFiatAmountDebounced], ) + const handleContinueOrAcknowledgement = useCallback( + (formValues: DepositValues) => { + setDepositValues(formValues) + if (isRunePool && thorchainMimirTimes?.runePoolDepositMaturityTime) { + setShouldShowInfoAcknowledgement(true) + return + } + + if (!isRunePool && thorchainMimirTimes?.liquidityLockupTime) { + setShouldShowInfoAcknowledgement(true) + return + } + + handleContinue(formValues) + }, + [thorchainMimirTimes, isRunePool, handleContinue], + ) + + const handleAcknowledge = useCallback(() => { + if (!depositValues) return + handleContinue(depositValues) + }, [depositValues, handleContinue]) + if (!state || !contextDispatch || !opportunityData) return null return ( - - {!isRunePool ? ( - <> - - {translate('common.slippage')} - - - - - - - - - - {translate('defi.modals.saversVaults.timeToBreakEven.title')} - - - - - {translate( - `defi.modals.saversVaults.${bnOrZero(daysToBreakEven).eq(1) ? 'day' : 'days'}`, - { amount: daysToBreakEven ?? '0' }, - )} - - - - - ) : null} - + + {!isRunePool ? ( + <> + + {translate('common.slippage')} + + + + + + + + + + {translate('defi.modals.saversVaults.timeToBreakEven.title')} + + + + + {translate( + `defi.modals.saversVaults.${bnOrZero(daysToBreakEven).eq(1) ? 'day' : 'days'}`, + { amount: daysToBreakEven ?? '0' }, + )} + + + + + ) : null} + + ) } diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Overview/ThorchainSaversOverview.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Overview/ThorchainSaversOverview.tsx index 5d25b3e205f..7d91685c921 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Overview/ThorchainSaversOverview.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Overview/ThorchainSaversOverview.tsx @@ -51,7 +51,7 @@ import { import type { StakingId } from 'state/slices/opportunitiesSlice/types' import { DefiProvider, DefiType } from 'state/slices/opportunitiesSlice/types' import { - isRunePoolUserStakingOpportunity, + isSaversUserStakingOpportunity, makeDefiProviderDisplayName, serializeUserStakingId, toOpportunityId, @@ -235,24 +235,9 @@ export const ThorchainSaversOverview: React.FC = ({ .length, ) - const isRunepoolWithdrawUnlocked = useMemo(() => { - if (!isRunePoolUserStakingOpportunity(earnOpportunityData)) return - - const maturityDate = new Date(earnOpportunityData.maturity) - const currentDate = new Date() - - return currentDate >= maturityDate - }, [earnOpportunityData]) - - const runepoolSecondsLeftBeforeWithdrawal = useMemo(() => { - if (!isRunePoolUserStakingOpportunity(earnOpportunityData)) return 0 - - const maturityDate = new Date(earnOpportunityData.maturity) - const currentDate = new Date() - - const diffTime = maturityDate.getTime() - currentDate.getTime() - - return Math.ceil(diffTime / 1000) + const remainingLockupTime = useMemo(() => { + if (!isSaversUserStakingOpportunity(earnOpportunityData)) return 0 + return Math.max(earnOpportunityData.dateUnlocked - Math.floor(Date.now() / 1000), 0) }, [earnOpportunityData]) const accountFilter = useMemo(() => ({ accountId: maybeAccountId }), [maybeAccountId]) @@ -280,9 +265,7 @@ export const ThorchainSaversOverview: React.FC = ({ isHaltedDeposits, isDisabledDeposits, isDisabledWithdrawals, - isRunepoolWithdrawUnlocked, - isRunePool, - runepoolSecondsLeftBeforeWithdrawal, + remainingLockupTime, }: { isFull?: boolean hasPendingTxs?: boolean @@ -290,9 +273,7 @@ export const ThorchainSaversOverview: React.FC = ({ isHaltedDeposits?: boolean isDisabledDeposits?: boolean isDisabledWithdrawals?: boolean - isRunepoolWithdrawUnlocked?: boolean - isRunePool?: boolean - runepoolSecondsLeftBeforeWithdrawal?: number + remainingLockupTime?: number } = {}): DefiButtonProps[] => [ ...(isFull ? [] @@ -325,21 +306,27 @@ export const ThorchainSaversOverview: React.FC = ({ hasPendingTxs || hasPendingQueries || isDisabledWithdrawals || - (isRunePool && !isRunepoolWithdrawUnlocked), + Boolean(remainingLockupTime), toolTip: (() => { - if (isRunePool && !isRunepoolWithdrawUnlocked && runepoolSecondsLeftBeforeWithdrawal) - return translate('defi.modals.saversVaults.runePoolWithdrawLockedTitle', { - timeHuman: formatSecondsToDuration(runepoolSecondsLeftBeforeWithdrawal), - }) - if (isDisabledWithdrawals) + if (isDisabledWithdrawals) { return translate('defi.modals.saversVaults.disabledWithdrawTitle') - if (hasPendingTxs || hasPendingQueries) + } + + if (remainingLockupTime) { + return translate('defi.modals.saversVaults.withdrawLockedTitle', { + timeHuman: formatSecondsToDuration(remainingLockupTime), + }) + } + + if (hasPendingTxs || hasPendingQueries) { return translate('defi.modals.saversVaults.cannotWithdrawWhilePendingTx') + } })(), }, ], [translate], ) + const menu: DefiButtonProps[] = useMemo(() => { if (!earnOpportunityData) return [] @@ -350,23 +337,19 @@ export const ThorchainSaversOverview: React.FC = ({ isHaltedDeposits: isTradingActive === false, isDisabledDeposits: isThorchainSaversDepositEnabled === false, isDisabledWithdrawals: isThorchainSaversWithdrawalsEnabled === false, - isRunepoolWithdrawUnlocked, - runepoolSecondsLeftBeforeWithdrawal, - isRunePool, + remainingLockupTime, }) }, [ earnOpportunityData, - makeDefaultMenu, - opportunityMetadata?.isFull, - isHardCapReached, - hasPendingTxs, hasPendingQueries, - isTradingActive, + hasPendingTxs, + isHardCapReached, isThorchainSaversDepositEnabled, isThorchainSaversWithdrawalsEnabled, - isRunepoolWithdrawUnlocked, - runepoolSecondsLeftBeforeWithdrawal, - isRunePool, + isTradingActive, + makeDefaultMenu, + opportunityMetadata?.isFull, + remainingLockupTime, ]) const renderVaultCap = useMemo(() => { diff --git a/src/lib/utils/thorchain/constants.ts b/src/lib/utils/thorchain/constants.ts index 11610787a42..b1c0a1a5ace 100644 --- a/src/lib/utils/thorchain/constants.ts +++ b/src/lib/utils/thorchain/constants.ts @@ -6,9 +6,6 @@ export const THORCHAIN_AFFILIATE_NAME = 'ss' export const THORCHAIN_POOL_MODULE_ADDRESS = 'thor1g98cy3n9mmjrpn0sxmn63lztelera37n8n67c0' // Current blocktime as per https://thorchain.network/stats -export const THORCHAIN_BLOCK_TIME_SECONDS = '6.1' +export const THORCHAIN_BLOCK_TIME_SECONDS = '6' export const thorchainBlockTimeMs = bn(THORCHAIN_BLOCK_TIME_SECONDS).times(1000).toNumber() export const RUNEPOOL_DEPOSIT_MEMO = 'POOL+' - -// Number of blocks to withdraw from RUNEPool (https://gitlab.com/thorchain/thornode/-/blob/develop/constants/constants_v1.go#L117) -export const RUNEPOOL_MINIMUM_WITHDRAW_BLOCKS = 14400 * 90 diff --git a/src/lib/utils/thorchain/hooks/useThorchainMimirTimes.tsx b/src/lib/utils/thorchain/hooks/useThorchainMimirTimes.tsx new file mode 100644 index 00000000000..47e41697019 --- /dev/null +++ b/src/lib/utils/thorchain/hooks/useThorchainMimirTimes.tsx @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query' +import { reactQueries } from 'react-queries' + +import { thorchainBlockTimeMs } from '../constants' +import { selectLiquidityLockupTime, selectRunePoolMaturityTime } from '../selectors' + +export const useThorchainMimirTimes = () => { + return useQuery({ + ...reactQueries.thornode.mimir(), + staleTime: thorchainBlockTimeMs, + select: mimir => ({ + liquidityLockupTime: selectLiquidityLockupTime(mimir), + runePoolDepositMaturityTime: selectRunePoolMaturityTime(mimir), + }), + }) +} diff --git a/src/lib/utils/thorchain/lending/types.ts b/src/lib/utils/thorchain/lending/types.ts index a8a29a8561b..1a8ba22d6ae 100644 --- a/src/lib/utils/thorchain/lending/types.ts +++ b/src/lib/utils/thorchain/lending/types.ts @@ -128,5 +128,3 @@ export const isLendingQuoteOpen = ( export const isLendingQuoteClose = ( quote: LendingQuoteOpen | LendingQuoteClose | null, ): quote is LendingQuoteClose => Boolean(quote && 'quoteDebtRepaidAmountUserCurrency' in quote) - -export type ThorchainMimir = Record diff --git a/src/lib/utils/thorchain/lp/types.ts b/src/lib/utils/thorchain/lp/types.ts index 71925f02d1c..692c84a61c5 100644 --- a/src/lib/utils/thorchain/lp/types.ts +++ b/src/lib/utils/thorchain/lp/types.ts @@ -269,6 +269,7 @@ export type UserLpDataPosition = { side: AsymSide } status: PositionStatus + remainingLockupTime: number // DO NOT REMOVE these two. While it looks like this would be superfluous because we already have AccountId, that's not exactly true. // AccountId refers to the AccountId the position was *fetched* with/for, e.g ETH account 0 or ROON account 0. diff --git a/src/lib/utils/thorchain/selectors.ts b/src/lib/utils/thorchain/selectors.ts new file mode 100644 index 00000000000..4e181e7ee47 --- /dev/null +++ b/src/lib/utils/thorchain/selectors.ts @@ -0,0 +1,19 @@ +import { bnOrZero } from 'lib/bignumber/bignumber' + +import { THORCHAIN_BLOCK_TIME_SECONDS } from './constants' +import type { ThorchainMimir } from './types' + +export const selectLiquidityLockupTime = (mimirData: ThorchainMimir): number => { + const liquidityLockupBlocks = mimirData.LIQUIDITYLOCKUPBLOCKS as number | undefined + return Number(bnOrZero(liquidityLockupBlocks).times(THORCHAIN_BLOCK_TIME_SECONDS).toFixed(0)) +} + +export const selectRunePoolMaturityTime = (mimirData: ThorchainMimir): number => { + const runePoolDepositMaturityBlocks = mimirData.RUNEPOOLDEPOSITMATURITYBLOCKS as + | number + | undefined + + return Number( + bnOrZero(runePoolDepositMaturityBlocks).times(THORCHAIN_BLOCK_TIME_SECONDS).toFixed(0), + ) +} diff --git a/src/lib/utils/thorchain/types.ts b/src/lib/utils/thorchain/types.ts index 395cccf320e..7dca62e6873 100644 --- a/src/lib/utils/thorchain/types.ts +++ b/src/lib/utils/thorchain/types.ts @@ -5,3 +5,5 @@ export type ThorchainBlock = { time: string } } + +export type ThorchainMimir = Record diff --git a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx index 87d57c74a0b..522805d31c4 100644 --- a/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx +++ b/src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx @@ -35,7 +35,10 @@ import { useTranslate } from 'react-polyglot' import { reactQueries } from 'react-queries' import { useIsTradingActive } from 'react-queries/hooks/useIsTradingActive' import { useHistory } from 'react-router' -import { WarningAcknowledgement } from 'components/Acknowledgement/Acknowledgement' +import { + InfoAcknowledgement, + WarningAcknowledgement, +} from 'components/Acknowledgement/Acknowledgement' import { Amount } from 'components/Amount/Amount' import { TradeAssetSelect } from 'components/AssetSelection/AssetSelection' import { ButtonWalletPredicate } from 'components/ButtonWalletPredicate/ButtonWalletPredicate' @@ -71,8 +74,10 @@ import { import { THOR_PRECISION } from 'lib/utils/thorchain/constants' import { useSendThorTx } from 'lib/utils/thorchain/hooks/useSendThorTx' import { useThorchainFromAddress } from 'lib/utils/thorchain/hooks/useThorchainFromAddress' +import { useThorchainMimirTimes } from 'lib/utils/thorchain/hooks/useThorchainMimirTimes' import { estimateAddThorchainLiquidityPosition } from 'lib/utils/thorchain/lp' import { AsymSide, type LpConfirmedDepositQuote } from 'lib/utils/thorchain/lp/types' +import { formatSecondsToDuration } from 'lib/utils/time' import { useIsSweepNeededQuery } from 'pages/Lending/hooks/useIsSweepNeededQuery' import { usePools } from 'pages/ThorChainLP/queries/hooks/usePools' import { useUserLpData } from 'pages/ThorChainLP/queries/hooks/useUserLpData' @@ -183,6 +188,7 @@ export const AddLiquidityInput: React.FC = ({ string | undefined >() const [shouldShowWarningAcknowledgement, setShouldShowWarningAcknowledgement] = useState(false) + const [shouldShowInfoAcknowledgement, setShouldShowInfoAcknowledgement] = useState(false) // Virtual as in, these are the amounts if depositing symetrically. But a user may deposit asymetrically, so these are not the *actual* amounts // Keeping these as virtual amounts is useful from a UI perspective, as it allows rebalancing to automagically work when switching from sym. type, @@ -231,6 +237,9 @@ export const AddLiquidityInput: React.FC = ({ selectPortfolioAccountMetadataByAccountId(state, poolAssetAccountMetadataFilter), ) + const { data: thorchainMimirTimes, isLoading: isThorchainMimirTimesLoading } = + useThorchainMimirTimes() + const { data: poolAssetAccountAddress } = useThorchainFromAddress({ accountId: poolAssetAccountId, assetId: poolAsset?.assetId, @@ -1457,157 +1466,177 @@ export const AddLiquidityInput: React.FC = ({ return isUnsafeQuote ? setShouldShowWarningAcknowledgement(true) : handleSubmit() }, [handleSubmit, isUnsafeQuote]) + const handleClick = useCallback(() => { + thorchainMimirTimes?.liquidityLockupTime + ? setShouldShowInfoAcknowledgement(true) + : handleDepositSubmit() + }, [thorchainMimirTimes, handleDepositSubmit]) + if (!poolAsset || !runeAsset) return null return ( - - {renderHeader} - - {pairSelect} - - - {translate('pools.depositAmounts')} - - {!opportunityId && activeOpportunityId && ( - - )} - {tradeAssetInputs} + + {renderHeader} + + {pairSelect} + + + {translate('pools.depositAmounts')} + + {!opportunityId && activeOpportunityId && ( + + )} + {tradeAssetInputs} + - - - + + + + + {translate('common.slippage')} + + + + + + + + {translate('common.gasFee')} + + + + + + + + + + {bnOrZero(confirmedQuote?.feeAmountFiatUserCurrency).gt(0) && ( + {`(${confirmedQuote?.feeBps ?? 0} bps)`} + )} + + + + {bnOrZero(confirmedQuote?.feeAmountFiatUserCurrency).gt(0) ? ( + <> + + + + ) : ( + <> + + + + )} + + + + + - - {translate('common.slippage')} - - - - - - - - {translate('common.gasFee')} - - - - - - - - - - {bnOrZero(confirmedQuote?.feeAmountFiatUserCurrency).gt(0) && ( - {`(${confirmedQuote?.feeBps ?? 0} bps)`} - )} - - - - {bnOrZero(confirmedQuote?.feeAmountFiatUserCurrency).gt(0) ? ( - <> - - - - ) : ( - <> - - - - )} - - - - - - - {incompleteAlert} - {maybeOpportunityNotSupportedExplainer} - {maybeAlert} - - - {confirmCopy} - - - - + (isSweepNeeded === undefined && isSweepNeededLoading && !isApprovalRequired) || + (runeTxFeeCryptoBaseUnit === undefined && isEstimatedPoolAssetFeesDataLoading) || + isThorchainMimirTimesLoading + } + onClick={handleClick} + > + {confirmCopy} + + + + + ) } diff --git a/src/pages/ThorChainLP/components/RemoveLiquidity/RemoveLiquidityInput.tsx b/src/pages/ThorChainLP/components/RemoveLiquidity/RemoveLiquidityInput.tsx index 42c6a1bcc65..e22c1232665 100644 --- a/src/pages/ThorChainLP/components/RemoveLiquidity/RemoveLiquidityInput.tsx +++ b/src/pages/ThorChainLP/components/RemoveLiquidity/RemoveLiquidityInput.tsx @@ -57,6 +57,7 @@ import { estimateRemoveThorchainLiquidityPosition } from 'lib/utils/thorchain/lp import type { LpConfirmedWithdrawalQuote, UserLpDataPosition } from 'lib/utils/thorchain/lp/types' import { AsymSide } from 'lib/utils/thorchain/lp/types' import { isLpConfirmedDepositQuote } from 'lib/utils/thorchain/lp/utils' +import { formatSecondsToDuration } from 'lib/utils/time' import { useIsSweepNeededQuery } from 'pages/Lending/hooks/useIsSweepNeededQuery' import { usePool } from 'pages/ThorChainLP/queries/hooks/usePool' import { useUserLpData } from 'pages/ThorChainLP/queries/hooks/useUserLpData' @@ -904,6 +905,10 @@ export const RemoveLiquidityInput: React.FC = ({ if (isUnsupportedWithdraw) return translate('common.unsupportedNetwork') if (isTradingActive === false) return translate('common.poolHalted') if (!isThorchainLpWithdrawEnabled) return translate('common.poolDisabled') + if (position?.remainingLockupTime) + return translate('defi.liquidityLocked', { + time: formatSecondsToDuration(position.remainingLockupTime), + }) if (poolAssetFeeAsset && !hasEnoughPoolAssetFeeAssetBalanceForTx) return translate('modals.send.errors.notEnoughNativeToken', { asset: poolAssetFeeAsset.symbol, @@ -920,6 +925,7 @@ export const RemoveLiquidityInput: React.FC = ({ isTradingActive, isUnsupportedWithdraw, poolAssetFeeAsset, + position, runeAsset, translate, ]) @@ -1083,7 +1089,8 @@ export const RemoveLiquidityInput: React.FC = ({ (isEstimatedPoolAssetFeesDataError && withdrawType !== AsymSide.Rune) || (isEstimatedRuneFeesDataError && withdrawType !== AsymSide.Asset) || !validInputAmount || - isSweepNeededLoading + isSweepNeededLoading || + Boolean(position?.remainingLockupTime) } isLoading={ isTradingActiveLoading || diff --git a/src/pages/ThorChainLP/queries/hooks/useAllUserLpData.ts b/src/pages/ThorChainLP/queries/hooks/useAllUserLpData.ts index 2968424bb21..351948a29cd 100644 --- a/src/pages/ThorChainLP/queries/hooks/useAllUserLpData.ts +++ b/src/pages/ThorChainLP/queries/hooks/useAllUserLpData.ts @@ -6,6 +6,7 @@ import { useQueries, useQuery, useQueryClient } from '@tanstack/react-query' import { useMemo } from 'react' import { reactQueries } from 'react-queries' import { isSome } from 'lib/utils' +import { useThorchainMimirTimes } from 'lib/utils/thorchain/hooks/useThorchainMimirTimes' import type { Position, UserLpDataPosition } from 'lib/utils/thorchain/lp/types' import { findAccountsByAssetId } from 'state/slices/portfolioSlice/utils' import { @@ -35,6 +36,9 @@ export const useAllUserLpData = (): UseQueryResult { if (!pools) return [] + if (!thorchainMimirTimes) return [] return pools .map(pool => { @@ -53,7 +58,7 @@ export const useAllUserLpData = (): UseQueryResult { @@ -84,6 +89,7 @@ export const useAllUserLpData = (): UseQueryResult { const asset = assets[assetId] if (!asset) return @@ -84,6 +87,12 @@ export const getUserLpDataPosition = ({ return { isPending, isIncomplete: false, incompleteAsset: undefined } })() + const remainingLockupTime = (() => { + const dateNow = Math.floor(Date.now() / 1000) + const dateUnlocked = Number(position.dateLastAdded) + liquidityLockupTime + return Math.max(dateUnlocked - dateNow, 0) + })() + return { name, dateFirstAdded: position.dateFirstAdded, @@ -106,6 +115,7 @@ export const getUserLpDataPosition = ({ assetId, runeAddress: position.runeAddress, assetAddress: position.assetAddress, + remainingLockupTime, } } @@ -134,6 +144,9 @@ export const useUserLpData = ({ selectMarketDataByAssetIdUserCurrency(state, thorchainAssetId), ) + const { data: thorchainMimirTimes, isSuccess: isThorchainMimirTimesSuccess } = + useThorchainMimirTimes() + const { data: pool } = useQuery({ ...reactQueries.thornode.poolData(assetId), // @lukemorales/query-key-factory only returns queryFn and queryKey - all others will be ignored in the returned object @@ -164,6 +177,7 @@ export const useUserLpData = ({ }, select: (positions: Position[] | undefined) => { if (!pool) return null + if (!thorchainMimirTimes) return null return (positions ?? []) .map(position => @@ -174,10 +188,11 @@ export const useUserLpData = ({ pool, position, runePrice: runeMarketData.price, + liquidityLockupTime: thorchainMimirTimes.liquidityLockupTime, }), ) .filter(isSome) }, - enabled: Boolean(assetId && currentWalletId && pool), + enabled: Boolean(assetId && currentWalletId && pool && isThorchainMimirTimesSuccess), }) } diff --git a/src/react-queries/queries/thornode.ts b/src/react-queries/queries/thornode.ts index 6bfa168a6fc..0b0b49406cb 100644 --- a/src/react-queries/queries/thornode.ts +++ b/src/react-queries/queries/thornode.ts @@ -9,8 +9,7 @@ import { thorService } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapp import { Ok } from '@sniptt/monads' import axios from 'axios' import { getConfig } from 'config' -import type { ThorchainMimir } from 'lib/utils/thorchain/lending/types' -import type { ThorchainBlock } from 'lib/utils/thorchain/types' +import type { ThorchainBlock, ThorchainMimir } from 'lib/utils/thorchain/types' const thornodeUrl = getConfig().REACT_APP_THORCHAIN_NODE_URL diff --git a/src/state/slices/opportunitiesSlice/resolvers/thorchainsavers/index.ts b/src/state/slices/opportunitiesSlice/resolvers/thorchainsavers/index.ts index 8a8bf4e16db..7d5c47a895c 100644 --- a/src/state/slices/opportunitiesSlice/resolvers/thorchainsavers/index.ts +++ b/src/state/slices/opportunitiesSlice/resolvers/thorchainsavers/index.ts @@ -1,5 +1,5 @@ import type { AssetId } from '@shapeshiftoss/caip' -import { fromAccountId, thorchainAssetId } from '@shapeshiftoss/caip' +import { thorchainAssetId } from '@shapeshiftoss/caip' import type { ThornodePoolResponse } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/types' import { poolAssetIdToAssetId } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers' import { isSome, toBaseUnit } from '@shapeshiftoss/utils' @@ -10,9 +10,10 @@ import { queryClient } from 'context/QueryClientProvider/queryClient' import { bn, bnOrZero } from 'lib/bignumber/bignumber' import { fromThorBaseUnit } from 'lib/utils/thorchain' import { - RUNEPOOL_MINIMUM_WITHDRAW_BLOCKS, - thorchainBlockTimeMs, -} from 'lib/utils/thorchain/constants' + selectLiquidityLockupTime, + selectRunePoolMaturityTime, +} from 'lib/utils/thorchain/selectors' +import type { ThorchainMimir } from 'lib/utils/thorchain/types' import { selectAssetById } from 'state/slices/assetsSlice/selectors' import { selectMarketDataByAssetIdUserCurrency } from 'state/slices/marketDataSlice/selectors' import { selectFeatureFlags } from 'state/slices/preferencesSlice/selectors' @@ -33,6 +34,7 @@ import type { OpportunitiesUserDataResolverInput, } from '../types' import type { + MidgardSaverResponse, ThorchainRunepoolInformationResponseSuccess, ThorchainRunepoolMemberPositionResponse, ThorchainRunepoolReservePositionsResponse, @@ -304,6 +306,13 @@ export const thorchainSaversStakingOpportunitiesUserDataResolver = async ({ } try { + const { data: mimir } = await axios.get( + `${getConfig().REACT_APP_THORCHAIN_NODE_URL}/lcd/thorchain/mimir`, + ) + + const liquidityLockupTime = selectLiquidityLockupTime(mimir) + const runePoolDepositMaturityTime = selectRunePoolMaturityTime(mimir) + for (const stakingOpportunityId of opportunityIds) { const asset = selectAssetById(state, stakingOpportunityId) if (!asset) @@ -327,7 +336,7 @@ export const thorchainSaversStakingOpportunitiesUserDataResolver = async ({ continue } - const { asset_deposit_value, asset_redeem_value } = accountPosition + const { asset_deposit_value, asset_redeem_value, asset_address } = accountPosition const stakedAmountCryptoBaseUnit = fromThorBaseUnit(asset_deposit_value).times( bn(10).pow(asset.precision), @@ -341,23 +350,29 @@ export const thorchainSaversStakingOpportunitiesUserDataResolver = async ({ stakedAmountCryptoBaseUnitIncludeRewards.minus(stakedAmountCryptoBaseUnit).toFixed(), ] - const maybeMaturity = await (async () => { - if (stakingOpportunityId !== thorchainAssetId) return {} - + const dateUnlocked = await (async () => { try { - const { data: userPosition } = await axios.get<[ThorchainRunepoolMemberPositionResponse]>( - `${getConfig().REACT_APP_MIDGARD_URL}/runepool/${fromAccountId(accountId).account}`, + if (stakingOpportunityId === thorchainAssetId) { + const { data } = await axios.get<[ThorchainRunepoolMemberPositionResponse]>( + `${getConfig().REACT_APP_MIDGARD_URL}/runepool/${asset_address}`, + ) + + return bnOrZero(data[0].dateLastAdded).plus(runePoolDepositMaturityTime).toNumber() + } + + const { data } = await axios.get( + `${getConfig().REACT_APP_MIDGARD_URL}/saver/${asset_address}`, ) - const maturity = - userPosition[0].dateLastAdded + thorchainBlockTimeMs * RUNEPOOL_MINIMUM_WITHDRAW_BLOCKS + const dateLastAdded = data.pools.find(({ pool }) => pool === accountPosition.asset) + ?.dateLastAdded - return { maturity } + return dateLastAdded + ? bnOrZero(dateLastAdded).plus(liquidityLockupTime).toNumber() + : undefined } catch (error) { - if (axios.isAxiosError(error) && error.response?.status === 404) { - return { maturity: undefined } - } - throw new Error('Error fetching RUNEpool maturity') + if (axios.isAxiosError(error) && error.response?.status === 404) return + throw new Error('Error fetching savers date last added') } })() @@ -366,7 +381,7 @@ export const thorchainSaversStakingOpportunitiesUserDataResolver = async ({ userStakingId, stakedAmountCryptoBaseUnit: stakedAmountCryptoBaseUnit.toFixed(), rewardsCryptoBaseUnit: { amounts: rewardsAmountsCryptoBaseUnit, claimable: false }, - ...maybeMaturity, + dateUnlocked, } } diff --git a/src/state/slices/opportunitiesSlice/resolvers/thorchainsavers/types.ts b/src/state/slices/opportunitiesSlice/resolvers/thorchainsavers/types.ts index d5799a4f7cd..2dd25012ba0 100644 --- a/src/state/slices/opportunitiesSlice/resolvers/thorchainsavers/types.ts +++ b/src/state/slices/opportunitiesSlice/resolvers/thorchainsavers/types.ts @@ -46,6 +46,20 @@ export type MidgardPoolResponse = { volume24h: string } +export type MidgardSaverResponse = { + pools: { + assetAdded: string + assetAddress: string + assetDeposit: string + assetRedeem: string + assetWithdrawn: string + dateFirstAdded: string + dateLastAdded: string + pool: string + saverUnits: string + }[] +} + export type ThorchainSaverPositionResponse = { asset: string asset_address: string diff --git a/src/state/slices/opportunitiesSlice/types.ts b/src/state/slices/opportunitiesSlice/types.ts index 48f3fe6a7d4..77058ca62e2 100644 --- a/src/state/slices/opportunitiesSlice/types.ts +++ b/src/state/slices/opportunitiesSlice/types.ts @@ -101,13 +101,13 @@ export type UserStakingOpportunityBase = { } } -export type RunepoolUserStakingOpportunity = { - maturity: number +export type SaversUserStakingOpportunity = { + dateUnlocked: number } & UserStakingOpportunityBase export type UserStakingOpportunity = | UserStakingOpportunityBase - | RunepoolUserStakingOpportunity + | SaversUserStakingOpportunity | CosmosSdkStakingSpecificUserStakingOpportunity | FoxySpecificUserStakingOpportunity diff --git a/src/state/slices/opportunitiesSlice/utils/index.ts b/src/state/slices/opportunitiesSlice/utils/index.ts index af9af65241a..48e6ab053f5 100644 --- a/src/state/slices/opportunitiesSlice/utils/index.ts +++ b/src/state/slices/opportunitiesSlice/utils/index.ts @@ -1,11 +1,5 @@ import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' -import { - fromAccountId, - fromAssetId, - thorchainAssetId, - toAccountId, - toAssetId, -} from '@shapeshiftoss/caip' +import { fromAccountId, fromAssetId, toAccountId, toAssetId } from '@shapeshiftoss/caip' import type { Asset, MarketData } from '@shapeshiftoss/types' import { bn, bnOrZero } from 'lib/bignumber/bignumber' import { fromBaseUnit } from 'lib/math' @@ -19,7 +13,7 @@ import type { FoxySpecificUserStakingOpportunity } from '../resolvers/foxy/types import type { OpportunityId, OpportunityMetadataBase, - RunepoolUserStakingOpportunity, + SaversUserStakingOpportunity, StakingEarnOpportunityType, StakingId, UserStakingId, @@ -234,12 +228,8 @@ export const getOpportunityAccessor: GetOpportunityAccessor = ({ provider, type return 'underlyingAssetIds' } -export const isRunePoolUserStakingOpportunity = ( +export const isSaversUserStakingOpportunity = ( opportunity: StakingEarnOpportunityType | undefined, -): opportunity is StakingEarnOpportunityType & RunepoolUserStakingOpportunity => { - return Boolean( - opportunity && - opportunity.provider === DefiProvider.ThorchainSavers && - opportunity.id === thorchainAssetId, - ) +): opportunity is StakingEarnOpportunityType & SaversUserStakingOpportunity => { + return Boolean(opportunity && opportunity.provider === DefiProvider.ThorchainSavers) }