Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sell eth warning for limit/twap #3573

Merged
merged 15 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions apps/cowswap-frontend/src/common/pure/InlineBanner/banners.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -121,3 +124,35 @@ export function CustomRecipientWarningBanner({
</InlineBanner>
)
}

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 (
<InlineBanner bannerType="alert" iconSize={32}>
<strong>Cannot sell {nativeSymbol}</strong>
<p>Selling {nativeSymbol} is only supported on SWAP orders.</p>
<p>
<Button onClick={sellWrapped}>Switch to {wrappedNativeSymbol}</Button>or
<Button onClick={wrapNative}>
Wrap {nativeSymbol} to {wrappedNativeSymbol}
</Button>
first.
</p>
</InlineBanner>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ export function AdvancedOrdersWidget({ children, updaters, params }: AdvancedOrd
const tradeWidgetParams = {
recipient,
compactView: true,
disableNativeSelling: true,
showRecipient,
isTradePriceUpdating,
priceImpact,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -100,14 +101,18 @@ 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 ||
showHighFeeWarning ||
showApprovalBundlingBanner ||
showSafeWcBundlingBanner ||
shouldZeroApprove ||
showRecipientWarning
showRecipientWarning ||
showNativeSellWarning

// Reset price impact flag when there is no price impact
useEffect(() => {
Expand Down Expand Up @@ -156,6 +161,7 @@ export function LimitOrdersWarnings(props: LimitOrdersWarningsProps) {
{showHighFeeWarning && <SmallVolumeWarningBanner feeAmount={feeAmount} feePercentage={feePercentage} />}
{showApprovalBundlingBanner && <BundleTxApprovalBanner />}
{showSafeWcBundlingBanner && <BundleTxSafeWcBanner />}
{showNativeSellWarning && <SellNativeWarningBanner />}
</Wrapper>
) : null
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,6 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => {

const params = {
compactView: false,
disableNativeSelling: true,
isExpertMode,
recipient,
showRecipient,
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 (
<InlineBanner bannerType="alert" iconSize={32}>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Pure
nativeSymbol={native.symbol}
wrappedNativeSymbol={wrapped.symbol}
sellWrapped={() => navigateOnCurrencySelection(Field.INPUT, wrapped)}
wrapNative={() => navigateOnCurrencySelection(Field.OUTPUT, wrapped, undefined, queryParams)}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -58,7 +61,8 @@ export function useNavigateOnCurrencySelection(): CurrencySelectionCallback {
: {
inputCurrencyId: targetInputCurrencyId,
outputCurrencyId: targetOutputCurrencyId,
}
},
searchParams
)

stateUpdateCallback?.()
Expand Down
24 changes: 18 additions & 6 deletions apps/cowswap-frontend/src/modules/trade/hooks/useTradeNavigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(
Expand All @@ -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]
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const tradeButtonsMap: Record<TradeFormValidation, ButtonErrorConfig | Bu
</TradeFormBlankButton>
)
},

[TradeFormValidation.CurrencyNotSet]: {
text: 'Select a token',
},
Expand Down Expand Up @@ -158,4 +159,16 @@ export const tradeButtonsMap: Record<TradeFormValidation, ButtonErrorConfig | Bu
</TradeApproveButton>
)
},
[TradeFormValidation.SellNativeToken]: (context) => {
const currency = context.derivedState.inputCurrency
const isNativeIn = !!currency && getIsNativeToken(currency)

if (!isNativeIn) return null

return (
<TradeFormBlankButton disabled>
<Trans>Selling {currency.symbol} is not supported</Trans>
</TradeFormBlankButton>
)
},
}
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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)
Expand All @@ -47,6 +48,10 @@ export function validateTradeForm(context: TradeFormValidationContext): TradeFor
return TradeFormValidation.CurrencyNotSet
}

if (isNativeIn) {
return TradeFormValidation.SellNativeToken
}

if (inputAmountIsNotSet) {
return TradeFormValidation.InputAmountNotSet
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export enum TradeFormValidation {
ExpertApproveAndSwap,
ApproveAndSwap,
ApproveRequired,

// Native
SellNativeToken,
}

export interface TradeFormValidationLocalContext {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -118,6 +119,10 @@ export function TwapFormWarnings({ localFormValidation, isConfirmationModal }: T
return <UnsupportedWalletWarning isSafeViaWc={isSafeViaWc} />
}

if (primaryFormValidation === TradeFormValidation.SellNativeToken) {
return <SellNativeWarningBanner />
}

if (localFormValidation === TwapFormState.SELL_AMOUNT_TOO_SMALL) {
return <SmallPartVolumeWarning chainId={chainId} />
}
Expand Down
Loading