diff --git a/apps/cowswap-frontend/src/common/pure/InlineBanner/banners.tsx b/apps/cowswap-frontend/src/common/pure/InlineBanner/banners.tsx index 29dc619be7..264fd77450 100644 --- a/apps/cowswap-frontend/src/common/pure/InlineBanner/banners.tsx +++ b/apps/cowswap-frontend/src/common/pure/InlineBanner/banners.tsx @@ -1,8 +1,11 @@ import { TokenAmount } from '@cowprotocol/ui' import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' +import styled from 'styled-components/macro' import { Nullish } from 'types' +import { LinkStyledButton } from 'legacy/theme' + import { ButtonSecondary } from '../ButtonSecondary' import { CowSwapSafeAppLink } from '../CowSwapSafeAppLink' @@ -121,3 +124,35 @@ export function CustomRecipientWarningBanner({ ) } + +export type SellNativeWarningBannerProps = { + sellWrapped: () => void + wrapNative: () => void + nativeSymbol: string | undefined + wrappedNativeSymbol: string | undefined +} + +const Button = styled(LinkStyledButton)` + text-decoration: underline; +` + +export function SellNativeWarningBanner({ + sellWrapped, + wrapNative, + nativeSymbol = 'native', + wrappedNativeSymbol = 'wrapped native', +}: SellNativeWarningBannerProps) { + return ( + + Cannot sell {nativeSymbol} +

Selling {nativeSymbol} is only supported on SWAP orders.

+

+ or + + first. +

