Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: thorchain savers withdraw sweep #5640

Merged
merged 14 commits into from
Nov 20, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,8 @@ import {
getErc20Allowance,
getFees,
} from 'lib/utils/evm'
import type { GetThorchainSaversDepositQuoteQueryKey } from 'lib/utils/thorchain/hooks/useGetThorchainSaversDepositQuoteQuery'
import {
queryFn as getThorchainSaversDepositQuoteQueryFn,
useGetThorchainSaversDepositQuoteQuery,
} from 'lib/utils/thorchain/hooks/useGetThorchainSaversDepositQuoteQuery'
import { fetchHasEnoughBalanceForTxPlusFeesPlusSweep } from 'lib/utils/thorchain'
import { useGetThorchainSaversDepositQuoteQuery } from 'lib/utils/thorchain/hooks/useGetThorchainSaversDepositQuoteQuery'
import type { EstimatedFeesQueryKey } from 'pages/Lending/hooks/useGetEstimatedFeesQuery'
import {
queryFn as getEstimatedFeesQueryFn,
Expand Down Expand Up @@ -450,31 +447,6 @@ export const Deposit: React.FC<DepositProps> = ({
const { data: isSweepNeeded, isLoading: isSweepNeededLoading } =
useIsSweepNeededQuery(isSweepNeededArgs)

const getHasEnoughBalanceForTxPlusFeesPlusSweep = useCallback(
({
balanceCryptoBaseUnit,
amountCryptoPrecision,
txFeeCryptoBaseUnit,
precision,
sweepTxFeeCryptoBaseUnit,
}: {
balanceCryptoBaseUnit: string
amountCryptoPrecision: string
txFeeCryptoBaseUnit: string
precision: number
sweepTxFeeCryptoBaseUnit: string
}) => {
const balanceCryptoBaseUnitBn = bnOrZero(balanceCryptoBaseUnit)
if (balanceCryptoBaseUnitBn.isZero()) return false

return bnOrZero(amountCryptoPrecision)
.plus(fromBaseUnit(txFeeCryptoBaseUnit, precision ?? 0))
.plus(fromBaseUnit(sweepTxFeeCryptoBaseUnit, precision ?? 0))
.lte(fromBaseUnit(balanceCryptoBaseUnitBn, precision ?? 0))
},
[],
)

const handleContinue = useCallback(
async (formValues: DepositValues) => {
if (
Expand Down Expand Up @@ -593,131 +565,10 @@ export const Deposit: React.FC<DepositProps> = ({
browserHistory.goBack()
}, [browserHistory])

// TODO(gomes): abstract me into a pure method when implementing sweep for withdraw, and handle non-UTXO chains
const fetchHasEnoughBalanceForTxPlusFeesPlusSweep = useCallback(
async (valueCryptoPrecision: string) => {
const isUtxoChain = isUtxoChainId(asset.chainId)
const estimateFeesQueryEnabled = Boolean(fromAddress && accountId && isUtxoChain)

const amountCryptoBaseUnit = toBaseUnit(valueCryptoPrecision, asset.precision)

const thorchainSaversDepositQuoteQueryKey: GetThorchainSaversDepositQuoteQueryKey = [
'thorchainSaversDepositQuote',
{ asset, amountCryptoBaseUnit },
]

const _thorchainSaversDepositQuote = await queryClient.fetchQuery({
queryKey: thorchainSaversDepositQuoteQueryKey,
queryFn: getThorchainSaversDepositQuoteQueryFn,
})

const estimatedFeesQueryArgs = {
estimateFeesInput: {
cryptoAmount: valueCryptoPrecision,
assetId,
to: _thorchainSaversDepositQuote?.inbound_address ?? '',
sendMax: false,
accountId: accountId ?? '',
contractAddress: undefined,
},
asset,
assetMarketData: marketData,
enabled: estimateFeesQueryEnabled,
}

const estimatedFeesQueryKey: EstimatedFeesQueryKey = ['estimateFees', estimatedFeesQueryArgs]

const _estimatedFeesData = estimateFeesQueryEnabled
? await queryClient.fetchQuery({
queryKey: estimatedFeesQueryKey,
queryFn: getEstimatedFeesQueryFn,
})
: undefined

const _hasEnoughBalanceForTxPlusFees = getHasEnoughBalanceForTxPlusFees({
precision: asset.precision,
balanceCryptoBaseUnit,
amountCryptoPrecision: valueCryptoPrecision,
txFeeCryptoBaseUnit: _estimatedFeesData?.txFeeCryptoBaseUnit ?? '',
})

const isSweepNeededQueryEnabled = Boolean(
bnOrZero(valueCryptoPrecision).gt(0) &&
_estimatedFeesData &&
_hasEnoughBalanceForTxPlusFees,
)

const isSweepNeededQueryArgs = {
assetId,
address: fromAddress,
txFeeCryptoBaseUnit: _estimatedFeesData?.txFeeCryptoBaseUnit ?? '0',
amountCryptoBaseUnit,
}

const isSweepNeededQueryKey: IsSweepNeededQueryKey = ['isSweepNeeded', isSweepNeededQueryArgs]

const _isSweepNeeded = isSweepNeededQueryEnabled
? await queryClient.fetchQuery({
queryKey: isSweepNeededQueryKey,
queryFn: isSweepNeededQueryFn,
})
: undefined

const isEstimateSweepFeesQueryEnabled = Boolean(_isSweepNeeded && accountId && isUtxoChain)

const estimatedSweepFeesQueryArgs = {
asset,
assetMarketData: marketData,
estimateFeesInput: {
cryptoAmount: '0',
assetId,
to: fromAddress ?? '',
sendMax: true,
accountId: accountId ?? '',
contractAddress: undefined,
},
enabled: isEstimateSweepFeesQueryEnabled,
}

const estimatedSweepFeesQueryKey: EstimatedFeesQueryKey = [
'estimateFees',
estimatedSweepFeesQueryArgs,
]

const _estimatedSweepFeesData = isEstimateSweepFeesQueryEnabled
? await queryClient.fetchQuery({
queryKey: estimatedSweepFeesQueryKey,
queryFn: getEstimatedFeesQueryFn,
})
: undefined

const hasEnoughBalanceForTxPlusFeesPlusSweep =
bnOrZero(balanceCryptoBaseUnit).gt(0) &&
getHasEnoughBalanceForTxPlusFeesPlusSweep({
precision: asset.precision,
balanceCryptoBaseUnit,
amountCryptoPrecision: valueCryptoPrecision,
txFeeCryptoBaseUnit: _estimatedFeesData?.txFeeCryptoBaseUnit ?? '0',
sweepTxFeeCryptoBaseUnit: _estimatedSweepFeesData?.txFeeCryptoBaseUnit ?? '0',
})

return hasEnoughBalanceForTxPlusFeesPlusSweep
},
[
accountId,
asset,
assetId,
balanceCryptoBaseUnit,
fromAddress,
getHasEnoughBalanceForTxPlusFees,
getHasEnoughBalanceForTxPlusFeesPlusSweep,
marketData,
queryClient,
],
)

const validateCryptoAmount = useCallback(
async (value: string) => {
if (!accountId) return

const valueCryptoBaseUnit = toBaseUnit(value, asset.precision)
const balanceCryptoPrecision = bn(fromBaseUnit(balanceCryptoBaseUnit, asset.precision))

Expand Down Expand Up @@ -745,23 +596,30 @@ export const Deposit: React.FC<DepositProps> = ({
return translate('trade.errors.amountTooSmall', { minLimit: outboundFeeLimit })

const hasEnoughBalanceForTxPlusFeesPlusSweep =
await fetchHasEnoughBalanceForTxPlusFeesPlusSweep(value)
await fetchHasEnoughBalanceForTxPlusFeesPlusSweep({
amountCryptoPrecision: value,
accountId,
asset,
type: 'deposit',
fromAddress,
})

return hasEnoughBalanceForTxPlusFeesPlusSweep || 'common.insufficientFunds'
},
[
asset.precision,
asset.symbol,
asset,
balanceCryptoBaseUnit,
assetId,
outboundFeeCryptoBaseUnit,
translate,
fetchHasEnoughBalanceForTxPlusFeesPlusSweep,
accountId,
fromAddress,
],
)

const validateFiatAmount = useCallback(
async (value: string) => {
if (!accountId) return
const valueCryptoPrecision = bnOrZero(value).div(marketData.price)
const balanceCryptoPrecision = bn(fromBaseUnit(balanceCryptoBaseUnit, asset.precision))

Expand All @@ -788,18 +646,24 @@ export const Deposit: React.FC<DepositProps> = ({
return translate('trade.errors.amountTooSmall', { minLimit: outboundFeeLimit })

const hasEnoughBalanceForTxPlusFeesPlusSweep =
await fetchHasEnoughBalanceForTxPlusFeesPlusSweep(valueCryptoPrecision.toFixed())
await fetchHasEnoughBalanceForTxPlusFeesPlusSweep({
amountCryptoPrecision: valueCryptoPrecision.toFixed(),
accountId,
asset,
type: 'deposit',
fromAddress,
})
return hasEnoughBalanceForTxPlusFeesPlusSweep || 'common.insufficientFunds'
},
[
marketData.price,
balanceCryptoBaseUnit,
asset.precision,
asset.symbol,
asset,
assetId,
outboundFeeCryptoBaseUnit,
translate,
fetchHasEnoughBalanceForTxPlusFeesPlusSweep,
accountId,
fromAddress,
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,26 @@ import type {
} from 'features/defi/contexts/DefiManagerProvider/DefiCommon'
import { DefiAction, DefiStep } from 'features/defi/contexts/DefiManagerProvider/DefiCommon'
import qs from 'qs'
import { useCallback, useEffect, useMemo, useReducer } from 'react'
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { useTranslate } from 'react-polyglot'
import type { AccountDropdownProps } from 'components/AccountDropdown/AccountDropdown'
import { CircularProgress } from 'components/CircularProgress/CircularProgress'
import type { DefiStepProps } from 'components/DeFi/components/Steps'
import type { DefiStepProps, StepComponentProps } from 'components/DeFi/components/Steps'
import { Steps } from 'components/DeFi/components/Steps'
import { Sweep } from 'components/Sweep'
import { useBrowserRouter } from 'hooks/useBrowserRouter/useBrowserRouter'
import { useWallet } from 'hooks/useWallet/useWallet'
import {
getThorchainFromAddress,
getThorchainSaversPosition,
} from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils'
import { serializeUserStakingId, toOpportunityId } from 'state/slices/opportunitiesSlice/utils'
import {
selectAssetById,
selectEarnUserStakingOpportunityByUserStakingId,
selectHighestBalanceAccountIdByStakingId,
selectMarketDataById,
selectPortfolioAccountMetadataByAccountId,
} from 'state/slices/selectors'
import { useAppSelector } from 'state/store'

Expand All @@ -39,6 +45,7 @@ type WithdrawProps = {
}

export const ThorchainSaversWithdraw: React.FC<WithdrawProps> = ({ accountId }) => {
const [fromAddress, setFromAddress] = useState<string | null>(null)
const [state, dispatch] = useReducer(reducer, initialState)
const translate = useTranslate()
const { query, history, location } = useBrowserRouter<DefiQueryParams, DefiParams>()
Expand Down Expand Up @@ -84,13 +91,37 @@ export const ThorchainSaversWithdraw: React.FC<WithdrawProps> = ({ accountId })
)

// user info
const { state: walletState } = useWallet()
const {
state: { wallet },
} = useWallet()

const accountFilter = useMemo(() => ({ accountId }), [accountId])
const accountMetadata = useAppSelector(state =>
selectPortfolioAccountMetadataByAccountId(state, accountFilter),
)

useEffect(() => {
if (!(accountId && wallet && accountMetadata)) return
if (fromAddress) return
;(async () => {
const _fromAddress = await getThorchainFromAddress({
accountId,
getPosition: getThorchainSaversPosition,
assetId,
wallet,
accountMetadata,
})

if (!_fromAddress) return
setFromAddress(_fromAddress)
})()
}, [accountId, accountMetadata, assetId, fromAddress, wallet])

useEffect(() => {
if (state.opportunity) return
if (!(walletState.wallet && opportunityData)) return
if (!(wallet && opportunityData)) return
dispatch({ type: ThorchainSaversWithdrawActionType.SET_OPPORTUNITY, payload: opportunityData })
}, [opportunityData, state.opportunity, walletState.wallet])
}, [opportunityData, state.opportunity, wallet])

const handleBack = useCallback(() => {
history.push({
Expand All @@ -102,14 +133,37 @@ export const ThorchainSaversWithdraw: React.FC<WithdrawProps> = ({ accountId })
})
}, [history, location, query])

const makeHandleSweepBack = useCallback(
(onNext: StepComponentProps['onNext']) => () => onNext(DefiStep.Info),
[],
)
const makeHandleSweepSeen = useCallback(
(onNext: StepComponentProps['onNext']) => () => onNext(DefiStep.Confirm),
[],
)

const StepConfig: DefiStepProps = useMemo(() => {
return {
[DefiStep.Info]: {
label: translate('defi.steps.withdraw.info.title'),
description: translate('defi.steps.withdraw.info.description', {
asset: asset.symbol,
}),
component: ownProps => <Withdraw {...ownProps} accountId={accountId} />,
component: ownProps => (
<Withdraw {...ownProps} accountId={accountId} fromAddress={fromAddress} />
),
},
[DefiStep.Sweep]: {
label: translate('modals.send.consolidate.consolidateFunds'),
component: ({ onNext }) => (
<Sweep
accountId={accountId}
fromAddress={fromAddress}
assetId={assetId}
onBack={makeHandleSweepBack(onNext)}
onSweepSeen={makeHandleSweepSeen(onNext)}
/>
),
},
[DefiStep.Confirm]: {
label: translate('defi.steps.confirm.title'),
Expand All @@ -121,7 +175,15 @@ export const ThorchainSaversWithdraw: React.FC<WithdrawProps> = ({ accountId })
},
}
// We only need this to update on symbol change
}, [accountId, asset.symbol, translate])
}, [
accountId,
asset.symbol,
assetId,
fromAddress,
makeHandleSweepBack,
makeHandleSweepSeen,
translate,
])

const value = useMemo(() => ({ state, dispatch }), [state])

Expand Down
Loading