diff --git a/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts b/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts index e5e0fcce0b..6014f3953c 100644 --- a/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/FeesUpdater.ts @@ -18,7 +18,7 @@ import { isWrappingTrade } from 'legacy/state/swap/utils' import { Field } from 'legacy/state/types' import { useUserTransactionTTL } from 'legacy/state/user/hooks' -import { useAppData } from 'modules/appData' +import { decodeAppData, useAppData } from 'modules/appData' import { useIsEoaEthFlow } from 'modules/swap/hooks/useIsEoaEthFlow' import { useDerivedSwapInfo, useSwapState } from 'modules/swap/hooks/useSwapState' @@ -61,6 +61,7 @@ function quoteUsingSameParameters(currentParams: FeeQuoteParams, quoteInfo: Quot } = 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 = @@ -68,20 +69,52 @@ function quoteUsingSameParameters(currentParams: FeeQuoteParams, quoteInfo: Quot buyToken === currentBuyToken && amount === currentAmount && kind === currentKind && - appData === currentAppData && + 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 }) +} + /** * 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 + quoteInformation?: QuoteInformationObject, ): boolean { // If there's no quote/fee information, we always re-fetch if (!quoteInformation) { @@ -219,7 +252,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/legacy/state/price/hooks.ts b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts index b6f4ee84e6..6fd2e8206d 100644 --- a/apps/cowswap-frontend/src/legacy/state/price/hooks.ts +++ b/apps/cowswap-frontend/src/legacy/state/price/hooks.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' @@ -71,7 +71,7 @@ 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 useMemo(() => ({ quote, isGettingNewQuote, isRefreshingQuote }), [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 2d10f8bb41..f5a6d7e01d 100644 --- a/apps/cowswap-frontend/src/modules/appData/index.ts +++ b/apps/cowswap-frontend/src/modules/appData/index.ts @@ -5,5 +5,6 @@ export { filterPermitSignerPermit } from './utils/appDataFilter' 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/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..4f9cf5462c 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' @@ -82,6 +82,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 +102,7 @@ export function RowSlippageContent(props: RowSlippageContentProps) { setAutoSlippage, smartSlippage, isSmartSlippageApplied, + isSmartSlippageLoading, } = props const tooltipContent = @@ -111,12 +113,17 @@ export function RowSlippageContent(props: RowSlippageContentProps) { const displayDefaultSlippage = isSlippageModified && setAutoSlippage && smartSlippage && !suggestedEqualToUserSlippage && ( - (Suggested: {smartSlippage}) - - - + {isSmartSlippageLoading ? () : ( + <> + (Suggested: {smartSlippage}) + + + + + )} ) + const loading = isSmartSlippageLoading && isSmartSlippageApplied && () return ( @@ -137,11 +144,11 @@ export function RowSlippageContent(props: RowSlippageContentProps) { {showSettingOnClick ? ( - {displaySlippage}{displayDefaultSlippage} + {loading ? loading : (<>{displaySlippage}{displayDefaultSlippage})} ) : ( - {displaySlippage}{displayDefaultSlippage} + {loading ? loading : (<>{displaySlippage}{displayDefaultSlippage})} )} diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts index 7e79d89102..2d52cb3073 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts +++ b/apps/cowswap-frontend/src/modules/swap/updaters/SmartSlippageUpdater.ts @@ -1,5 +1,5 @@ import { useSetAtom } from 'jotai' -import { useEffect } from 'react' +import { useEffect, useMemo } from 'react' import { BFF_BASE_URL } from '@cowprotocol/common-const' import { useFeatureFlags } from '@cowprotocol/common-hooks' @@ -11,6 +11,7 @@ import useSWR from 'swr' import { useDerivedTradeState, useIsWrapOrUnwrap } from 'modules/trade' +import { useDerivedSwapInfo, useHighFeeWarning } from '../hooks/useSwapState' import { smartSwapSlippageAtom } from '../state/slippageValueAndTypeAtom' const SWR_OPTIONS = { @@ -31,7 +32,7 @@ export function SmartSlippageUpdater() { const sellTokenAddress = inputCurrency && getCurrencyAddress(inputCurrency).toLowerCase() const buyTokenAddress = outputCurrency && getCurrencyAddress(outputCurrency).toLowerCase() - const slippageBps = useSWR( + const bffSlippageBps = useSWR( !sellTokenAddress || !buyTokenAddress || isWrapOrUnwrap || !isSmartSlippageEnabled ? null : [chainId, sellTokenAddress, buyTokenAddress], @@ -42,12 +43,53 @@ export function SmartSlippageUpdater() { return response.slippageBps }, - SWR_OPTIONS + SWR_OPTIONS, ).data + const tradeSizeSlippageBps = useSmartSlippageFromFeePercentage() + useEffect(() => { - setSmartSwapSlippage(typeof slippageBps === 'number' ? slippageBps : null) - }, [slippageBps, setSmartSwapSlippage]) + // Trade size slippage takes precedence + if (tradeSizeSlippageBps !== undefined) { + setSmartSwapSlippage(tradeSizeSlippageBps) + } else { + setSmartSwapSlippage(typeof bffSlippageBps === 'number' ? bffSlippageBps : null) + } + }, [bffSlippageBps, setSmartSwapSlippage, tradeSizeSlippageBps]) return null } + +/** + * 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]) +}