diff --git a/src/components/ArrowIcon.tsx b/src/components/ArrowIcon.tsx new file mode 100644 index 000000000..b48b69b4e --- /dev/null +++ b/src/components/ArrowIcon.tsx @@ -0,0 +1,23 @@ +import styled, { css } from 'styled-components'; + +import { Icon, IconName } from './Icon'; + +export const ArrowIcon = (props: { direction: 'up' | 'down'; color: string }) => { + return <$ArrowIcon {...props} iconName={IconName.Arrow} />; +}; + +const $ArrowIcon = styled(Icon)<{ direction: 'up' | 'down'; color: string }>` + position: absolute; + ${({ direction }) => + ({ + up: css` + transform: rotate(-90deg); + `, + down: css` + transform: rotate(90deg); + `, + })[direction]} + ${({ color }) => css` + color: var(${color}); + `}; +`; diff --git a/src/components/FormInput.tsx b/src/components/FormInput.tsx index 689c10f76..7adc5ab5d 100644 --- a/src/components/FormInput.tsx +++ b/src/components/FormInput.tsx @@ -13,6 +13,7 @@ import { WithLabel } from '@/components/WithLabel'; type StyleProps = { className?: string; + backgroundColorOverride?: string; }; type ElementProps = { @@ -28,15 +29,33 @@ type ElementProps = { export type FormInputProps = ElementProps & StyleProps & InputProps; export const FormInput = forwardRef( - ({ id, label, slotRight, className, validationConfig, ...otherProps }, ref) => ( + ( + { id, label, slotRight, className, validationConfig, backgroundColorOverride, ...otherProps }, + ref + ) => ( <$FormInputContainer className={className} isValidationAttached={validationConfig?.attached}> <$InputContainer hasLabel={!!label} hasSlotRight={!!slotRight}> {label ? ( - <$WithLabel label={label} inputID={id} disabled={otherProps.disabled}> - + <$WithLabel + label={label} + inputID={id} + disabled={otherProps.disabled} + $backgroundColorOverride={backgroundColorOverride} + > + ) : ( - + )} {slotRight} @@ -65,9 +84,11 @@ const $FormInputContainer = styled.div<{ isValidationAttached?: boolean }>` `} `; -const $InputContainer = styled.div<{ hasLabel?: boolean; hasSlotRight?: boolean }>` +const $InputContainer = styled.div<{ + hasLabel?: boolean; + hasSlotRight?: boolean; +}>` ${formMixins.inputContainer} - input { ${({ hasLabel }) => !hasLabel && @@ -89,8 +110,13 @@ const $InputContainer = styled.div<{ hasLabel?: boolean; hasSlotRight?: boolean `} `; -const $WithLabel = styled(WithLabel)<{ disabled?: boolean }>` +const $WithLabel = styled(WithLabel)<{ disabled?: boolean; $backgroundColorOverride?: string }>` ${formMixins.inputLabel} + ${({ $backgroundColorOverride }) => + $backgroundColorOverride && + css` + background-color: ${$backgroundColorOverride}; + `} label { ${({ disabled }) => !disabled && 'cursor: text;'} diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 04013b2a3..917b1bd29 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -100,6 +100,7 @@ import { } from '@/icons'; import { ChaosLabsIcon } from '@/icons/chaos-labs'; import { LogoShortIcon } from '@/icons/logo-short'; +import UsdcIcon from '@/icons/usdc.svg'; export enum IconName { AddressConnector = 'AddressConnector', @@ -194,6 +195,7 @@ export enum IconName { Translate = 'Translate', Triangle = 'Triangle', TryAgain = 'TryAgain', + Usdc = 'Usdc', Warning = 'Warning', Website = 'Website', Whitepaper = 'Whitepaper', @@ -293,6 +295,7 @@ const icons = { [IconName.Translate]: TranslateIcon, [IconName.Triangle]: TriangleIcon, [IconName.TryAgain]: TryAgainIcon, + [IconName.Usdc]: UsdcIcon, [IconName.Warning]: WarningIcon, [IconName.Website]: WebsiteIcon, [IconName.Whitepaper]: WhitepaperIcon, diff --git a/src/components/Input.tsx b/src/components/Input.tsx index a00216178..ea31254c0 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -26,6 +26,7 @@ export enum InputType { type StyleProps = { className?: string; + $backgroundColorOverride?: string; }; type ElementProps = { @@ -81,6 +82,9 @@ export const Input = forwardRef( onFocus, onInput, type = InputType.Number, + // TODO: https://linear.app/dydx/issue/OTE-888/simplify-input-component-bg-styles + // simplify input component styles. backgroundColorOverride has to override styles in too many places + $backgroundColorOverride: backgroundColorOverride, ...otherProps }, ref @@ -126,6 +130,7 @@ export const Input = forwardRef( <$InputContainer className={className}> {type === InputType.Text || type === InputType.Search ? ( <$Input + $backgroundColorOverride={backgroundColorOverride} // React ref={ref} id={id} @@ -145,6 +150,7 @@ export const Input = forwardRef( /> ) : ( <$NumericFormat + $backgroundColorOverride={backgroundColorOverride} // React getInputRef={ref} id={id} @@ -234,11 +240,21 @@ const InputStyle = css` } `; -const $NumericFormat = styled(NumericFormat)` +const $NumericFormat = styled(NumericFormat)<{ $backgroundColorOverride?: string }>` ${InputStyle} font-feature-settings: var(--fontFeature-monoNumbers); + ${({ $backgroundColorOverride }) => + $backgroundColorOverride && + css` + background-color: ${$backgroundColorOverride}; + `} `; -const $Input = styled.input` +const $Input = styled.input<{ $backgroundColorOverride?: string }>` ${InputStyle} + ${({ $backgroundColorOverride }) => + $backgroundColorOverride && + css` + background-color: ${$backgroundColorOverride}; + `} `; diff --git a/src/hooks/transfers/useTransfers.tsx b/src/hooks/transfers/useTransfers.tsx index 7064f61c2..637d9531b 100644 --- a/src/hooks/transfers/useTransfers.tsx +++ b/src/hooks/transfers/useTransfers.tsx @@ -142,6 +142,24 @@ export const useTransfers = () => { return getDefaultTokenDenomFromAssets(assetsForSelectedChain); }, [assetsForSelectedChain]); + const cosmosChainAddresses = useMemo(() => { + if (!dydxAddress) return {}; + return { + [getOsmosisChainId()]: convertBech32Address({ + address: dydxAddress, + bech32Prefix: OSMO_BECH32_PREFIX, + }), + [getNeutronChainId()]: convertBech32Address({ + address: dydxAddress, + bech32Prefix: NEUTRON_BECH32_PREFIX, + }), + [getNobleChainId()]: convertBech32Address({ + address: dydxAddress, + bech32Prefix: NOBLE_BECH32_PREFIX, + }), + }; + }, [dydxAddress]); + const hasAllParams = !!fromToken?.denom && !!toToken?.denom && @@ -204,21 +222,6 @@ export const useTransfers = () => { amountIn: parseUnits(amount, fromToken.decimals).toString(), }; - // consider moving to useMemo outside of this query - const cosmosChainAddresses = { - [getOsmosisChainId()]: convertBech32Address({ - address: dydxAddress, - bech32Prefix: OSMO_BECH32_PREFIX, - }), - [getNeutronChainId()]: convertBech32Address({ - address: dydxAddress, - bech32Prefix: NEUTRON_BECH32_PREFIX, - }), - [getNobleChainId()]: convertBech32Address({ - address: dydxAddress, - bech32Prefix: NOBLE_BECH32_PREFIX, - }), - }; // WITHDRAWALS if (transferType === TransferType.Withdraw) { return skipClient.msgsDirect({ @@ -261,6 +264,7 @@ export const useTransfers = () => { }); const { route, txs } = routeQuery.data ?? {}; + const routeLoading = routeQuery.isLoading; return { // TODO [onboarding-rewrite]: Think about trimming this list // Right now we're exposing everything, but there's a good chance we can only expose a few properties @@ -291,5 +295,7 @@ export const useTransfers = () => { fromToken, debouncedAmount, debouncedAmountBN, + routeLoading, + cosmosChainAddresses, }; }; diff --git a/src/icons/usdc.svg b/src/icons/usdc.svg new file mode 100644 index 000000000..7c85365a2 --- /dev/null +++ b/src/icons/usdc.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/lib/testFlags.ts b/src/lib/testFlags.ts index 6364ec5be..f6d65c9d5 100644 --- a/src/lib/testFlags.ts +++ b/src/lib/testFlags.ts @@ -65,6 +65,10 @@ class TestFlags { get uiRefresh() { return !!this.queryParams.uirefresh || isDev; } + + get onboardingRewrite() { + return !!this.queryParams.onboarding_rewrite; + } } export const testFlags = new TestFlags(); diff --git a/src/views/dialogs/WithdrawDialog.tsx b/src/views/dialogs/WithdrawDialog.tsx index 874a660d9..66114713b 100644 --- a/src/views/dialogs/WithdrawDialog.tsx +++ b/src/views/dialogs/WithdrawDialog.tsx @@ -9,22 +9,33 @@ import { useStringGetter } from '@/hooks/useStringGetter'; import { layoutMixins } from '@/styles/layoutMixins'; import { Dialog, DialogPlacement } from '@/components/Dialog'; +import { Icon, IconName } from '@/components/Icon'; import { WithdrawForm } from '@/views/forms/AccountManagementForms/WithdrawForm'; +import { WithdrawForm as WithdrawFormV2 } from '@/views/forms/AccountManagementFormsNew/WithdrawForm/WithdrawForm'; + +import { testFlags } from '@/lib/testFlags'; export const WithdrawDialog = ({ setIsOpen }: DialogProps) => { const stringGetter = useStringGetter(); const { isTablet } = useBreakpoints(); - return ( + + {/* TODO [onboarding-rewrite]: localize */} + {stringGetter({ key: STRING_KEYS.WITHDRAW })} USDC + + ) : ( + stringGetter({ key: STRING_KEYS.WITHDRAW }) + ) + } placement={isTablet ? DialogPlacement.FullScreen : DialogPlacement.Default} > - <$Content> - - + <$Content>{testFlags.onboardingRewrite ? : } ); }; diff --git a/src/views/forms/AccountManagementFormsNew/WithdrawForm/WithdrawForm.tsx b/src/views/forms/AccountManagementFormsNew/WithdrawForm/WithdrawForm.tsx new file mode 100644 index 000000000..25710ca4f --- /dev/null +++ b/src/views/forms/AccountManagementFormsNew/WithdrawForm/WithdrawForm.tsx @@ -0,0 +1,528 @@ +import type { ChangeEvent, FormEvent } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { TYPE_URL_MSG_WITHDRAW_FROM_SUBACCOUNT } from '@dydxprotocol/v4-client-js'; +import { parseUnits } from 'ethers'; +import type { NumberFormatValues } from 'react-number-format'; +import { shallowEqual } from 'react-redux'; +import styled from 'styled-components'; + +import { AutoSweepConfig } from '@/constants/abacus'; +import { AlertType } from '@/constants/alerts'; +import { AnalyticsEvents } from '@/constants/analytics'; +import { ButtonAction, ButtonSize } from '@/constants/buttons'; +import { isTokenCctp } from '@/constants/cctp'; +import { getNobleChainId, getSolanaChainId, GRAZ_CHAINS } from '@/constants/graz'; +import { STRING_KEYS } from '@/constants/localization'; +import { TransferNotificationTypes } from '@/constants/notifications'; +import { USD_DECIMALS } from '@/constants/numbers'; +import { TransferType } from '@/constants/transfers'; +import { WalletType } from '@/constants/wallets'; + +import { useSkipClient } from '@/hooks/transfers/skipClient'; +import { useTransfers } from '@/hooks/transfers/useTransfers'; +import { useAccounts } from '@/hooks/useAccounts'; +import { useDydxClient } from '@/hooks/useDydxClient'; +import { useLocalNotifications } from '@/hooks/useLocalNotifications'; +import { useStringGetter } from '@/hooks/useStringGetter'; +import { useTokenConfigs } from '@/hooks/useTokenConfigs'; + +import { formMixins } from '@/styles/formMixins'; + +import { AlertMessage } from '@/components/AlertMessage'; +import { ArrowIcon } from '@/components/ArrowIcon'; +import { Button } from '@/components/Button'; +import { FormInput } from '@/components/FormInput'; +import { FormMaxInputToggleButton } from '@/components/FormMaxInputToggleButton'; +import { Icon, IconName } from '@/components/Icon'; +import { InputType } from '@/components/Input'; + +import { getSubaccount } from '@/state/accountSelectors'; +import { getSelectedDydxChainId } from '@/state/appSelectors'; +import { useAppSelector } from '@/state/appTypes'; +import { getTransferInputs } from '@/state/inputsSelectors'; + +import { isValidAddress } from '@/lib/addressUtils'; +import { track } from '@/lib/analytics/analytics'; +import { dd } from '@/lib/analytics/datadog'; +import { MustBigNumber } from '@/lib/numbers'; +import { log } from '@/lib/telemetry'; + +import { NetworkSelectMenu } from './NetworkSelectMenu'; +import { WithdrawButtonAndReceipt } from './WithdrawButtonAndReceipt'; +import { useWithdrawFormValidation } from './useWithdrawFormValidation'; + +const DUMMY_TX_HASH = 'withdraw_dummy_tx_hash'; + +export const WithdrawForm = () => { + const stringGetter = useStringGetter(); + const [onSubmitErrorMessage, setOnSubmitErrorMessage] = useState(); + const [isSubmitting, setIsSubmitting] = useState(false); + const selectedDydxChainId = useAppSelector(getSelectedDydxChainId); + + const { dydxAddress, sourceAccount, localDydxWallet, localNobleWallet } = useAccounts(); + const { freeCollateral } = useAppSelector(getSubaccount, shallowEqual) ?? {}; + + // TODO [onboarding-rewrite]: https://linear.app/dydx/issue/OTE-867/coinbase-withdrawals + const { exchange } = useAppSelector(getTransferInputs, shallowEqual) ?? {}; + + // User input + const { usdcDenom, usdcDecimals } = useTokenConfigs(); + + const { + setFromTokenDenom, + defaultTokenDenom, + setToTokenDenom, + defaultChainId, + fromChainId, + setFromChainId, + toChainId, + setToChainId, + toAddress, + setToAddress, + setFromAddress, + debouncedAmount, + debouncedAmountBN, + setAmount, + setTransferType, + route, + txs, + toToken, + chainsForNetwork, + routeLoading, + cosmosChainAddresses, + } = useTransfers(); + const { skipClient } = useSkipClient(); + + const isCctp = isTokenCctp(toToken); + const isValidDestinationAddress = useMemo(() => { + const grazChainPrefix = + GRAZ_CHAINS.find((chain) => chain.chainId === toChainId)?.bech32Config.bech32PrefixAccAddr ?? + ''; + const prefix = exchange ? 'noble' : grazChainPrefix; + return isValidAddress({ + address: toAddress, + network: toChainId === getSolanaChainId() ? 'solana' : prefix ? 'cosmos' : 'evm', + prefix, + }); + }, [exchange, toAddress, toChainId]); + + const { addOrUpdateTransferNotification } = useLocalNotifications(); + + const freeCollateralBN = useMemo(() => MustBigNumber(freeCollateral?.current), [freeCollateral]); + + // TODO [onboarding-rewrite]: https://linear.app/dydx/issue/OTE-869/optimize-usetransfers + // Stop doing this. This is pretty slow and requires multiple render cycles to set initial state + // Set default values for withdraw form + useEffect(() => { + setTransferType(TransferType.Withdraw); + setFromChainId(selectedDydxChainId); + setFromAddress(dydxAddress); + setFromTokenDenom(usdcDenom); + // Cosmos chains connect to the keplr wallet, which has a unique address per chain id + const calculatedCosmosAddress = toChainId && cosmosChainAddresses[toChainId]; + setToAddress(calculatedCosmosAddress ?? sourceAccount.address); + }, [ + setTransferType, + setFromChainId, + selectedDydxChainId, + setFromAddress, + dydxAddress, + setFromTokenDenom, + usdcDenom, + setToAddress, + sourceAccount.address, + toChainId, + cosmosChainAddresses, + ]); + + useEffect(() => { + setToChainId(defaultChainId); + }, [defaultChainId, setToChainId]); + + useEffect(() => { + setToTokenDenom(defaultTokenDenom); + }, [defaultTokenDenom, setToTokenDenom]); + + const { screenAddresses } = useDydxClient(); + const nobleChainId = getNobleChainId(); + + const submitCCTPWithdrawal = useCallback( + async (notificationId: string) => { + if ( + !route || + !dydxAddress || + !toAddress || + !toChainId || + !localNobleWallet?.address || + !toToken + ) + return; + AutoSweepConfig.disable_autosweep = true; + await skipClient.executeRoute({ + route, + getCosmosSigner: async (chainID) => { + if (chainID === getNobleChainId()) { + if (!localNobleWallet.offlineSigner) { + throw new Error('No local noblewallet offline signer. Cannot submit tx'); + } + return localNobleWallet.offlineSigner; + } + if (!localDydxWallet?.offlineSigner) + throw new Error('No local dydxwallet offline signer. Cannot submit tx'); + return localDydxWallet.offlineSigner; + }, + beforeMsg: { + msg: JSON.stringify({ + sender: { + owner: dydxAddress, + number: 0, + }, + recipient: dydxAddress, + assetId: 0, + quantums: parseUnits(debouncedAmount, usdcDecimals), + }), + msgTypeURL: TYPE_URL_MSG_WITHDRAW_FROM_SUBACCOUNT, + }, + // TODO [onboarding-rewrite]: think about building this dynamically + // Right now we don't need to because every withdrawal follows the same cctp route + // dydx -> noble -> final destination + userAddresses: [ + { chainID: selectedDydxChainId, address: dydxAddress }, + { + chainID: getNobleChainId(), + address: localNobleWallet?.address, + }, + { + chainID: toChainId, + address: toAddress, + }, + ], + onTransactionBroadcast: async ({ txHash, chainID }) => { + // TODO [onboarding-rewrite]: enable transfer notifications. This does not work yet + // https://linear.app/dydx/issue/OTE-868/transfer-status-notifications + if (chainID === toChainId) { + const notificationParams = { + id: notificationId, + txHash, + type: TransferNotificationTypes.Withdrawal, + toChainId, + fromChainId, + toAmount: Number(debouncedAmount), + triggeredAt: Date.now(), + isCctp, + isExchange: Boolean(exchange), + requestId: undefined, + }; + addOrUpdateTransferNotification({ ...notificationParams, txHash, isDummy: false }); + const transferWithdrawContext = { + chainId: toChainId, + tokenAddress: toToken.denom, + tokenSymbol: toToken.symbol, + slippage: undefined, + // TODO [onboarding-rewrite]: connect slippage + bridgeFee: + route.usdAmountIn && route.usdAmountOut + ? Number(route.usdAmountIn) - Number(route.usdAmountOut) + : undefined, + exchangeRate: undefined, + estimatedRouteDuration: route.estimatedRouteDurationSeconds, + toAmount: route.amountOut ? Number(route.amountOut) : undefined, + toAmountMin: route.estimatedAmountOut ? Number(route.estimatedAmountOut) : undefined, + txHash, + }; + track(AnalyticsEvents.TransferWithdraw(transferWithdrawContext)); + dd.info('Transfer withdraw submitted', transferWithdrawContext); + } + }, + onTransactionCompleted: async (chainID) => { + // once the transaction in noble is complete, we can be confident that + // there are no more funds in the noble wallet that need to be transferred + if (chainID === getNobleChainId()) { + AutoSweepConfig.disable_autosweep = false; + } + }, + }); + }, + [ + route, + dydxAddress, + toAddress, + toChainId, + localNobleWallet?.address, + localNobleWallet?.offlineSigner, + toToken, + skipClient, + debouncedAmount, + usdcDecimals, + selectedDydxChainId, + localDydxWallet?.offlineSigner, + fromChainId, + isCctp, + exchange, + addOrUpdateTransferNotification, + ] + ); + + const onSubmit = useCallback( + async (e: FormEvent) => { + const notificationId = crypto.randomUUID(); + + try { + e.preventDefault(); + + if (!txs || !debouncedAmount || !toAddress || !dydxAddress) { + throw new Error('Invalid request payload'); + } + + setIsSubmitting(true); + setOnSubmitErrorMessage(undefined); + + const screenResults = await screenAddresses({ + addresses: [toAddress, dydxAddress], + }); + + if (screenResults?.[dydxAddress]) { + setOnSubmitErrorMessage( + stringGetter({ + key: STRING_KEYS.WALLET_RESTRICTED_WITHDRAWAL_TRANSFER_ORIGINATION_ERROR_MESSAGE, + }) + ); + } else if (screenResults?.[toAddress]) { + setOnSubmitErrorMessage( + stringGetter({ + key: STRING_KEYS.WALLET_RESTRICTED_WITHDRAWAL_TRANSFER_DESTINATION_ERROR_MESSAGE, + }) + ); + } else { + if (!isCctp) { + throw new Error('Only cctp routes are eligible for withdrawal'); + } + await submitCCTPWithdrawal(notificationId); + } + } catch (err) { + log('WithdrawForm/onSubmit', err); + if (err?.code === 429) { + setOnSubmitErrorMessage( + stringGetter({ key: STRING_KEYS.RATE_LIMIT_REACHED_ERROR_MESSAGE }) + ); + } else { + setOnSubmitErrorMessage( + err.message + ? stringGetter({ + key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE, + params: { + ERROR_MESSAGE: err.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }), + }, + }) + : stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG }) + ); + } + // if error update dummy notification with error + addOrUpdateTransferNotification({ + id: notificationId, + txHash: DUMMY_TX_HASH, + status: { error: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG }) }, + }); + } finally { + setIsSubmitting(false); + } + }, + [ + txs, + debouncedAmount, + toAddress, + dydxAddress, + screenAddresses, + stringGetter, + isCctp, + submitCCTPWithdrawal, + addOrUpdateTransferNotification, + ] + ); + + const onChangeAddress = (e: ChangeEvent) => { + setToAddress(e.target.value); + }; + + const onChangeAmount = ({ value }: NumberFormatValues) => { + setAmount(value); + setOnSubmitErrorMessage(undefined); + }; + + const onClickMax = () => { + setAmount(freeCollateralBN.toString()); + }; + + useEffect(() => { + if (sourceAccount?.walletInfo?.name === WalletType.Privy) { + // TODO [onboarding-rewrite]: https://linear.app/dydx/issue/OTE-867/coinbase-withdrawals + // abacusStateManager.setTransferValue({ + // field: TransferInputField.exchange, + // value: 'coinbase', + // }); + } + }, [sourceAccount, nobleChainId, setToChainId]); + + const onSelectNetwork = useCallback( + (chainID: string, type: 'chain' | 'exchange') => { + if (chainID) { + setAmount(''); + if (type === 'chain') { + setToChainId(chainID); + } + } + }, + [setAmount, setToChainId] + ); + + const { errorMessage, alertType } = useWithdrawFormValidation({ + isCctp, + debouncedAmountBN, + toAddress, + isValidDestinationAddress, + onSubmitErrorMessage, + toChainId, + toToken, + freeCollateralBN, + }); + + const isDisabled = + !!errorMessage || + !toToken || + (!toChainId && !exchange) || + debouncedAmountBN.isNaN() || + debouncedAmountBN.isZero() || + isSubmitting || + !isValidDestinationAddress; + + const [isPreviewing, setIsPreviewing] = useState(false); + if (isPreviewing) { + return ( + <$Form onSubmit={onSubmit}> +
+ {/* TODO [onboarding-rewrite]: localize */} + Make sure everything looks correct with your withdrawal before confirming +
+ + + + + ); + } + + return ( + <$Form> + + + {stringGetter({ key: STRING_KEYS.DESTINATION })}{' '} + {isValidDestinationAddress ? ( + + ) : null} + + } + validationConfig={ + toAddress && !!exchange && !isValidDestinationAddress + ? { + type: AlertType.Error, + message: stringGetter({ key: STRING_KEYS.NOBLE_ADDRESS_VALIDATION }), + } + : undefined + } + slotRight={ + setToAddress('')} + /> + } + /> + +
{stringGetter({ key: STRING_KEYS.AMOUNT })}
+
+
{freeCollateral?.current?.toFixed(2)} USDC Held
+ + } + slotRight={ + (isPressed ? onClickMax() : setAmount(''))} + /> + } + /> + + + + + {errorMessage && ( + + {errorMessage} + + )} +
+ +
+ + ); +}; +const $Form = styled.form` + ${formMixins.transfersForm} +`; + +const ArrowContainer = styled.div` + border: solid var(--border-width) var(--color-layer-6); + background-color: var(--color-layer-4); + justify-content: center; + z-index: 1; + margin-bottom: -1.25; + margin-top: -1.25; + display: flex; + width: 1.5; +`; diff --git a/src/views/forms/AccountManagementFormsNew/WithdrawForm/useValidation.tsx b/src/views/forms/AccountManagementFormsNew/WithdrawForm/useWithdrawFormValidation.tsx similarity index 100% rename from src/views/forms/AccountManagementFormsNew/WithdrawForm/useValidation.tsx rename to src/views/forms/AccountManagementFormsNew/WithdrawForm/useWithdrawFormValidation.tsx