diff --git a/src/views/forms/AccountManagementFormsNew/WithdrawForm/WithdrawButtonAndReceipt.tsx b/src/views/forms/AccountManagementFormsNew/WithdrawForm/WithdrawButtonAndReceipt.tsx
new file mode 100644
index 0000000000..a7d49a464d
--- /dev/null
+++ b/src/views/forms/AccountManagementFormsNew/WithdrawForm/WithdrawButtonAndReceipt.tsx
@@ -0,0 +1,210 @@
+import { useState } from 'react';
+
+import { Asset, RouteResponse } from '@skip-go/client';
+import { shallowEqual } from 'react-redux';
+import styled from 'styled-components';
+import tw from 'twin.macro';
+import { formatUnits } from 'viem';
+
+import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
+import { STRING_KEYS } from '@/constants/localization';
+import { NumberSign, TOKEN_DECIMALS } from '@/constants/numbers';
+
+import { ConnectionErrorType, useApiState } from '@/hooks/useApiState';
+import { useStringGetter } from '@/hooks/useStringGetter';
+import { useTokenConfigs } from '@/hooks/useTokenConfigs';
+
+import { Button } from '@/components/Button';
+import { Details } from '@/components/Details';
+import { DiffOutput } from '@/components/DiffOutput';
+import { Output, OutputType } from '@/components/Output';
+import { Tag } from '@/components/Tag';
+import { WithReceipt } from '@/components/WithReceipt';
+import { WithTooltip } from '@/components/WithTooltip';
+import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton';
+
+import { calculateCanAccountTrade } from '@/state/accountCalculators';
+import { getSubaccount } from '@/state/accountSelectors';
+import { useAppSelector } from '@/state/appTypes';
+import { getTransferInputs } from '@/state/inputsSelectors';
+
+import { isTruthy } from '@/lib/isTruthy';
+
+import { RouteWarningMessage } from '../RouteWarningMessage';
+import { SlippageEditor } from '../SlippageEditor';
+
+type ElementProps = {
+ setSlippage: (slippage: number) => void;
+
+ slippage: number;
+ withdrawToken?: Asset;
+ route?: RouteResponse;
+
+ isDisabled?: boolean;
+ isLoading?: boolean;
+};
+
+export const WithdrawButtonAndReceipt = ({
+ setSlippage,
+
+ slippage,
+ withdrawToken,
+ route,
+
+ isDisabled,
+ isLoading,
+}: ElementProps) => {
+ const [isEditingSlippage, setIsEditingSlipapge] = useState(false);
+ const stringGetter = useStringGetter();
+
+ const { leverage } = useAppSelector(getSubaccount, shallowEqual) ?? {};
+ // TODO: https://linear.app/dydx/issue/OTE-867/coinbase-withdrawals
+ const { exchange } = useAppSelector(getTransferInputs, shallowEqual) ?? {};
+
+ const canAccountTrade = useAppSelector(calculateCanAccountTrade, shallowEqual);
+ const { usdcLabel } = useTokenConfigs();
+ const { connectionError } = useApiState();
+
+ const fees = Number(route?.usdAmountIn) - Number(route?.usdAmountOut);
+ const submitButtonReceipt = [
+ {
+ key: 'expected-amount-received',
+
+ label: (
+ <$RowWithGap>
+ {stringGetter({ key: STRING_KEYS.EXPECTED_AMOUNT_RECEIVED })}
+ {withdrawToken && {withdrawToken?.symbol}}
+ $RowWithGap>
+ ),
+ value: (
+
+ ),
+ },
+ withdrawToken &&
+ !withdrawToken.symbol?.toLowerCase().includes('usd') && {
+ key: 'expected-amount-received-usd',
+
+ label: (
+ <$RowWithGap>
+ {stringGetter({ key: STRING_KEYS.EXPECTED_AMOUNT_RECEIVED })}
+ {withdrawToken && {usdcLabel}}
+ $RowWithGap>
+ ),
+ value: (
+
+ ),
+ },
+ fees && {
+ key: 'bridge-fees',
+ label: (
+
+ {stringGetter({ key: STRING_KEYS.BRIDGE_FEE })}
+
+ ),
+ value: ,
+ },
+ !exchange && {
+ key: 'slippage',
+ label: {stringGetter({ key: STRING_KEYS.MAX_SLIPPAGE })},
+ value: (
+
+ ),
+ },
+ {
+ key: 'estimated-route-duration',
+ label: {stringGetter({ key: STRING_KEYS.ESTIMATED_TIME })},
+ value:
+ typeof route?.estimatedRouteDurationSeconds === 'number' ? (
+
+ ) : undefined,
+ },
+ {
+ key: 'leverage',
+ label: {stringGetter({ key: STRING_KEYS.ACCOUNT_LEVERAGE })},
+ value: (
+
+ ),
+ },
+ ].filter(isTruthy);
+
+ const [hasAcknowledged, setHasAcknowledged] = useState(false);
+ const requiresAcknowledgement = Boolean(route?.warning && !hasAcknowledged);
+ const isFormValid =
+ !isDisabled &&
+ !isEditingSlippage &&
+ connectionError !== ConnectionErrorType.CHAIN_DISRUPTION &&
+ !requiresAcknowledgement;
+
+ if (!canAccountTrade) {
+ return (
+ <$WithReceipt slotReceipt={<$Details items={submitButtonReceipt} />}>
+
+ $WithReceipt>
+ );
+ }
+
+ return (
+ <$WithReceipt slotReceipt={<$Details items={submitButtonReceipt} />}>
+
+
+ $WithReceipt>
+ );
+};
+const $RowWithGap = tw.span`row gap-[0.5ch]`;
+
+const $WithReceipt = tw(WithReceipt)`[--withReceipt-backgroundColor:--color-layer-2]`;
+
+const $Details = styled(Details)`
+ --details-item-vertical-padding: 0.33rem;
+ padding: var(--form-input-paddingY) var(--form-input-paddingX);
+ font-size: 0.8125em;
+`;
diff --git a/src/views/forms/AccountManagementFormsNew/WithdrawForm/useValidation.tsx b/src/views/forms/AccountManagementFormsNew/WithdrawForm/useValidation.tsx
new file mode 100644
index 0000000000..988266332a
--- /dev/null
+++ b/src/views/forms/AccountManagementFormsNew/WithdrawForm/useValidation.tsx
@@ -0,0 +1,203 @@
+import { useMemo } from 'react';
+
+import { exchange } from '@dydxprotocol/v4-abacus';
+import { Asset } from '@skip-go/client';
+import BigNumber from 'bignumber.js';
+
+import { AlertType } from '@/constants/alerts';
+import { STRING_KEYS } from '@/constants/localization';
+import {
+ MAX_CCTP_TRANSFER_AMOUNT,
+ MIN_CCTP_TRANSFER_AMOUNT,
+ TOKEN_DECIMALS,
+} from '@/constants/numbers';
+
+import { useLocaleSeparators } from '@/hooks/useLocaleSeparators';
+import { useRestrictions } from '@/hooks/useRestrictions';
+import { useStringGetter } from '@/hooks/useStringGetter';
+import { useTokenConfigs } from '@/hooks/useTokenConfigs';
+import { useWithdrawalInfo } from '@/hooks/useWithdrawalInfo';
+
+import { formatNumberOutput, OutputType } from '@/components/Output';
+import { Tag } from '@/components/Tag';
+
+import { useAppSelector } from '@/state/appTypes';
+import { getSelectedLocale } from '@/state/localizationSelectors';
+
+import { MustBigNumber } from '@/lib/numbers';
+
+type UseValidationProps = {
+ isCctp: boolean;
+ debouncedAmountBN: BigNumber;
+ freeCollateralBN: BigNumber;
+ isValidDestinationAddress: boolean;
+ error?: string;
+ toAddress?: string;
+ toChainId?: string;
+ toToken?: Asset;
+};
+
+export const useValidation = ({
+ error,
+ isCctp,
+ debouncedAmountBN,
+ toAddress,
+ isValidDestinationAddress,
+ toChainId,
+ toToken,
+ freeCollateralBN,
+}: UseValidationProps) => {
+ const stringGetter = useStringGetter();
+ const { sanctionedAddresses } = useRestrictions();
+ const { decimal: decimalSeparator, group: groupSeparator } = useLocaleSeparators();
+ const selectedLocale = useAppSelector(getSelectedLocale);
+ const { usdcLabel } = useTokenConfigs();
+ const { usdcWithdrawalCapacity } = useWithdrawalInfo({ transferType: 'withdrawal' });
+
+ const { alertType, errorMessage } = useMemo(() => {
+ if (isCctp) {
+ if (debouncedAmountBN.gte(MAX_CCTP_TRANSFER_AMOUNT)) {
+ return {
+ errorMessage: stringGetter({
+ key: STRING_KEYS.MAX_CCTP_TRANSFER_LIMIT_EXCEEDED,
+ params: {
+ MAX_CCTP_TRANSFER_AMOUNT,
+ },
+ }),
+ };
+ }
+ if (
+ !debouncedAmountBN.isZero() &&
+ MustBigNumber(debouncedAmountBN).lte(MIN_CCTP_TRANSFER_AMOUNT)
+ ) {
+ return {
+ errorMessage: stringGetter({
+ key: STRING_KEYS.AMOUNT_MINIMUM_ERROR,
+ params: {
+ NUMBER: MIN_CCTP_TRANSFER_AMOUNT,
+ TOKEN: usdcLabel,
+ },
+ }),
+ };
+ }
+ }
+ if (error) {
+ return {
+ errorMessage: error,
+ };
+ }
+
+ if (!toAddress) {
+ return {
+ alertType: AlertType.Warning,
+ errorMessage: stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ADDRESS }),
+ };
+ }
+
+ if (sanctionedAddresses.has(toAddress))
+ return {
+ errorMessage: stringGetter({
+ key: STRING_KEYS.TRANSFER_INVALID_DYDX_ADDRESS,
+ }),
+ };
+
+ if (!isValidDestinationAddress) {
+ return {
+ errorMessage: stringGetter({
+ key: STRING_KEYS.ENTER_VALID_ADDRESS,
+ }),
+ };
+ }
+
+ // TODO: https://linear.app/dydx/issue/OTE-874/handle-skip-request-error-responses
+ // skip Client does not return error codes. work with skip on this
+ // if (route?.code) {
+ // const routeErrorMessageOverride = getRouteErrorMessageOverride(route?.code, route?.message);
+ // const routeErrorContext = {
+ // transferType: TransferType.Withdraw,
+ // errorMessage: routeErrorMessageOverride ?? undefined,
+ // amount: debouncedAmount,
+ // chainId: toChainId ?? undefined,
+ // assetAddress: toToken?.denom ?? undefined,
+ // assetSymbol: toToken?.symbol ?? undefined,
+ // assetName: toToken?.name ?? undefined,
+ // assetId: toToken?.toString() ?? undefined,
+ // };
+ // track(AnalyticsEvents.RouteError(routeErrorContext));
+ // dd.info('Route error received', routeErrorContext);
+ // return {
+ // errorMessage: routeErrorMessageOverride
+ // ? stringGetter({
+ // key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
+ // params: { ERROR_MESSAGE: routeErrorMessageOverride },
+ // })
+ // : stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG }),
+ // };
+ // }
+
+ if (debouncedAmountBN) {
+ if (!toChainId && !exchange) {
+ return {
+ errorMessage: stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_CHAIN }),
+ };
+ }
+ if (!toToken) {
+ return {
+ errorMessage: stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ASSET }),
+ };
+ }
+ }
+
+ if (debouncedAmountBN.gt(MustBigNumber(freeCollateralBN))) {
+ return {
+ errorMessage: stringGetter({ key: STRING_KEYS.WITHDRAW_MORE_THAN_FREE }),
+ };
+ }
+
+ // Withdrawal Safety
+ if (usdcWithdrawalCapacity.gt(0) && debouncedAmountBN.gt(usdcWithdrawalCapacity)) {
+ return {
+ alertType: AlertType.Warning,
+ errorMessage: stringGetter({
+ key: STRING_KEYS.WITHDRAWAL_LIMIT_OVER,
+ params: {
+ USDC_LIMIT: (
+
+ {formatNumberOutput(usdcWithdrawalCapacity, OutputType.Number, {
+ decimalSeparator,
+ groupSeparator,
+ selectedLocale,
+ fractionDigits: TOKEN_DECIMALS,
+ })}
+ {usdcLabel}
+
+ ),
+ },
+ }),
+ };
+ }
+ return {
+ errorMessage: undefined,
+ };
+ }, [
+ isCctp,
+ error,
+ toAddress,
+ sanctionedAddresses,
+ stringGetter,
+ isValidDestinationAddress,
+ debouncedAmountBN,
+ freeCollateralBN,
+ usdcWithdrawalCapacity,
+ usdcLabel,
+ toChainId,
+ toToken,
+ decimalSeparator,
+ groupSeparator,
+ selectedLocale,
+ ]);
+ return {
+ alertType,
+ errorMessage,
+ };
+};