From ba39e01ee86a9b577a623a897be4dd6b262ade82 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 20 Nov 2023 10:53:57 +0100 Subject: [PATCH 01/13] feat: poll sleep at beginning of method to honor the interval on first call --- src/lib/poll/poll.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/poll/poll.ts b/src/lib/poll/poll.ts index 73679225921..4aaad6bdc95 100644 --- a/src/lib/poll/poll.ts +++ b/src/lib/poll/poll.ts @@ -41,13 +41,13 @@ export const poll = ({ const execute = async (resolve: (arg: T) => void, reject: (err: unknown) => void) => { for (let attempts = 0; attempts < maxAttempts; attempts++) { if (isCancelled) return // dont resolve/reject - leave promise on event loop + await sleep(interval) try { const result = await fn() if (validate(result)) { resolve(result) return } - await sleep(interval) } catch (e) { reject(e) return From 7afee99a2f4f7f9770c31a8bbbd2bfccd2cfae54 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:01:24 +0100 Subject: [PATCH 02/13] feat: cleanup --- .../Withdraw/components/Confirm.tsx | 113 +----------------- 1 file changed, 1 insertion(+), 112 deletions(-) diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Confirm.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Confirm.tsx index 1ce25277b07..563e7360b11 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Confirm.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Confirm.tsx @@ -470,81 +470,6 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { })() }, [contextDispatch, getCustomTxFees, getEstimateFeesArgs, isTokenWithdraw]) - const getPreWithdrawInput: () => Promise = useCallback(async () => { - if ( - !( - accountId && - assetId && - state?.withdraw?.estimatedGasCryptoBaseUnit && - opportunityData?.stakedAmountCryptoBaseUnit && - contextDispatch - ) - ) - return - - try { - const estimateFeesArgs = await getEstimateFeesArgs() - if (!estimateFeesArgs) return - const estimatedFees = await estimateFees(estimateFeesArgs) - - contextDispatch({ - type: ThorchainSaversWithdrawActionType.SET_WITHDRAW, - payload: { - networkFeeCryptoBaseUnit: estimatedFees.fast.txFee, - }, - }) - - const amountCryptoBaseUnit = toBaseUnit(state?.withdraw.cryptoAmount, asset.precision) - const bps = getWithdrawBps({ - withdrawAmountCryptoBaseUnit: amountCryptoBaseUnit, - stakedAmountCryptoBaseUnit: opportunityData?.stakedAmountCryptoBaseUnit, - rewardsAmountCryptoBaseUnit: opportunityData?.rewardsCryptoBaseUnit?.amounts[0] ?? '0', - }) - - const maybeQuote = await getThorchainSaversWithdrawQuote({ asset, accountId, bps }) - - if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) - const quote = maybeQuote.unwrap() - - if (isUtxoChainId(chainId) && !maybeFromUTXOAccountAddress) { - throw new Error('Account address required to withdraw from THORChain savers') - } - - const sendInput: SendInput = { - cryptoAmount: '', - assetId, - from: '', // Let coinselect do its magic here - to: maybeFromUTXOAccountAddress, - sendMax: true, - accountId, - amountFieldError: '', - estimatedFees, - feeType: FeeDataKey.Fast, - fiatAmount: '', - fiatSymbol: selectedCurrency, - vanityAddress: '', - input: quote.inbound_address, - } - - return sendInput - } catch (e) { - console.error(e) - } - }, [ - accountId, - assetId, - state?.withdraw?.estimatedGasCryptoBaseUnit, - state?.withdraw.cryptoAmount, - opportunityData?.stakedAmountCryptoBaseUnit, - opportunityData?.rewardsCryptoBaseUnit?.amounts, - contextDispatch, - getEstimateFeesArgs, - asset, - chainId, - maybeFromUTXOAccountAddress, - selectedCurrency, - ]) - const getWithdrawInput: () => Promise = useCallback(async () => { if (!(accountId && assetId && opportunityData?.stakedAmountCryptoBaseUnit && contextDispatch)) return @@ -637,51 +562,15 @@ export const Confirm: React.FC = ({ accountId, onNext }) => { const handleMultiTxSend = useCallback(async (): Promise => { if (!wallet) return - // THORChain Txs need to always be sent from the same address, since the address (NOT the pubkey) is used to identify an active position - // The way THORChain does this is by not being xpub-compliant, and only exposing a single address for UTXOs in their UI - // All deposit/withdraws done from their UI are always done with one/many UTXOs from the same address, and change sent back to the same address - // We also do this EXCLUSIVELY for THORChain Txs. The rest of the app uses xpubs, so the initially deposited from address isn't guaranteed to be populated - // if users send other UTXO Txs in the meantime after depositing - // Additionally, we select their highest balance UTXO address as a first deposit, which isn't guaranteed to contain enough value - // - // For both re/deposit flows, we will possibly need a pre-Tx to populate their highest UTXO/previously deposited from address with enough value - const withdrawInput = await getWithdrawInput() if (!withdrawInput) throw new Error('Error building send input') - // Try/catching and evaluating to something in the catch isn't a good pattern usually - // In our case, handleSend() catching means that after all our previous checks, building a Tx failed at coinselect time - // So we actually send reconciliate a reconciliate Tx, retry the original send within the same block - // and finally evaluate to either the original Tx or a falsy empty string - // 1. Try to deposit from the originally deposited from / highest UTXO balance address - // If this is enough, no other Tx is needed const txId = await handleSend({ sendInput: withdrawInput, wallet, - }).catch(async e => { - if (!isUtxoChainId(chainId)) throw e - - // TODO(gomes): remove me and use the same logic as deposits - // 2. coinselect threw when building a Tx, meaning there's not enough value in the picked address - send funds to it - const preWithdrawInput = await getPreWithdrawInput() - if (!preWithdrawInput) throw new Error('Error building send input') - - return handleSend({ - sendInput: preWithdrawInput, - wallet: wallet!, - }).then(async () => { - // Safety factor for the Tx to be seen in the mempool - await new Promise(resolve => setTimeout(resolve, 5000)) - // 3. Sign and broadcast the depooosit Tx again - return handleSend({ - sendInput: withdrawInput, - wallet: wallet!, - }) - }) }) - return txId - }, [chainId, getPreWithdrawInput, getWithdrawInput, wallet]) + }, [getWithdrawInput, wallet]) const handleConfirm = useCallback(async () => { if (!contextDispatch || !bip44Params || !accountId || !assetId || !opportunityData) return From 15994898fcfd4c7dda32f7aa043c386d52301218 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:01:30 +0100 Subject: [PATCH 03/13] feat: thorchain savers withdraw sweep --- .../Overview/ThorchainSaversOverview.tsx | 2 +- .../Withdraw/ThorchainSaversWithdraw.tsx | 76 ++++++- .../Withdraw/components/Withdraw.tsx | 192 +++++++++++++++--- ...seGetThorchainSaversWithdrawQuoteQuery.tsx | 76 +++++++ 4 files changed, 306 insertions(+), 40 deletions(-) create mode 100644 src/lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery.tsx 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 31c72c18a29..29abd9ee10d 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 @@ -271,7 +271,7 @@ export const ThorchainSaversOverview: React.FC = ({ label: 'common.withdraw', icon: , action: DefiAction.Withdraw, - isDisabled: hasPendingTxs || hasPendingQueries, + isDisabled: false, toolTip: hasPendingTxs || hasPendingQueries ? translate('defi.modals.saversVaults.cannotWithdrawWhilePendingTx') diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/ThorchainSaversWithdraw.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/ThorchainSaversWithdraw.tsx index ac0229aac28..f69a1acd6e5 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/ThorchainSaversWithdraw.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/ThorchainSaversWithdraw.tsx @@ -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' @@ -39,6 +45,7 @@ type WithdrawProps = { } export const ThorchainSaversWithdraw: React.FC = ({ accountId }) => { + const [fromAddress, setFromAddress] = useState(null) const [state, dispatch] = useReducer(reducer, initialState) const translate = useTranslate() const { query, history, location } = useBrowserRouter() @@ -84,13 +91,37 @@ export const ThorchainSaversWithdraw: React.FC = ({ 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({ @@ -102,6 +133,15 @@ export const ThorchainSaversWithdraw: React.FC = ({ 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]: { @@ -109,7 +149,21 @@ export const ThorchainSaversWithdraw: React.FC = ({ accountId }) description: translate('defi.steps.withdraw.info.description', { asset: asset.symbol, }), - component: ownProps => , + component: ownProps => ( + + ), + }, + [DefiStep.Sweep]: { + label: translate('modals.send.consolidate.consolidateFunds'), + component: ({ onNext }) => ( + + ), }, [DefiStep.Confirm]: { label: translate('defi.steps.confirm.title'), @@ -121,7 +175,15 @@ export const ThorchainSaversWithdraw: React.FC = ({ 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]) diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx index 144cc4ebd55..7fc0da2ef8d 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx @@ -26,12 +26,15 @@ import { getSupportedEvmChainIds } from 'hooks/useEvm/useEvm' import { useWallet } from 'hooks/useWallet/useWallet' import type { Asset } from 'lib/asset-service' import { bn, bnOrZero } from 'lib/bignumber/bignumber' -import { toBaseUnit } from 'lib/math' +import { fromBaseUnit, toBaseUnit } from 'lib/math' import { trackOpportunityEvent } from 'lib/mixpanel/helpers' import { MixPanelEvents } from 'lib/mixpanel/types' import { useRouterContractAddress } from 'lib/swapper/swappers/ThorchainSwapper/utils/useRouterContractAddress' import { isToken } from 'lib/utils' import { assertGetEvmChainAdapter, createBuildCustomTxInput } from 'lib/utils/evm' +import { useGetThorchainSaversWithdrawQuoteQuery } from 'lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery' +import { useGetEstimatedFeesQuery } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' +import { useIsSweepNeededQuery } from 'pages/Lending/hooks/useIsSweepNeededQuery' import type { ThorchainSaversWithdrawQuoteResponseSuccess } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/types' import { BASE_BPS_POINTS, @@ -41,6 +44,7 @@ import { THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT, } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' import { serializeUserStakingId, toOpportunityId } from 'state/slices/opportunitiesSlice/utils' +import { isUtxoChainId } from 'state/slices/portfolioSlice/utils' import { selectAccountNumberByAccountId, selectAssetById, @@ -49,17 +53,21 @@ import { selectFeeAssetById, selectHighestBalanceAccountIdByStakingId, selectMarketDataById, + selectPortfolioCryptoBalanceBaseUnitByFilter, } from 'state/slices/selectors' import { useAppSelector } from 'state/store' import { ThorchainSaversWithdrawActionType } from '../WithdrawCommon' import { WithdrawContext } from '../WithdrawContext' -type WithdrawProps = StepComponentProps & { accountId: AccountId | undefined } +type WithdrawProps = StepComponentProps & { + accountId: AccountId | undefined + fromAddress: string | null +} const percentOptions = [0.25, 0.5, 0.75, 1] -export const Withdraw: React.FC = ({ accountId, onNext }) => { +export const Withdraw: React.FC = ({ accountId, fromAddress, onNext }) => { const [slippageCryptoAmountPrecision, setSlippageCryptoAmountPrecision] = useState( null, ) @@ -137,6 +145,12 @@ export const Withdraw: React.FC = ({ accountId, onNext }) => { opportunityData?.stakedAmountCryptoBaseUnit, ]) + const balanceFilter = useMemo(() => ({ assetId, accountId }), [accountId, assetId]) + // user info + const balanceCryptoBaseUnit = useAppSelector(state => + selectPortfolioCryptoBalanceBaseUnitByFilter(state, balanceFilter), + ) + const assetMarketData = useAppSelector(state => selectMarketDataById(state, assetId)) const fiatAmountAvailable = useMemo( () => bnOrZero(amountAvailableCryptoPrecision).times(assetMarketData.price), @@ -188,6 +202,10 @@ export const Withdraw: React.FC = ({ accountId, onNext }) => { skip: !isTokenWithdraw || !feeAsset?.assetId, }) + // TODO(gomes): use useGetEstimatedFeesQuery instead of this. + // The logic of useGetEstimatedFeesQuery and its consumption will need some touching up to work with custom Txs + // since the guts of it are made to accomodate Tx/fees/sweep fees deduction and there are !isUtxoChainId checks in place currently + // The method below is now only used for non-UTXO chains const getWithdrawGasEstimateCryptoBaseUnit = useCallback( async ( maybeQuote: Result, @@ -307,6 +325,99 @@ export const Withdraw: React.FC = ({ accountId, onNext }) => { ], ) + // TODO(gomes): this will work for UTXO but is invalid for tokens since they use diff. denoms + // the current workaround is to not do fee deduction for non-UTXO chains, + // but for consistency, we should for native EVM assets, and ensure this is a no-op for tokens + // Note when implementing this, fee checks/deduction will need to either be done for *native* assets only + // or handle different denoms for tokens/native assets and display insufficientFundsForProtocolFee copy + const getHasEnoughBalanceForTxPlusFees = useCallback( + ({ + balanceCryptoBaseUnit, + amountCryptoPrecision, + txFeeCryptoBaseUnit, + precision, + }: { + balanceCryptoBaseUnit: string + amountCryptoPrecision: string + txFeeCryptoBaseUnit: string + precision: number + }) => { + const balanceCryptoBaseUnitBn = bnOrZero(balanceCryptoBaseUnit) + if (balanceCryptoBaseUnitBn.isZero()) return false + + return bnOrZero(amountCryptoPrecision) + .plus(fromBaseUnit(txFeeCryptoBaseUnit, precision ?? 0)) + .lte(fromBaseUnit(balanceCryptoBaseUnitBn, precision)) + }, + [], + ) + + const { + data: thorchainSaversWithdrawQuote, + isLoading: isThorchainSaversWithdrawQuoteLoading, + isSuccess: isThorchainSaversWithdrawQuoteSuccess, + } = useGetThorchainSaversWithdrawQuoteQuery({ + asset, + accountId, + amountCryptoBaseUnit: toBaseUnit(getValues()?.cryptoAmount, asset.precision), + }) + + const dustAmountCryptoBaseUnit = useMemo( + () => + thorchainSaversWithdrawQuote + ? toBaseUnit(fromThorBaseUnit(thorchainSaversWithdrawQuote.dust_amount), feeAsset.precision) + : '0', + [feeAsset.precision, thorchainSaversWithdrawQuote], + ) + const { + data: estimatedFeesData, + isLoading: isEstimatedFeesDataLoading, + isSuccess: isEstimatedFeesDataSuccess, + } = useGetEstimatedFeesQuery({ + cryptoAmount: dustAmountCryptoBaseUnit, + assetId, + to: thorchainSaversWithdrawQuote?.inbound_address ?? '', + sendMax: false, + accountId: accountId ?? '', + contractAddress: undefined, + enabled: Boolean(thorchainSaversWithdrawQuote && accountId && isUtxoChainId(asset.chainId)), + }) + + const isSweepNeededArgs = useMemo( + () => ({ + assetId, + address: fromAddress, + amountCryptoBaseUnit: dustAmountCryptoBaseUnit, + txFeeCryptoBaseUnit: estimatedFeesData?.txFeeCryptoBaseUnit ?? '0', + // Don't fetch sweep needed if there isn't enough balance for the dust amount + fees, since adding in a sweep Tx would obviously fail too + enabled: Boolean( + isEstimatedFeesDataSuccess && + isThorchainSaversWithdrawQuoteSuccess && + getHasEnoughBalanceForTxPlusFees({ + precision: asset.precision, + balanceCryptoBaseUnit, + amountCryptoPrecision: fromBaseUnit(dustAmountCryptoBaseUnit, feeAsset.precision), + txFeeCryptoBaseUnit: estimatedFeesData?.txFeeCryptoBaseUnit ?? '', + }), + ), + }), + [ + asset.precision, + assetId, + balanceCryptoBaseUnit, + dustAmountCryptoBaseUnit, + estimatedFeesData?.txFeeCryptoBaseUnit, + feeAsset.precision, + fromAddress, + getHasEnoughBalanceForTxPlusFees, + isEstimatedFeesDataSuccess, + isThorchainSaversWithdrawQuoteSuccess, + ], + ) + + const { data: isSweepNeeded, isLoading: isSweepNeededLoading } = + useIsSweepNeededQuery(isSweepNeededArgs) + const handleContinue = useCallback( async (formValues: WithdrawValues) => { if (!(userAddress && opportunityData && accountId && dispatch)) return @@ -317,40 +428,49 @@ export const Withdraw: React.FC = ({ accountId, onNext }) => { dispatch({ type: ThorchainSaversWithdrawActionType.SET_WITHDRAW, payload: formValues }) dispatch({ type: ThorchainSaversWithdrawActionType.SET_LOADING, payload: true }) try { - const { cryptoAmount } = inputValues - const amountCryptoBaseUnit = toBaseUnit(cryptoAmount, asset.precision) - const withdrawBps = getWithdrawBps({ - withdrawAmountCryptoBaseUnit: amountCryptoBaseUnit, - stakedAmountCryptoBaseUnit: opportunityData.stakedAmountCryptoBaseUnit ?? '0', - rewardsAmountCryptoBaseUnit: opportunityData.rewardsCryptoBaseUnit?.amounts[0] ?? '0', - }) - setQuoteLoading(true) - const maybeQuote = await getThorchainSaversWithdrawQuote({ - asset, - accountId, - bps: withdrawBps, - }) - setQuoteLoading(false) + const estimatedGasCryptoBaseUnit = await (async () => { + if (isUtxoChainId(chainId)) return estimatedFeesData?.txFeeCryptoBaseUnit + + const { cryptoAmount } = inputValues + const amountCryptoBaseUnit = toBaseUnit(cryptoAmount, asset.precision) + const withdrawBps = getWithdrawBps({ + withdrawAmountCryptoBaseUnit: amountCryptoBaseUnit, + stakedAmountCryptoBaseUnit: opportunityData.stakedAmountCryptoBaseUnit ?? '0', + rewardsAmountCryptoBaseUnit: opportunityData.rewardsCryptoBaseUnit?.amounts[0] ?? '0', + }) + setQuoteLoading(true) + const maybeQuote = await getThorchainSaversWithdrawQuote({ + asset, + accountId, + bps: withdrawBps, + }) + setQuoteLoading(false) - if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) - const quote = maybeQuote.unwrap() - const { dust_amount } = quote - const _dustAmountCryptoBaseUnit = toBaseUnit(fromThorBaseUnit(dust_amount), asset.precision) + if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) + const quote = maybeQuote.unwrap() + const { dust_amount } = quote + const _dustAmountCryptoBaseUnit = toBaseUnit( + fromThorBaseUnit(dust_amount), + asset.precision, + ) - const maybeWithdrawGasEstimateCryptoBaseUnit = await getWithdrawGasEstimateCryptoBaseUnit( - maybeQuote, - _dustAmountCryptoBaseUnit, - ) - if (!maybeWithdrawGasEstimateCryptoBaseUnit) return - if (maybeWithdrawGasEstimateCryptoBaseUnit.isErr()) return + const maybeWithdrawGasEstimateCryptoBaseUnit = await getWithdrawGasEstimateCryptoBaseUnit( + maybeQuote, + _dustAmountCryptoBaseUnit, + ) + if (!maybeWithdrawGasEstimateCryptoBaseUnit) return + if (maybeWithdrawGasEstimateCryptoBaseUnit.isErr()) return - const estimatedGasCryptoBaseUnit = maybeWithdrawGasEstimateCryptoBaseUnit.unwrap() + return maybeWithdrawGasEstimateCryptoBaseUnit.unwrap() + })() + + if (!estimatedGasCryptoBaseUnit) return dispatch({ type: ThorchainSaversWithdrawActionType.SET_WITHDRAW, payload: { estimatedGasCryptoBaseUnit }, }) - onNext(DefiStep.Confirm) + onNext(isSweepNeeded ? DefiStep.Sweep : DefiStep.Confirm) trackOpportunityEvent( MixPanelEvents.WithdrawContinue, @@ -379,11 +499,14 @@ export const Withdraw: React.FC = ({ accountId, onNext }) => { accountId, dispatch, getValues, - asset, - getWithdrawGasEstimateCryptoBaseUnit, onNext, + isSweepNeeded, assetId, assets, + chainId, + estimatedFeesData?.txFeeCryptoBaseUnit, + asset, + getWithdrawGasEstimateCryptoBaseUnit, toast, translate, ], @@ -569,7 +692,12 @@ export const Withdraw: React.FC = ({ accountId, onNext }) => { marketData={marketData} onCancel={handleCancel} onContinue={handleContinue} - isLoading={state.loading} + isLoading={ + isEstimatedFeesDataLoading || + isSweepNeededLoading || + isThorchainSaversWithdrawQuoteLoading || + state.loading + } percentOptions={percentOptions} enableSlippage={false} handlePercentClick={handlePercentClick} diff --git a/src/lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery.tsx b/src/lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery.tsx new file mode 100644 index 00000000000..f3a6a168ec5 --- /dev/null +++ b/src/lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery.tsx @@ -0,0 +1,76 @@ +import { type AccountId, fromAssetId } from '@shapeshiftoss/caip' +import { useQuery } from '@tanstack/react-query' +import { useMemo } from 'react' +import type { Asset } from 'lib/asset-service' +import type { BigNumber } from 'lib/bignumber/bignumber' +import { + getThorchainSaversWithdrawQuote, + getWithdrawBps, +} from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' +import { serializeUserStakingId, toOpportunityId } from 'state/slices/opportunitiesSlice/utils' +import { selectEarnUserStakingOpportunityByUserStakingId } from 'state/slices/selectors' +import { store } from 'state/store' + +export type GetThorchainSaversWithdrawQuoteQueryKey = [ + 'thorchainSaversWithdrawQuote', + { + asset: Asset + accountId: AccountId | undefined + amountCryptoBaseUnit: BigNumber.Value | null | undefined + }, +] +export const queryFn = async ({ + queryKey, +}: { + queryKey: GetThorchainSaversWithdrawQuoteQueryKey +}) => { + const [, { asset, accountId, amountCryptoBaseUnit }] = queryKey + if (!accountId) return + + const { chainId, assetNamespace, assetReference } = fromAssetId(asset.assetId) + const opportunityId = toOpportunityId({ chainId, assetNamespace, assetReference }) + const opportunityData = selectEarnUserStakingOpportunityByUserStakingId(store.getState(), { + userStakingId: serializeUserStakingId(accountId, opportunityId ?? ''), + }) + + const withdrawBps = getWithdrawBps({ + withdrawAmountCryptoBaseUnit: amountCryptoBaseUnit ?? 0, + stakedAmountCryptoBaseUnit: opportunityData?.stakedAmountCryptoBaseUnit ?? '0', + rewardsAmountCryptoBaseUnit: opportunityData?.rewardsCryptoBaseUnit?.amounts[0] ?? '0', + }) + + const maybeQuote = await getThorchainSaversWithdrawQuote({ + asset, + bps: withdrawBps, + accountId, + }) + + if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) + + return maybeQuote.unwrap() +} + +// TODO(gomes): consume me everywhere instead of getThorchainSaversWithdrawQuote +export const useGetThorchainSaversWithdrawQuoteQuery = ({ + asset, + accountId, + amountCryptoBaseUnit, +}: { + asset: Asset + accountId: AccountId | undefined + amountCryptoBaseUnit: BigNumber.Value | null | undefined +}) => { + const withdrawQuoteQueryKey: GetThorchainSaversWithdrawQuoteQueryKey = useMemo( + () => ['thorchainSaversWithdrawQuote', { asset, accountId, amountCryptoBaseUnit }], + [accountId, amountCryptoBaseUnit, asset], + ) + + const withdrawQuoteQuery = useQuery({ + queryKey: withdrawQuoteQueryKey, + queryFn, + enabled: Boolean(accountId), + staleTime: 5000, + }) + + return withdrawQuoteQuery +} From a41ac8670ffbbc0a44befc35d1c2d7398a484c63 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:29:30 +0100 Subject: [PATCH 04/13] feat: perf --- .../Deposit/components/Deposit.tsx | 1 + .../Withdraw/components/Withdraw.tsx | 326 ++++++++++++++---- ...useGetThorchainSaversDepositQuoteQuery.tsx | 4 +- ...seGetThorchainSaversWithdrawQuoteQuery.tsx | 38 +- 4 files changed, 286 insertions(+), 83 deletions(-) diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx index 3b1f1e8235d..172a4079ec7 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx @@ -609,6 +609,7 @@ export const Deposit: React.FC = ({ const _thorchainSaversDepositQuote = await queryClient.fetchQuery({ queryKey: thorchainSaversDepositQuoteQueryKey, queryFn: getThorchainSaversDepositQuoteQueryFn, + staleTime: 5000, }) const estimatedFeesQueryArgs = { diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx index 7fc0da2ef8d..030a4ec3e69 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx @@ -4,6 +4,7 @@ import type { AccountId } from '@shapeshiftoss/caip' import { fromAccountId, fromAssetId, toAssetId } from '@shapeshiftoss/caip' import type { GetFeeDataInput, UtxoBaseAdapter, UtxoChainId } from '@shapeshiftoss/chain-adapters' import { Err, Ok, type Result } from '@sniptt/monads' +import { useQueryClient } from '@tanstack/react-query' import { getOrCreateContractByType } from 'contracts/contractManager' import { ContractType } from 'contracts/types' import type { WithdrawValues } from 'features/defi/components/Withdraw/Withdraw' @@ -32,15 +33,25 @@ import { MixPanelEvents } from 'lib/mixpanel/types' import { useRouterContractAddress } from 'lib/swapper/swappers/ThorchainSwapper/utils/useRouterContractAddress' import { isToken } from 'lib/utils' import { assertGetEvmChainAdapter, createBuildCustomTxInput } from 'lib/utils/evm' -import { useGetThorchainSaversWithdrawQuoteQuery } from 'lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery' -import { useGetEstimatedFeesQuery } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' -import { useIsSweepNeededQuery } from 'pages/Lending/hooks/useIsSweepNeededQuery' +import type { GetThorchainSaversWithdrawQuoteQueryKey } from 'lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery' +import { + queryFn as getThorchainSaversWithdrawQuoteQueryFn, + useGetThorchainSaversWithdrawQuoteQuery, +} from 'lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery' +import type { EstimatedFeesQueryKey } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' +import { + queryFn as getEstimatedFeesQueryFn, + useGetEstimatedFeesQuery, +} from 'pages/Lending/hooks/useGetEstimatedFeesQuery' +import type { IsSweepNeededQueryKey } from 'pages/Lending/hooks/useIsSweepNeededQuery' +import { + queryFn as isSweepNeededQueryFn, + useIsSweepNeededQuery, +} from 'pages/Lending/hooks/useIsSweepNeededQuery' import type { ThorchainSaversWithdrawQuoteResponseSuccess } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/types' import { BASE_BPS_POINTS, fromThorBaseUnit, - getThorchainSaversWithdrawQuote, - getWithdrawBps, THORCHAIN_SAVERS_DUST_THRESHOLDS_CRYPTO_BASE_UNIT, } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' import { serializeUserStakingId, toOpportunityId } from 'state/slices/opportunitiesSlice/utils' @@ -78,6 +89,7 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe const { query, history: browserHistory } = useBrowserRouter() const { chainId, assetNamespace, assetReference } = query + const queryClient = useQueryClient() const methods = useForm({ mode: 'onChange' }) const { getValues, setValue } = methods @@ -171,9 +183,21 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe // and will allow us to gracefully handle amounts that are lower than the outbound fee // - If this fails, we know that the withdraw amount is too low anyway, regarding of how many bps are withdrawn setQuoteLoading(true) - const quote = await getThorchainSaversWithdrawQuote({ asset, accountId, bps: '10000' }) + const thorchainSaversWithdrawQuoteQueryKey: GetThorchainSaversWithdrawQuoteQueryKey = [ + 'thorchainSaversWithdrawQuote', + { asset, accountId, withdrawBps: '10000' }, + ] + + const _thorchainSaversWithdrawQuote = await queryClient + .fetchQuery({ + queryKey: thorchainSaversWithdrawQuoteQueryKey, + queryFn: getThorchainSaversWithdrawQuoteQueryFn, + staleTime: 5000, + }) + .then(res => Ok(res)) + .catch((err: Error) => Err(err.message)) setQuoteLoading(false) - return quote + return _thorchainSaversWithdrawQuote })() // Neither the passed quote, nor the safer 10,000 bps quote succeeded @@ -192,7 +216,7 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe // Add 5% as as a safety factor since the dust threshold fee is not necessarily going to cut it return Ok(safeOutboundFee) }, - [accountId, asset, translate], + [accountId, asset, queryClient, translate], ) const supportedEvmChainIds = useMemo(() => getSupportedEvmChainIds(), []) @@ -358,7 +382,7 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe isSuccess: isThorchainSaversWithdrawQuoteSuccess, } = useGetThorchainSaversWithdrawQuoteQuery({ asset, - accountId, + accountId: accountId ?? '', amountCryptoBaseUnit: toBaseUnit(getValues()?.cryptoAmount, asset.precision), }) @@ -418,6 +442,31 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe 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: WithdrawValues) => { if (!(userAddress && opportunityData && accountId && dispatch)) return @@ -433,21 +482,19 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe const { cryptoAmount } = inputValues const amountCryptoBaseUnit = toBaseUnit(cryptoAmount, asset.precision) - const withdrawBps = getWithdrawBps({ - withdrawAmountCryptoBaseUnit: amountCryptoBaseUnit, - stakedAmountCryptoBaseUnit: opportunityData.stakedAmountCryptoBaseUnit ?? '0', - rewardsAmountCryptoBaseUnit: opportunityData.rewardsCryptoBaseUnit?.amounts[0] ?? '0', - }) setQuoteLoading(true) - const maybeQuote = await getThorchainSaversWithdrawQuote({ - asset, - accountId, - bps: withdrawBps, + const thorchainSaversWithdrawQuoteQueryKey: GetThorchainSaversWithdrawQuoteQueryKey = [ + 'thorchainSaversWithdrawQuote', + { asset, accountId, amountCryptoBaseUnit }, + ] + + const quote = await queryClient.fetchQuery({ + queryKey: thorchainSaversWithdrawQuoteQueryKey, + queryFn: getThorchainSaversWithdrawQuoteQueryFn, + staleTime: 5000, }) setQuoteLoading(false) - if (maybeQuote.isErr()) throw new Error(maybeQuote.unwrapErr()) - const quote = maybeQuote.unwrap() const { dust_amount } = quote const _dustAmountCryptoBaseUnit = toBaseUnit( fromThorBaseUnit(dust_amount), @@ -455,7 +502,7 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe ) const maybeWithdrawGasEstimateCryptoBaseUnit = await getWithdrawGasEstimateCryptoBaseUnit( - maybeQuote, + Ok(quote), _dustAmountCryptoBaseUnit, ) if (!maybeWithdrawGasEstimateCryptoBaseUnit) return @@ -506,6 +553,7 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe chainId, estimatedFeesData?.txFeeCryptoBaseUnit, asset, + queryClient, getWithdrawGasEstimateCryptoBaseUnit, toast, translate, @@ -527,6 +575,135 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe [amountAvailableCryptoPrecision, assetMarketData, setValue], ) + // TODO(gomes): abstract me into a pure method when implementing sweep for withdraw, and handle non-UTXO chains + const fetchHasEnoughBalanceForTxPlusFeesPlusSweep = useCallback( + async (withdrawAmountCryptoPrecision: string) => { + if (!accountId) return + const isUtxoChain = isUtxoChainId(asset.chainId) + const estimateFeesQueryEnabled = Boolean(fromAddress && accountId && isUtxoChain) + + const amountCryptoBaseUnit = toBaseUnit(withdrawAmountCryptoPrecision, asset.precision) + + const thorchainSaversWithdrawQuoteQueryKey: GetThorchainSaversWithdrawQuoteQueryKey = [ + 'thorchainSaversWithdrawQuote', + { asset, accountId, amountCryptoBaseUnit }, + ] + + const _thorchainSaversWithdrawQuote = await queryClient.fetchQuery({ + queryKey: thorchainSaversWithdrawQuoteQueryKey, + queryFn: getThorchainSaversWithdrawQuoteQueryFn, + staleTime: 5000, + }) + + const dustAmountCryptoPrecision = fromThorBaseUnit( + _thorchainSaversWithdrawQuote?.dust_amount ?? 0, + ).toFixed() + + const estimatedFeesQueryArgs = { + estimateFeesInput: { + cryptoAmount: dustAmountCryptoPrecision, + assetId, + to: _thorchainSaversWithdrawQuote?.inbound_address ?? '', + sendMax: false, + accountId: accountId ?? '', + contractAddress: undefined, + }, + asset, + assetMarketData, + 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: withdrawAmountCryptoPrecision, + txFeeCryptoBaseUnit: _estimatedFeesData?.txFeeCryptoBaseUnit ?? '', + }) + + const isSweepNeededQueryEnabled = Boolean( + bnOrZero(withdrawAmountCryptoPrecision).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, + 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: withdrawAmountCryptoPrecision, + txFeeCryptoBaseUnit: _estimatedFeesData?.txFeeCryptoBaseUnit ?? '0', + sweepTxFeeCryptoBaseUnit: _estimatedSweepFeesData?.txFeeCryptoBaseUnit ?? '0', + }) + + return hasEnoughBalanceForTxPlusFeesPlusSweep + }, + [ + accountId, + asset, + assetId, + assetMarketData, + balanceCryptoBaseUnit, + fromAddress, + getHasEnoughBalanceForTxPlusFees, + getHasEnoughBalanceForTxPlusFeesPlusSweep, + queryClient, + ], + ) + const validateCryptoAmount = useCallback( async (value: string) => { if (!opportunityData) return false @@ -534,23 +711,29 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe if (!dispatch) return false try { - const cryptoAmount = value - const amountCryptoBaseUnit = toBaseUnit(cryptoAmount, asset.precision) - const withdrawBps = getWithdrawBps({ - withdrawAmountCryptoBaseUnit: amountCryptoBaseUnit, - stakedAmountCryptoBaseUnit: opportunityData.stakedAmountCryptoBaseUnit ?? '0', - rewardsAmountCryptoBaseUnit: opportunityData.rewardsCryptoBaseUnit?.amounts[0] ?? '0', - }) + const withdrawAmountCryptoPrecision = bnOrZero(value) + const withdrawAmountCryptoBaseUnit = toBaseUnit(value, asset.precision) + const amountCryptoBaseUnit = toBaseUnit(withdrawAmountCryptoPrecision, asset.precision) setQuoteLoading(true) - const maybeQuote = await getThorchainSaversWithdrawQuote({ - asset, - accountId, - bps: withdrawBps, - }) + + const thorchainSaversWithdrawQuoteQueryKey: GetThorchainSaversWithdrawQuoteQueryKey = [ + 'thorchainSaversWithdrawQuote', + { asset, accountId, amountCryptoBaseUnit }, + ] + + const maybeQuote: Result = + await queryClient + .fetchQuery({ + queryKey: thorchainSaversWithdrawQuoteQueryKey, + queryFn: getThorchainSaversWithdrawQuoteQueryFn, + staleTime: 5000, + }) + // Re-wrapping into a Result since react-query expects promises to reject and doesn't speak monads + .then(res => Ok(res)) + .catch((err: Error) => Err(err.message)) const maybeOutboundFeeCryptoBaseUnit = await getOutboundFeeCryptoBaseUnit(maybeQuote) - if (maybeQuote.isErr()) return translate('trade.errors.amountTooSmallUnknownMinimum') const quote = maybeQuote.unwrap() const { fees: { slippage_bps }, @@ -559,7 +742,9 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe const percentage = bnOrZero(slippage_bps).div(BASE_BPS_POINTS).times(100) // total downside (slippage going into position) - 0.007 ETH for 5 ETH deposit - const cryptoSlippageAmountPrecision = bnOrZero(cryptoAmount).times(percentage).div(100) + const cryptoSlippageAmountPrecision = withdrawAmountCryptoPrecision + .times(percentage) + .div(100) setSlippageCryptoAmountPrecision(cryptoSlippageAmountPrecision.toString()) const _dustAmountCryptoBaseUnit = toBaseUnit(fromThorBaseUnit(dust_amount), asset.precision) @@ -583,14 +768,14 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe const outboundFeeCryptoBaseUnit = maybeOutboundFeeCryptoBaseUnit.unwrap() const balanceCryptoPrecision = bnOrZero(amountAvailableCryptoPrecision.toPrecision()) - const valueCryptoPrecision = bnOrZero(value) - const valueCryptoBaseUnit = toBaseUnit(value, asset.precision) const hasValidBalance = balanceCryptoPrecision.gt(0) && - valueCryptoPrecision.gt(0) && - balanceCryptoPrecision.gte(valueCryptoPrecision) - const isBelowWithdrawThreshold = bn(valueCryptoBaseUnit) + withdrawAmountCryptoPrecision.gt(0) && + (await fetchHasEnoughBalanceForTxPlusFeesPlusSweep( + withdrawAmountCryptoPrecision.toFixed(), + )) + const isBelowWithdrawThreshold = bn(withdrawAmountCryptoBaseUnit) .minus(outboundFeeCryptoBaseUnit) .lt(0) @@ -604,10 +789,9 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe }) } - if (valueCryptoPrecision.isEqualTo(0)) return '' - return hasValidBalance || 'common.insufficientFunds' + if (withdrawAmountCryptoPrecision.isEqualTo(0)) return '' + return hasValidBalance || 'trade.errors.insufficientFunds' } catch (e) { - // This should never happen since all errors are monadic, but allows us to use the finally block console.error(e) } finally { setQuoteLoading(false) @@ -617,38 +801,60 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe [ opportunityData, accountId, + dispatch, asset, + queryClient, getOutboundFeeCryptoBaseUnit, - translate, getWithdrawGasEstimateCryptoBaseUnit, amountAvailableCryptoPrecision, - dispatch, + fetchHasEnoughBalanceForTxPlusFeesPlusSweep, + translate, ], ) const validateFiatAmount = useCallback( - (value: string) => { + async (value: string) => { if (!(opportunityData && accountId && dispatch)) return false dispatch({ type: ThorchainSaversWithdrawActionType.SET_LOADING, payload: true }) + setQuoteLoading(true) + const withdrawAmountcryptoPrecision = bnOrZero(value).div(assetMarketData.price) try { - const crypto = bnOrZero(amountAvailableCryptoPrecision.toPrecision()) + const amountAvailableCryptoPrecisionBn = bnOrZero( + amountAvailableCryptoPrecision.toPrecision(), + ) - const fiat = crypto.times(assetMarketData.price) + const amountAvailableFiat = amountAvailableCryptoPrecisionBn.times(assetMarketData.price) const valueCryptoPrecision = bnOrZero(value) - const hasValidBalance = fiat.gt(0) && valueCryptoPrecision.gt(0) && fiat.gte(value) + const hasValidBalance = + amountAvailableFiat.gt(0) && + valueCryptoPrecision.gt(0) && + amountAvailableFiat.gte(value) && + (await fetchHasEnoughBalanceForTxPlusFeesPlusSweep( + withdrawAmountcryptoPrecision.toFixed(), + )) + + console.log({ hasValidBalance }) + if (valueCryptoPrecision.isEqualTo(0)) return '' - return hasValidBalance || 'common.insufficientFunds' + return hasValidBalance || 'trade.errors.insufficientFunds' } catch (e) { - // This should never happen since all errors are monadic, but allows us to use the finally block - console.error(e) + return translate('trade.errors.amountTooSmallUnknownMinimum') } finally { setQuoteLoading(false) dispatch({ type: ThorchainSaversWithdrawActionType.SET_LOADING, payload: false }) } }, - [accountId, amountAvailableCryptoPrecision, assetMarketData.price, dispatch, opportunityData], + [ + accountId, + amountAvailableCryptoPrecision, + assetMarketData.price, + dispatch, + fetchHasEnoughBalanceForTxPlusFeesPlusSweep, + opportunityData, + translate, + ], ) const cryptoInputValidation = useMemo( @@ -667,18 +873,6 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe [validateFiatAmount], ) - const marketData = useMemo( - () => ({ - // The vault asset doesnt have market data. - // We're making our own market data object for the withdraw view - price: assetMarketData.price, - marketCap: '0', - volume: '0', - changePercent24Hr: 0, - }), - [assetMarketData], - ) - if (!state) return null return ( @@ -689,7 +883,7 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe cryptoInputValidation={cryptoInputValidation} fiatAmountAvailable={fiatAmountAvailable.toString()} fiatInputValidation={fiatInputValidation} - marketData={marketData} + marketData={assetMarketData} onCancel={handleCancel} onContinue={handleContinue} isLoading={ diff --git a/src/lib/utils/thorchain/hooks/useGetThorchainSaversDepositQuoteQuery.tsx b/src/lib/utils/thorchain/hooks/useGetThorchainSaversDepositQuoteQuery.tsx index 9072fbdc73d..caa3a1d81df 100644 --- a/src/lib/utils/thorchain/hooks/useGetThorchainSaversDepositQuoteQuery.tsx +++ b/src/lib/utils/thorchain/hooks/useGetThorchainSaversDepositQuoteQuery.tsx @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import type { Asset } from 'lib/asset-service' -import type { BigNumber } from 'lib/bignumber/bignumber' +import { type BigNumber, bnOrZero } from 'lib/bignumber/bignumber' import { getMaybeThorchainSaversDepositQuote } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' export type GetThorchainSaversDepositQuoteQueryKey = [ @@ -43,7 +43,7 @@ export const useGetThorchainSaversDepositQuoteQuery = ({ const depositQuoteQuery = useQuery({ queryKey: depositQuoteQueryKey, queryFn, - enabled: true, + enabled: Boolean(bnOrZero(amountCryptoBaseUnit).gt(0)), staleTime: 5000, }) diff --git a/src/lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery.tsx b/src/lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery.tsx index f3a6a168ec5..14f23f6e143 100644 --- a/src/lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery.tsx +++ b/src/lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery.tsx @@ -2,7 +2,7 @@ import { type AccountId, fromAssetId } from '@shapeshiftoss/caip' import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import type { Asset } from 'lib/asset-service' -import type { BigNumber } from 'lib/bignumber/bignumber' +import { type BigNumber, bnOrZero } from 'lib/bignumber/bignumber' import { getThorchainSaversWithdrawQuote, getWithdrawBps, @@ -15,17 +15,24 @@ export type GetThorchainSaversWithdrawQuoteQueryKey = [ 'thorchainSaversWithdrawQuote', { asset: Asset - accountId: AccountId | undefined - amountCryptoBaseUnit: BigNumber.Value | null | undefined - }, + accountId: AccountId + } & ( + | { + amountCryptoBaseUnit: BigNumber.Value | null | undefined + withdrawBps?: string + } + | { + amountCryptoBaseUnit?: never + withdrawBps: string + } + ), ] export const queryFn = async ({ queryKey, }: { queryKey: GetThorchainSaversWithdrawQuoteQueryKey }) => { - const [, { asset, accountId, amountCryptoBaseUnit }] = queryKey - if (!accountId) return + const [, { asset, accountId, amountCryptoBaseUnit, withdrawBps }] = queryKey const { chainId, assetNamespace, assetReference } = fromAssetId(asset.assetId) const opportunityId = toOpportunityId({ chainId, assetNamespace, assetReference }) @@ -33,15 +40,17 @@ export const queryFn = async ({ userStakingId: serializeUserStakingId(accountId, opportunityId ?? ''), }) - const withdrawBps = getWithdrawBps({ - withdrawAmountCryptoBaseUnit: amountCryptoBaseUnit ?? 0, - stakedAmountCryptoBaseUnit: opportunityData?.stakedAmountCryptoBaseUnit ?? '0', - rewardsAmountCryptoBaseUnit: opportunityData?.rewardsCryptoBaseUnit?.amounts[0] ?? '0', - }) + const _withdrawBps = + withdrawBps || + getWithdrawBps({ + withdrawAmountCryptoBaseUnit: amountCryptoBaseUnit ?? 0, + stakedAmountCryptoBaseUnit: opportunityData?.stakedAmountCryptoBaseUnit ?? '0', + rewardsAmountCryptoBaseUnit: opportunityData?.rewardsCryptoBaseUnit?.amounts[0] ?? '0', + }) const maybeQuote = await getThorchainSaversWithdrawQuote({ asset, - bps: withdrawBps, + bps: _withdrawBps, accountId, }) @@ -50,14 +59,13 @@ export const queryFn = async ({ return maybeQuote.unwrap() } -// TODO(gomes): consume me everywhere instead of getThorchainSaversWithdrawQuote export const useGetThorchainSaversWithdrawQuoteQuery = ({ asset, accountId, amountCryptoBaseUnit, }: { asset: Asset - accountId: AccountId | undefined + accountId: AccountId amountCryptoBaseUnit: BigNumber.Value | null | undefined }) => { const withdrawQuoteQueryKey: GetThorchainSaversWithdrawQuoteQueryKey = useMemo( @@ -68,7 +76,7 @@ export const useGetThorchainSaversWithdrawQuoteQuery = ({ const withdrawQuoteQuery = useQuery({ queryKey: withdrawQuoteQueryKey, queryFn, - enabled: Boolean(accountId), + enabled: Boolean(accountId && bnOrZero(amountCryptoBaseUnit).gt(0)), staleTime: 5000, }) From c5ada3bcd45ed6a1df0a170b6e406d26a93922d5 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:43:09 +0100 Subject: [PATCH 05/13] fix: derp --- .../ThorchainSaversManager/Withdraw/components/Withdraw.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx index 030a4ec3e69..838e38278de 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx @@ -625,7 +625,7 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe const _hasEnoughBalanceForTxPlusFees = getHasEnoughBalanceForTxPlusFees({ precision: asset.precision, balanceCryptoBaseUnit, - amountCryptoPrecision: withdrawAmountCryptoPrecision, + amountCryptoPrecision: dustAmountCryptoPrecision, txFeeCryptoBaseUnit: _estimatedFeesData?.txFeeCryptoBaseUnit ?? '', }) @@ -684,7 +684,7 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe getHasEnoughBalanceForTxPlusFeesPlusSweep({ precision: asset.precision, balanceCryptoBaseUnit, - amountCryptoPrecision: withdrawAmountCryptoPrecision, + amountCryptoPrecision: dustAmountCryptoPrecision, txFeeCryptoBaseUnit: _estimatedFeesData?.txFeeCryptoBaseUnit ?? '0', sweepTxFeeCryptoBaseUnit: _estimatedSweepFeesData?.txFeeCryptoBaseUnit ?? '0', }) From 4951540f731dd9b7f4b4704ddfa9e18ba0837c3b Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:57:51 +0100 Subject: [PATCH 06/13] fix: minimums --- .../Withdraw/components/Withdraw.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx index 838e38278de..afb06ae2b2d 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx @@ -176,7 +176,14 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe if (!accountId) return null const maybeQuote = await (async () => { - if (_quote && _quote.isOk()) return _quote + if ( + _quote && + _quote.isOk() && + // Too small of quotes may not be able to be withdrawn, hence will not include any fees.outbound + bnOrZero(_quote.unwrap().expected_amount_out).gt(0) && + _quote.unwrap().fees.outbound + ) + return _quote // Attempt getting a quote with 100000 bps, i.e 100% withdraw // - If this succeeds, this allows us to know the oubtound fee, which is always the same regarding of the withdraw bps From 523236fcb04e67e1b69d534f15203510f500357f Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:59:09 +0100 Subject: [PATCH 07/13] feat: cleanup --- .../ThorchainSaversManager/Withdraw/components/Withdraw.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx index afb06ae2b2d..27368cbc5f1 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx @@ -842,8 +842,6 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe withdrawAmountcryptoPrecision.toFixed(), )) - console.log({ hasValidBalance }) - if (valueCryptoPrecision.isEqualTo(0)) return '' return hasValidBalance || 'trade.errors.insufficientFunds' } catch (e) { From 2251eb40029cd4f85e030621fb3dcee73f2a9584 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:26:18 +0100 Subject: [PATCH 08/13] feat: abstract util and remove TODO --- .../Deposit/components/Deposit.tsx | 187 ++------------- .../Withdraw/components/Withdraw.tsx | 192 ++------------- src/lib/utils/thorchain/index.ts | 223 ++++++++++++++++++ 3 files changed, 268 insertions(+), 334 deletions(-) create mode 100644 src/lib/utils/thorchain/index.ts diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx index 172a4079ec7..db69ea0ac26 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Deposit/components/Deposit.tsx @@ -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, @@ -450,31 +447,6 @@ export const Deposit: React.FC = ({ 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 ( @@ -593,132 +565,10 @@ export const Deposit: React.FC = ({ 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, - staleTime: 5000, - }) - - 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)) @@ -746,23 +596,30 @@ export const Deposit: React.FC = ({ 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)) @@ -789,18 +646,24 @@ export const Deposit: React.FC = ({ 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, ], ) diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx index 27368cbc5f1..b1c2d40a522 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx @@ -33,21 +33,14 @@ import { MixPanelEvents } from 'lib/mixpanel/types' import { useRouterContractAddress } from 'lib/swapper/swappers/ThorchainSwapper/utils/useRouterContractAddress' import { isToken } from 'lib/utils' import { assertGetEvmChainAdapter, createBuildCustomTxInput } from 'lib/utils/evm' +import { fetchHasEnoughBalanceForTxPlusFeesPlusSweep } from 'lib/utils/thorchain' import type { GetThorchainSaversWithdrawQuoteQueryKey } from 'lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery' import { queryFn as getThorchainSaversWithdrawQuoteQueryFn, useGetThorchainSaversWithdrawQuoteQuery, } from 'lib/utils/thorchain/hooks/useGetThorchainSaversWithdrawQuoteQuery' -import type { EstimatedFeesQueryKey } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' -import { - queryFn as getEstimatedFeesQueryFn, - useGetEstimatedFeesQuery, -} from 'pages/Lending/hooks/useGetEstimatedFeesQuery' -import type { IsSweepNeededQueryKey } from 'pages/Lending/hooks/useIsSweepNeededQuery' -import { - queryFn as isSweepNeededQueryFn, - useIsSweepNeededQuery, -} from 'pages/Lending/hooks/useIsSweepNeededQuery' +import { useGetEstimatedFeesQuery } from 'pages/Lending/hooks/useGetEstimatedFeesQuery' +import { useIsSweepNeededQuery } from 'pages/Lending/hooks/useIsSweepNeededQuery' import type { ThorchainSaversWithdrawQuoteResponseSuccess } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/types' import { BASE_BPS_POINTS, @@ -449,31 +442,6 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe 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: WithdrawValues) => { if (!(userAddress && opportunityData && accountId && dispatch)) return @@ -582,135 +550,6 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe [amountAvailableCryptoPrecision, assetMarketData, setValue], ) - // TODO(gomes): abstract me into a pure method when implementing sweep for withdraw, and handle non-UTXO chains - const fetchHasEnoughBalanceForTxPlusFeesPlusSweep = useCallback( - async (withdrawAmountCryptoPrecision: string) => { - if (!accountId) return - const isUtxoChain = isUtxoChainId(asset.chainId) - const estimateFeesQueryEnabled = Boolean(fromAddress && accountId && isUtxoChain) - - const amountCryptoBaseUnit = toBaseUnit(withdrawAmountCryptoPrecision, asset.precision) - - const thorchainSaversWithdrawQuoteQueryKey: GetThorchainSaversWithdrawQuoteQueryKey = [ - 'thorchainSaversWithdrawQuote', - { asset, accountId, amountCryptoBaseUnit }, - ] - - const _thorchainSaversWithdrawQuote = await queryClient.fetchQuery({ - queryKey: thorchainSaversWithdrawQuoteQueryKey, - queryFn: getThorchainSaversWithdrawQuoteQueryFn, - staleTime: 5000, - }) - - const dustAmountCryptoPrecision = fromThorBaseUnit( - _thorchainSaversWithdrawQuote?.dust_amount ?? 0, - ).toFixed() - - const estimatedFeesQueryArgs = { - estimateFeesInput: { - cryptoAmount: dustAmountCryptoPrecision, - assetId, - to: _thorchainSaversWithdrawQuote?.inbound_address ?? '', - sendMax: false, - accountId: accountId ?? '', - contractAddress: undefined, - }, - asset, - assetMarketData, - 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: dustAmountCryptoPrecision, - txFeeCryptoBaseUnit: _estimatedFeesData?.txFeeCryptoBaseUnit ?? '', - }) - - const isSweepNeededQueryEnabled = Boolean( - bnOrZero(withdrawAmountCryptoPrecision).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, - 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: dustAmountCryptoPrecision, - txFeeCryptoBaseUnit: _estimatedFeesData?.txFeeCryptoBaseUnit ?? '0', - sweepTxFeeCryptoBaseUnit: _estimatedSweepFeesData?.txFeeCryptoBaseUnit ?? '0', - }) - - return hasEnoughBalanceForTxPlusFeesPlusSweep - }, - [ - accountId, - asset, - assetId, - assetMarketData, - balanceCryptoBaseUnit, - fromAddress, - getHasEnoughBalanceForTxPlusFees, - getHasEnoughBalanceForTxPlusFeesPlusSweep, - queryClient, - ], - ) - const validateCryptoAmount = useCallback( async (value: string) => { if (!opportunityData) return false @@ -779,9 +618,13 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe const hasValidBalance = balanceCryptoPrecision.gt(0) && withdrawAmountCryptoPrecision.gt(0) && - (await fetchHasEnoughBalanceForTxPlusFeesPlusSweep( - withdrawAmountCryptoPrecision.toFixed(), - )) + (await fetchHasEnoughBalanceForTxPlusFeesPlusSweep({ + amountCryptoPrecision: withdrawAmountCryptoPrecision.toFixed(), + accountId, + asset, + type: 'withdraw', + fromAddress, + })) const isBelowWithdrawThreshold = bn(withdrawAmountCryptoBaseUnit) .minus(outboundFeeCryptoBaseUnit) .lt(0) @@ -814,7 +657,7 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe getOutboundFeeCryptoBaseUnit, getWithdrawGasEstimateCryptoBaseUnit, amountAvailableCryptoPrecision, - fetchHasEnoughBalanceForTxPlusFeesPlusSweep, + fromAddress, translate, ], ) @@ -838,9 +681,13 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe amountAvailableFiat.gt(0) && valueCryptoPrecision.gt(0) && amountAvailableFiat.gte(value) && - (await fetchHasEnoughBalanceForTxPlusFeesPlusSweep( - withdrawAmountcryptoPrecision.toFixed(), - )) + (await fetchHasEnoughBalanceForTxPlusFeesPlusSweep({ + amountCryptoPrecision: withdrawAmountcryptoPrecision.toFixed(), + accountId, + asset, + type: 'withdraw', + fromAddress, + })) if (valueCryptoPrecision.isEqualTo(0)) return '' return hasValidBalance || 'trade.errors.insufficientFunds' @@ -854,9 +701,10 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe [ accountId, amountAvailableCryptoPrecision, + asset, assetMarketData.price, dispatch, - fetchHasEnoughBalanceForTxPlusFeesPlusSweep, + fromAddress, opportunityData, translate, ], diff --git a/src/lib/utils/thorchain/index.ts b/src/lib/utils/thorchain/index.ts new file mode 100644 index 00000000000..dbc4f0e3a95 --- /dev/null +++ b/src/lib/utils/thorchain/index.ts @@ -0,0 +1,223 @@ +import type { AccountId } from '@shapeshiftoss/caip' +import { queryClient } from 'context/QueryClientProvider/queryClient' +import type { Asset } from 'lib/asset-service' +import { bnOrZero } from 'lib/bignumber/bignumber' +import { fromBaseUnit, toBaseUnit } from 'lib/math' +import { + type EstimatedFeesQueryKey, + queryFn as getEstimatedFeesQueryFn, +} from 'pages/Lending/hooks/useGetEstimatedFeesQuery' +import type { IsSweepNeededQueryKey } from 'pages/Lending/hooks/useIsSweepNeededQuery' +import { queryFn as isSweepNeededQueryFn } from 'pages/Lending/hooks/useIsSweepNeededQuery' +import { selectPortfolioCryptoBalanceBaseUnitByFilter } from 'state/slices/common-selectors' +import type { ThorchainSaversWithdrawQuoteResponseSuccess } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/types' +import { fromThorBaseUnit } from 'state/slices/opportunitiesSlice/resolvers/thorchainsavers/utils' +import { isUtxoChainId } from 'state/slices/portfolioSlice/utils' +import { selectMarketDataById } from 'state/slices/selectors' +import { store } from 'state/store' + +import type { GetThorchainSaversDepositQuoteQueryKey } from './hooks/useGetThorchainSaversDepositQuoteQuery' +import { queryFn as getThorchainSaversDepositQuoteQueryFn } from './hooks/useGetThorchainSaversDepositQuoteQuery' +import { + type GetThorchainSaversWithdrawQuoteQueryKey, + queryFn as getThorchainSaversWithdrawQuoteQueryFn, +} from './hooks/useGetThorchainSaversWithdrawQuoteQuery' + +// TODO(gomes): this will work for UTXO but is invalid for tokens since they use diff. denoms +// the current workaround is to not do fee deduction for non-UTXO chains, +// but for consistency, we should for native EVM assets, and ensure this is a no-op for tokens +// Note when implementing this, fee checks/deduction will need to either be done for *native* assets only +// or handle different denoms for tokens/native assets and display insufficientFundsForProtocolFee copy +const getHasEnoughBalanceForTxPlusFees = ({ + balanceCryptoBaseUnit, + amountCryptoPrecision, + txFeeCryptoBaseUnit, + precision, +}: { + balanceCryptoBaseUnit: string + amountCryptoPrecision: string + txFeeCryptoBaseUnit: string + precision: number +}) => { + const balanceCryptoBaseUnitBn = bnOrZero(balanceCryptoBaseUnit) + if (balanceCryptoBaseUnitBn.isZero()) return false + + return bnOrZero(amountCryptoPrecision) + .plus(fromBaseUnit(txFeeCryptoBaseUnit, precision ?? 0)) + .lte(fromBaseUnit(balanceCryptoBaseUnitBn, precision)) +} + +const getHasEnoughBalanceForTxPlusFeesPlusSweep = ({ + 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)) +} + +export const fetchHasEnoughBalanceForTxPlusFeesPlusSweep = async ({ + amountCryptoPrecision: _amountCryptoPrecision, + accountId, + asset, + type, + fromAddress, +}: { + asset: Asset + fromAddress: string | null + amountCryptoPrecision: string + accountId: AccountId + type: 'deposit' | 'withdraw' +}) => { + const isUtxoChain = isUtxoChainId(asset.chainId) + const estimateFeesQueryEnabled = Boolean(fromAddress && accountId && isUtxoChain) + + const balanceCryptoBaseUnit = selectPortfolioCryptoBalanceBaseUnitByFilter(store.getState(), { + assetId: asset.assetId, + accountId, + }) + const assetMarketData = selectMarketDataById(store.getState(), asset.assetId) + const quote = await (async () => { + switch (type) { + case 'withdraw': { + const withdrawAmountCryptoBaseUnit = toBaseUnit(_amountCryptoPrecision, asset.precision) + + const thorchainSaversWithdrawQuoteQueryKey: GetThorchainSaversWithdrawQuoteQueryKey = [ + 'thorchainSaversWithdrawQuote', + { asset, accountId, amountCryptoBaseUnit: withdrawAmountCryptoBaseUnit }, + ] + + return queryClient.fetchQuery({ + queryKey: thorchainSaversWithdrawQuoteQueryKey, + queryFn: getThorchainSaversWithdrawQuoteQueryFn, + staleTime: 5000, + }) + } + case 'deposit': { + const amountCryptoBaseUnit = toBaseUnit(_amountCryptoPrecision, asset.precision) + + const thorchainSaversDepositQuoteQueryKey: GetThorchainSaversDepositQuoteQueryKey = [ + 'thorchainSaversDepositQuote', + { asset, amountCryptoBaseUnit }, + ] + + return await queryClient.fetchQuery({ + queryKey: thorchainSaversDepositQuoteQueryKey, + queryFn: getThorchainSaversDepositQuoteQueryFn, + staleTime: 5000, + }) + } + default: + throw new Error('Invalid type') + } + })() + const amountCryptoPrecision = + type === 'deposit' + ? _amountCryptoPrecision + : fromThorBaseUnit( + (quote as ThorchainSaversWithdrawQuoteResponseSuccess).dust_amount, + ).toFixed() + const amountCryptoBaseUnit = toBaseUnit(amountCryptoPrecision, asset.precision) + const estimatedFeesQueryArgs = { + estimateFeesInput: { + cryptoAmount: amountCryptoPrecision, + assetId: asset.assetId, + to: quote?.inbound_address ?? '', + sendMax: false, + accountId: accountId ?? '', + contractAddress: undefined, + }, + asset, + assetMarketData, + 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, + txFeeCryptoBaseUnit: _estimatedFeesData?.txFeeCryptoBaseUnit ?? '', + }) + + const isSweepNeededQueryEnabled = Boolean( + bnOrZero(amountCryptoPrecision).gt(0) && _estimatedFeesData && _hasEnoughBalanceForTxPlusFees, + ) + + const isSweepNeededQueryArgs = { + assetId: asset.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, + estimateFeesInput: { + cryptoAmount: '0', + assetId: asset.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, + txFeeCryptoBaseUnit: _estimatedFeesData?.txFeeCryptoBaseUnit ?? '0', + sweepTxFeeCryptoBaseUnit: _estimatedSweepFeesData?.txFeeCryptoBaseUnit ?? '0', + }) + + return hasEnoughBalanceForTxPlusFeesPlusSweep +} From 45c32f5a3e157947b655de7974519db85c404440 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:53:55 +0100 Subject: [PATCH 09/13] feat: cleanup monkey patch --- .../ThorchainSaversManager/Overview/ThorchainSaversOverview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 29abd9ee10d..31c72c18a29 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 @@ -271,7 +271,7 @@ export const ThorchainSaversOverview: React.FC = ({ label: 'common.withdraw', icon: , action: DefiAction.Withdraw, - isDisabled: false, + isDisabled: hasPendingTxs || hasPendingQueries, toolTip: hasPendingTxs || hasPendingQueries ? translate('defi.modals.saversVaults.cannotWithdrawWhilePendingTx') From c5d6d385caf6cdc392cf4830855f4529864b301d Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 20 Nov 2023 15:40:07 +0100 Subject: [PATCH 10/13] feat: ocd --- .../ThorchainSaversManager/Withdraw/components/Withdraw.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx index b1c2d40a522..59eb2c96546 100644 --- a/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx +++ b/src/features/defi/providers/thorchain-savers/components/ThorchainSaversManager/Withdraw/components/Withdraw.tsx @@ -151,7 +151,6 @@ export const Withdraw: React.FC = ({ accountId, fromAddress, onNe ]) const balanceFilter = useMemo(() => ({ assetId, accountId }), [accountId, assetId]) - // user info const balanceCryptoBaseUnit = useAppSelector(state => selectPortfolioCryptoBalanceBaseUnitByFilter(state, balanceFilter), ) From 58c28c7d59ea5bef92ec708d35f30113e15534f5 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 20 Nov 2023 21:35:14 +0100 Subject: [PATCH 11/13] feat: juicy UI improvements --- .../defi/components/Withdraw/Withdraw.tsx | 9 ++- .../Deposit/components/Deposit.tsx | 4 +- .../Withdraw/components/Withdraw.tsx | 66 ++++++++++++++----- src/lib/utils/thorchain/index.ts | 39 ++++++----- 4 files changed, 82 insertions(+), 36 deletions(-) diff --git a/src/features/defi/components/Withdraw/Withdraw.tsx b/src/features/defi/components/Withdraw/Withdraw.tsx index 07c61558862..089b213ca32 100644 --- a/src/features/defi/components/Withdraw/Withdraw.tsx +++ b/src/features/defi/components/Withdraw/Withdraw.tsx @@ -187,6 +187,13 @@ export const Withdraw: React.FC = ({ [handlePercentClick], ) + const colorScheme = useMemo(() => { + if (isLoading) return 'blue' + if (fieldError) return 'red' + + return 'blue' + }, [fieldError, isLoading]) + if (!asset) return null return ( @@ -242,7 +249,7 @@ export const Withdraw: React.FC = ({ )}