From 6f1d0eeac6a7ae745ebf4ae290a090adb85c12dc Mon Sep 17 00:00:00 2001 From: Tom McGuire Date: Wed, 2 Oct 2024 15:08:12 -0700 Subject: [PATCH 001/118] chore: replace EarnDepositMode with EarnEnterMode --- src/earn/EarnEnterAmount.tsx | 4 ++-- src/earn/prepareTransactions.ts | 4 ++-- src/earn/types.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/earn/EarnEnterAmount.tsx b/src/earn/EarnEnterAmount.tsx index bdd41e5c77b..1c125fbe440 100644 --- a/src/earn/EarnEnterAmount.tsx +++ b/src/earn/EarnEnterAmount.tsx @@ -22,7 +22,7 @@ import Touchable from 'src/components/Touchable' import CustomHeader from 'src/components/header/CustomHeader' import EarnDepositBottomSheet from 'src/earn/EarnDepositBottomSheet' import { usePrepareDepositTransactions } from 'src/earn/prepareTransactions' -import { EarnDepositMode } from 'src/earn/types' +import { EarnEnterMode } from 'src/earn/types' import { getSwapToAmountInDecimals } from 'src/earn/utils' import { CICOFlow } from 'src/fiatExchanges/utils' import ArrowRightThick from 'src/icons/ArrowRightThick' @@ -59,7 +59,7 @@ const TOKEN_SELECTOR_BORDER_RADIUS = 100 const MAX_BORDER_RADIUS = 96 const FETCH_UPDATED_TRANSACTIONS_DEBOUNCE_TIME = 250 -function useTokens({ pool, mode }: { pool: EarnPosition; mode: EarnDepositMode }) { +function useTokens({ pool, mode }: { pool: EarnPosition; mode: EarnEnterMode }) { const depositToken = useTokenInfo(pool.dataProps.depositTokenId) const swappableTokens = useSelector((state) => swappableFromTokensByNetworkIdSelector(state, [pool.networkId]) diff --git a/src/earn/prepareTransactions.ts b/src/earn/prepareTransactions.ts index 61b14269ac9..176f50e7c3c 100644 --- a/src/earn/prepareTransactions.ts +++ b/src/earn/prepareTransactions.ts @@ -1,7 +1,7 @@ import BigNumber from 'bignumber.js' import _ from 'lodash' import { useAsyncCallback } from 'react-async-hook' -import { EarnDepositMode, PrepareWithdrawAndClaimParams } from 'src/earn/types' +import { EarnEnterMode, PrepareWithdrawAndClaimParams } from 'src/earn/types' import { isGasSubsidizedForNetwork } from 'src/earn/utils' import { triggerShortcutRequest } from 'src/positions/saga' import { RawShortcutTransaction } from 'src/positions/slice' @@ -34,7 +34,7 @@ export async function prepareDepositTransactions({ feeCurrencies: TokenBalance[] pool: EarnPosition hooksApiUrl: string - shortcutId: EarnDepositMode + shortcutId: EarnEnterMode }) { const { enableAppFee } = getDynamicConfigParams(DynamicConfigs[StatsigDynamicConfigs.SWAP_CONFIG]) const args = diff --git a/src/earn/types.ts b/src/earn/types.ts index 883645eefad..9f17cfb211e 100644 --- a/src/earn/types.ts +++ b/src/earn/types.ts @@ -64,4 +64,4 @@ export interface BeforeDepositAction { onPress: () => void } -export type EarnDepositMode = 'deposit' | 'swap-deposit' +export type EarnEnterMode = 'deposit' | 'swap-deposit' | 'withdraw' From 06ce84ec79f70d765c6032060a4b470bfcbe51e2 Mon Sep 17 00:00:00 2001 From: Tom McGuire Date: Wed, 2 Oct 2024 16:50:22 -0700 Subject: [PATCH 002/118] feat(earn): support partial withdrawals --- locales/base/translation.json | 2 + src/earn/EarnEnterAmount.tsx | 133 +++++++++++++++++++++++++++++--- src/earn/EarnPoolInfoScreen.tsx | 36 ++++++--- src/earn/prepareTransactions.ts | 66 ++++++++++++++++ 4 files changed, 215 insertions(+), 22 deletions(-) diff --git a/locales/base/translation.json b/locales/base/translation.json index e4b4e748ad5..89acd1455f0 100644 --- a/locales/base/translation.json +++ b/locales/base/translation.json @@ -2554,8 +2554,10 @@ }, "enterAmount": { "title": "How much would you like to deposit?", + "titleWithdraw": "How much would you like to withdraw?", "deposit": "Deposit", "fees": "Fees", + "available": "Available", "swap": "Swap", "earnUpToLabel": "You could earn up to:", "rateLabel": "Rate (est.)", diff --git a/src/earn/EarnEnterAmount.tsx b/src/earn/EarnEnterAmount.tsx index 1c125fbe440..51df5b29f64 100644 --- a/src/earn/EarnEnterAmount.tsx +++ b/src/earn/EarnEnterAmount.tsx @@ -21,7 +21,10 @@ import TokenIcon, { IconSize } from 'src/components/TokenIcon' import Touchable from 'src/components/Touchable' import CustomHeader from 'src/components/header/CustomHeader' import EarnDepositBottomSheet from 'src/earn/EarnDepositBottomSheet' -import { usePrepareDepositTransactions } from 'src/earn/prepareTransactions' +import { + usePrepareDepositTransactions, + usePrepareWithdrawTransactions, +} from 'src/earn/prepareTransactions' import { EarnEnterMode } from 'src/earn/types' import { getSwapToAmountInDecimals } from 'src/earn/utils' import { CICOFlow } from 'src/fiatExchanges/utils' @@ -61,6 +64,7 @@ const FETCH_UPDATED_TRANSACTIONS_DEBOUNCE_TIME = 250 function useTokens({ pool, mode }: { pool: EarnPosition; mode: EarnEnterMode }) { const depositToken = useTokenInfo(pool.dataProps.depositTokenId) + const withdrawToken = useTokenInfo(pool.dataProps.withdrawTokenId) const swappableTokens = useSelector((state) => swappableFromTokensByNetworkIdSelector(state, [pool.networkId]) ) @@ -81,7 +85,19 @@ function useTokens({ pool, mode }: { pool: EarnPosition; mode: EarnEnterMode }) throw new Error(`Token info not found for token ID ${pool.dataProps.depositTokenId}`) } - return mode === 'deposit' ? [depositToken] : eligibleSwappableTokens + if (!withdrawToken) { + // should never happen + throw new Error(`Token info not found for token ID ${pool.dataProps.withdrawTokenId}`) + } + + switch (mode) { + case 'deposit': + return [depositToken] + case 'withdraw': + return [withdrawToken] + case 'swap-deposit': + return eligibleSwappableTokens + } } function EarnEnterAmount({ route }: Props) { @@ -102,6 +118,7 @@ function EarnEnterAmount({ route }: Props) { const [tokenAmountInput, setTokenAmountInput] = useState('') const [localAmountInput, setLocalAmountInput] = useState('') + const [maxPressed, setMaxPressed] = useState(false) const [enteredIn, setEnteredIn] = useState('token') // this should never be null, just adding a default to make TS happy const localCurrencySymbol = useSelector(getLocalCurrencySymbol) ?? LocalCurrencySymbol.USD @@ -122,13 +139,17 @@ function EarnEnterAmount({ route }: Props) { // NOTE: analytics is already fired by the bottom sheet, don't need one here } + // Avoid conditionally calling hooks + const depositTransaction = usePrepareDepositTransactions() + const withdrawTransaction = usePrepareWithdrawTransactions() + const { prepareTransactionsResult: { prepareTransactionsResult, swapTransaction } = {}, refreshPreparedTransactions, clearPreparedTransactions, prepareTransactionError, isPreparingTransactions, - } = usePrepareDepositTransactions() + } = mode === 'withdraw' ? withdrawTransaction : depositTransaction const walletAddress = useSelector(walletAddressSelector) @@ -150,6 +171,7 @@ function EarnEnterAmount({ route }: Props) { pool, hooksApiUrl, shortcutId: mode, + useMax: maxPressed, }) } @@ -225,7 +247,10 @@ function EarnEnterAmount({ route }: Props) { const { estimatedFeeAmount, feeCurrency, maxFeeAmount } = getFeeCurrencyAndAmounts(prepareTransactionsResult) - const isAmountLessThanBalance = tokenAmount && tokenAmount.lte(token.balance) + const isAmountLessThanBalance = + mode === 'withdraw' + ? tokenAmount && tokenAmount.lte(pool.balance) + : tokenAmount && tokenAmount.lte(token.balance) const showNotEnoughBalanceForGasWarning = isAmountLessThanBalance && prepareTransactionsResult && @@ -241,6 +266,7 @@ function EarnEnterAmount({ route }: Props) { !!tokenAmount?.isZero() || !transactionIsPossible const onTokenAmountInputChange = (value: string) => { + setMaxPressed(false) if (!value) { setTokenAmountInput('') setEnteredIn('token') @@ -256,6 +282,7 @@ function EarnEnterAmount({ route }: Props) { } const onLocalAmountInputChange = (value: string) => { + setMaxPressed(false) // remove leading currency symbol and grouping separators if (value.startsWith(localCurrencySymbol)) { value = value.slice(1) @@ -282,8 +309,13 @@ function EarnEnterAmount({ route }: Props) { // eventually we may want to do something smarter here, like subtracting gas fees from the max amount if // this is a gas-paying token. for now, we are just showing a warning to the user prompting them to lower the amount // if there is not enough for gas - setTokenAmountInput(token.balance.toFormat({ decimalSeparator })) + if (mode === 'withdraw') { + setTokenAmountInput(new BigNumber(pool.balance).toFormat({ decimalSeparator })) + } else { + setTokenAmountInput(token.balance.toFormat({ decimalSeparator })) + } setEnteredIn('token') + setMaxPressed(true) tokenAmountInputRef.current?.blur() localAmountInputRef.current?.blur() AppAnalytics.track(SendEvents.max_pressed, { @@ -312,6 +344,7 @@ function EarnEnterAmount({ route }: Props) { ? getSwapToAmountInDecimals({ swapTransaction, fromAmount: tokenAmount }).toString() : tokenAmount.toString(), }) + // TODO(ACT-1389) if mode === 'withdraw' navigate to EarnConfirmationScreen reviewBottomSheetRef.current?.snapToIndex(0) } @@ -320,7 +353,11 @@ function EarnEnterAmount({ route }: Props) { } /> - {t('earnFlow.enterAmount.title')} + + {mode === 'withdraw' + ? t('earnFlow.enterAmount.titleWithdraw') + : t('earnFlow.enterAmount.title')} + - {tokenAmount && prepareTransactionsResult && ( - )} + {mode === 'withdraw' && ( + + )} {showNotEnoughBalanceForGasWarning && ( )} - {prepareTransactionError && ( + {prepareTransactionError && mode !== 'withdraw' && ( +}) { + const { t } = useTranslation() + const { maxFeeAmount, feeCurrency } = getFeeCurrencyAndAmounts(prepareTransactionsResult) + + return ( + + + + + + + {'('} + + {')'} + + + + {feeCurrency && maxFeeAmount && ( + + { + feeDetailsBottomSheetRef?.current?.snapToIndex(0) + }} + testID="LabelWithInfo/FeeLabel" + /> + + + + + )} + + ) +} + +function TransactionDepositDetails({ pool, token, tokenAmount, @@ -523,7 +634,7 @@ function TransactionDetails({ return ( feeCurrency && maxFeeAmount && ( - + {swapTransaction && ( void + onPressWithdraw: () => void }) { const { bottom } = useSafeAreaInsets() const insetsStyle = { @@ -440,16 +442,7 @@ function ActionButtons({ {withdraw && (