From 7b2a49c41ecfd62107a3128e771003743094d246 Mon Sep 17 00:00:00 2001 From: Leandro Date: Fri, 11 Oct 2024 11:02:18 +0100 Subject: [PATCH 01/12] feat(slippage): small order slippage v2 (#4934) * fix: ignore quote metadata when deciding to to trigger a new quote * fix: memoize useGetQuoteAndStatus response * feat: use smart slippage based on trade size in relation to fee * feat: show loading indicator on suggested slippage when trade is loading * chore: fix cosmos build * chore: fix lint * feat: use smart slippage based on fee amount % * feat: use fee multiplier factor from launch darkly * refactor: exit earlier if LD multiplier is falsy * fix: return undefined and avoid setting smart slippage when both are missing * feat: cap slippage at 50% * refactor: split FeesUpdater * refactor: use useSafeMemoObject instead of useMemo * refactor: avoid repeating the same code * refactor: split SmartSlippageUpdater * refactor: extract calculateBpsFromFeeMultiplier and added unittests * feat: cap the sum of calculated slippages at 50% * refactor: rename variable to calculateBpsFromFeeMultiplier * fix: fix before and after as they don't mean the same thing for buy and sell orders * fix: reset smart slippage when both are disabled * fix: don't update smart slippage if trade review modal is open * feat: set max suggested slippage to 5% (down from 50%) * feat: set min suggested slippage to 0.5% * refactor: adjust import * chore: fix lint * feat: show warning when >2% slippage is suggested * feat: do not show warning while price is loading * feat: do not show warning when not connected --- .../{FeesUpdater.ts => FeesUpdater/index.ts} | 94 +------------------ .../FeesUpdater/isRefetchQuoteRequired.ts | 63 +++++++++++++ .../FeesUpdater/quoteUsingSameParameters.ts | 70 ++++++++++++++ .../legacy/components/SwapWarnings/index.tsx | 29 +++++- .../src/legacy/state/price/hooks.ts | 8 +- .../src/modules/appData/index.ts | 1 + .../swap/containers/Row/RowSlippage/index.tsx | 5 +- .../swap/containers/SwapWidget/index.tsx | 7 +- .../src/modules/swap/hooks/useSwapState.tsx | 1 - .../Row/RowSlippageContent/index.cosmos.tsx | 1 + .../pure/Row/RowSlippageContent/index.tsx | 68 ++++++++++---- .../src/modules/swap/pure/warnings.tsx | 9 +- .../calculateBpsFromFeeMultiplier.test.ts | 51 ++++++++++ .../calculateBpsFromFeeMultiplier.ts | 47 ++++++++++ .../updaters/SmartSlippageUpdater/index.ts | 86 +++++++++++++++++ .../useSmartSlippageFromBff.ts} | 18 +--- .../useSmartSlippageFromFeeMultiplier.ts | 28 ++++++ 17 files changed, 455 insertions(+), 131 deletions(-) rename apps/cowswap-frontend/src/common/updaters/{FeesUpdater.ts => FeesUpdater/index.ts} (63%) create mode 100644 apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts create mode 100644 apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.test.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts rename apps/cowswap-frontend/src/modules/swap/updaters/{SmartSlippageUpdater.ts => SmartSlippageUpdater/useSmartSlippageFromBff.ts} (74%) create mode 100644 apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts similarity index 63% rename from apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts rename to apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts index a45e3bc669..4eeb7b8030 100644 --- a/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/index.ts @@ -12,8 +12,6 @@ import ms from 'ms.macro' import { useRefetchQuoteCallback } from 'legacy/hooks/useRefetchPriceCallback' import { useAllQuotes, useIsBestQuoteLoading, useSetQuoteError } from 'legacy/state/price/hooks' -import { QuoteInformationObject } from 'legacy/state/price/reducer' -import { LegacyFeeQuoteParams } from 'legacy/state/price/types' import { isWrappingTrade } from 'legacy/state/swap/utils' import { Field } from 'legacy/state/types' import { useUserTransactionTTL } from 'legacy/state/user/hooks' @@ -22,95 +20,11 @@ import { useAppData } from 'modules/appData' import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useDerivedSwapInfo, useSwapState } from 'modules/swap/hooks/useSwapState' +import { isRefetchQuoteRequired } from './isRefetchQuoteRequired' +import { quoteUsingSameParameters } from './quoteUsingSameParameters' + export const TYPED_VALUE_DEBOUNCE_TIME = 350 export const SWAP_QUOTE_CHECK_INTERVAL = ms`30s` // Every 30s -const RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME = ms`30s` // Will renew the quote if there's less than 30 seconds left for the quote to expire -const WAITING_TIME_BETWEEN_EQUAL_REQUESTS = ms`5s` // Prevents from sending the same request to often (max, every 5s) - -type FeeQuoteParams = Omit - -/** - * Returns if the quote has been recently checked - */ -function wasQuoteCheckedRecently(lastQuoteCheck: number): boolean { - return lastQuoteCheck + WAITING_TIME_BETWEEN_EQUAL_REQUESTS > Date.now() -} - -/** - * Returns true if the fee quote expires soon (in less than RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME milliseconds) - */ -function isExpiringSoon(quoteExpirationIsoDate: string, threshold: number): boolean { - const feeExpirationDate = Date.parse(quoteExpirationIsoDate) - return feeExpirationDate <= Date.now() + threshold -} - -/** - * Checks if the parameters for the current quote are correct - * - * Quotes are only valid for a given token-pair and amount. If any of these parameter change, the fee needs to be re-fetched - */ -function quoteUsingSameParameters(currentParams: FeeQuoteParams, quoteInfo: QuoteInformationObject): boolean { - const { - amount: currentAmount, - sellToken: currentSellToken, - buyToken: currentBuyToken, - kind: currentKind, - userAddress: currentUserAddress, - receiver: currentReceiver, - appData: currentAppData, - } = currentParams - const { amount, buyToken, sellToken, kind, userAddress, receiver, appData } = quoteInfo - const hasSameReceiver = currentReceiver && receiver ? currentReceiver === receiver : true - - // cache the base quote params without quoteInfo user address to check - const paramsWithoutAddress = - sellToken === currentSellToken && - buyToken === currentBuyToken && - amount === currentAmount && - kind === currentKind && - appData === currentAppData && - hasSameReceiver - // 2 checks: if there's a quoteInfo user address (meaning quote was already calculated once) and one without - // in case user is not connected - return userAddress ? currentUserAddress === userAddress && paramsWithoutAddress : paramsWithoutAddress -} - -/** - * Decides if we need to refetch the fee information given the current parameters (selected by the user), and the current feeInfo (in the state) - */ -function isRefetchQuoteRequired( - isLoading: boolean, - currentParams: FeeQuoteParams, - quoteInformation?: QuoteInformationObject -): boolean { - // If there's no quote/fee information, we always re-fetch - if (!quoteInformation) { - return true - } - - if (!quoteUsingSameParameters(currentParams, quoteInformation)) { - // If the current parameters don't match the fee, the fee information is invalid and needs to be re-fetched - return true - } - - // The query params are the same, so we only ask for a new quote if: - // - If the quote was not queried recently - // - There's not another price query going on right now - // - The quote will expire soon - if (wasQuoteCheckedRecently(quoteInformation.lastCheck)) { - // Don't Re-fetch if it was queried recently - return false - } else if (isLoading) { - // Don't Re-fetch if there's another quote going on with the same params - // It's better to wait for the timeout or resolution. Also prevents an issue of refreshing too fast with slow APIs - return false - } else if (quoteInformation.fee) { - // Re-fetch if the fee is expiring soon - return isExpiringSoon(quoteInformation.fee.expirationDate, RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME) - } - - return false -} export function FeesUpdater(): null { const { chainId, account } = useWalletInfo() @@ -219,7 +133,7 @@ export function FeesUpdater(): null { // Callback to re-fetch both the fee and the price const refetchQuoteIfRequired = () => { // if no token is unsupported and needs refetching - const hasToRefetch = !unsupportedToken && isRefetchQuoteRequired(isLoading, quoteParams, quoteInfo) + const hasToRefetch = !unsupportedToken && isRefetchQuoteRequired(isLoading, quoteParams, quoteInfo) // if (hasToRefetch) { // Decide if this is a new quote, or just a refresh diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts new file mode 100644 index 0000000000..5b4714631b --- /dev/null +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/isRefetchQuoteRequired.ts @@ -0,0 +1,63 @@ +import ms from 'ms.macro' + +import { QuoteInformationObject } from 'legacy/state/price/reducer' +import { LegacyFeeQuoteParams } from 'legacy/state/price/types' + +import { quoteUsingSameParameters } from './quoteUsingSameParameters' + +const RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME = ms`30s` // Will renew the quote if there's less than 30 seconds left for the quote to expire +const WAITING_TIME_BETWEEN_EQUAL_REQUESTS = ms`5s` // Prevents from sending the same request to often (max, every 5s) + +type FeeQuoteParams = Omit + +/** + * Returns if the quote has been recently checked + */ +function wasQuoteCheckedRecently(lastQuoteCheck: number): boolean { + return lastQuoteCheck + WAITING_TIME_BETWEEN_EQUAL_REQUESTS > Date.now() +} + +/** + * Returns true if the fee quote expires soon (in less than RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME milliseconds) + */ +function isExpiringSoon(quoteExpirationIsoDate: string, threshold: number): boolean { + const feeExpirationDate = Date.parse(quoteExpirationIsoDate) + return feeExpirationDate <= Date.now() + threshold +} + +/** + * Decides if we need to refetch the fee information given the current parameters (selected by the user), and the current feeInfo (in the state) + */ +export function isRefetchQuoteRequired( + isLoading: boolean, + currentParams: FeeQuoteParams, + quoteInformation?: QuoteInformationObject, +): boolean { + // If there's no quote/fee information, we always re-fetch + if (!quoteInformation) { + return true + } + + if (!quoteUsingSameParameters(currentParams, quoteInformation)) { + // If the current parameters don't match the fee, the fee information is invalid and needs to be re-fetched + return true + } + + // The query params are the same, so we only ask for a new quote if: + // - If the quote was not queried recently + // - There's not another price query going on right now + // - The quote will expire soon + if (wasQuoteCheckedRecently(quoteInformation.lastCheck)) { + // Don't Re-fetch if it was queried recently + return false + } else if (isLoading) { + // Don't Re-fetch if there's another quote going on with the same params + // It's better to wait for the timeout or resolution. Also prevents an issue of refreshing too fast with slow APIs + return false + } else if (quoteInformation.fee) { + // Re-fetch if the fee is expiring soon + return isExpiringSoon(quoteInformation.fee.expirationDate, RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME) + } + + return false +} diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts new file mode 100644 index 0000000000..b41f5e0a66 --- /dev/null +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater/quoteUsingSameParameters.ts @@ -0,0 +1,70 @@ +import { QuoteInformationObject } from 'legacy/state/price/reducer' +import { LegacyFeeQuoteParams } from 'legacy/state/price/types' + +import { decodeAppData } from 'modules/appData' + +type FeeQuoteParams = Omit + +/** + * Checks if the parameters for the current quote are correct + * + * Quotes are only valid for a given token-pair and amount. If any of these parameter change, the fee needs to be re-fetched + */ +export function quoteUsingSameParameters(currentParams: FeeQuoteParams, quoteInfo: QuoteInformationObject): boolean { + const { + amount: currentAmount, + sellToken: currentSellToken, + buyToken: currentBuyToken, + kind: currentKind, + userAddress: currentUserAddress, + receiver: currentReceiver, + appData: currentAppData, + } = currentParams + const { amount, buyToken, sellToken, kind, userAddress, receiver, appData } = quoteInfo + const hasSameReceiver = currentReceiver && receiver ? currentReceiver === receiver : true + const hasSameAppData = compareAppDataWithoutQuoteData(appData, currentAppData) + + // cache the base quote params without quoteInfo user address to check + const paramsWithoutAddress = + sellToken === currentSellToken && + buyToken === currentBuyToken && + amount === currentAmount && + kind === currentKind && + hasSameAppData && + hasSameReceiver + // 2 checks: if there's a quoteInfo user address (meaning quote was already calculated once) and one without + // in case user is not connected + return userAddress ? currentUserAddress === userAddress && paramsWithoutAddress : paramsWithoutAddress +} + +/** + * Compares appData without taking into account the `quote` metadata + */ +function compareAppDataWithoutQuoteData(a: T, b: T): boolean { + if (a === b) { + return true + } + const cleanedA = removeQuoteMetadata(a) + const cleanedB = removeQuoteMetadata(b) + + return cleanedA === cleanedB +} + +/** + * If appData is set and is valid, remove `quote` metadata from it + */ +function removeQuoteMetadata(appData: string | undefined): string | undefined { + if (!appData) { + return + } + + const decoded = decodeAppData(appData) + + if (!decoded) { + return + } + + const { metadata: fullMetadata, ...rest } = decoded + const { quote: _, ...metadata } = fullMetadata + return JSON.stringify({ ...rest, metadata }) +} diff --git a/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx b/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx index fca6d9cf67..b2b1caa0e9 100644 --- a/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx @@ -1,4 +1,3 @@ - import { Command } from '@cowprotocol/types' import { HoverTooltip } from '@cowprotocol/ui' import { Fraction } from '@uniswap/sdk-core' @@ -45,7 +44,7 @@ const WarningCheckboxContainer = styled.span` const WarningContainer = styled(AuxInformationContainer).attrs((props) => ({ ...props, hideInput: true, -})) ` +}))` --warningColor: ${({ theme, level }) => level === HIGH_TIER_FEE ? theme.danger @@ -171,3 +170,29 @@ export const HighFeeWarning = (props: WarningProps) => { ) } + +export type HighSuggestedSlippageWarningProps = { + isSuggestedSlippage: boolean | undefined + slippageBps: number | undefined + className?: string +} & HighFeeContainerProps + +export function HighSuggestedSlippageWarning(props: HighSuggestedSlippageWarningProps) { + const { isSuggestedSlippage, slippageBps, ...rest } = props + + if (!isSuggestedSlippage || !slippageBps || slippageBps <= 200) { + return null + } + + return ( + +
+ + Beware! High dynamic slippage suggested ({`${slippageBps / 100}`}%) + + + +
+
+ ) +} diff --git a/apps/cowswap-frontend/src/legacy/state/price/hooks.ts b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts index 61df713d5d..2386fb8193 100644 --- a/apps/cowswap-frontend/src/legacy/state/price/hooks.ts +++ b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts @@ -4,6 +4,8 @@ import { SupportedChainId, SupportedChainId as ChainId } from '@cowprotocol/cow- import { useDispatch, useSelector } from 'react-redux' +import { useSafeMemoObject } from 'common/hooks/useSafeMemo' + import { getNewQuote, GetQuoteParams, @@ -62,7 +64,11 @@ export const useGetQuoteAndStatus = (params: QuoteParams): UseGetQuoteAndStatus const isGettingNewQuote = Boolean(isLoading && !quote?.price?.amount) const isRefreshingQuote = Boolean(isLoading && quote?.price?.amount) - return { quote, isGettingNewQuote, isRefreshingQuote } + return useSafeMemoObject({ + quote, + isGettingNewQuote, + isRefreshingQuote, + }) } export const useGetNewQuote = (): GetNewQuoteCallback => { diff --git a/apps/cowswap-frontend/src/modules/appData/index.ts b/apps/cowswap-frontend/src/modules/appData/index.ts index da9816331c..6e5d063d5d 100644 --- a/apps/cowswap-frontend/src/modules/appData/index.ts +++ b/apps/cowswap-frontend/src/modules/appData/index.ts @@ -6,5 +6,6 @@ export { decodeAppData } from './utils/decodeAppData' export { replaceHooksOnAppData, buildAppData, removePermitHookFromAppData } from './utils/buildAppData' export { buildAppDataHooks } from './utils/buildAppDataHooks' export * from './utils/getAppDataHooks' +export * from './utils/decodeAppData' export { addPermitHookToHooks, removePermitHookFromHooks } from './utils/typedHooks' export type { AppDataInfo, UploadAppDataParams, TypedAppDataHooks } from './types' diff --git a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx index 681cec67ab..812b97be9e 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/Row/RowSlippage/index.tsx @@ -10,6 +10,7 @@ import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' import { useSetSlippage } from 'modules/swap/hooks/useSetSlippage' import { useSmartSwapSlippage } from 'modules/swap/hooks/useSwapSlippage' +import { useTradePricesUpdate } from 'modules/swap/hooks/useTradePricesUpdate' import { RowSlippageContent } from 'modules/swap/pure/Row/RowSlippageContent' import useNativeCurrency from 'lib/hooks/useNativeCurrency' @@ -37,6 +38,7 @@ export function RowSlippage({ const smartSwapSlippage = useSmartSwapSlippage() const isSmartSlippageApplied = useIsSmartSlippageApplied() const setSlippage = useSetSlippage() + const isTradePriceUpdating = useTradePricesUpdate() const props = useMemo( () => ({ @@ -49,10 +51,11 @@ export function RowSlippage({ slippageTooltip, displaySlippage: `${formatPercent(allowedSlippage)}%`, isSmartSlippageApplied, + isSmartSlippageLoading: isTradePriceUpdating, smartSlippage: smartSwapSlippage && !isEoaEthFlow ? `${formatPercent(new Percent(smartSwapSlippage, 10_000))}%` : undefined, setAutoSlippage: smartSwapSlippage && !isEoaEthFlow ? () => setSlippage(null) : undefined, }), - [chainId, isEoaEthFlow, nativeCurrency.symbol, showSettingOnClick, allowedSlippage, slippageLabel, slippageTooltip, smartSwapSlippage, isSmartSlippageApplied] + [chainId, isEoaEthFlow, nativeCurrency.symbol, showSettingOnClick, allowedSlippage, slippageLabel, slippageTooltip, smartSwapSlippage, isSmartSlippageApplied, isTradePriceUpdating] ) return diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx index 555a036e89..b9db3140fb 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -2,7 +2,7 @@ import { ReactNode, useCallback, useMemo, useState } from 'react' import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { NATIVE_CURRENCIES, TokenWithLogo } from '@cowprotocol/common-const' -import { isFractionFalsy } from '@cowprotocol/common-utils' +import { isFractionFalsy, percentToBps } from '@cowprotocol/common-utils' import { useIsTradeUnsupported } from '@cowprotocol/tokens' import { useIsSafeViaWc, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' import { TradeType } from '@cowprotocol/widget-lib' @@ -43,6 +43,7 @@ import { SWAP_QUOTE_CHECK_INTERVAL } from 'common/updaters/FeesUpdater' import useNativeCurrency from 'lib/hooks/useNativeCurrency' import { useIsSlippageModified } from '../../hooks/useIsSlippageModified' +import { useIsSmartSlippageApplied } from '../../hooks/useIsSmartSlippageApplied' import { useIsSwapEth } from '../../hooks/useIsSwapEth' import { useSwapSlippage } from '../../hooks/useSwapSlippage' import { @@ -223,6 +224,8 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { const nativeCurrencySymbol = useNativeCurrency().symbol || 'ETH' const wrappedCurrencySymbol = useWrappedToken().symbol || 'WETH' + const isSuggestedSlippage = useIsSmartSlippageApplied() && !isTradePriceUpdating && !!account + const swapWarningsTopProps: SwapWarningsTopProps = { chainId, trade, @@ -242,6 +245,8 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { buyingFiatAmount, priceImpact: priceImpactParams.priceImpact, tradeUrlParams, + slippageBps: percentToBps(slippage), + isSuggestedSlippage, } const swapWarningsBottomProps: SwapWarningsBottomProps = { diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx index c506885a3b..d0a98efd80 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx @@ -312,7 +312,6 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { } // compare input balance to max input based on version - // const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], trade.trade?.maximumAmountIn(allowedSlippage)] // mod const balanceIn = currencyBalances[Field.INPUT] const amountIn = slippageAdjustedSellAmount diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx index 40671ff0f1..f180f5effb 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.cosmos.tsx @@ -19,6 +19,7 @@ const defaultProps: RowSlippageContentProps = { setAutoSlippage: () => { console.log('setAutoSlippage called!') }, + isSmartSlippageLoading: false } export default diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx index a3385962ef..c1105bc66c 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx @@ -1,7 +1,7 @@ import { INPUT_OUTPUT_EXPLANATION, MINIMUM_ETH_FLOW_SLIPPAGE, PERCENTAGE_PRECISION } from '@cowprotocol/common-const' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' -import { HoverTooltip, LinkStyledButton, RowFixed, UI } from '@cowprotocol/ui' +import { CenteredDots, HoverTooltip, LinkStyledButton, RowFixed, UI } from '@cowprotocol/ui' import { Percent } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' @@ -64,7 +64,8 @@ export const getNonNativeSlippageTooltip = () => ( ) -const SUGGESTED_SLIPPAGE_TOOLTIP = "Based on recent volatility for the selected token pair, this is the suggested slippage for ensuring quick execution of your order." +const SUGGESTED_SLIPPAGE_TOOLTIP = + 'Based on recent volatility for the selected token pair, this is the suggested slippage for ensuring quick execution of your order.' export interface RowSlippageContentProps { chainId: SupportedChainId @@ -82,6 +83,7 @@ export interface RowSlippageContentProps { setAutoSlippage?: Command // todo: make them optional smartSlippage?: string isSmartSlippageApplied: boolean + isSmartSlippageLoading: boolean } // TODO: RowDeadlineContent and RowSlippageContent are very similar. Refactor and extract base component? @@ -101,6 +103,7 @@ export function RowSlippageContent(props: RowSlippageContentProps) { setAutoSlippage, smartSlippage, isSmartSlippageApplied, + isSmartSlippageLoading, } = props const tooltipContent = @@ -109,14 +112,33 @@ export function RowSlippageContent(props: RowSlippageContentProps) { // In case the user happened to set the same slippage as the suggestion, do not show the suggestion const suggestedEqualToUserSlippage = smartSlippage && smartSlippage === displaySlippage - const displayDefaultSlippage = isSlippageModified && setAutoSlippage && smartSlippage && !suggestedEqualToUserSlippage && ( - - (Suggested: {smartSlippage}) - - - - - ) + const displayDefaultSlippage = isSlippageModified && + setAutoSlippage && + smartSlippage && + !suggestedEqualToUserSlippage && ( + + {isSmartSlippageLoading ? ( + + ) : ( + <> + (Suggested: {smartSlippage}) + + + + + )} + + ) + + const displaySlippageWithLoader = + isSmartSlippageLoading && isSmartSlippageApplied ? ( + + ) : ( + <> + {displaySlippage} + {displayDefaultSlippage} + + ) return ( @@ -124,10 +146,18 @@ export function RowSlippageContent(props: RowSlippageContentProps) { {showSettingOnClick ? ( - + ) : ( - + )} @@ -136,20 +166,20 @@ export function RowSlippageContent(props: RowSlippageContentProps) { {showSettingOnClick ? ( - - {displaySlippage}{displayDefaultSlippage} - + {displaySlippageWithLoader} ) : ( - - {displaySlippage}{displayDefaultSlippage} - + {displaySlippageWithLoader} )} ) } -type SlippageTextContentsProps = { isEoaEthFlow: boolean; slippageLabel?: React.ReactNode, isDynamicSlippageSet: boolean } +type SlippageTextContentsProps = { + isEoaEthFlow: boolean + slippageLabel?: React.ReactNode + isDynamicSlippageSet: boolean +} function SlippageTextContents({ isEoaEthFlow, slippageLabel, isDynamicSlippageSet }: SlippageTextContentsProps) { return ( diff --git a/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx b/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx index 8ef2d09252..5f3b3db0df 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/warnings.tsx @@ -7,7 +7,7 @@ import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import styled from 'styled-components/macro' -import { HighFeeWarning } from 'legacy/components/SwapWarnings' +import { HighFeeWarning, HighSuggestedSlippageWarning } from 'legacy/components/SwapWarnings' import TradeGp from 'legacy/state/swap/TradeGp' import { CompatibilityIssuesWarning } from 'modules/trade/pure/CompatibilityIssuesWarning' @@ -35,7 +35,11 @@ export interface SwapWarningsTopProps { buyingFiatAmount: CurrencyAmount | null priceImpact: Percent | undefined tradeUrlParams: TradeUrlParams + isSuggestedSlippage: boolean | undefined + slippageBps: number | undefined + setFeeWarningAccepted(cb: (state: boolean) => boolean): void + setImpactWarningAccepted(cb: (state: boolean) => boolean): void } @@ -70,6 +74,8 @@ export const SwapWarningsTop = React.memo(function (props: SwapWarningsTopProps) buyingFiatAmount, priceImpact, tradeUrlParams, + isSuggestedSlippage, + slippageBps, } = props return ( @@ -80,6 +86,7 @@ export const SwapWarningsTop = React.memo(function (props: SwapWarningsTopProps) acceptedStatus={feeWarningAccepted} acceptWarningCb={account ? () => setFeeWarningAccepted((state) => !state) : undefined} /> + {!hideUnknownImpactWarning && ( { + const sellAmount = CurrencyAmount.fromRawAmount(USDC[1], '1000000') // 1.2 USDC + const feeAmount = CurrencyAmount.fromRawAmount(USDC[1], '200000') // 0.2 USDC + const isSell = true + const multiplierPercentage = 50 + + it('should return undefined for missing parameters', () => { + expect(calculateBpsFromFeeMultiplier(undefined, feeAmount, isSell, multiplierPercentage)).toBeUndefined() + expect(calculateBpsFromFeeMultiplier(sellAmount, undefined, isSell, multiplierPercentage)).toBeUndefined() + expect(calculateBpsFromFeeMultiplier(sellAmount, feeAmount, undefined, multiplierPercentage)).toBeUndefined() + expect(calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, 0)).toBeUndefined() + }) + + it('should return undefined for a negative multiplier percentage', () => { + const result = calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, -50) + expect(result).toBeUndefined() + }) + + it('should calculate the correct percentage for selling with different multiplier percentages', () => { + const testCases = [ + [25, 625], // 25%, 6.25% + [50, 1250], // 50%, 12.5% + [75, 1875], // 75%, 18.75% + ] + + testCases.forEach(([multiplier, expectedResult]) => { + const result = calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, multiplier) + expect(result).toBeDefined() + expect(result).toBe(expectedResult) + }) + }) + + it('should calculate the correct percentage for buying with different multiplier percentages', () => { + const testCases = [ + [25, 417], // 25%, 4.17% + [50, 833], // 50%, 8.33% + [75, 1250], // 75%, 12.5% + ] + + testCases.forEach(([multiplier, expectedResult]) => { + const result = calculateBpsFromFeeMultiplier(sellAmount, feeAmount, !isSell, multiplier) + expect(result).toBeDefined() + expect(result).toBe(expectedResult) + }) + }) +}) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts new file mode 100644 index 0000000000..73d161d60d --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts @@ -0,0 +1,47 @@ +import { Currency, CurrencyAmount, Fraction } from '@uniswap/sdk-core' + +const ONE = new Fraction(1) + +export function calculateBpsFromFeeMultiplier( + sellAmount: CurrencyAmount | undefined, + feeAmount: CurrencyAmount | undefined, + isSell: boolean | undefined, + multiplierPercentage: number, +): number | undefined { + if (!sellAmount || !feeAmount || isSell === undefined || multiplierPercentage <= 0) { + return undefined + } + + const feeMultiplierFactor = new Fraction(100 + multiplierPercentage, 100) // 50% more fee, applied to the whole value => 150% => 15/10 in fraction + + if (isSell) { + // sell + // 1 - ((sellAmount - feeAmount * 1.5) / (sellAmount - feeAmount)) + // 1 - (sellAmount - feeAmount * feeMultiplierFactor) / sellAmount - feeAmount + return percentageToBps( + ONE.subtract( + sellAmount + .subtract(feeAmount.multiply(feeMultiplierFactor)) + // !!! Need to convert to fraction before division to not lose precision + .asFraction.divide(sellAmount.subtract(feeAmount).asFraction), + ), + ) + } else { + // buy + // (sellAmount + feeAmount * 1.5) / (sellAmount + feeAmount) - 1 + // ((sellAmount + feeAmount * feeMultiplierFactor) / (sellAmount - feeAmount)) - 1 + return percentageToBps( + sellAmount + .add(feeAmount.multiply(feeMultiplierFactor)) + // !!! Need to convert to fraction before division to not lose precision + .asFraction.divide(sellAmount.add(feeAmount).asFraction) + .subtract(ONE), + ) + } +} + +function percentageToBps(value: Fraction | undefined): number | undefined { + const bps = value?.multiply(10_000).toFixed(0) + + return bps ? +bps : undefined +} diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts new file mode 100644 index 0000000000..5c8b79700e --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts @@ -0,0 +1,86 @@ +import { useSetAtom } from 'jotai' +import { useEffect, useMemo } from 'react' + +import { useTradeConfirmState } from 'modules/trade' + +import { useSmartSlippageFromBff } from './useSmartSlippageFromBff' +import { useSmartSlippageFromFeeMultiplier } from './useSmartSlippageFromFeeMultiplier' + +import { useDerivedSwapInfo, useHighFeeWarning } from '../../hooks/useSwapState' +import { smartSwapSlippageAtom } from '../../state/slippageValueAndTypeAtom' + +const MAX_BPS = 500 // 5% +const MIN_BPS = 50 // 0.5% + +export function SmartSlippageUpdater() { + const setSmartSwapSlippage = useSetAtom(smartSwapSlippageAtom) + + const bffSlippageBps = useSmartSlippageFromBff() + // TODO: remove v1 + const tradeSizeSlippageBpsV1 = useSmartSlippageFromFeePercentage() + const feeMultiplierSlippageBps = useSmartSlippageFromFeeMultiplier() + + const { isOpen: isTradeReviewOpen } = useTradeConfirmState() + + useEffect(() => { + // Don't update it once review is open + if (isTradeReviewOpen) { + return + } + // If both are unset, don't use smart slippage + if (feeMultiplierSlippageBps === undefined && bffSlippageBps === undefined) { + setSmartSwapSlippage(null) + return + } + // Add both slippage values, when present + const slippage = (feeMultiplierSlippageBps || 0) + (bffSlippageBps || 0) + + setSmartSwapSlippage(Math.max(MIN_BPS, Math.min(slippage, MAX_BPS))) + }, [bffSlippageBps, setSmartSwapSlippage, feeMultiplierSlippageBps, isTradeReviewOpen]) + + // TODO: remove before merging + useEffect(() => { + console.log(`SmartSlippageUpdater`, { + granularSlippage: tradeSizeSlippageBpsV1, + fiftyPercentFeeSlippage: feeMultiplierSlippageBps, + bffSlippageBps, + }) + }, [tradeSizeSlippageBpsV1, feeMultiplierSlippageBps]) + + return null +} + +// TODO: remove +/** + * Calculates smart slippage in bps, based on trade size in relation to fee + */ +function useSmartSlippageFromFeePercentage(): number | undefined { + const { trade } = useDerivedSwapInfo() || {} + const { feePercentage } = useHighFeeWarning(trade) + + const percentage = feePercentage && +feePercentage.toFixed(3) + + return useMemo(() => { + if (percentage === undefined) { + // Unset, return undefined + return + } + if (percentage < 1) { + // bigger volume compared to the fee, trust on smart slippage from BFF + return + } else if (percentage < 5) { + // Between 1 and 5, 2% + return 200 + } else if (percentage < 10) { + // Between 5 and 10, 5% + return 500 + } else if (percentage < 20) { + // Between 10 and 20, 10% + return 1000 + } + // TODO: more granularity? + + // > 20%, cap it at 20% slippage + return 2000 + }, [percentage]) +} diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts similarity index 74% rename from apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts rename to apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts index 7e79d89102..11a70cff07 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromBff.ts @@ -1,6 +1,3 @@ -import { useSetAtom } from 'jotai' -import { useEffect } from 'react' - import { BFF_BASE_URL } from '@cowprotocol/common-const' import { useFeatureFlags } from '@cowprotocol/common-hooks' import { getCurrencyAddress } from '@cowprotocol/common-utils' @@ -11,8 +8,6 @@ import useSWR from 'swr' import { useDerivedTradeState, useIsWrapOrUnwrap } from 'modules/trade' -import { smartSwapSlippageAtom } from '../state/slippageValueAndTypeAtom' - const SWR_OPTIONS = { dedupingInterval: ms`1m`, } @@ -21,17 +16,16 @@ interface SlippageApiResponse { slippageBps: number } -export function SmartSlippageUpdater() { +export function useSmartSlippageFromBff(): number | undefined { const { isSmartSlippageEnabled } = useFeatureFlags() const { chainId } = useWalletInfo() const { inputCurrency, outputCurrency } = useDerivedTradeState() || {} - const setSmartSwapSlippage = useSetAtom(smartSwapSlippageAtom) const isWrapOrUnwrap = useIsWrapOrUnwrap() const sellTokenAddress = inputCurrency && getCurrencyAddress(inputCurrency).toLowerCase() const buyTokenAddress = outputCurrency && getCurrencyAddress(outputCurrency).toLowerCase() - const slippageBps = useSWR( + return useSWR( !sellTokenAddress || !buyTokenAddress || isWrapOrUnwrap || !isSmartSlippageEnabled ? null : [chainId, sellTokenAddress, buyTokenAddress], @@ -42,12 +36,6 @@ export function SmartSlippageUpdater() { return response.slippageBps }, - SWR_OPTIONS + SWR_OPTIONS, ).data - - useEffect(() => { - setSmartSwapSlippage(typeof slippageBps === 'number' ? slippageBps : null) - }, [slippageBps, setSmartSwapSlippage]) - - return null } diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts new file mode 100644 index 0000000000..f5566085a1 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts @@ -0,0 +1,28 @@ +import { useMemo } from 'react' + +import { useFeatureFlags } from '@cowprotocol/common-hooks' + +import { useReceiveAmountInfo } from 'modules/trade' + +import { calculateBpsFromFeeMultiplier } from './calculateBpsFromFeeMultiplier' + + +/** + * Calculates smart slippage in bps, based on quoted fee + * + * Apply a multiplying factor to the fee (e.g.: 50%), and from there calculate how much slippage would be needed + * for the limit price to take this much more fee. + * More relevant for small orders in relation to fee amount, negligent for larger orders. + */ +export function useSmartSlippageFromFeeMultiplier(): number | undefined { + const { beforeNetworkCosts, afterNetworkCosts, costs, isSell } = useReceiveAmountInfo() || {} + const sellAmount = isSell ? afterNetworkCosts?.sellAmount : beforeNetworkCosts?.sellAmount + const feeAmount = costs?.networkFee?.amountInSellCurrency + + const { smartSlippageFeeMultiplierPercentage = 50 } = useFeatureFlags() + + return useMemo( + () => calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, smartSlippageFeeMultiplierPercentage), + [isSell, sellAmount, feeAmount, smartSlippageFeeMultiplierPercentage], + ) +} From 4b89ecbf661e6c30193586c704e23c78b2bfc22b Mon Sep 17 00:00:00 2001 From: Leandro Date: Mon, 14 Oct 2024 17:57:52 +0100 Subject: [PATCH 02/12] feat(smart-slippage): update smart slippage text (#4982) * feat: update smart slippage related text * refactor: memoize fns * feat: when smart slippage is set, use that as limit for settings * refactor: remove unused consts * chore: remove dead code * fix: replace geat with settings icon * fix: always show the dynamic slippage text --- .../legacy/components/SwapWarnings/index.tsx | 9 +- .../components/TransactionSettings/index.tsx | 139 +++++++++--------- .../pure/Row/RowSlippageContent/index.tsx | 30 ++-- .../updaters/SmartSlippageUpdater/index.ts | 49 +----- libs/common-const/src/misc.ts | 22 +-- 5 files changed, 100 insertions(+), 149 deletions(-) diff --git a/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx b/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx index b2b1caa0e9..8fdc4411e9 100644 --- a/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/SwapWarnings/index.tsx @@ -188,8 +188,13 @@ export function HighSuggestedSlippageWarning(props: HighSuggestedSlippageWarning
- Beware! High dynamic slippage suggested ({`${slippageBps / 100}`}%) - + Slippage adjusted to {`${slippageBps / 100}`}% to ensure quick execution +
diff --git a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx index ab1060e8df..388726d36c 100644 --- a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx @@ -47,7 +47,7 @@ enum DeadlineError { InvalidInput = 'InvalidInput', } -const Option = styled(FancyButton) <{ active: boolean }>` +const Option = styled(FancyButton)<{ active: boolean }>` margin-right: 8px; :hover { @@ -75,7 +75,7 @@ export const Input = styled.input` text-align: right; ` -export const OptionCustom = styled(FancyButton) <{ active?: boolean; warning?: boolean }>` +export const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>` height: 2rem; position: relative; padding: 0 0.75rem; @@ -84,7 +84,7 @@ export const OptionCustom = styled(FancyButton) <{ active?: boolean; warning?: b :hover { border: ${({ theme, active, warning }) => - active && `1px solid ${warning ? darken(theme.error, 0.1) : darken(theme.bg2, 0.1)}`}; + active && `1px solid ${warning ? darken(theme.error, 0.1) : darken(theme.bg2, 0.1)}`}; } input { @@ -97,6 +97,7 @@ export const OptionCustom = styled(FancyButton) <{ active?: boolean; warning?: b const SlippageEmojiContainer = styled.span` color: #f3841e; + ${Media.upToSmall()} { display: none; } @@ -204,78 +205,84 @@ export function TransactionSettings() { const placeholderSlippage = isSlippageModified ? defaultSwapSlippage : swapSlippage - function parseSlippageInput(value: string) { - // populate what the user typed and clear the error - setSlippageInput(value) - setSlippageError(false) + const parseSlippageInput = useCallback( + (value: string) => { + // populate what the user typed and clear the error + setSlippageInput(value) + setSlippageError(false) - if (value.length === 0) { - slippageToleranceAnalytics('Default', placeholderSlippage.toFixed(2)) - setSwapSlippage(isEoaEthFlow ? percentToBps(minEthFlowSlippage) : null) - } else { - let v = value - - // Prevent inserting more than 2 decimal precision - if (value.split('.')[1]?.length > 2) { - // indexOf + 3 because we are cutting it off at `.XX` - v = value.slice(0, value.indexOf('.') + 3) - // Update the input to remove the extra numbers from UI input - setSlippageInput(v) - } + if (value.length === 0) { + slippageToleranceAnalytics('Default', placeholderSlippage.toFixed(2)) + setSwapSlippage(isEoaEthFlow ? percentToBps(minEthFlowSlippage) : null) + } else { + let v = value + + // Prevent inserting more than 2 decimal precision + if (value.split('.')[1]?.length > 2) { + // indexOf + 3 because we are cutting it off at `.XX` + v = value.slice(0, value.indexOf('.') + 3) + // Update the input to remove the extra numbers from UI input + setSlippageInput(v) + } - const parsed = Math.round(Number.parseFloat(v) * 100) + const parsed = Math.round(Number.parseFloat(v) * 100) - if ( - !Number.isInteger(parsed) || - parsed < (isEoaEthFlow ? minEthFlowSlippageBps : MIN_SLIPPAGE_BPS) || - parsed > MAX_SLIPPAGE_BPS - ) { - if (v !== '.') { - setSlippageError(SlippageError.InvalidInput) + if ( + !Number.isInteger(parsed) || + parsed < (isEoaEthFlow ? minEthFlowSlippageBps : MIN_SLIPPAGE_BPS) || + parsed > MAX_SLIPPAGE_BPS + ) { + if (v !== '.') { + setSlippageError(SlippageError.InvalidInput) + } } - } - slippageToleranceAnalytics('Custom', parsed) - setSwapSlippage(percentToBps(new Percent(parsed, 10_000))) - } - } + slippageToleranceAnalytics('Custom', parsed) + setSwapSlippage(percentToBps(new Percent(parsed, 10_000))) + } + }, + [placeholderSlippage, isEoaEthFlow, minEthFlowSlippage], + ) const tooLow = swapSlippage.lessThan(new Percent(isEoaEthFlow ? minEthFlowSlippageBps : LOW_SLIPPAGE_BPS, 10_000)) const tooHigh = swapSlippage.greaterThan( - new Percent(isEoaEthFlow ? HIGH_ETH_FLOW_SLIPPAGE_BPS : HIGH_SLIPPAGE_BPS, 10_000) + new Percent(isEoaEthFlow ? HIGH_ETH_FLOW_SLIPPAGE_BPS : smartSlippage || HIGH_SLIPPAGE_BPS, 10_000), ) - function parseCustomDeadline(value: string) { - // populate what the user typed and clear the error - setDeadlineInput(value) - setDeadlineError(false) - - if (value.length === 0) { - orderExpirationTimeAnalytics('Default', DEFAULT_DEADLINE_FROM_NOW) - setDeadline(DEFAULT_DEADLINE_FROM_NOW) - } else { - try { - const parsed: number = Math.floor(Number.parseFloat(value) * 60) - if ( - !Number.isInteger(parsed) || // Check deadline is a number - parsed < - (isEoaEthFlow - ? // 10 minute low threshold for eth flow - MINIMUM_ETH_FLOW_DEADLINE_SECONDS - : MINIMUM_ORDER_VALID_TO_TIME_SECONDS) || // Check deadline is not too small - parsed > MAX_DEADLINE_MINUTES * 60 // Check deadline is not too big - ) { + const parseCustomDeadline = useCallback( + (value: string) => { + // populate what the user typed and clear the error + setDeadlineInput(value) + setDeadlineError(false) + + if (value.length === 0) { + orderExpirationTimeAnalytics('Default', DEFAULT_DEADLINE_FROM_NOW) + setDeadline(DEFAULT_DEADLINE_FROM_NOW) + } else { + try { + const parsed: number = Math.floor(Number.parseFloat(value) * 60) + if ( + !Number.isInteger(parsed) || // Check deadline is a number + parsed < + (isEoaEthFlow + ? // 10 minute low threshold for eth flow + MINIMUM_ETH_FLOW_DEADLINE_SECONDS + : MINIMUM_ORDER_VALID_TO_TIME_SECONDS) || // Check deadline is not too small + parsed > MAX_DEADLINE_MINUTES * 60 // Check deadline is not too big + ) { + setDeadlineError(DeadlineError.InvalidInput) + } else { + orderExpirationTimeAnalytics('Custom', parsed) + setDeadline(parsed) + } + } catch (error: any) { + console.error(error) setDeadlineError(DeadlineError.InvalidInput) - } else { - orderExpirationTimeAnalytics('Custom', parsed) - setDeadline(parsed) } - } catch (error: any) { - console.error(error) - setDeadlineError(DeadlineError.InvalidInput) } - } - } + }, + [isEoaEthFlow], + ) const showCustomDeadlineRow = Boolean(chainId) @@ -299,14 +306,14 @@ export function TransactionSettings() { - MEV protected slippage + MEV-protected slippage Your transaction will revert if the price changes unfavorably by more than this percentage. isEoaEthFlow ? getNativeSlippageTooltip(chainId, [nativeCurrency.symbol, getWrappedToken(nativeCurrency).symbol]) - : getNonNativeSlippageTooltip() + : getNonNativeSlippageTooltip(true) } /> @@ -366,8 +373,8 @@ export function TransactionSettings() { - Based on recent volatility observed for this token pair, it's recommended to leave the default - to account for price changes. + CoW Swap has dynamically selected this slippage amount to account for current gas prices and + volatility. Changes may result in slower execution. } /> diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx index c1105bc66c..07a343a2fd 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx @@ -1,4 +1,4 @@ -import { INPUT_OUTPUT_EXPLANATION, MINIMUM_ETH_FLOW_SLIPPAGE, PERCENTAGE_PRECISION } from '@cowprotocol/common-const' +import { MINIMUM_ETH_FLOW_SLIPPAGE, PERCENTAGE_PRECISION } from '@cowprotocol/common-const' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' import { CenteredDots, HoverTooltip, LinkStyledButton, RowFixed, UI } from '@cowprotocol/ui' @@ -49,23 +49,29 @@ export const getNativeSlippageTooltip = (chainId: SupportedChainId, symbols: (st matching, even in volatile market conditions.

- Orders on CoW Swap are always protected from MEV, so your slippage tolerance cannot be exploited. + {symbols?.[0] || 'Native currency'} orders can, in rare cases, be frontrun due to their on-chain component. For more + robust MEV protection, consider wrapping your {symbols?.[0] || 'native currency'} before trading. ) -export const getNonNativeSlippageTooltip = () => ( +export const getNonNativeSlippageTooltip = (isSettingsModal?: boolean) => ( - Your slippage is MEV protected: all orders are submitted with tight spread (0.1%) on-chain. -
-
- The slippage set enables a resubmission of your order in case of unfavourable price movements. -
-
- {INPUT_OUTPUT_EXPLANATION} + CoW Swap dynamically adjusts your slippage tolerance to ensure your trade executes quickly while still getting the + best price.{' '} + {isSettingsModal ? ( + <> + To override this, enter your desired slippage amount. +
+
+ Either way, your slippage is protected from MEV! + + ) : ( + "Trades are protected from MEV, so your slippage can't be exploited!" + )}
) const SUGGESTED_SLIPPAGE_TOOLTIP = - 'Based on recent volatility for the selected token pair, this is the suggested slippage for ensuring quick execution of your order.' + 'This is the recommended slippage tolerance based on current gas prices & volatility. A lower amount may result in slower execution.' export interface RowSlippageContentProps { chainId: SupportedChainId @@ -121,7 +127,7 @@ export function RowSlippageContent(props: RowSlippageContentProps) { ) : ( <> - (Suggested: {smartSlippage}) + (Recommended: {smartSlippage}) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts index 5c8b79700e..73e6173e77 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/index.ts @@ -1,12 +1,11 @@ import { useSetAtom } from 'jotai' -import { useEffect, useMemo } from 'react' +import { useEffect } from 'react' import { useTradeConfirmState } from 'modules/trade' import { useSmartSlippageFromBff } from './useSmartSlippageFromBff' import { useSmartSlippageFromFeeMultiplier } from './useSmartSlippageFromFeeMultiplier' -import { useDerivedSwapInfo, useHighFeeWarning } from '../../hooks/useSwapState' import { smartSwapSlippageAtom } from '../../state/slippageValueAndTypeAtom' const MAX_BPS = 500 // 5% @@ -16,8 +15,6 @@ export function SmartSlippageUpdater() { const setSmartSwapSlippage = useSetAtom(smartSwapSlippageAtom) const bffSlippageBps = useSmartSlippageFromBff() - // TODO: remove v1 - const tradeSizeSlippageBpsV1 = useSmartSlippageFromFeePercentage() const feeMultiplierSlippageBps = useSmartSlippageFromFeeMultiplier() const { isOpen: isTradeReviewOpen } = useTradeConfirmState() @@ -38,49 +35,5 @@ export function SmartSlippageUpdater() { setSmartSwapSlippage(Math.max(MIN_BPS, Math.min(slippage, MAX_BPS))) }, [bffSlippageBps, setSmartSwapSlippage, feeMultiplierSlippageBps, isTradeReviewOpen]) - // TODO: remove before merging - useEffect(() => { - console.log(`SmartSlippageUpdater`, { - granularSlippage: tradeSizeSlippageBpsV1, - fiftyPercentFeeSlippage: feeMultiplierSlippageBps, - bffSlippageBps, - }) - }, [tradeSizeSlippageBpsV1, feeMultiplierSlippageBps]) - return null } - -// TODO: remove -/** - * Calculates smart slippage in bps, based on trade size in relation to fee - */ -function useSmartSlippageFromFeePercentage(): number | undefined { - const { trade } = useDerivedSwapInfo() || {} - const { feePercentage } = useHighFeeWarning(trade) - - const percentage = feePercentage && +feePercentage.toFixed(3) - - return useMemo(() => { - if (percentage === undefined) { - // Unset, return undefined - return - } - if (percentage < 1) { - // bigger volume compared to the fee, trust on smart slippage from BFF - return - } else if (percentage < 5) { - // Between 1 and 5, 2% - return 200 - } else if (percentage < 10) { - // Between 5 and 10, 5% - return 500 - } else if (percentage < 20) { - // Between 10 and 20, 10% - return 1000 - } - // TODO: more granularity? - - // > 20%, cap it at 20% slippage - return 2000 - }, [percentage]) -} diff --git a/libs/common-const/src/misc.ts b/libs/common-const/src/misc.ts index dcc1b586df..3fd037eb9f 100644 --- a/libs/common-const/src/misc.ts +++ b/libs/common-const/src/misc.ts @@ -1,29 +1,15 @@ -import { Percent, Fraction } from '@uniswap/sdk-core' +import { Fraction, Percent } from '@uniswap/sdk-core' import JSBI from 'jsbi' export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' -export const NetworkContextName = 'NETWORK' - -export const IS_IN_IFRAME = typeof window !== 'undefined' && window.parent !== window - // 30 minutes, denominated in seconds export const DEFAULT_DEADLINE_FROM_NOW = 60 * 30 export const L2_DEADLINE_FROM_NOW = 60 * 5 -// transaction popup dismisal amounts -export const DEFAULT_TXN_DISMISS_MS = 25000 -export const L2_TXN_DISMISS_MS = 5000 - -// used for rewards deadlines -export const BIG_INT_SECONDS_IN_WEEK = JSBI.BigInt(60 * 60 * 24 * 7) - -export const BIG_INT_ZERO = JSBI.BigInt(0) - // one basis JSBI.BigInt const BPS_BASE = JSBI.BigInt(10000) -export const ONE_BPS = new Percent(JSBI.BigInt(1), BPS_BASE) // used for warning states export const ALLOWED_PRICE_IMPACT_LOW: Percent = new Percent(JSBI.BigInt(100), BPS_BASE) // 1% @@ -34,12 +20,6 @@ export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.Bi // for non expert mode disable swaps above this export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(1500), BPS_BASE) // 15% -export const BETTER_TRADE_LESS_HOPS_THRESHOLD = new Percent(JSBI.BigInt(50), BPS_BASE) - -export const ZERO_PERCENT = new Percent('0') -export const TWO_PERCENT = new Percent(JSBI.BigInt(200), BPS_BASE) export const ONE_HUNDRED_PERCENT = new Percent('1') -export const IS_SIDE_BANNER_VISIBLE_KEY = 'IS_SIDEBAR_BANNER_VISIBLE' - export const ONE_FRACTION = new Fraction(1, 1) From 395f48f57d93de67305791fdb9a668bdd693074e Mon Sep 17 00:00:00 2001 From: Jean Neiverth <79885562+JeanNeiverth@users.noreply.github.com> Date: Wed, 16 Oct 2024 04:03:32 -0300 Subject: [PATCH 03/12] feat(hooks-store): add claim vesting iframe hook (#4924) * feat: add claim vesting IFrame hook * chore: update claim vesting hook name to claim llamapay vesting hook * fix: name of create llamapay hook in hookRegistry --- .../containers/IframeDappContainer/index.tsx | 2 +- .../src/modules/hooksStore/hookRegistry.tsx | 1 + libs/hook-dapp-lib/src/hookDappsRegistry.json | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx index 0a25b9ffb5..c463218c7b 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/IframeDappContainer/index.tsx @@ -10,7 +10,7 @@ import { HookDappContext as HookDappContextType, HookDappIframe } from '../../ty const Iframe = styled.iframe` border: 0; - min-height: 350px; + min-height: 300px; ` interface IframeDappContainerProps { diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx index 0fcf02b699..73e31b0da9 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/hookRegistry.tsx @@ -23,4 +23,5 @@ export const ALL_HOOK_DAPPS = [ ...hookDappsRegistry.CLAIM_COW_AIRDROP, component: (props) => , }, + hookDappsRegistry.CLAIM_LLAMAPAY_VESTING, ] as HookDapp[] diff --git a/libs/hook-dapp-lib/src/hookDappsRegistry.json b/libs/hook-dapp-lib/src/hookDappsRegistry.json index ce98edb976..976c65095f 100644 --- a/libs/hook-dapp-lib/src/hookDappsRegistry.json +++ b/libs/hook-dapp-lib/src/hookDappsRegistry.json @@ -49,5 +49,19 @@ "conditions": { "supportedNetworks": [11155111] } + }, + "CLAIM_LLAMAPAY_VESTING": { + "id": "5d2c081d11a01ca0b76e2fafbc0d3c62a4c9945ce404706fb1e49e826c0f99eb", + "type": "IFRAME", + "name": "Claim LlamaPay Vesting Hook", + "description": "The Claim LlamaPay Vesting Hook is a powerful and user-friendly feature designed to streamline the process of claiming funds from LlamaPay vesting contracts. This tool empowers users to easily access and manage their vested tokens, ensuring a smooth and efficient experience in handling time-locked assets.", + "descriptionShort": "Claim your LlamaPay vesting contract funds", + "image": "https://cow-hooks-dapps-claim-vesting.vercel.app/llama-pay-icon.png", + "version": "0.1.0", + "website": "https://github.com/bleu/cow-hooks-dapps", + "url": "https://cow-hooks-dapps-claim-vesting.vercel.app", + "conditions": { + "supportedNetworks": [1, 100, 42161] + } } } From 26cbffbbfe8edbc0a4a9ba31fe9c0d42852118d9 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 16 Oct 2024 16:03:27 +0500 Subject: [PATCH 04/12] feat(hooks-store): add sell/buy amounts to hook-dapp context (#4990) --- .../modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts | 2 ++ libs/hook-dapp-lib/src/types.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts index 7bc2cb2dce..7f531ad420 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts @@ -16,6 +16,8 @@ export function useSetupHooksStoreOrderParams() { setOrderParams({ validTo: orderParams.validTo, + sellAmount: orderParams.inputAmount.quotient.toString(), + buyAmount: orderParams.outputAmount.quotient.toString(), sellTokenAddress: getCurrencyAddress(orderParams.inputAmount.currency), buyTokenAddress: getCurrencyAddress(orderParams.outputAmount.currency), }) diff --git a/libs/hook-dapp-lib/src/types.ts b/libs/hook-dapp-lib/src/types.ts index f44aa21a06..9adea48b49 100644 --- a/libs/hook-dapp-lib/src/types.ts +++ b/libs/hook-dapp-lib/src/types.ts @@ -44,6 +44,8 @@ export interface HookDappOrderParams { validTo: number sellTokenAddress: string buyTokenAddress: string + sellAmount: string + buyAmount: string } export interface HookDappContext { From eaa29f3ed421d92214b857bf1c57d75b0317cbba Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 16 Oct 2024 17:02:47 +0500 Subject: [PATCH 05/12] fix(explorer): display hook details of unknown hook-dapp (#4995) --- .../OrderHooksDetails/HookItem/index.tsx | 77 ++++++++++--------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx b/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx index b80bb6d9b6..c21e92859b 100644 --- a/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx +++ b/apps/explorer/src/components/orders/OrderHooksDetails/HookItem/index.tsx @@ -11,42 +11,49 @@ export function HookItem({ item, number }: { item: HookToDappMatch; number: numb return ( - {item.dapp ? ( - -

- #{number} - {item.dapp.name} {item.dapp.name}{' '} - {showDetails ? '[-] Show less' : '[+] Show more'} -

- - {showDetails && ( -
-

- Version: {item.dapp.version} -

-

- Description: {item.dapp.descriptionShort} -

-

- Website: - - {item.dapp.website} - -

-

- Call Data: {item.hook.callData} -

-

- Gas Limit: {item.hook.gasLimit} -

-

- Target: {item.hook.target} -

-
+ +

+ #{number} -{' '} + {item.dapp ? ( + <> + {item.dapp.name} {item.dapp.name}{' '} + + ) : ( + 'Unknown hook dapp' )} - - ) : ( -

Unknown hook dapp
- )} + {showDetails ? '[-] Show less' : '[+] Show more'} +

+ + {showDetails && ( +
+ {item.dapp && ( + <> +

+ Version: {item.dapp.version} +

+

+ Description: {item.dapp.descriptionShort} +

+

+ Website: + + {item.dapp.website} + +

+ + )} +

+ Call Data: {item.hook.callData} +

+

+ Gas Limit: {item.hook.gasLimit} +

+

+ Target: {item.hook.target} +

+
+ )} +
) } From cf004bdd06f63404b950b11817d768fccefb767d Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 16 Oct 2024 17:03:00 +0500 Subject: [PATCH 06/12] chore: update hook docs link (#4996) --- .../hooksStore/containers/HooksStoreWidget/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx index 9cf80a3a42..18999dd0ba 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -81,8 +81,12 @@ export function HooksStoreWidget() { bannerId="hooks-store-banner-tradeContainer" >

- With hooks you can add specific actions before and after your swap. {/*TODO: update the link*/} - + With hooks you can add specific actions before and after your swap.{' '} + Learn more.

From 531e63f666ffcafdaf8e2b1c2850991facbe5cf1 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 16 Oct 2024 17:10:30 +0500 Subject: [PATCH 07/12] feat: display new label for cow amm (#4994) * feat: display new label for cow amm * fix: add new label to Hooks tab * chore: add new to hook menu * chore: remove CoWAMM banner --- .../src/common/constants/routes.ts | 9 +++- .../application/containers/App/index.tsx | 8 +--- .../application/containers/App/menuConsts.tsx | 2 + .../TradeWidgetLinks/index.cosmos.tsx | 35 -------------- .../containers/TradeWidgetLinks/index.tsx | 36 ++++----------- .../containers/TradeWidgetLinks/styled.ts | 46 ++----------------- libs/ui/src/index.ts | 1 + libs/ui/src/pure/Badge/index.tsx | 44 ++++++++++++++++++ libs/ui/src/pure/MenuBar/index.tsx | 25 ++++++---- libs/ui/src/pure/MenuBar/styled.ts | 3 ++ 10 files changed, 88 insertions(+), 121 deletions(-) delete mode 100644 apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.cosmos.tsx create mode 100644 libs/ui/src/pure/Badge/index.tsx diff --git a/apps/cowswap-frontend/src/common/constants/routes.ts b/apps/cowswap-frontend/src/common/constants/routes.ts index 1457da5df3..bdc140310a 100644 --- a/apps/cowswap-frontend/src/common/constants/routes.ts +++ b/apps/cowswap-frontend/src/common/constants/routes.ts @@ -37,7 +37,13 @@ export const Routes = { export type RoutesKeys = keyof typeof Routes export type RoutesValues = (typeof Routes)[RoutesKeys] -export const MENU_ITEMS: { route: RoutesValues; label: string; fullLabel?: string; description: string }[] = [ +export const MENU_ITEMS: { + route: RoutesValues + label: string + fullLabel?: string + description: string + badge?: string +}[] = [ { route: Routes.SWAP, label: 'Swap', description: 'Trade tokens' }, { route: Routes.LIMIT_ORDER, label: 'Limit', fullLabel: 'Limit order', description: 'Set your own price' }, { route: Routes.ADVANCED_ORDERS, label: 'TWAP', description: 'Place orders with a time-weighted average price' }, @@ -47,4 +53,5 @@ export const HOOKS_STORE_MENU_ITEM = { route: Routes.HOOKS, label: 'Hooks', description: 'Powerful tool to generate pre/post interaction for CoW Protocol', + badge: 'New', } diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx index c08a02680e..e2db6d73bf 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx @@ -24,7 +24,6 @@ import { useInitializeUtm } from 'modules/utm' import { InvalidLocalTimeWarning } from 'common/containers/InvalidLocalTimeWarning' import { useCategorizeRecentActivity } from 'common/hooks/useCategorizeRecentActivity' import { useMenuItems } from 'common/hooks/useMenuItems' -import { CoWAmmBanner } from 'common/pure/CoWAMMBanner' import { LoadingApp } from 'common/pure/LoadingApp' import { CoWDAOFonts } from 'common/styles/CoWDAOFonts' import RedirectAnySwapAffectedUsers from 'pages/error/AnySwapAffectedUsers/RedirectAnySwapAffectedUsers' @@ -62,7 +61,7 @@ export function App() { onClick: toggleDarkMode, }, ], - [darkMode, toggleDarkMode] + [darkMode, toggleDarkMode], ) const tradeContext = useTradeRouteContext() @@ -74,7 +73,7 @@ export function App() { children: menuItems.map((item) => { const href = parameterizeTradeRoute(tradeContext, item.route, true) - return { href, label: item.label, description: item.description } + return { href, label: item.label, description: item.description, badge: item.badge } }), }, ...NAV_ITEMS, @@ -128,9 +127,6 @@ export function App() { /> )} - {/* CoW AMM banner */} - {!isInjectedWidgetMode && } - diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx index 5faf9ff31c..f8bf52d06a 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx @@ -41,6 +41,7 @@ export const NAV_ITEMS: MenuItem[] = [ }, { label: 'More', + badge: 'New', children: [ { href: 'https://cow.fi/cow-protocol', @@ -50,6 +51,7 @@ export const NAV_ITEMS: MenuItem[] = [ { href: 'https://cow.fi/cow-amm', label: 'CoW AMM', + badge: 'New', external: true, }, { diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.cosmos.tsx deleted file mode 100644 index 77fed8e4d1..0000000000 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.cosmos.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { BadgeType } from '@cowprotocol/ui' - -import { Widget } from 'modules/application/pure/Widget' - -import { TradeWidgetLinks } from './index' - -type BadgeInfo = { - text: string - type: BadgeType -} - -const BADGES: BadgeInfo[] = [ - { text: 'BETA', type: 'default' }, - { text: 'NEW!', type: 'success' }, - { text: 'ALPHA', type: 'alert' }, - { text: 'NEW!', type: 'alert2' }, - { text: 'RELEASE', type: 'information' }, -] - -type Fixtures = { - [key: string]: React.FunctionComponent -} - -const BadgeFixtures = BADGES.reduce((fixtures, badge) => { - const Fixture = () => ( - - - - ) - - fixtures[`Badge - ${badge.text} (${badge.type})`] = Fixture - return fixtures -}, {}) - -export default BadgeFixtures diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx index cb7c298f86..08ac319e6d 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from 'react' import { Command } from '@cowprotocol/types' -import { BadgeType } from '@cowprotocol/ui' +import { Badge } from '@cowprotocol/ui' import type { TradeType } from '@cowprotocol/widget-lib' import { Trans } from '@lingui/macro' @@ -23,6 +23,7 @@ import { parameterizeTradeRoute } from '../../utils/parameterizeTradeRoute' interface MenuItemConfig { route: RoutesValues label: string + badge?: string } const TRADE_TYPE_TO_ROUTE: Record = { @@ -32,16 +33,10 @@ const TRADE_TYPE_TO_ROUTE: Record = { } interface TradeWidgetLinksProps { - highlightedBadgeText?: string - highlightedBadgeType?: BadgeType isDropdown?: boolean } -export function TradeWidgetLinks({ - highlightedBadgeText, - highlightedBadgeType, - isDropdown = false, -}: TradeWidgetLinksProps) { +export function TradeWidgetLinks({ isDropdown = false }: TradeWidgetLinksProps) { const tradeContext = useTradeRouteContext() const location = useLocation() const [isDropdownVisible, setDropdownVisible] = useState(false) @@ -72,23 +67,12 @@ export function TradeWidgetLinks({ routePath={routePath} item={item} isActive={isActive} - badgeText={highlightedBadgeText} - badgeType={highlightedBadgeType} onClick={() => handleMenuItemClick(item)} isDropdownVisible={isDropdown && isDropdownVisible} /> ) }) - }, [ - isDropdown, - isDropdownVisible, - enabledItems, - tradeContext, - location.pathname, - highlightedBadgeText, - highlightedBadgeType, - handleMenuItemClick, - ]) + }, [isDropdown, isDropdownVisible, enabledItems, tradeContext, location.pathname, handleMenuItemClick]) const singleMenuItem = menuItemsElements.length === 1 @@ -122,26 +106,22 @@ const MenuItem = ({ routePath, item, isActive, - badgeText, - badgeType, onClick, isDropdownVisible, }: { routePath: string item: MenuItemConfig isActive: boolean - badgeText?: string - badgeType?: BadgeType onClick: Command isDropdownVisible: boolean }) => ( {item.label} - {!isActive && badgeText && ( - - {badgeText} - + {!isActive && item.badge && ( + + {item.badge} + )} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts index 3ed5310d80..770b0e0db9 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/styled.ts @@ -1,48 +1,8 @@ -import { UI, BadgeType } from '@cowprotocol/ui' +import { Badge, UI } from '@cowprotocol/ui' import { NavLink } from 'react-router-dom' import styled, { css } from 'styled-components/macro' -const badgeBackgrounds: Record = { - information: `var(${UI.COLOR_INFO_BG})`, - alert: `var(${UI.COLOR_BADGE_YELLOW_BG})`, - alert2: `var(${UI.COLOR_BADGE_YELLOW_BG})`, - success: `var(${UI.COLOR_SUCCESS_BG})`, - default: 'transparent', // text only -} - -const badgeColors: Record = { - information: `var(${UI.COLOR_INFO_TEXT})`, - alert: `var(${UI.COLOR_BADGE_YELLOW_TEXT})`, - alert2: `var(${UI.COLOR_BADGE_YELLOW_TEXT})`, - success: `var(${UI.COLOR_SUCCESS_TEXT})`, - default: `var(${UI.COLOR_DISABLED_TEXT})`, // text only -} - -export const Badge = styled.div<{ type?: BadgeType }>` - background: ${({ type }) => badgeBackgrounds[type || 'default']}; - color: ${({ type }) => badgeColors[type || 'default']}; - border: 0; - cursor: pointer; - border-radius: 16px; - font-size: 9px; - font-weight: inherit; - text-transform: uppercase; - padding: ${({ type }) => (!type || type === 'default' ? '0' : '4px 6px')}; - letter-spacing: 0.2px; - font-weight: 600; - transition: color var(${UI.ANIMATION_DURATION}) ease-in-out; - margin: 0; - - a & { - color: ${({ type }) => badgeColors[type || 'default']}; - } -` - -Badge.defaultProps = { - type: 'default', -} - export const Link = styled(NavLink)` display: flex; align-items: center; @@ -52,7 +12,9 @@ export const Link = styled(NavLink)` gap: 4px; font-weight: inherit; line-height: 1; - transition: color var(${UI.ANIMATION_DURATION}) ease-in-out, fill var(${UI.ANIMATION_DURATION}) ease-in-out; + transition: + color var(${UI.ANIMATION_DURATION}) ease-in-out, + fill var(${UI.ANIMATION_DURATION}) ease-in-out; &:hover { color: inherit; diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index 403e897f4f..3a41db39e3 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -33,6 +33,7 @@ export * from './pure/PercentDisplay' export * from './pure/CmsImage' export * from './pure/DismissableInlineBanner' export * from './pure/Input' +export * from './pure/Badge' export * from './containers/CowSwapSafeAppLink' export * from './containers/InlineBanner' diff --git a/libs/ui/src/pure/Badge/index.tsx b/libs/ui/src/pure/Badge/index.tsx new file mode 100644 index 0000000000..3ce2c3a7cf --- /dev/null +++ b/libs/ui/src/pure/Badge/index.tsx @@ -0,0 +1,44 @@ +import styled from 'styled-components/macro' + +import { UI } from '../../enum' +import { BadgeType } from '../../types' + +const badgeBackgrounds: Record = { + information: `var(${UI.COLOR_INFO_BG})`, + alert: `var(${UI.COLOR_BADGE_YELLOW_BG})`, + alert2: `var(${UI.COLOR_BADGE_YELLOW_BG})`, + success: `var(${UI.COLOR_SUCCESS_BG})`, + default: 'transparent', // text only +} + +const badgeColors: Record = { + information: `var(${UI.COLOR_INFO_TEXT})`, + alert: `var(${UI.COLOR_BADGE_YELLOW_TEXT})`, + alert2: `var(${UI.COLOR_BADGE_YELLOW_TEXT})`, + success: `var(${UI.COLOR_SUCCESS_TEXT})`, + default: `var(${UI.COLOR_DISABLED_TEXT})`, // text only +} + +export const Badge = styled.div<{ type?: BadgeType }>` + background: ${({ type }) => badgeBackgrounds[type || 'default']}; + color: ${({ type }) => badgeColors[type || 'default']}; + border: 0; + cursor: pointer; + border-radius: 16px; + font-size: 9px; + font-weight: inherit; + text-transform: uppercase; + padding: ${({ type }) => (!type || type === 'default' ? '0' : '4px 6px')}; + letter-spacing: 0.2px; + font-weight: 600; + transition: color var(${UI.ANIMATION_DURATION}) ease-in-out; + margin: 0; + + a & { + color: ${({ type }) => badgeColors[type || 'default']}; + } +` + +Badge.defaultProps = { + type: 'default', +} diff --git a/libs/ui/src/pure/MenuBar/index.tsx b/libs/ui/src/pure/MenuBar/index.tsx index a93ee332f1..346d412053 100644 --- a/libs/ui/src/pure/MenuBar/index.tsx +++ b/libs/ui/src/pure/MenuBar/index.tsx @@ -34,6 +34,7 @@ import { import { Color } from '../../consts' import { Media } from '../../consts' +import { Badge } from '../Badge' import { ProductLogo, ProductVariant } from '../ProductLogo' const DAO_NAV_ITEMS: MenuItem[] = [ @@ -84,6 +85,7 @@ type LinkComponentType = ComponentType> export interface MenuItem { href?: string label?: string + badge?: string children?: DropdownMenuItem[] productVariant?: ProductVariant icon?: string @@ -105,6 +107,7 @@ interface DropdownMenuItem { external?: boolean label?: string icon?: string + badge?: string description?: string isButton?: boolean children?: DropdownMenuItem[] @@ -127,7 +130,7 @@ interface DropdownMenuContent { interface DropdownProps { isOpen: boolean - content: DropdownMenuContent + item: MenuItem onTrigger: () => void closeDropdown: () => void interaction: 'hover' | 'click' @@ -169,7 +172,7 @@ const NavItem = ({ return item.children ? ( = ({ isOpen, - content, + item, onTrigger, interaction, mobileMode, @@ -390,8 +393,8 @@ const GenericDropdown: React.FC = ({ rootDomain, LinkComponent, }) => { - if (!content.title) { - throw new Error('Dropdown content must have a title') + if (!item.label) { + throw new Error('Dropdown content must have a title and children') } const interactionProps = useMemo(() => { @@ -408,12 +411,13 @@ const GenericDropdown: React.FC = ({ return ( - {content.title} - {content.items && } + {item.label} + {item.badge && {item.badge}} + {item.children && } {isOpen && ( = ({ <> {item.icon && } - {item.label} + + {item.label} + {item.badge && {item.badge}} + {item.description && {item.description}} {item.children && } diff --git a/libs/ui/src/pure/MenuBar/styled.ts b/libs/ui/src/pure/MenuBar/styled.ts index e7d1c1395e..ef4f904131 100644 --- a/libs/ui/src/pure/MenuBar/styled.ts +++ b/libs/ui/src/pure/MenuBar/styled.ts @@ -491,6 +491,9 @@ export const DropdownContentItemTitle = styled.span` font-weight: bold; font-size: 18px; line-height: 1.2; + display: flex; + align-items: center; + gap: 8px; ` export const DropdownContentItemDescription = styled.span` From 9842afdb887497d235a01538663488b0b8852bb5 Mon Sep 17 00:00:00 2001 From: Leandro Date: Wed, 16 Oct 2024 14:34:48 +0100 Subject: [PATCH 08/12] feat(widget): hide bridge info (#4992) * feat: add option to hide brigde info to widget * feat: use hideBridgeInfo on Network alert --- .../components/NetworkAlert/NetworkAlert.tsx | 14 +++++++++++--- .../hooks/useWidgetParamsAndSettings.ts | 2 ++ .../src/app/configurator/index.tsx | 17 ++++++++++++++--- .../src/app/configurator/types.ts | 1 + libs/widget-lib/src/types.ts | 5 +++++ 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/apps/cowswap-frontend/src/legacy/components/NetworkAlert/NetworkAlert.tsx b/apps/cowswap-frontend/src/legacy/components/NetworkAlert/NetworkAlert.tsx index 7fb20f05ed..03bd936cb5 100644 --- a/apps/cowswap-frontend/src/legacy/components/NetworkAlert/NetworkAlert.tsx +++ b/apps/cowswap-frontend/src/legacy/components/NetworkAlert/NetworkAlert.tsx @@ -10,6 +10,8 @@ import styled from 'styled-components/macro' import { useDarkModeManager } from 'legacy/state/user/hooks' +import { useInjectedWidgetParams } from 'modules/injectedWidget' + const HideSmall = styled.span` ${Media.upToSmall()} { display: none; @@ -61,7 +63,9 @@ const StyledArrowUpRight = styled(ArrowUpRight)` const ContentWrapper = styled.div<{ chainId: NetworkAlertChains; darkMode: boolean; logoUrl: string }>` background: var(${UI.COLOR_PAPER_DARKER}); - transition: color var(${UI.ANIMATION_DURATION}) ease-in-out, background var(${UI.ANIMATION_DURATION}) ease-in-out; // MOD + transition: + color var(${UI.ANIMATION_DURATION}) ease-in-out, + background var(${UI.ANIMATION_DURATION}) ease-in-out; // MOD border-radius: 20px; display: flex; flex-direction: row; @@ -88,7 +92,9 @@ const ContentWrapper = styled.div<{ chainId: NetworkAlertChains; darkMode: boole color: inherit; stroke: currentColor; text-decoration: none; - transition: transform var(${UI.ANIMATION_DURATION}) ease-in-out, stroke var(${UI.ANIMATION_DURATION}) ease-in-out, + transition: + transform var(${UI.ANIMATION_DURATION}) ease-in-out, + stroke var(${UI.ANIMATION_DURATION}) ease-in-out, color var(${UI.ANIMATION_DURATION}) ease-in-out; } @@ -136,7 +142,9 @@ export function NetworkAlert() { const theme = useTheme() - if (!shouldShowAlert(chainId) || !isActive) { + const { hideBridgeInfo } = useInjectedWidgetParams() + + if (!shouldShowAlert(chainId) || !isActive || hideBridgeInfo) { return null } diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts index 69143ec592..b00d73748b 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts @@ -39,6 +39,7 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi standaloneMode, disableToastMessages, disableProgressBar, + hideBridgeInfo, } = configuratorState const themeColors = { @@ -84,6 +85,7 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi recipient: partnerFeeRecipient, } : undefined, + hideBridgeInfo, } return params diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx index 1a6b9cf699..2bd0b12d5f 100644 --- a/apps/widget-configurator/src/app/configurator/index.tsx +++ b/apps/widget-configurator/src/app/configurator/index.tsx @@ -128,9 +128,10 @@ export function Configurator({ title }: { title: string }) { const firstToast = toasts?.[0] const [disableProgressBar, setDisableProgressBar] = useState(false) - const toggleDisableProgressBar = useCallback(() => { - setDisableProgressBar((curr) => !curr) - }, []) + const toggleDisableProgressBar = useCallback(() => setDisableProgressBar((curr) => !curr), []) + + const [hideBridgeInfo, setHideBridgeInfo] = useState(false) + const toggleHideBridgeInfo = useCallback(() => setHideBridgeInfo((curr) => !curr), []) const LINKS = [ { icon: , label: 'View embed code', onClick: () => handleDialogOpen() }, @@ -161,6 +162,7 @@ export function Configurator({ title }: { title: string }) { standaloneMode, disableToastMessages, disableProgressBar, + hideBridgeInfo, } const computedParams = useWidgetParams(state) @@ -283,6 +285,7 @@ export function Configurator({ title }: { title: string }) { } label="Dapp mode" /> + Progress bar: @@ -291,6 +294,14 @@ export function Configurator({ title }: { title: string }) { + + Hide bridge info: + + } label="Show bridge info" /> + } label="Hide bridge info" /> + + + {isDrawerOpen && ( Date: Thu, 17 Oct 2024 07:03:46 +0100 Subject: [PATCH 09/12] chore: style enhancements for badge (#4999) --- .../trade/containers/TradeWidgetLinks/index.tsx | 11 +++++------ libs/ui/src/pure/Badge/index.tsx | 6 +++--- libs/ui/src/pure/MenuBar/index.tsx | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx index 08ac319e6d..65bf8dda31 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidgetLinks/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useMemo, useState, useCallback } from 'react' import { Command } from '@cowprotocol/types' import { Badge } from '@cowprotocol/ui' @@ -43,10 +43,9 @@ export function TradeWidgetLinks({ isDropdown = false }: TradeWidgetLinksProps) const { enabledTradeTypes } = useInjectedWidgetParams() const menuItems = useMenuItems() - const handleMenuItemClick = (_item?: MenuItemConfig) => { - if (menuItemsElements.length === 1) return + const handleMenuItemClick = useCallback((_item?: MenuItemConfig): void => { setDropdownVisible(false) - } + }, []) const enabledItems = useMemo(() => { return menuItems.filter((item) => { @@ -56,7 +55,7 @@ export function TradeWidgetLinks({ isDropdown = false }: TradeWidgetLinksProps) }) }, [menuItems, enabledTradeTypes]) - const menuItemsElements = useMemo(() => { + const menuItemsElements: JSX.Element[] = useMemo(() => { return enabledItems.map((item) => { const routePath = parameterizeTradeRoute(tradeContext, item.route, true) const isActive = !!matchPath(location.pathname, routePath.split('?')[0]) @@ -119,7 +118,7 @@ const MenuItem = ({ {item.label} {!isActive && item.badge && ( - + {item.badge} )} diff --git a/libs/ui/src/pure/Badge/index.tsx b/libs/ui/src/pure/Badge/index.tsx index 3ce2c3a7cf..5346854894 100644 --- a/libs/ui/src/pure/Badge/index.tsx +++ b/libs/ui/src/pure/Badge/index.tsx @@ -5,7 +5,7 @@ import { BadgeType } from '../../types' const badgeBackgrounds: Record = { information: `var(${UI.COLOR_INFO_BG})`, - alert: `var(${UI.COLOR_BADGE_YELLOW_BG})`, + alert: `var(${UI.COLOR_ALERT_BG})`, alert2: `var(${UI.COLOR_BADGE_YELLOW_BG})`, success: `var(${UI.COLOR_SUCCESS_BG})`, default: 'transparent', // text only @@ -13,7 +13,7 @@ const badgeBackgrounds: Record = { const badgeColors: Record = { information: `var(${UI.COLOR_INFO_TEXT})`, - alert: `var(${UI.COLOR_BADGE_YELLOW_TEXT})`, + alert: `var(${UI.COLOR_ALERT_TEXT})`, alert2: `var(${UI.COLOR_BADGE_YELLOW_TEXT})`, success: `var(${UI.COLOR_SUCCESS_TEXT})`, default: `var(${UI.COLOR_DISABLED_TEXT})`, // text only @@ -25,7 +25,7 @@ export const Badge = styled.div<{ type?: BadgeType }>` border: 0; cursor: pointer; border-radius: 16px; - font-size: 9px; + font-size: 10px; font-weight: inherit; text-transform: uppercase; padding: ${({ type }) => (!type || type === 'default' ? '0' : '4px 6px')}; diff --git a/libs/ui/src/pure/MenuBar/index.tsx b/libs/ui/src/pure/MenuBar/index.tsx index 346d412053..852b07499f 100644 --- a/libs/ui/src/pure/MenuBar/index.tsx +++ b/libs/ui/src/pure/MenuBar/index.tsx @@ -412,7 +412,7 @@ const GenericDropdown: React.FC = ({ {item.label} - {item.badge && {item.badge}} + {item.badge && {item.badge}} {item.children && } {isOpen && ( @@ -483,7 +483,7 @@ const DropdownContentWrapper: React.FC = ({ {item.label} - {item.badge && {item.badge}} + {item.badge && {item.badge}} {item.description && {item.description}} From ce3b5b8adb5cc95a5ca3097d5cf2d45b249748c2 Mon Sep 17 00:00:00 2001 From: Leandro Date: Thu, 17 Oct 2024 11:19:05 +0100 Subject: [PATCH 10/12] feat(widget): deadline widget param (#4991) * feat: add deadlines widget parameter * refactor: remove dead code * feat: rename OrderDeadlines to ForcedOrderDeadline and use FlexibleConfig * feat: add useInjectedWidgetDeadline * fix: add spaces to tooltip * feat: use widget deadline on swap form * chore: remove duplicated css property * feat: use widget deadline on limit form * feat: use widget deadline on twap form * chore: remove debug logs * chore: fix build * fix: round timestamp * refactor: rename limitOrdersDeadlines to LIMIT_ORDERS_DEADLINES * fix: use deadlineMilliseconds instead of customDeadline for forcedOrderDeadline * fix: allow deadline input to be cleared * fix: add chainId to hook deps --- .../components/TransactionSettings/index.tsx | 39 ++++++++++--- .../src/legacy/state/user/hooks.tsx | 17 +++--- .../hooks/useInjectedWidgetDeadline.ts | 33 +++++++++++ .../src/modules/injectedWidget/index.ts | 1 + .../containers/DeadlineInput/index.tsx | 47 ++++++++++++--- .../pure/DeadlineSelector/deadlines.ts | 27 ++++++++- .../pure/DeadlineSelector/index.cosmos.tsx | 1 + .../pure/DeadlineSelector/index.tsx | 58 +++++++++++-------- .../updaters/AlternativeLimitOrderUpdater.ts | 6 +- .../swap/pure/Row/RowDeadline/index.tsx | 3 + .../modules/trade/pure/TradeSelect/index.tsx | 3 +- .../twap/containers/TwapFormWidget/index.tsx | 41 ++++++++++++- .../twap/pure/DeadlineSelector/index.tsx | 40 +++++++++---- .../configurator/controls/DeadlineControl.tsx | 23 ++++++++ .../hooks/useWidgetParamsAndSettings.ts | 14 ++++- .../src/app/configurator/index.tsx | 24 ++++++++ .../src/app/configurator/types.ts | 4 ++ libs/common-const/src/misc.ts | 1 - libs/widget-lib/src/types.ts | 12 ++++ 19 files changed, 324 insertions(+), 70 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/injectedWidget/hooks/useInjectedWidgetDeadline.ts create mode 100644 apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx diff --git a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx index 388726d36c..7ad9ea8175 100644 --- a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useRef, useState } from 'react' +import { useCallback, useContext, useEffect, useRef, useState } from 'react' import { DEFAULT_DEADLINE_FROM_NOW, @@ -16,6 +16,7 @@ import { useOnClickOutside } from '@cowprotocol/common-hooks' import { getWrappedToken, percentToBps } from '@cowprotocol/common-utils' import { FancyButton, HelpTooltip, Media, RowBetween, RowFixed, UI } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' +import { TradeType } from '@cowprotocol/widget-lib' import { Percent } from '@uniswap/sdk-core' import { Trans } from '@lingui/macro' @@ -27,6 +28,7 @@ import { AutoColumn } from 'legacy/components/Column' import { useUserTransactionTTL } from 'legacy/state/user/hooks' import { orderExpirationTimeAnalytics, slippageToleranceAnalytics } from 'modules/analytics' +import { useInjectedWidgetDeadline } from 'modules/injectedWidget' import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useIsSlippageModified } from 'modules/swap/hooks/useIsSlippageModified' import { useIsSmartSlippageApplied } from 'modules/swap/hooks/useIsSmartSlippageApplied' @@ -191,6 +193,7 @@ export function TransactionSettings() { const chosenSlippageMatchesSmartSlippage = smartSlippage && new Percent(smartSlippage, 10_000).equalTo(swapSlippage) const [deadline, setDeadline] = useUserTransactionTTL() + const widgetDeadline = useInjectedWidgetDeadline(TradeType.SWAP) const [slippageInput, setSlippageInput] = useState('') const [slippageError, setSlippageError] = useState(false) @@ -249,6 +252,12 @@ export function TransactionSettings() { new Percent(isEoaEthFlow ? HIGH_ETH_FLOW_SLIPPAGE_BPS : smartSlippage || HIGH_SLIPPAGE_BPS, 10_000), ) + const minDeadline = isEoaEthFlow + ? // 10 minute low threshold for eth flow + MINIMUM_ETH_FLOW_DEADLINE_SECONDS + : MINIMUM_ORDER_VALID_TO_TIME_SECONDS + const maxDeadline = MAX_DEADLINE_MINUTES * 60 + const parseCustomDeadline = useCallback( (value: string) => { // populate what the user typed and clear the error @@ -263,12 +272,8 @@ export function TransactionSettings() { const parsed: number = Math.floor(Number.parseFloat(value) * 60) if ( !Number.isInteger(parsed) || // Check deadline is a number - parsed < - (isEoaEthFlow - ? // 10 minute low threshold for eth flow - MINIMUM_ETH_FLOW_DEADLINE_SECONDS - : MINIMUM_ORDER_VALID_TO_TIME_SECONDS) || // Check deadline is not too small - parsed > MAX_DEADLINE_MINUTES * 60 // Check deadline is not too big + parsed < minDeadline || // Check deadline is not too small + parsed > maxDeadline // Check deadline is not too big ) { setDeadlineError(DeadlineError.InvalidInput) } else { @@ -281,9 +286,26 @@ export function TransactionSettings() { } } }, - [isEoaEthFlow], + [minDeadline, maxDeadline], ) + useEffect(() => { + if (widgetDeadline) { + // Deadline is stored in seconds + const value = Math.floor(widgetDeadline) * 60 + + if (value < minDeadline) { + setDeadline(minDeadline) + } else if (value > maxDeadline) { + setDeadline(maxDeadline) + } else { + setDeadline(value) + } + } + }, [widgetDeadline, minDeadline, maxDeadline]) + + const isDeadlineDisabled = !!widgetDeadline + const showCustomDeadlineRow = Boolean(chainId) const onSlippageInputBlur = useCallback(() => { @@ -417,6 +439,7 @@ export function TransactionSettings() { setDeadlineError(false) }} color={deadlineError ? 'red' : ''} + disabled={isDeadlineDisabled} /> diff --git a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx index 01fa1094ad..f347ddb113 100644 --- a/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx +++ b/apps/cowswap-frontend/src/legacy/state/user/hooks.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react' -import { L2_DEADLINE_FROM_NOW, NATIVE_CURRENCIES, SupportedLocale, TokenWithLogo } from '@cowprotocol/common-const' +import { NATIVE_CURRENCIES, SupportedLocale, TokenWithLogo } from '@cowprotocol/common-const' import { getIsNativeToken } from '@cowprotocol/common-utils' import { SupportedChainId } from '@cowprotocol/cow-sdk' import { Command } from '@cowprotocol/types' @@ -20,11 +20,12 @@ export function useIsDarkMode(): boolean { userDarkMode, matchesDarkMode, }), - shallowEqual + shallowEqual, ) return userDarkMode === null ? matchesDarkMode : userDarkMode } + export function useDarkModeManager(): [boolean, Command] { const dispatch = useAppDispatch() const darkMode = useIsDarkMode() @@ -48,7 +49,7 @@ export function useUserLocaleManager(): [SupportedLocale | null, (newLocale: Sup (newLocale: SupportedLocale) => { dispatch(updateUserLocale({ userLocale: newLocale })) }, - [dispatch] + [dispatch], ) return [locale, setLocale] @@ -67,7 +68,7 @@ export function useRecipientToggleManager(): [boolean, (value?: boolean) => void (recipient: string | null) => { dispatch(setRecipient({ recipient })) }, - [dispatch] + [dispatch], ) const toggleVisibility = useCallback( @@ -78,7 +79,7 @@ export function useRecipientToggleManager(): [boolean, (value?: boolean) => void onChangeRecipient(null) } }, - [isVisible, dispatch, onChangeRecipient] + [isVisible, dispatch, onChangeRecipient], ) return [isVisible, toggleVisibility] @@ -86,15 +87,13 @@ export function useRecipientToggleManager(): [boolean, (value?: boolean) => void export function useUserTransactionTTL(): [number, (slippage: number) => void] { const dispatch = useAppDispatch() - const userDeadline = useAppSelector((state) => state.user.userDeadline) - const onL2 = false - const deadline = onL2 ? L2_DEADLINE_FROM_NOW : userDeadline + const deadline = useAppSelector((state) => state.user.userDeadline) const setUserDeadline = useCallback( (userDeadline: number) => { dispatch(updateUserDeadline({ userDeadline })) }, - [dispatch] + [dispatch], ) return [deadline, setUserDeadline] diff --git a/apps/cowswap-frontend/src/modules/injectedWidget/hooks/useInjectedWidgetDeadline.ts b/apps/cowswap-frontend/src/modules/injectedWidget/hooks/useInjectedWidgetDeadline.ts new file mode 100644 index 0000000000..c1ac9c2db9 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/injectedWidget/hooks/useInjectedWidgetDeadline.ts @@ -0,0 +1,33 @@ +import { useMemo } from 'react' + +import { isInjectedWidget } from '@cowprotocol/common-utils' +import { useWalletInfo } from '@cowprotocol/wallet' +import { ForcedOrderDeadline, resolveFlexibleConfig, SupportedChainId, TradeType } from '@cowprotocol/widget-lib' + +import { useInjectedWidgetParams } from './useInjectedWidgetParams' + +/** + * Returns the deadline set in the widget for the specific order type in minutes, if any + * + * Additional validation is needed + */ +export function useInjectedWidgetDeadline(tradeType: TradeType): number | undefined { + const { forcedOrderDeadline } = useInjectedWidgetParams() + const { chainId } = useWalletInfo() + + return useMemo(() => { + if (!isInjectedWidget()) { + return + } + + return getDeadline(forcedOrderDeadline, chainId, tradeType) + }, [tradeType, forcedOrderDeadline, chainId]) +} + +function getDeadline(deadline: ForcedOrderDeadline | undefined, chainId: SupportedChainId, tradeType: TradeType) { + if (!deadline) { + return + } + + return resolveFlexibleConfig(deadline, chainId, tradeType) +} diff --git a/apps/cowswap-frontend/src/modules/injectedWidget/index.ts b/apps/cowswap-frontend/src/modules/injectedWidget/index.ts index 7190aa48c3..0b412e48b7 100644 --- a/apps/cowswap-frontend/src/modules/injectedWidget/index.ts +++ b/apps/cowswap-frontend/src/modules/injectedWidget/index.ts @@ -1,6 +1,7 @@ export { InjectedWidgetUpdater } from './updaters/InjectedWidgetUpdater' export { CowEventsUpdater } from './updaters/CowEventsUpdater' export { useInjectedWidgetParams, useWidgetPartnerFee } from './hooks/useInjectedWidgetParams' +export { useInjectedWidgetDeadline } from './hooks/useInjectedWidgetDeadline' export { useInjectedWidgetMetaData } from './hooks/useInjectedWidgetMetaData' export { useInjectedWidgetPalette } from './hooks/useInjectedWidgetPalette' export { injectedWidgetPartnerFeeAtom } from './state/injectedWidgetParamsAtom' diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/DeadlineInput/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/DeadlineInput/index.tsx index a44991e3ee..5ed0816e20 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/DeadlineInput/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/DeadlineInput/index.tsx @@ -1,9 +1,15 @@ -import { useSetAtom } from 'jotai' -import { useAtomValue } from 'jotai' -import { useCallback, useMemo, useRef } from 'react' +import { useAtomValue, useSetAtom } from 'jotai' +import { useCallback, useEffect, useMemo, useRef } from 'react' +import { TradeType } from '@cowprotocol/widget-lib' + +import { useInjectedWidgetDeadline } from 'modules/injectedWidget' import { DeadlineSelector } from 'modules/limitOrders/pure/DeadlineSelector' -import { LimitOrderDeadline, limitOrdersDeadlines } from 'modules/limitOrders/pure/DeadlineSelector/deadlines' +import { + getLimitOrderDeadlines, + LIMIT_ORDERS_DEADLINES, + LimitOrderDeadline, +} from 'modules/limitOrders/pure/DeadlineSelector/deadlines' import { limitOrdersSettingsAtom, updateLimitOrdersSettingsAtom, @@ -13,29 +19,52 @@ export function DeadlineInput() { const { deadlineMilliseconds, customDeadlineTimestamp } = useAtomValue(limitOrdersSettingsAtom) const updateSettingsState = useSetAtom(updateLimitOrdersSettingsAtom) const currentDeadlineNode = useRef() - const existingDeadline = useMemo(() => { - return limitOrdersDeadlines.find((item) => item.value === deadlineMilliseconds) - }, [deadlineMilliseconds]) + const existingDeadline = useMemo( + () => getLimitOrderDeadlines(deadlineMilliseconds).find((item) => item.value === deadlineMilliseconds), + [deadlineMilliseconds], + ) + + const widgetDeadlineMinutes = useInjectedWidgetDeadline(TradeType.LIMIT) + + useEffect(() => { + if (widgetDeadlineMinutes) { + const widgetDeadlineDelta = widgetDeadlineMinutes * 60 * 1000 + const min = LIMIT_ORDERS_DEADLINES[0].value + const max = LIMIT_ORDERS_DEADLINES[LIMIT_ORDERS_DEADLINES.length - 1].value + + let deadlineMilliseconds = widgetDeadlineDelta + if (widgetDeadlineDelta < min) { + deadlineMilliseconds = min + } else if (widgetDeadlineDelta > max) { + deadlineMilliseconds = max + } + + updateSettingsState({ customDeadlineTimestamp: null, deadlineMilliseconds }) + } + }, [widgetDeadlineMinutes, updateSettingsState]) + + const isDeadlineDisabled = !!widgetDeadlineMinutes const selectDeadline = useCallback( (deadline: LimitOrderDeadline) => { updateSettingsState({ deadlineMilliseconds: deadline.value, customDeadlineTimestamp: null }) currentDeadlineNode.current?.click() // Close dropdown }, - [updateSettingsState] + [updateSettingsState], ) const selectCustomDeadline = useCallback( (customDeadline: number | null) => { updateSettingsState({ customDeadlineTimestamp: customDeadline }) }, - [updateSettingsState] + [updateSettingsState], ) return ( diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts index 3f183cf2cc..b0a04d3be0 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts @@ -1,4 +1,5 @@ import ms from 'ms.macro' +import { format } from 'timeago.js' import { MAX_ORDER_DEADLINE } from 'common/constants/common' @@ -12,7 +13,7 @@ export const MAX_CUSTOM_DEADLINE = MAX_ORDER_DEADLINE export const defaultLimitOrderDeadline: LimitOrderDeadline = { title: '7 Days', value: ms`7d` } -export const limitOrdersDeadlines: LimitOrderDeadline[] = [ +export const LIMIT_ORDERS_DEADLINES: LimitOrderDeadline[] = [ { title: '5 Minutes', value: ms`5m` }, { title: '30 Minutes', value: ms`30m` }, { title: '1 Hour', value: ms`1 hour` }, @@ -22,3 +23,27 @@ export const limitOrdersDeadlines: LimitOrderDeadline[] = [ { title: '1 Month', value: ms`30d` }, { title: '6 Months (max)', value: MAX_CUSTOM_DEADLINE }, ] + +/** + * Get limit order deadlines and optionally adds + * @param value + */ +export function getLimitOrderDeadlines(value?: number | LimitOrderDeadline): LimitOrderDeadline[] { + if (!value || LIMIT_ORDERS_DEADLINES.find((item) => item === value || item.value === value)) { + return LIMIT_ORDERS_DEADLINES + } + + const itemToAdd = typeof value === 'number' ? buildLimitOrderDeadline(value) : value + + return [...LIMIT_ORDERS_DEADLINES, itemToAdd].sort((a, b) => a.value - b.value) +} + +/** + * Builds a LimitOrderDeadline from milliseconds value. + * Uses timeago to an approximate title + */ +export function buildLimitOrderDeadline(value: number): LimitOrderDeadline { + const title = format(Date.now() + value, undefined).replace(/in /, '') + + return { title, value } +} diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.cosmos.tsx index cb8d3b0b06..f0687b47e3 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.cosmos.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.cosmos.tsx @@ -9,6 +9,7 @@ const Fixtures = { customDeadline={null} selectDeadline={() => void 0} selectCustomDeadline={() => void 0} + isDeadlineDisabled={false} /> ), } diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx index 25fb8a440e..247d86c01f 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx @@ -16,7 +16,7 @@ import { import { CowModal as Modal } from 'common/pure/Modal' -import { LimitOrderDeadline, limitOrdersDeadlines } from './deadlines' +import { getLimitOrderDeadlines, LimitOrderDeadline } from './deadlines' import * as styledEl from './styled' const CUSTOM_DATE_OPTIONS: Intl.DateTimeFormatOptions = { @@ -31,12 +31,15 @@ const CUSTOM_DATE_OPTIONS: Intl.DateTimeFormatOptions = { export interface DeadlineSelectorProps { deadline: LimitOrderDeadline | undefined customDeadline: number | null + isDeadlineDisabled: boolean + selectDeadline(deadline: LimitOrderDeadline): void + selectCustomDeadline(deadline: number | null): void } export function DeadlineSelector(props: DeadlineSelectorProps) { - const { deadline, customDeadline, selectDeadline, selectCustomDeadline } = props + const { deadline, customDeadline, isDeadlineDisabled, selectDeadline, selectCustomDeadline } = props const currentDeadlineNode = useRef(null) const [[minDate, maxDate], setMinMax] = useState<[Date, Date]>(calculateMinMax) @@ -66,7 +69,7 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { } }, [maxDate, minDate, selectCustomDeadline, value]) - const existingDeadline = useMemo(() => limitOrdersDeadlines.find((item) => item === deadline), [deadline]) + const limitOrderDeadlines = useMemo(() => getLimitOrderDeadlines(deadline), [deadline]) const customDeadlineTitle = useMemo(() => { if (!customDeadline) { @@ -81,7 +84,7 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { selectCustomDeadline(null) // reset custom deadline currentDeadlineNode.current?.click() // Close dropdown }, - [selectCustomDeadline, selectDeadline] + [selectCustomDeadline, selectDeadline], ) // Sets value from input, if it exists @@ -92,7 +95,7 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { // In that case, use the default min value setValue(value || formatDateToLocalTime(minDate)) }, - [minDate] + [minDate], ) const [isOpen, setIsOpen] = useState(false) @@ -118,29 +121,38 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { onDismiss() }, [onDismiss, selectCustomDeadline, value]) + const deadlineDisplay = customDeadline ? customDeadlineTitle : deadline?.title + return ( Expiry - - - {customDeadline ? customDeadlineTitle : existingDeadline?.title} - - - - {limitOrdersDeadlines.map((item) => ( -
  • - setDeadline(item)}> - {item.title} - -
  • - ))} - - Custom - -
    -
    + + {isDeadlineDisabled ? ( +
    + {deadlineDisplay} +
    + ) : ( + + + {deadlineDisplay} + + + + {limitOrderDeadlines.map((item) => ( +
  • + setDeadline(item)}> + {item.title} + +
  • + ))} + + Custom + +
    +
    + )} {/* Custom deadline modal */} diff --git a/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts b/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts index 6045c3c672..a9cfc54141 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts +++ b/apps/cowswap-frontend/src/modules/limitOrders/updaters/AlternativeLimitOrderUpdater.ts @@ -15,7 +15,7 @@ import { updateLimitOrdersSettingsAtom, } from 'modules/limitOrders' import { useUpdateLimitOrdersRawState } from 'modules/limitOrders/hooks/useLimitOrdersRawState' -import { limitOrdersDeadlines } from 'modules/limitOrders/pure/DeadlineSelector/deadlines' +import { LIMIT_ORDERS_DEADLINES } from 'modules/limitOrders/pure/DeadlineSelector/deadlines' import { partiallyFillableOverrideAtom } from 'modules/limitOrders/state/partiallyFillableOverride' import { useAlternativeOrder, useHideAlternativeOrderModal } from 'modules/trade/state/alternativeOrder' @@ -118,7 +118,7 @@ function useSetAlternativeRate(): null { // Set new active rate // The rate expects a raw fraction which is NOT a Price instace const activeRate = FractionUtils.fromPrice( - new Price({ baseAmount: inputCurrencyAmount, quoteAmount: outputCurrencyAmount }) + new Price({ baseAmount: inputCurrencyAmount, quoteAmount: outputCurrencyAmount }), ) updateRate({ activeRate, isTypedValue: false, isRateFromUrl: false, isAlternativeOrderRate: true }) @@ -170,7 +170,7 @@ function getDuration(order: Order | ParsedOrder): number { */ function getMatchingDeadline(duration: number) { // Match duration with approximate time - return limitOrdersDeadlines.find(({ value }) => { + return LIMIT_ORDERS_DEADLINES.find(({ value }) => { const ratio = value / duration // If the ratio is +/-10% off of 1, consider it a match return ratio > 0.9 && ratio < 1.1 diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx index d74a91ad0d..ac549413d6 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowDeadline/index.tsx @@ -20,10 +20,13 @@ export function getNativeOrderDeadlineTooltip(symbols: (string | undefined)[] | ) } + export function getNonNativeOrderDeadlineTooltip() { return ( Your swap expires and will not execute if it is pending for longer than the selected duration. +
    +
    {INPUT_OUTPUT_EXPLANATION}
    ) diff --git a/apps/cowswap-frontend/src/modules/trade/pure/TradeSelect/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/TradeSelect/index.tsx index 591ce87d25..1d56933f36 100644 --- a/apps/cowswap-frontend/src/modules/trade/pure/TradeSelect/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/pure/TradeSelect/index.tsx @@ -1,6 +1,6 @@ import { UI } from '@cowprotocol/ui' -import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button' +import { Menu, MenuButton, MenuItem, MenuList } from '@reach/menu-button' import { ChevronDown } from 'react-feather' import styled from 'styled-components/macro' @@ -47,7 +47,6 @@ const StyledMenuButton = styled(MenuButton)` cursor: pointer; width: 100%; justify-content: space-between; - color: inherit; ` const StyledMenuItem = styled(MenuItem)` diff --git a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx index fb804210f3..41050dad54 100644 --- a/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/containers/TwapFormWidget/index.tsx @@ -3,9 +3,11 @@ import { useEffect, useLayoutEffect, useMemo, useState } from 'react' import { renderTooltip } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' +import { TradeType } from '@cowprotocol/widget-lib' import { useAdvancedOrdersDerivedState } from 'modules/advancedOrders' import { openAdvancedOrdersTabAnalytics, twapWalletCompatibilityAnalytics } from 'modules/analytics' +import { useInjectedWidgetDeadline } from 'modules/injectedWidget' import { useReceiveAmountInfo } from 'modules/trade' import { useIsWrapOrUnwrap } from 'modules/trade/hooks/useIsWrapOrUnwrap' import { useTradeState } from 'modules/trade/hooks/useTradeState' @@ -21,7 +23,14 @@ import { useRateInfoParams } from 'common/hooks/useRateInfoParams' import * as styledEl from './styled' import { LABELS_TOOLTIPS } from './tooltips' -import { DEFAULT_NUM_OF_PARTS, DEFAULT_TWAP_SLIPPAGE, MAX_TWAP_SLIPPAGE, ORDER_DEADLINES } from '../../const' +import { + DEFAULT_NUM_OF_PARTS, + DEFAULT_TWAP_SLIPPAGE, + MAX_PART_TIME, + MAX_TWAP_SLIPPAGE, + MINIMUM_PART_TIME, + ORDER_DEADLINES, +} from '../../const' import { useFallbackHandlerVerification, useIsFallbackHandlerCompatible, @@ -65,9 +74,34 @@ export function TwapFormWidget() { const limitPriceAfterSlippage = usePrice( receiveAmountInfo?.afterSlippage.sellAmount, - receiveAmountInfo?.afterSlippage.buyAmount + receiveAmountInfo?.afterSlippage.buyAmount, ) + const widgetDeadline = useInjectedWidgetDeadline(TradeType.ADVANCED) + + useEffect(() => { + if (widgetDeadline) { + // Ensure min part duration + const minDuration = Math.floor(MINIMUM_PART_TIME / 60) * 2 // it must have at least 2 parts + + const maxDuration = Math.floor(MAX_PART_TIME / 60) * numberOfPartsValue + + let minutes = widgetDeadline + if (widgetDeadline < minDuration) { + minutes = minDuration + } else if (widgetDeadline > maxDuration) { + minutes = maxDuration + } + + updateSettingsState({ + customDeadline: { hours: 0, minutes }, + isCustomDeadline: true, + }) + } + }, [widgetDeadline, updateSettingsState, numberOfPartsValue]) + + const isDeadlineDisabled = !!widgetDeadline + const deadlineState = { deadline, customDeadline, @@ -166,8 +200,9 @@ export function TwapFormWidget() { updateSettingsState(value)} + setDeadline={updateSettingsState} label={LABELS_TOOLTIPS.totalDuration.label} tooltip={renderTooltip(LABELS_TOOLTIPS.totalDuration.tooltip, { parts: numberOfPartsValue, diff --git a/apps/cowswap-frontend/src/modules/twap/pure/DeadlineSelector/index.tsx b/apps/cowswap-frontend/src/modules/twap/pure/DeadlineSelector/index.tsx index 8169b99b0a..6999fd7636 100644 --- a/apps/cowswap-frontend/src/modules/twap/pure/DeadlineSelector/index.tsx +++ b/apps/cowswap-frontend/src/modules/twap/pure/DeadlineSelector/index.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useMemo, useState } from 'react' -import { UI } from '@cowprotocol/ui' -import { renderTooltip } from '@cowprotocol/ui' +import { renderTooltip, UI } from '@cowprotocol/ui' import styled from 'styled-components/macro' @@ -10,14 +9,17 @@ import { Content } from 'modules/trade/pure/TradeWidgetField/styled' import { LabelTooltip } from 'modules/twap' import { customDeadlineToSeconds, deadlinePartsDisplay } from 'modules/twap/utils/deadlinePartsDisplay' +import { TradeWidgetField } from '../../../trade/pure/TradeWidgetField' import { defaultCustomDeadline, TwapOrdersDeadline } from '../../state/twapOrdersSettingsAtom' import { CustomDeadlineSelector } from '../CustomDeadlineSelector' interface DeadlineSelectorProps { items: TradeSelectItem[] deadline: TwapOrdersDeadline + isDeadlineDisabled: boolean label: LabelTooltip['label'] tooltip: LabelTooltip['tooltip'] + setDeadline(value: TwapOrdersDeadline): void } @@ -48,10 +50,22 @@ const StyledTradeSelect = styled(TradeSelect)` } ` +const StyledTradeField = styled(TradeWidgetField)` + ${Content} { + width: 100%; + color: inherit; + } + + ${Content} > div { + width: 100%; + } +` + export function DeadlineSelector(props: DeadlineSelectorProps) { const { items, deadline: { deadline, customDeadline, isCustomDeadline }, + isDeadlineDisabled, label, tooltip, setDeadline, @@ -74,7 +88,7 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { }) } }, - [setIsCustomModalOpen, setDeadline] + [setIsCustomModalOpen, setDeadline], ) const activeLabel = useMemo(() => { @@ -87,13 +101,19 @@ export function DeadlineSelector(props: DeadlineSelectorProps) { return ( <> - + {isDeadlineDisabled ? ( + +
    {activeLabel}
    +
    + ) : ( + + )} setDeadline({ isCustomDeadline: true, customDeadline: value, deadline: 0 })} customDeadline={customDeadline} diff --git a/apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx b/apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx new file mode 100644 index 0000000000..57aff95d71 --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx @@ -0,0 +1,23 @@ +import { Dispatch, SetStateAction } from 'react' + +import { FormControl, TextField } from '@mui/material' + +export type DeadlineControlProps = { + label: string + deadlineState: [number | undefined, Dispatch>] +} + +export function DeadlineControl({ label, deadlineState: [state, setState] }: DeadlineControlProps) { + return ( + + setState(value && !isNaN(+value) ? Math.max(1, Number(value)) : undefined)} + size="small" + inputProps={{ min: 1 }} // Set minimum value to 1 + /> + + ) +} diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts index b00d73748b..41c736f6d9 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' -import type { CowSwapWidgetParams } from '@cowprotocol/widget-lib' +import { CowSwapWidgetParams, TradeType } from '@cowprotocol/widget-lib' import { isDev, isLocalHost, isVercel } from '../../../env' import { ConfiguratorState } from '../types' @@ -31,6 +31,10 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi sellTokenAmount, buyToken, buyTokenAmount, + deadline, + swapDeadline, + limitDeadline, + advancedDeadline, tokenListUrls, customColors, defaultColors, @@ -57,6 +61,14 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi tradeType: currentTradeType, sell: { asset: sellToken, amount: sellTokenAmount ? sellTokenAmount.toString() : undefined }, buy: { asset: buyToken, amount: buyTokenAmount?.toString() }, + forcedOrderDeadline: + swapDeadline || limitDeadline || advancedDeadline + ? { + [TradeType.SWAP]: swapDeadline, + [TradeType.LIMIT]: limitDeadline, + [TradeType.ADVANCED]: advancedDeadline, + } + : deadline, enabledTradeTypes, theme: JSON.stringify(customColors) === JSON.stringify(defaultColors) diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx index 2bd0b12d5f..5aed4286e6 100644 --- a/apps/widget-configurator/src/app/configurator/index.tsx +++ b/apps/widget-configurator/src/app/configurator/index.tsx @@ -34,6 +34,7 @@ import { CurrencyInputControl } from './controls/CurrencyInputControl' import { CurrentTradeTypeControl } from './controls/CurrentTradeTypeControl' import { CustomImagesControl } from './controls/CustomImagesControl' import { CustomSoundsControl } from './controls/CustomSoundsControl' +import { DeadlineControl } from './controls/DeadlineControl' import { NetworkControl, NetworkOption, NetworkOptions } from './controls/NetworkControl' import { PaletteControl } from './controls/PaletteControl' import { PartnerFeeControl } from './controls/PartnerFeeControl' @@ -106,6 +107,15 @@ export function Configurator({ title }: { title: string }) { const [buyToken] = buyTokenState const [buyTokenAmount] = buyTokenAmountState + const deadlineState = useState() + const [deadline] = deadlineState + const swapDeadlineState = useState() + const [swapDeadline] = swapDeadlineState + const limitDeadlineState = useState() + const [limitDeadline] = limitDeadlineState + const advancedDeadlineState = useState() + const [advancedDeadline] = advancedDeadlineState + const tokenListUrlsState = useState(DEFAULT_TOKEN_LISTS) const customTokensState = useState([]) const [tokenListUrls] = tokenListUrlsState @@ -146,6 +156,10 @@ export function Configurator({ title }: { title: string }) { // Don't change chainId in the widget URL if the user is connected to a wallet // Because useSyncWidgetNetwork() will send a request to change the network const state: ConfiguratorState = { + deadline, + swapDeadline, + limitDeadline, + advancedDeadline, chainId: IS_IFRAME ? undefined : !isConnected || !walletChainId ? chainId : walletChainId, theme: mode, currentTradeType, @@ -261,6 +275,16 @@ export function Configurator({ title }: { title: string }) { + Forced Order Deadline + + Global deadline settings + + + Individual deadline settings + + + + Integrations diff --git a/apps/widget-configurator/src/app/configurator/types.ts b/apps/widget-configurator/src/app/configurator/types.ts index 2755dbb135..d48f94df67 100644 --- a/apps/widget-configurator/src/app/configurator/types.ts +++ b/apps/widget-configurator/src/app/configurator/types.ts @@ -21,6 +21,10 @@ export interface ConfiguratorState { sellTokenAmount: number | undefined buyToken: string buyTokenAmount: number | undefined + deadline: number | undefined + swapDeadline: number | undefined + limitDeadline: number | undefined + advancedDeadline: number | undefined tokenListUrls: TokenListItem[] customColors: ColorPalette defaultColors: ColorPalette diff --git a/libs/common-const/src/misc.ts b/libs/common-const/src/misc.ts index 3fd037eb9f..2e8bed590c 100644 --- a/libs/common-const/src/misc.ts +++ b/libs/common-const/src/misc.ts @@ -6,7 +6,6 @@ export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' // 30 minutes, denominated in seconds export const DEFAULT_DEADLINE_FROM_NOW = 60 * 30 -export const L2_DEADLINE_FROM_NOW = 60 * 5 // one basis JSBI.BigInt const BPS_BASE = JSBI.BigInt(10000) diff --git a/libs/widget-lib/src/types.ts b/libs/widget-lib/src/types.ts index 368ef01970..4f94816c43 100644 --- a/libs/widget-lib/src/types.ts +++ b/libs/widget-lib/src/types.ts @@ -1,5 +1,6 @@ import type { SupportedChainId } from '@cowprotocol/cow-sdk' import { CowWidgetEventListeners, CowWidgetEventPayloadMap, CowWidgetEvents } from '@cowprotocol/events' + export type { SupportedChainId } from '@cowprotocol/cow-sdk' export type PerTradeTypeConfig = Partial> @@ -82,6 +83,8 @@ interface TradeAsset { amount?: string } +export type ForcedOrderDeadline = FlexibleConfig + export enum TradeType { SWAP = 'swap', LIMIT = 'limit', @@ -230,6 +233,15 @@ export interface CowSwapWidgetParams { */ buy?: TradeAsset + /** + * Forced order deadline in minutes. When set, user's won't be able to edit the deadline. + * + * Either a single value applied to each individual order type accordingly or an optional individual value per order type. + * + * The app will use the appropriated min/max value per order type. + */ + forcedOrderDeadline?: ForcedOrderDeadline + /** * Enables the ability to switch between trade types in the widget. */ From 681fb20dab0b4155d50ad7f32c7a48cb95e084a3 Mon Sep 17 00:00:00 2001 From: Leandro Date: Thu, 17 Oct 2024 11:29:43 +0100 Subject: [PATCH 11/12] feat(widget): hide orders table (#4993) * feat: add widget option to hide orders table * feat: use hideOrdersTable widget param * fix: show orders button on SWAP form * feat: add warning regarding the behaviour --- .../containers/TradeWidget/TradeWidgetForm.tsx | 3 ++- .../src/pages/AdvancedOrders/index.tsx | 16 +++++++++++----- .../src/pages/LimitOrders/RegularLimitOrders.tsx | 15 ++++++++++----- .../hooks/useWidgetParamsAndSettings.ts | 2 ++ .../src/app/configurator/index.tsx | 12 ++++++++++++ .../src/app/configurator/types.ts | 1 + libs/widget-lib/src/types.ts | 9 ++++++++- 7 files changed, 46 insertions(+), 12 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx index bba2c13940..100517aa7a 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx @@ -49,7 +49,7 @@ const scrollToMyOrders = () => { export function TradeWidgetForm(props: TradeWidgetProps) { const isInjectedWidgetMode = isInjectedWidget() - const { standaloneMode } = useInjectedWidgetParams() + const { standaloneMode, hideOrdersTable } = useInjectedWidgetParams() const isAlternativeOrderModalVisible = useIsAlternativeOrderModalVisible() const { pendingActivity } = useCategorizeRecentActivity() @@ -113,6 +113,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) { const shouldShowMyOrdersButton = !alternativeOrderModalVisible && (!isInjectedWidgetMode && isConnectedSwapMode ? isUpToLarge : true) && + (isConnectedSwapMode || !hideOrdersTable) && ((isConnectedSwapMode && standaloneMode !== true) || (isLimitOrderMode && isUpToLarge && isLimitOrdersUnlocked) || (isAdvancedMode && isUpToLarge && isAdvancedOrdersUnlocked)) diff --git a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx index 72644944e9..c1b37aa56f 100644 --- a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx +++ b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx @@ -6,6 +6,7 @@ import { FillAdvancedOrdersDerivedStateUpdater, SetupAdvancedOrderAmountsFromUrlUpdater, } from 'modules/advancedOrders' +import { useInjectedWidgetParams } from 'modules/injectedWidget' import { OrdersTableWidget, TabOrderTypes } from 'modules/ordersTable' import * as styledEl from 'modules/trade/pure/TradePageLayout' import { @@ -19,6 +20,7 @@ import { } from 'modules/twap' import { TwapFormState } from 'modules/twap/pure/PrimaryActionButton/getTwapFormState' + export default function AdvancedOrdersPage() { const { isUnlocked } = useAtomValue(advancedOrdersAtom) @@ -32,6 +34,8 @@ export default function AdvancedOrdersPage() { const advancedWidgetParams = { disablePriceImpact } + const { hideOrdersTable } = useInjectedWidgetParams() + return ( <> @@ -50,11 +54,13 @@ export default function AdvancedOrdersPage() { - + {!hideOrdersTable && ( + + )} diff --git a/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx b/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx index e90cde808a..542305fa8f 100644 --- a/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx +++ b/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx @@ -3,14 +3,17 @@ import { useWalletInfo } from '@cowprotocol/wallet' import { useOrders } from 'legacy/state/orders/hooks' +import { useInjectedWidgetParams } from 'modules/injectedWidget' import { LimitOrdersWidget, useIsWidgetUnlocked } from 'modules/limitOrders' import { OrdersTableWidget, TabOrderTypes } from 'modules/ordersTable' import * as styledEl from 'modules/trade/pure/TradePageLayout' + export function RegularLimitOrders() { const isUnlocked = useIsWidgetUnlocked() const { chainId, account } = useWalletInfo() const allLimitOrders = useOrders(chainId, account, UiOrderType.LIMIT) + const { hideOrdersTable } = useInjectedWidgetParams() return ( @@ -19,11 +22,13 @@ export function RegularLimitOrders() { - + {!hideOrdersTable && ( + + )} ) diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts index 41c736f6d9..0e9c49a740 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts @@ -44,6 +44,7 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi disableToastMessages, disableProgressBar, hideBridgeInfo, + hideOrdersTable, } = configuratorState const themeColors = { @@ -98,6 +99,7 @@ export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWi } : undefined, hideBridgeInfo, + hideOrdersTable, } return params diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx index 5aed4286e6..cb387444d9 100644 --- a/apps/widget-configurator/src/app/configurator/index.tsx +++ b/apps/widget-configurator/src/app/configurator/index.tsx @@ -143,6 +143,9 @@ export function Configurator({ title }: { title: string }) { const [hideBridgeInfo, setHideBridgeInfo] = useState(false) const toggleHideBridgeInfo = useCallback(() => setHideBridgeInfo((curr) => !curr), []) + const [hideOrdersTable, setHideOrdersTable] = useState(false) + const toggleHideOrdersTable = useCallback(() => setHideOrdersTable((curr) => !curr), []) + const LINKS = [ { icon: , label: 'View embed code', onClick: () => handleDialogOpen() }, { icon: , label: 'Widget web', url: `https://cow.fi/widget/?${UTM_PARAMS}` }, @@ -177,6 +180,7 @@ export function Configurator({ title }: { title: string }) { disableToastMessages, disableProgressBar, hideBridgeInfo, + hideOrdersTable, } const computedParams = useWidgetParams(state) @@ -326,6 +330,14 @@ export function Configurator({ title }: { title: string }) { + + Hide orders table: + + } label="Show orders table" /> + } label="Hide orders table" /> + + + {isDrawerOpen && ( Date: Thu, 17 Oct 2024 16:42:54 +0100 Subject: [PATCH 12/12] fix(smart-slippage): fix smart slip tooltip and feature flag (#5004) * fix: remove hard coded smart slippage value * fix: handle case when multiplier percentage in falsy * feat: use different tooltip when feature flag is off * fix: pass dynamic slippage settings to row slippage content * chore: fix lint --- .../components/TransactionSettings/index.tsx | 2 +- .../ConfirmSwapModalSetup/index.tsx | 4 ++- .../pure/Row/RowSlippageContent/index.tsx | 34 +++++++++++++------ .../calculateBpsFromFeeMultiplier.ts | 2 +- .../useSmartSlippageFromFeeMultiplier.ts | 3 +- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx index 7ad9ea8175..371ac7abbb 100644 --- a/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/TransactionSettings/index.tsx @@ -335,7 +335,7 @@ export function TransactionSettings() { // Your transaction will revert if the price changes unfavorably by more than this percentage. isEoaEthFlow ? getNativeSlippageTooltip(chainId, [nativeCurrency.symbol, getWrappedToken(nativeCurrency).symbol]) - : getNonNativeSlippageTooltip(true) + : getNonNativeSlippageTooltip({ isDynamic: !!smartSlippage, isSettingsModal: true }) } /> diff --git a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx index 9b58d26442..e904e03fa6 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/ConfirmSwapModalSetup/index.tsx @@ -35,6 +35,7 @@ import { useIsEoaEthFlow } from '../../hooks/useIsEoaEthFlow' import { useNavigateToNewOrderCallback } from '../../hooks/useNavigateToNewOrderCallback' import { useShouldPayGas } from '../../hooks/useShouldPayGas' import { useSwapConfirmButtonText } from '../../hooks/useSwapConfirmButtonText' +import { useSmartSwapSlippage } from '../../hooks/useSwapSlippage' import { useSwapState } from '../../hooks/useSwapState' import { NetworkCostsTooltipSuffix } from '../../pure/NetworkCostsTooltipSuffix' import { getNativeSlippageTooltip, getNonNativeSlippageTooltip } from '../../pure/Row/RowSlippageContent' @@ -87,6 +88,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { const buttonText = useSwapConfirmButtonText(slippageAdjustedSellAmount) const isSmartSlippageApplied = useIsSmartSlippageApplied() + const smartSlippage = useSmartSwapSlippage() const labelsAndTooltips = useMemo( () => ({ @@ -96,7 +98,7 @@ export function ConfirmSwapModalSetup(props: ConfirmSwapModalSetupProps) { : undefined, slippageTooltip: isEoaEthFlow ? getNativeSlippageTooltip(chainId, [nativeCurrency.symbol]) - : getNonNativeSlippageTooltip(), + : getNonNativeSlippageTooltip({ isDynamic: !!smartSlippage }), expectReceiveLabel: isExactIn ? 'Expected to receive' : 'Expected to sell', minReceivedLabel: isExactIn ? 'Minimum receive' : 'Maximum sent', minReceivedTooltip: getMinimumReceivedTooltip(allowedSlippage, isExactIn), diff --git a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx index 07a343a2fd..4facd945c0 100644 --- a/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/pure/Row/RowSlippageContent/index.tsx @@ -53,19 +53,30 @@ export const getNativeSlippageTooltip = (chainId: SupportedChainId, symbols: (st robust MEV protection, consider wrapping your {symbols?.[0] || 'native currency'} before trading. ) -export const getNonNativeSlippageTooltip = (isSettingsModal?: boolean) => ( + +export const getNonNativeSlippageTooltip = (params?: { isDynamic?: boolean; isSettingsModal?: boolean }) => ( - CoW Swap dynamically adjusts your slippage tolerance to ensure your trade executes quickly while still getting the - best price.{' '} - {isSettingsModal ? ( + {params?.isDynamic ? ( <> - To override this, enter your desired slippage amount. -
    -
    - Either way, your slippage is protected from MEV! + CoW Swap dynamically adjusts your slippage tolerance to ensure your trade executes quickly while still getting + the best price.{' '} + {params?.isSettingsModal ? ( + <> + To override this, enter your desired slippage amount. +
    +
    + Either way, your slippage is protected from MEV! + + ) : ( + <> +
    +
    + Trades are protected from MEV, so your slippage can't be exploited! + + )} ) : ( - "Trades are protected from MEV, so your slippage can't be exploited!" + <>CoW Swap trades are protected from MEV, so your slippage can't be exploited! )}
    ) @@ -113,7 +124,10 @@ export function RowSlippageContent(props: RowSlippageContentProps) { } = props const tooltipContent = - slippageTooltip || (isEoaEthFlow ? getNativeSlippageTooltip(chainId, symbols) : getNonNativeSlippageTooltip()) + slippageTooltip || + (isEoaEthFlow + ? getNativeSlippageTooltip(chainId, symbols) + : getNonNativeSlippageTooltip({ isDynamic: !!smartSlippage })) // In case the user happened to set the same slippage as the suggestion, do not show the suggestion const suggestedEqualToUserSlippage = smartSlippage && smartSlippage === displaySlippage diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts index 73d161d60d..fdd71e5f9f 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/calculateBpsFromFeeMultiplier.ts @@ -8,7 +8,7 @@ export function calculateBpsFromFeeMultiplier( isSell: boolean | undefined, multiplierPercentage: number, ): number | undefined { - if (!sellAmount || !feeAmount || isSell === undefined || multiplierPercentage <= 0) { + if (!sellAmount || !feeAmount || isSell === undefined || !multiplierPercentage || multiplierPercentage <= 0) { return undefined } diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts index f5566085a1..d80af85a40 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater/useSmartSlippageFromFeeMultiplier.ts @@ -6,7 +6,6 @@ import { useReceiveAmountInfo } from 'modules/trade' import { calculateBpsFromFeeMultiplier } from './calculateBpsFromFeeMultiplier' - /** * Calculates smart slippage in bps, based on quoted fee * @@ -19,7 +18,7 @@ export function useSmartSlippageFromFeeMultiplier(): number | undefined { const sellAmount = isSell ? afterNetworkCosts?.sellAmount : beforeNetworkCosts?.sellAmount const feeAmount = costs?.networkFee?.amountInSellCurrency - const { smartSlippageFeeMultiplierPercentage = 50 } = useFeatureFlags() + const { smartSlippageFeeMultiplierPercentage } = useFeatureFlags() return useMemo( () => calculateBpsFromFeeMultiplier(sellAmount, feeAmount, isSell, smartSlippageFeeMultiplierPercentage),