+
+ ) +} diff --git a/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersWidget/index.tsx b/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersWidget/index.tsx index e2f1e20f9e..0c9d32d6fe 100644 --- a/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersWidget/index.tsx @@ -107,7 +107,6 @@ export function AdvancedOrdersWidget({ children, updaters, params }: AdvancedOrd const tradeWidgetParams = { recipient, compactView: true, - disableNativeSelling: true, showRecipient, isTradePriceUpdating, priceImpact, diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx index e1a89bdd35..5105e0237d 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWarnings/index.tsx @@ -17,6 +17,7 @@ import { updateLimitOrdersWarningsAtom, } from 'modules/limitOrders/state/limitOrdersWarningsAtom' import { useTradePriceImpact } from 'modules/trade' +import { SellNativeWarningBanner } from 'modules/trade/containers/SellNativeWarningBanner' import { useDerivedTradeState } from 'modules/trade/hooks/useDerivedTradeState' import { NoImpactWarning } from 'modules/trade/pure/NoImpactWarning' import { TradeFormValidation, useGetTradeFormValidation } from 'modules/tradeFormValidation' @@ -100,6 +101,9 @@ export function LimitOrdersWarnings(props: LimitOrdersWarningsProps) { const { state } = useDerivedTradeState() const showRecipientWarning = isConfirmScreen && state?.recipient && account !== state.recipient + // TODO: implement Safe App EthFlow bundling for LIMIT and disable the warning in that case + const showNativeSellWarning = primaryFormValidation === TradeFormValidation.SellNativeToken + const isVisible = showPriceImpactWarning || rateImpact < 0 || @@ -107,7 +111,8 @@ export function LimitOrdersWarnings(props: LimitOrdersWarningsProps) { showApprovalBundlingBanner || showSafeWcBundlingBanner || shouldZeroApprove || - showRecipientWarning + showRecipientWarning || + showNativeSellWarning // Reset price impact flag when there is no price impact useEffect(() => { @@ -156,6 +161,7 @@ export function LimitOrdersWarnings(props: LimitOrdersWarningsProps) { {showHighFeeWarning && } {showApprovalBundlingBanner && } {showSafeWcBundlingBanner && } + {showNativeSellWarning && } ) : null } diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx index ad624603fb..cde6d5eea4 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx @@ -237,7 +237,6 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => { const params = { compactView: false, - disableNativeSelling: true, isExpertMode, recipient, showRecipient, diff --git a/apps/cowswap-frontend/src/modules/swap/pure/banners/TwapSuggestionBanner.tsx b/apps/cowswap-frontend/src/modules/swap/pure/banners/TwapSuggestionBanner.tsx index 46a76fb09c..27db439e23 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/banners/TwapSuggestionBanner.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/banners/TwapSuggestionBanner.tsx @@ -1,12 +1,12 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk' +import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk' import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import { NavLink } from 'react-router-dom' import styled from 'styled-components/macro' -import { TRADE_URL_SELL_AMOUNT_KEY } from 'modules/trade/const/tradeUrl' import { TradeUrlParams } from 'modules/trade/types/TradeRawState' import { parameterizeTradeRoute } from 'modules/trade/utils/parameterizeTradeRoute' +import { parameterizeTradeSearch } from 'modules/trade/utils/parameterizeTradeSearch' import { Routes } from 'common/constants/routes' import { InlineBanner } from 'common/pure/InlineBanner' @@ -52,7 +52,12 @@ export function TwapSuggestionBanner({ if (!shouldSuggestTwap) return null const routePath = - parameterizeTradeRoute(tradeUrlParams, Routes.ADVANCED_ORDERS) + `?${TRADE_URL_SELL_AMOUNT_KEY}=${sellAmount}` + parameterizeTradeRoute(tradeUrlParams, Routes.ADVANCED_ORDERS) + + '?' + + parameterizeTradeSearch('', { + amount: sellAmount, + kind: OrderKind.SELL, + }) return ( diff --git a/apps/cowswap-frontend/src/modules/trade/containers/SellNativeWarningBanner/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/SellNativeWarningBanner/index.tsx new file mode 100644 index 0000000000..b8794a2dd9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/containers/SellNativeWarningBanner/index.tsx @@ -0,0 +1,34 @@ +import { OrderKind } from '@cowprotocol/cow-sdk' + +import { Field } from 'legacy/state/types' + +import { SellNativeWarningBanner as Pure } from 'common/pure/InlineBanner/banners' +import useNativeCurrency from 'lib/hooks/useNativeCurrency' + +import { useDerivedTradeState } from '../../hooks/useDerivedTradeState' +import { useNavigateOnCurrencySelection } from '../../hooks/useNavigateOnCurrencySelection' +import { useWrappedToken } from '../../hooks/useWrappedToken' + +export function SellNativeWarningBanner() { + const native = useNativeCurrency() + const wrapped = useWrappedToken() + const navigateOnCurrencySelection = useNavigateOnCurrencySelection() + + const { state } = useDerivedTradeState() + + const queryParams = state?.inputCurrencyAmount + ? { + kind: OrderKind.SELL, + amount: state.inputCurrencyAmount.toFixed(state.inputCurrencyAmount.currency.decimals), + } + : undefined + + return ( + navigateOnCurrencySelection(Field.INPUT, wrapped)} + wrapNative={() => navigateOnCurrencySelection(Field.OUTPUT, wrapped, undefined, queryParams)} + /> + ) +} diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useNavigateOnCurrencySelection.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useNavigateOnCurrencySelection.ts index 6edad21866..ec4a55aa12 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useNavigateOnCurrencySelection.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useNavigateOnCurrencySelection.ts @@ -6,13 +6,16 @@ import { Currency, Token } from '@uniswap/sdk-core' import { Field } from 'legacy/state/types' -import { useTradeNavigate } from 'modules/trade/hooks/useTradeNavigate' -import { useTradeState } from 'modules/trade/hooks/useTradeState' +import { useTradeNavigate } from './useTradeNavigate' +import { useTradeState } from './useTradeState' + +import { TradeSearchParams } from '../utils/parameterizeTradeSearch' export type CurrencySelectionCallback = ( field: Field, currency: Currency | null, - stateUpdateCallback?: () => void + stateUpdateCallback?: () => void, + searchParams?: TradeSearchParams ) => void function useResolveCurrencyAddressOrSymbol(): (currency: Currency | null) => string | null { @@ -40,7 +43,7 @@ export function useNavigateOnCurrencySelection(): CurrencySelectionCallback { const resolveCurrencyAddressOrSymbol = useResolveCurrencyAddressOrSymbol() return useCallback( - (field: Field, currency: Currency | null, stateUpdateCallback?: () => void) => { + (field: Field, currency: Currency | null, stateUpdateCallback?: () => void, searchParams?: TradeSearchParams) => { if (!state) return const { inputCurrencyId, outputCurrencyId } = state @@ -58,7 +61,8 @@ export function useNavigateOnCurrencySelection(): CurrencySelectionCallback { : { inputCurrencyId: targetInputCurrencyId, outputCurrencyId: targetOutputCurrencyId, - } + }, + searchParams ) stateUpdateCallback?.() diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useTradeNavigate.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useTradeNavigate.ts index 9599bd225f..f16af94932 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useTradeNavigate.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useTradeNavigate.ts @@ -4,12 +4,18 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk' import { useLocation, useNavigate } from 'react-router-dom' -import { useTradeTypeInfo } from 'modules/trade/hooks/useTradeTypeInfo' -import { TradeCurrenciesIds } from 'modules/trade/types/TradeRawState' -import { parameterizeTradeRoute } from 'modules/trade/utils/parameterizeTradeRoute' +import { useTradeTypeInfo } from './useTradeTypeInfo' + +import { TradeCurrenciesIds } from '../types/TradeRawState' +import { parameterizeTradeRoute } from '../utils/parameterizeTradeRoute' +import { parameterizeTradeSearch, TradeSearchParams } from '../utils/parameterizeTradeSearch' interface UseTradeNavigateCallback { - (chainId: SupportedChainId | null | undefined, { inputCurrencyId, outputCurrencyId }: TradeCurrenciesIds): void + ( + chainId: SupportedChainId | null | undefined, + { inputCurrencyId, outputCurrencyId }: TradeCurrenciesIds, + searchParams?: TradeSearchParams + ): void } export function useTradeNavigate(): UseTradeNavigateCallback { @@ -19,7 +25,11 @@ export function useTradeNavigate(): UseTradeNavigateCallback { const tradeRoute = tradeTypeInfo?.route return useCallback( - (chainId: SupportedChainId | null | undefined, { inputCurrencyId, outputCurrencyId }: TradeCurrenciesIds) => { + ( + chainId: SupportedChainId | null | undefined, + { inputCurrencyId, outputCurrencyId }: TradeCurrenciesIds, + searchParams?: TradeSearchParams + ) => { if (!tradeRoute) return const route = parameterizeTradeRoute( @@ -33,7 +43,9 @@ export function useTradeNavigate(): UseTradeNavigateCallback { if (location.pathname === route) return - navigate({ pathname: route, search: location.search }) + const search = parameterizeTradeSearch(location.search, searchParams) + + navigate({ pathname: route, search }) }, [tradeRoute, navigate, location.pathname, location.search] ) diff --git a/apps/cowswap-frontend/src/modules/trade/utils/parameterizeTradeSearch.ts b/apps/cowswap-frontend/src/modules/trade/utils/parameterizeTradeSearch.ts new file mode 100644 index 0000000000..b37477b0e5 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/trade/utils/parameterizeTradeSearch.ts @@ -0,0 +1,27 @@ +import { OrderKind } from '@cowprotocol/cow-sdk' + +import { TRADE_URL_BUY_AMOUNT_KEY, TRADE_URL_SELL_AMOUNT_KEY } from '../const/tradeUrl' + +export type TradeSearchParams = { + amount: string | undefined + kind: OrderKind +} + +/** + * Add/replace searchParams to existing search string + * @param search Existing search params string + * @param searchParamsToAdd Stuff to add + */ +export function parameterizeTradeSearch(search: string, searchParamsToAdd?: TradeSearchParams): string { + const searchParams = new URLSearchParams(search) + + const amountQueryKey = searchParamsToAdd + ? searchParamsToAdd.kind === OrderKind.SELL + ? TRADE_URL_SELL_AMOUNT_KEY + : TRADE_URL_BUY_AMOUNT_KEY + : undefined + + searchParamsToAdd?.amount && amountQueryKey && searchParams.set(amountQueryKey, searchParamsToAdd.amount) + + return searchParams.toString() +} diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx b/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx index 5c917f3495..50f833373b 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx @@ -69,6 +69,7 @@ export const tradeButtonsMap: Record ) }, + [TradeFormValidation.CurrencyNotSet]: { text: 'Select a token', }, @@ -158,4 +159,16 @@ export const tradeButtonsMap: Record ) }, + [TradeFormValidation.SellNativeToken]: (context) => { + const currency = context.derivedState.inputCurrency + const isNativeIn = !!currency && getIsNativeToken(currency) + + if (!isNativeIn) return null + + return ( + + Selling {currency.symbol} is not supported + + ) + }, } diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts b/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts index c128750939..4ced40755e 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/services/validateTradeForm.ts @@ -1,4 +1,4 @@ -import { isAddress, isFractionFalsy } from '@cowprotocol/common-utils' +import { getIsNativeToken, isAddress, isFractionFalsy } from '@cowprotocol/common-utils' import { ApprovalState } from 'legacy/hooks/useApproveCallback/useApproveCallbackMod' @@ -21,6 +21,7 @@ export function validateTradeForm(context: TradeFormValidationContext): TradeFor } = context const { inputCurrency, outputCurrency, inputCurrencyAmount, inputCurrencyBalance, recipient } = derivedTradeState + const isNativeIn = inputCurrency && getIsNativeToken(inputCurrency) && !isWrapUnwrap const approvalRequired = !isPermitSupported && (approvalState === ApprovalState.NOT_APPROVED || approvalState === ApprovalState.PENDING) @@ -47,6 +48,10 @@ export function validateTradeForm(context: TradeFormValidationContext): TradeFor return TradeFormValidation.CurrencyNotSet } + if (isNativeIn) { + return TradeFormValidation.SellNativeToken + } + if (inputAmountIsNotSet) { return TradeFormValidation.InputAmountNotSet } diff --git a/apps/cowswap-frontend/src/modules/tradeFormValidation/types.ts b/apps/cowswap-frontend/src/modules/tradeFormValidation/types.ts index 5f14fa9cb9..c2cb36cf7a 100644 --- a/apps/cowswap-frontend/src/modules/tradeFormValidation/types.ts +++ b/apps/cowswap-frontend/src/modules/tradeFormValidation/types.ts @@ -32,6 +32,9 @@ export enum TradeFormValidation { ExpertApproveAndSwap, ApproveAndSwap, ApproveRequired, + + // Native + SellNativeToken, } export interface TradeFormValidationLocalContext { diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx index 914d19ea6e..876f385396 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWarnings/index.tsx @@ -4,8 +4,11 @@ import { useCallback } from 'react' import { modifySafeHandlerAnalytics } from '@cowprotocol/analytics' import { useIsSafeViaWc, useWalletInfo } from '@cowprotocol/wallet' +import { useAdvancedOrdersDerivedState } from 'modules/advancedOrders' +import { SellNativeWarningBanner } from 'modules/trade/containers/SellNativeWarningBanner' import { useTradeRouteContext } from 'modules/trade/hooks/useTradeRouteContext' import { NoImpactWarning } from 'modules/trade/pure/NoImpactWarning' +import { TradeFormValidation, useGetTradeFormValidation } from 'modules/tradeFormValidation' import { useTradeQuoteFeeFiatAmount } from 'modules/tradeQuote' import { useShouldZeroApprove } from 'common/hooks/useShouldZeroApprove' @@ -26,8 +29,6 @@ import { BigPartTimeWarning } from './warnings/BigPartTimeWarning' import { SmallPriceProtectionWarning } from './warnings/SmallPriceProtectionWarning' import { SwapPriceDifferenceWarning } from './warnings/SwapPriceDifferenceWarning' -import { useAdvancedOrdersDerivedState } from '../../../advancedOrders' -import { TradeFormValidation, useGetTradeFormValidation } from '../../../tradeFormValidation' import { useIsFallbackHandlerRequired } from '../../hooks/useFallbackHandlerVerification' import { useTwapWarningsContext } from '../../hooks/useTwapWarningsContext' import { TwapFormState } from '../../pure/PrimaryActionButton/getTwapFormState' @@ -118,6 +119,10 @@ export function TwapFormWarnings({ localFormValidation, isConfirmationModal }: T return } + if (primaryFormValidation === TradeFormValidation.SellNativeToken) { + return + } + if (localFormValidation === TwapFormState.SELL_AMOUNT_TOO_SMALL) { return }