Skip to content

Commit

Permalink
handle liquidity lockup for lp add and remove
Browse files Browse the repository at this point in the history
  • Loading branch information
kaladinlight committed Sep 17, 2024
1 parent a16fcd5 commit ec435a8
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 144 deletions.
2 changes: 2 additions & 0 deletions src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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} in which you will be unable to withdraw your liquidity.",
"liquidityLocked": "Withdrawing liquidity is locked for %{time}",
"stakingVaults": "Staking & Vaults",
"farming": "Farming",
"totalEarningBalance": "Total Earning Balance",
Expand Down
1 change: 1 addition & 0 deletions src/lib/utils/thorchain/lp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
310 changes: 173 additions & 137 deletions src/pages/ThorChainLP/components/AddLiquidity/AddLiquidityInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
} from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/utils/poolAssetHelpers/poolAssetHelpers'
import type { Asset, MarketData } from '@shapeshiftoss/types'
import { TxStatus } from '@shapeshiftoss/unchained-client'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import BigNumber from 'bignumber.js'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { BiErrorCircle, BiSolidBoltCircle } from 'react-icons/bi'
Expand All @@ -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'
Expand Down Expand Up @@ -68,11 +71,16 @@ import {
isSome,
isToken,
} from 'lib/utils'
import { THOR_PRECISION } from 'lib/utils/thorchain/constants'
import {
THOR_PRECISION,
THORCHAIN_BLOCK_TIME_SECONDS,
thorchainBlockTimeMs,
} from 'lib/utils/thorchain/constants'
import { useSendThorTx } from 'lib/utils/thorchain/hooks/useSendThorTx'
import { useThorchainFromAddress } from 'lib/utils/thorchain/hooks/useThorchainFromAddress'
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'
Expand Down Expand Up @@ -183,6 +191,7 @@ export const AddLiquidityInput: React.FC<AddLiquidityInputProps> = ({
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,
Expand Down Expand Up @@ -231,6 +240,15 @@ export const AddLiquidityInput: React.FC<AddLiquidityInputProps> = ({
selectPortfolioAccountMetadataByAccountId(state, poolAssetAccountMetadataFilter),
)

const liquidityLockupTime = useQuery({
...reactQueries.thornode.mimir(),
staleTime: thorchainBlockTimeMs,
select: mimirData => {
const liquidityLockupBlocks = mimirData.LIQUIDITYLOCKUPBLOCKS as number | undefined
return Number(bnOrZero(liquidityLockupBlocks).times(THORCHAIN_BLOCK_TIME_SECONDS).toFixed(0))
},
})

const { data: poolAssetAccountAddress } = useThorchainFromAddress({
accountId: poolAssetAccountId,
assetId: poolAsset?.assetId,
Expand Down Expand Up @@ -1457,157 +1475,175 @@ export const AddLiquidityInput: React.FC<AddLiquidityInputProps> = ({
return isUnsafeQuote ? setShouldShowWarningAcknowledgement(true) : handleSubmit()
}, [handleSubmit, isUnsafeQuote])

const handleClick = useCallback(() => {
liquidityLockupTime.data ? setShouldShowInfoAcknowledgement(true) : handleDepositSubmit()
}, [liquidityLockupTime, handleDepositSubmit])

if (!poolAsset || !runeAsset) return null

return (
<SlideTransition>
<WarningAcknowledgement
message={translate('warningAcknowledgement.highSlippageDeposit', {
slippagePercentage: bnOrZero(slippageDecimalPercentage).times(100).toFixed(2).toString(),
<InfoAcknowledgement
message={translate('defi.liquidityLockupWarning', {
time: formatSecondsToDuration(liquidityLockupTime.data ?? 0),
})}
onAcknowledge={handleSubmit}
shouldShowAcknowledgement={shouldShowWarningAcknowledgement}
setShouldShowAcknowledgement={setShouldShowWarningAcknowledgement}
onAcknowledge={handleDepositSubmit}
shouldShowAcknowledgement={shouldShowInfoAcknowledgement}
setShouldShowAcknowledgement={setShouldShowInfoAcknowledgement}
>
{renderHeader}
<Stack divider={divider} spacing={4} pb={4}>
{pairSelect}
<Stack>
<FormLabel mb={0} px={6} fontSize='sm'>
{translate('pools.depositAmounts')}
</FormLabel>
{!opportunityId && activeOpportunityId && (
<LpType
assetId={poolAsset.assetId}
opportunityId={activeOpportunityId}
onAsymSideChange={handleAsymSideChange}
isDeposit={true}
hasAsymRunePosition={hasAsymRunePosition}
hasAsymAssetPosition={hasAsymAssetPosition}
hasSymPosition={hasSymPosition}
amountsByPosition={amountsByPosition}
/>
)}
{tradeAssetInputs}
<WarningAcknowledgement
message={translate('warningAcknowledgement.highSlippageDeposit', {
slippagePercentage: bnOrZero(slippageDecimalPercentage)
.times(100)
.toFixed(2)
.toString(),
})}
onAcknowledge={handleSubmit}
shouldShowAcknowledgement={shouldShowWarningAcknowledgement}
setShouldShowAcknowledgement={setShouldShowWarningAcknowledgement}
>
{renderHeader}
<Stack divider={divider} spacing={4} pb={4}>
{pairSelect}
<Stack>
<FormLabel mb={0} px={6} fontSize='sm'>
{translate('pools.depositAmounts')}
</FormLabel>
{!opportunityId && activeOpportunityId && (
<LpType
assetId={poolAsset.assetId}
opportunityId={activeOpportunityId}
onAsymSideChange={handleAsymSideChange}
isDeposit={true}
hasAsymRunePosition={hasAsymRunePosition}
hasAsymAssetPosition={hasAsymAssetPosition}
hasSymPosition={hasSymPosition}
amountsByPosition={amountsByPosition}
/>
)}
{tradeAssetInputs}
</Stack>
</Stack>
</Stack>
<Collapse in={hasUserEnteredValue}>
<PoolSummary
assetId={poolAsset.assetId}
runePerAsset={runePerAsset}
shareOfPoolDecimalPercent={shareOfPoolDecimalPercent}
isLoading={isSlippageLoading}
/>
<Collapse in={hasUserEnteredValue}>
<PoolSummary
assetId={poolAsset.assetId}
runePerAsset={runePerAsset}
shareOfPoolDecimalPercent={shareOfPoolDecimalPercent}
isLoading={isSlippageLoading}
/>
<CardFooter
borderTopWidth={1}
borderColor='border.subtle'
flexDir='column'
gap={4}
px={6}
py={4}
bg='background.surface.raised.accent'
>
<Row fontSize='sm' fontWeight='medium'>
<Row.Label>{translate('common.slippage')}</Row.Label>
<Row.Value>
<Skeleton isLoaded={!isSlippageLoading}>
<Amount.Fiat value={slippageFiatUserCurrency ?? ''} />
</Skeleton>
</Row.Value>
</Row>
<Row fontSize='sm' fontWeight='medium'>
<Row.Label>{translate('common.gasFee')}</Row.Label>
<Row.Value>
<Skeleton isLoaded={Boolean(confirmedQuote)}>
<Amount.Fiat value={confirmedQuote?.totalGasFeeFiatUserCurrency ?? 0} />
</Skeleton>
</Row.Value>
</Row>
<Row fontSize='sm' fontWeight='medium' isLoading={Boolean(!confirmedQuote)}>
<Row.Label display='flex'>
<Text translation={shapeshiftFeeTranslation} />
{bnOrZero(confirmedQuote?.feeAmountFiatUserCurrency).gt(0) && (
<RawText>{`(${confirmedQuote?.feeBps ?? 0} bps)`}</RawText>
)}
</Row.Label>
<Row.Value onClick={toggleFeeModal} _hover={shapeShiftFeeModalRowHover}>
<Flex alignItems='center' gap={2}>
{bnOrZero(confirmedQuote?.feeAmountFiatUserCurrency).gt(0) ? (
<>
<Amount.Fiat value={confirmedQuote?.feeAmountFiatUserCurrency ?? 0} />
<QuestionIcon />
</>
) : (
<>
<Text translation='trade.free' fontWeight='semibold' color={greenColor} />
<QuestionIcon color={greenColor} />
</>
)}
</Flex>
</Row.Value>
</Row>
</CardFooter>
</Collapse>
<CardFooter
borderTopWidth={1}
borderColor='border.subtle'
flexDir='column'
gap={4}
px={6}
py={4}
bg='background.surface.raised.accent'
borderBottomRadius='xl'
>
<Row fontSize='sm' fontWeight='medium'>
<Row.Label>{translate('common.slippage')}</Row.Label>
<Row.Value>
<Skeleton isLoaded={!isSlippageLoading}>
<Amount.Fiat value={slippageFiatUserCurrency ?? ''} />
</Skeleton>
</Row.Value>
</Row>
<Row fontSize='sm' fontWeight='medium'>
<Row.Label>{translate('common.gasFee')}</Row.Label>
<Row.Value>
<Skeleton isLoaded={Boolean(confirmedQuote)}>
<Amount.Fiat value={confirmedQuote?.totalGasFeeFiatUserCurrency ?? 0} />
</Skeleton>
</Row.Value>
</Row>
<Row fontSize='sm' fontWeight='medium' isLoading={Boolean(!confirmedQuote)}>
<Row.Label display='flex'>
<Text translation={shapeshiftFeeTranslation} />
{bnOrZero(confirmedQuote?.feeAmountFiatUserCurrency).gt(0) && (
<RawText>{`(${confirmedQuote?.feeBps ?? 0} bps)`}</RawText>
)}
</Row.Label>
<Row.Value onClick={toggleFeeModal} _hover={shapeShiftFeeModalRowHover}>
<Flex alignItems='center' gap={2}>
{bnOrZero(confirmedQuote?.feeAmountFiatUserCurrency).gt(0) ? (
<>
<Amount.Fiat value={confirmedQuote?.feeAmountFiatUserCurrency ?? 0} />
<QuestionIcon />
</>
) : (
<>
<Text translation='trade.free' fontWeight='semibold' color={greenColor} />
<QuestionIcon color={greenColor} />
</>
)}
</Flex>
</Row.Value>
</Row>
</CardFooter>
</Collapse>
<CardFooter
borderTopWidth={1}
borderColor='border.subtle'
flexDir='column'
gap={4}
px={6}
bg='background.surface.raised.accent'
borderBottomRadius='xl'
>
{incompleteAlert}
{maybeOpportunityNotSupportedExplainer}
{maybeAlert}

<ButtonWalletPredicate
isValidWallet={Boolean(walletSupportsOpportunity)}
mx={-2}
size='lg'
colorScheme={errorCopy ? 'red' : 'blue'}
isDisabled={Boolean(
disabledSymDepositAfterRune ||
isTradingActive === false ||
!isThorchainLpDepositEnabled ||
!confirmedQuote ||
{incompleteAlert}
{maybeOpportunityNotSupportedExplainer}
{maybeAlert}

<ButtonWalletPredicate
isValidWallet={Boolean(walletSupportsOpportunity)}
mx={-2}
size='lg'
colorScheme={errorCopy ? 'red' : 'blue'}
isDisabled={Boolean(
disabledSymDepositAfterRune ||
isTradingActive === false ||
!isThorchainLpDepositEnabled ||
!confirmedQuote ||
isVotingPowerLoading ||
!hasEnoughAssetBalance ||
!hasEnoughRuneBalance ||
isApprovalTxPending ||
(isSweepNeededEnabled && isSweepNeeded === undefined && !isApprovalRequired) ||
isSweepNeededError ||
isEstimatedPoolAssetFeesDataError ||
isEstimatedRuneFeesDataError ||
isSmartContractAccountAddress ||
bnOrZero(actualAssetDepositAmountCryptoPrecision)
.plus(bnOrZero(actualRuneDepositAmountCryptoPrecision))
.isZero() ||
notEnoughFeeAssetError ||
notEnoughRuneFeeError,
)}
isLoading={
(poolAssetTxFeeCryptoBaseUnit === undefined &&
isEstimatedPoolAssetFeesDataLoading) ||
isVotingPowerLoading ||
!hasEnoughAssetBalance ||
!hasEnoughRuneBalance ||
isTradingActiveLoading ||
isSmartContractAccountAddressLoading ||
allowanceCryptoBaseUnitResult.isLoading ||
isApprovalTxPending ||
(isSweepNeededEnabled && isSweepNeeded === undefined && !isApprovalRequired) ||
isSweepNeededError ||
isEstimatedPoolAssetFeesDataError ||
isEstimatedRuneFeesDataError ||
isSmartContractAccountAddress ||
bnOrZero(actualAssetDepositAmountCryptoPrecision)
.plus(bnOrZero(actualRuneDepositAmountCryptoPrecision))
.isZero() ||
notEnoughFeeAssetError ||
notEnoughRuneFeeError,
)}
isLoading={
(poolAssetTxFeeCryptoBaseUnit === undefined && isEstimatedPoolAssetFeesDataLoading) ||
isVotingPowerLoading ||
isTradingActiveLoading ||
isSmartContractAccountAddressLoading ||
allowanceCryptoBaseUnitResult.isLoading ||
isApprovalTxPending ||
(isSweepNeeded === undefined && isSweepNeededLoading && !isApprovalRequired) ||
(runeTxFeeCryptoBaseUnit === undefined && isEstimatedPoolAssetFeesDataLoading)
}
onClick={handleDepositSubmit}
>
{confirmCopy}
</ButtonWalletPredicate>
</CardFooter>
<FeeModal
isOpen={showFeeModal}
onClose={toggleFeeModal}
inputAmountUsd={confirmedQuote?.totalAmountUsd}
feeModel='THORCHAIN_LP'
/>
</WarningAcknowledgement>
(isSweepNeeded === undefined && isSweepNeededLoading && !isApprovalRequired) ||
(runeTxFeeCryptoBaseUnit === undefined && isEstimatedPoolAssetFeesDataLoading) ||
liquidityLockupTime.isLoading
}
onClick={handleClick}
>
{confirmCopy}
</ButtonWalletPredicate>
</CardFooter>
<FeeModal
isOpen={showFeeModal}
onClose={toggleFeeModal}
inputAmountUsd={confirmedQuote?.totalAmountUsd}
feeModel='THORCHAIN_LP'
/>
</WarningAcknowledgement>
</InfoAcknowledgement>
</SlideTransition>
)
}
Loading

0 comments on commit ec435a8

Please sign in to comment.