From 5e171937609ab6b5c3bfd32aa5a60b98049a753a Mon Sep 17 00:00:00 2001 From: Tina <59578595+tinaszheng@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:27:29 -0500 Subject: [PATCH] feat: deposit modal form invalid states (#1415) --- .../dialogs/DepositDialog2/AmountInput.tsx | 17 ++- .../dialogs/DepositDialog2/DepositForm.tsx | 90 ++++++++++------ .../dialogs/DepositDialog2/RouteOptions.tsx | 101 +++++++++--------- src/views/dialogs/DepositDialog2/queries.ts | 3 + 4 files changed, 127 insertions(+), 84 deletions(-) diff --git a/src/views/dialogs/DepositDialog2/AmountInput.tsx b/src/views/dialogs/DepositDialog2/AmountInput.tsx index 017c8de5e..a025141e7 100644 --- a/src/views/dialogs/DepositDialog2/AmountInput.tsx +++ b/src/views/dialogs/DepositDialog2/AmountInput.tsx @@ -16,7 +16,6 @@ import { AssetIcon } from '@/components/AssetIcon'; import { Icon, IconName } from '@/components/Icon'; import { Output, OutputType } from '@/components/Output'; -import { useBalance } from './queries'; import { getTokenSymbol, isNativeTokenDenom } from './utils'; export type AmountInputProps = { @@ -24,6 +23,8 @@ export type AmountInputProps = { onChange: (newValue: string) => void; token: TokenForTransfer; onTokenClick: () => void; + tokenBalance: { raw?: string; formatted?: string }; + error?: Error | null; }; const numericValueRegex = /^\d*(?:\\[.])?\d*$/; @@ -33,7 +34,14 @@ function escapeRegExp(string: string): string { const GAS_RESERVE_AMOUNT = parseUnits('0.01', ETH_DECIMALS); -export const AmountInput = ({ value, onChange, token, onTokenClick }: AmountInputProps) => { +export const AmountInput = ({ + value, + onChange, + token, + onTokenClick, + tokenBalance, + error, +}: AmountInputProps) => { const stringGetter = useStringGetter(); const { sourceAccount } = useAccounts(); @@ -45,8 +53,6 @@ export const AmountInput = ({ value, onChange, token, onTokenClick }: AmountInpu onChange(e.target.value); }; - const tokenBalance = useBalance(token.chainId, token.denom); - const onClickMax = () => { if (!tokenBalance.raw) return; @@ -98,9 +104,12 @@ export const AmountInput = ({ value, onChange, token, onTokenClick }: AmountInpu )} diff --git a/src/views/dialogs/DepositDialog2/DepositForm.tsx b/src/views/dialogs/DepositDialog2/DepositForm.tsx index 008b6d111..c5f6064f2 100644 --- a/src/views/dialogs/DepositDialog2/DepositForm.tsx +++ b/src/views/dialogs/DepositDialog2/DepositForm.tsx @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'; import { formatUnits, parseUnits } from 'viem'; @@ -11,7 +11,7 @@ import { SkipRouteSpeed } from '@/hooks/transfers/skipClient'; import { useDebounce } from '@/hooks/useDebounce'; import { useStringGetter } from '@/hooks/useStringGetter'; -import { CoinbaseBrandIcon } from '@/icons'; +import { CoinbaseBrandIcon, WarningIcon } from '@/icons'; import { Button } from '@/components/Button'; import { Output, OutputType } from '@/components/Output'; @@ -21,7 +21,8 @@ import { openDialog } from '@/state/dialogs'; import { AmountInput } from './AmountInput'; import { RouteOptions } from './RouteOptions'; -import { useDepositRoutes } from './queries'; +import { useBalance, useDepositRoutes } from './queries'; +import { getTokenSymbol } from './utils'; export const DepositForm = ({ onTokenSelect, @@ -38,35 +39,64 @@ export const DepositForm = ({ }) => { const dispatch = useAppDispatch(); const stringGetter = useStringGetter(); - const [selectedSpeed, setSelectedSpeed] = useState('fast'); + const tokenBalance = useBalance(token.chainId, token.denom); + const [selectedSpeed, setSelectedSpeed] = useState('fast'); const debouncedAmount = useDebounce(amount); - const { data: routes, isFetching } = useDepositRoutes(token, debouncedAmount); + const { + data: routes, + isFetching, + isPlaceholderData, + error, + } = useDepositRoutes(token, debouncedAmount); useEffect(() => { - if (debouncedAmount && !isFetching && !routes?.fast) setSelectedSpeed('slow'); + if (debouncedAmount && !isFetching && routes && !routes.fast) setSelectedSpeed('slow'); }, [isFetching, routes, debouncedAmount]); const selectedRoute = selectedSpeed === 'fast' ? routes?.fast : routes?.slow; + const depositRoute = !isPlaceholderData ? selectedRoute : undefined; + + const hasSufficientBalance = depositRoute + ? tokenBalance.raw && BigInt(depositRoute.amountIn) <= BigInt(tokenBalance.raw) + : true; + + const depositDisabled = isFetching || !hasSufficientBalance || !depositRoute; + + const depositButtonInner = useMemo(() => { + if (!hasSufficientBalance) return `Insufficient ${getTokenSymbol(token.denom)}`; + if (error) + return ( +
+
+ +
+ {/* TODO(deposit2.0): localization */} +
Min deposit is $10
+
+ ); + + return stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS }); + }, [error, hasSufficientBalance, stringGetter, token.denom]); return (
+ - {routes && ( - - )}

@@ -95,28 +125,26 @@ export const DepositForm = ({
{/* TODO(deposit2.0): Show difference between current and new balance here */} - {selectedRoute && ( -
- {/* TODO(deposit2.0): localization */} -
Available balance
-
- + - -
+
+ {/* TODO(deposit2.0): localization */} +
Available balance
+
+ + +
- )} +
); diff --git a/src/views/dialogs/DepositDialog2/RouteOptions.tsx b/src/views/dialogs/DepositDialog2/RouteOptions.tsx index f4c6696f1..71eae60c7 100644 --- a/src/views/dialogs/DepositDialog2/RouteOptions.tsx +++ b/src/views/dialogs/DepositDialog2/RouteOptions.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react'; +import { ReactNode, useMemo } from 'react'; import { RouteResponse } from '@skip-go/client'; import { formatUnits } from 'viem'; @@ -12,7 +12,7 @@ import { LightningIcon, ShieldIcon } from '@/icons'; import { Output, OutputType } from '@/components/Output'; type Props = { - routes: { slow?: RouteResponse; fast?: RouteResponse }; + routes?: { slow?: RouteResponse; fast?: RouteResponse }; isLoading: boolean; disabled: boolean; selectedSpeed: SkipRouteSpeed; @@ -26,24 +26,55 @@ export const RouteOptions = ({ onSelectSpeed, disabled, }: Props) => { - // TODO(deposit2.0): finalize error handling here - if (!routes.slow && !routes.fast) { - return
There was an error loading deposit quotes.
; - } + const fastRouteDescription = useMemo(() => { + const fastOperationFee = // @ts-ignore + routes?.fast?.operations.find((op) => Boolean(op.goFastTransfer))?.goFastTransfer?.fee; + const totalFastFee = fastOperationFee + ? formatUnits( + BigInt(fastOperationFee.bpsFeeAmount ?? 0) + + BigInt(fastOperationFee.destinationChainFeeAmount ?? 0) + + BigInt(fastOperationFee.sourceChainFeeAmount ?? 0), + 6 + ) + : '-'; - const fastOperationFee = // @ts-ignore - routes.fast?.operations.find((op) => Boolean(op.goFastTransfer))?.goFastTransfer?.fee; - const totalFastFee = fastOperationFee - ? formatUnits( - BigInt(fastOperationFee.bpsFeeAmount ?? 0) + - BigInt(fastOperationFee.destinationChainFeeAmount ?? 0) + - BigInt(fastOperationFee.sourceChainFeeAmount ?? 0), - 6 - ) - : '-'; + // TODO(deposit2.0): localization + if (!routes || disabled) return '$$ fee, $10k limit'; + if (!routes.fast) return 'Unavailable'; + + return ( + + {' '} + fee, $10k limit + + ); + }, [routes, disabled]); + + const slowRouteDescription = useMemo(() => { + // TODO(deposit2.0): localization + if (!routes || disabled) return '$ fee, no limit'; + if (!routes.slow) return 'Unavailable'; + + return ( + + {' '} + fee, no limit + + ); + }, [routes, disabled]); return ( -
+
} selected={selectedSpeed === 'fast'} - disabled={disabled || !routes.fast || isLoading} + disabled={disabled || !routes?.fast || isLoading} onClick={() => onSelectSpeed('fast')} // TODO(deposit2.0): localization title="Instant" - description={ - routes.fast ? ( - - {' '} - fee, $10k limit - - ) : ( - 'Unavailable' - ) - } + description={fastRouteDescription} /> } selected={selectedSpeed === 'slow'} - disabled={disabled || isLoading || !routes.slow} + disabled={disabled || isLoading || !routes?.slow} onClick={() => onSelectSpeed('slow')} // TODO(deposit2.0): localization title="~20 mins" - description={ - routes.slow ? ( - - {' '} - fee, no limit - - ) : ( - 'Unavailable' - ) - } + description={slowRouteDescription} />
); diff --git a/src/views/dialogs/DepositDialog2/queries.ts b/src/views/dialogs/DepositDialog2/queries.ts index e0e295121..c01d232e9 100644 --- a/src/views/dialogs/DepositDialog2/queries.ts +++ b/src/views/dialogs/DepositDialog2/queries.ts @@ -124,6 +124,8 @@ async function getSkipDepositRoutes( destAssetChainID: DYDX_DEPOSIT_CHAIN, amountIn: parseUnits(amount, token.decimals).toString(), smartRelay: true, + // allow quotes even if they have large price impact, as the user would see the difference in fees anyway + allowUnsafe: true, smartSwapOptions: { evmSwaps: true }, }; @@ -147,6 +149,7 @@ export function useDepositRoutes(token: TokenForTransfer, amount: string) { staleTime: 1 * timeUnits.minute, refetchOnMount: 'always', placeholderData: (prev) => prev, + retry: false, }); }