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} {' '}
- {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} {' '}
+ >
+ ) : (
+ '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),