From 0774e8f1aa24d4e09cffb08a3f50d16105b4638c Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 4 Mar 2025 13:37:23 +0500 Subject: [PATCH 01/15] refactor: remove unused tradeQuoteStateOverride --- .../containers/TradeWidget/TradeWidgetUpdaters.tsx | 13 ++----------- .../modules/trade/containers/TradeWidget/index.tsx | 8 +------- .../modules/trade/containers/TradeWidget/types.ts | 3 --- 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetUpdaters.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetUpdaters.tsx index ae2243872b..aa0a4be4a5 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetUpdaters.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetUpdaters.tsx @@ -1,10 +1,10 @@ -import { ReactNode, useEffect } from 'react' +import { ReactNode } from 'react' import { PriorityTokensUpdater } from '@cowprotocol/balances-and-allowances' import { useWalletInfo } from '@cowprotocol/wallet' import { TradeFormValidationUpdater } from 'modules/tradeFormValidation' -import { TradeQuoteState, TradeQuoteUpdater, useUpdateTradeQuote } from 'modules/tradeQuote' +import { TradeQuoteUpdater } from 'modules/tradeQuote' import { SmartSlippageUpdater } from 'modules/tradeSlippage' import { usePriorityTokenAddresses } from '../../hooks/usePriorityTokenAddresses' @@ -19,28 +19,19 @@ interface TradeWidgetUpdatersProps { disableNativeSelling: boolean enableSmartSlippage?: boolean children: ReactNode - tradeQuoteStateOverride?: TradeQuoteState | null onChangeRecipient: (recipient: string | null) => void } export function TradeWidgetUpdaters({ disableQuotePolling, disableNativeSelling, - tradeQuoteStateOverride, enableSmartSlippage, onChangeRecipient, children, }: TradeWidgetUpdatersProps) { const { chainId, account } = useWalletInfo() - const updateQuoteState = useUpdateTradeQuote() const priorityTokenAddresses = usePriorityTokenAddresses() - useEffect(() => { - if (disableQuotePolling && tradeQuoteStateOverride) { - updateQuoteState(tradeQuoteStateOverride) - } - }, [tradeQuoteStateOverride, disableQuotePolling, updateQuoteState]) - useResetRecipient(onChangeRecipient) return ( diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx index 7cb410a5a3..78e8147e5f 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/index.tsx @@ -8,12 +8,7 @@ export const TradeWidgetContainer = styledEl.Container export function TradeWidget(props: TradeWidgetProps) { const { id, slots, params, confirmModal, genericModal } = props - const { - disableQuotePolling = false, - disableNativeSelling = false, - tradeQuoteStateOverride, - enableSmartSlippage, - } = params + const { disableQuotePolling = false, disableNativeSelling = false, enableSmartSlippage } = params const modals = TradeWidgetModals({ confirmModal, genericModal, selectTokenWidget: slots.selectTokenWidget }) return ( @@ -22,7 +17,6 @@ export function TradeWidget(props: TradeWidgetProps) { diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts index 4271d64ed6..53229dd805 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts @@ -2,8 +2,6 @@ import { ReactNode } from 'react' import { PriceImpact } from 'legacy/hooks/usePriceImpact' -import { TradeQuoteState } from 'modules/tradeQuote' - import { CurrencyInputPanelProps } from 'common/pure/CurrencyInputPanel' import { CurrencyInfo } from 'common/pure/CurrencyInputPanel/types' @@ -22,7 +20,6 @@ interface TradeWidgetParams { isTradePriceUpdating: boolean isSellingEthSupported?: boolean priceImpact: PriceImpact - tradeQuoteStateOverride?: TradeQuoteState | null disableQuotePolling?: boolean disableNativeSelling?: boolean disablePriceImpact?: boolean From e3a748d4a808a630669053f14d57a7cc483a7eae Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 4 Mar 2025 13:38:34 +0500 Subject: [PATCH 02/15] refactor: remove unused useResetTradeQuote --- .../advancedOrders/hooks/useAdvancedOrdersActions.ts | 12 ++---------- .../modules/tradeQuote/hooks/useResetTradeQuote.ts | 11 ----------- .../cowswap-frontend/src/modules/tradeQuote/index.ts | 1 - 3 files changed, 2 insertions(+), 22 deletions(-) delete mode 100644 apps/cowswap-frontend/src/modules/tradeQuote/hooks/useResetTradeQuote.ts diff --git a/apps/cowswap-frontend/src/modules/advancedOrders/hooks/useAdvancedOrdersActions.ts b/apps/cowswap-frontend/src/modules/advancedOrders/hooks/useAdvancedOrdersActions.ts index f38a5007f1..5a87f22977 100644 --- a/apps/cowswap-frontend/src/modules/advancedOrders/hooks/useAdvancedOrdersActions.ts +++ b/apps/cowswap-frontend/src/modules/advancedOrders/hooks/useAdvancedOrdersActions.ts @@ -7,7 +7,6 @@ import { Field } from 'legacy/state/types' import { useNavigateOnCurrencySelection, useSwitchTokensPlaces, useUpdateCurrencyAmount } from 'modules/trade' import { createDebouncedTradeAmountAnalytics } from 'modules/trade/utils/analytics' -import { useResetTradeQuote } from 'modules/tradeQuote' import { useAdvancedOrdersDerivedState } from './useAdvancedOrdersDerivedState' import { useUpdateAdvancedOrdersRawState } from './useAdvancedOrdersRawState' @@ -21,7 +20,6 @@ export function useAdvancedOrdersActions() { const naviageOnCurrencySelection = useNavigateOnCurrencySelection() const updateCurrencyAmount = useUpdateCurrencyAmount() - const resetTradeQuote = useResetTradeQuote() const cowAnalytics = useCowAnalytics() const debouncedTradeAmountAnalytics = useMemo(() => createDebouncedTradeAmountAnalytics(cowAnalytics), [cowAnalytics]) @@ -37,9 +35,8 @@ export function useAdvancedOrdersActions() { currency, }) naviageOnCurrencySelection(field, currency) - resetTradeQuote() }, - [naviageOnCurrencySelection, updateCurrencyAmount, resetTradeQuote], + [naviageOnCurrencySelection, updateCurrencyAmount], ) const onUserInput = useCallback( @@ -61,12 +58,7 @@ export function useAdvancedOrdersActions() { [updateAdvancedOrdersState], ) - const onSwitchTokensDefault = useSwitchTokensPlaces(onSwitchTradeOverride) - - const onSwitchTokens = useCallback(() => { - onSwitchTokensDefault() - resetTradeQuote() - }, [resetTradeQuote, onSwitchTokensDefault]) + const onSwitchTokens = useSwitchTokensPlaces(onSwitchTradeOverride) return useMemo( () => ({ diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useResetTradeQuote.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useResetTradeQuote.ts deleted file mode 100644 index e1f23890d4..0000000000 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useResetTradeQuote.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useCallback } from 'react' - -import { useUpdateTradeQuote } from './useUpdateTradeQuote' - -export function useResetTradeQuote() { - const updateQuoteState = useUpdateTradeQuote() - - return useCallback(() => { - updateQuoteState({ response: null }) - }, [updateQuoteState]) -} diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/index.ts b/apps/cowswap-frontend/src/modules/tradeQuote/index.ts index 5b3888c59f..8891dc5ec9 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/index.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/index.ts @@ -4,6 +4,5 @@ export * from './hooks/useTradeQuote' export * from './hooks/useSetTradeQuoteParams' export * from './hooks/useQuoteParams' export * from './hooks/useTradeQuoteFeeFiatAmount' -export * from './hooks/useResetTradeQuote' export * from './hooks/useUpdateTradeQuote' export * from './utils/quoteDeadline' From c7bf8e0f2e2ca8aa72a592cdc63c97496003e7ec Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 4 Mar 2025 13:47:59 +0500 Subject: [PATCH 03/15] fix(twap): do not reset amounts in currency selection --- .../advancedOrders/hooks/useAdvancedOrdersActions.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/advancedOrders/hooks/useAdvancedOrdersActions.ts b/apps/cowswap-frontend/src/modules/advancedOrders/hooks/useAdvancedOrdersActions.ts index 5a87f22977..d7527749a5 100644 --- a/apps/cowswap-frontend/src/modules/advancedOrders/hooks/useAdvancedOrdersActions.ts +++ b/apps/cowswap-frontend/src/modules/advancedOrders/hooks/useAdvancedOrdersActions.ts @@ -27,16 +27,9 @@ export function useAdvancedOrdersActions() { const onCurrencySelection = useCallback( (field: Field, currency: Currency | null) => { - // Reset the output field until we fetch quote for new selected token - // This is to avoid displaying wrong amounts in output field - updateCurrencyAmount({ - amount: { isTyped: false, value: '' }, - field: Field.OUTPUT, - currency, - }) naviageOnCurrencySelection(field, currency) }, - [naviageOnCurrencySelection, updateCurrencyAmount], + [naviageOnCurrencySelection], ) const onUserInput = useCallback( From cc15319bb69d869d9116ace83ae031620fc71d2a Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 4 Mar 2025 15:24:50 +0500 Subject: [PATCH 04/15] feat(trade): cache quote results --- .../containers/LimitOrdersWidget/index.tsx | 4 +- .../src/modules/swap/updaters/index.tsx | 10 ++- .../hooks/useSetTradeQuoteParams.ts | 16 +++-- .../tradeQuote/hooks/useTradeQuoteManager.ts | 55 ++++++++++++++++ .../tradeQuote/hooks/useTradeQuotePolling.ts | 29 ++++----- .../tradeQuote/hooks/useUpdateTradeQuote.ts | 7 -- .../src/modules/tradeQuote/index.ts | 2 +- .../tradeQuote/state/tradeQuoteAtom.ts | 65 ++++++++++++------- .../tradeQuote/state/tradeQuoteInputAtom.ts | 5 +- .../twap/updaters/QuoteParamsUpdater.ts | 6 +- .../src/modules/yield/updaters/index.tsx | 6 +- 11 files changed, 144 insertions(+), 61 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts delete mode 100644 apps/cowswap-frontend/src/modules/tradeQuote/hooks/useUpdateTradeQuote.ts diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx index 11f54a64fb..724320c845 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx @@ -2,7 +2,7 @@ import { useAtomValue } from 'jotai' import React, { useCallback, useEffect, useMemo } from 'react' import { useFeatureFlags } from '@cowprotocol/common-hooks' -import { isSellOrder } from '@cowprotocol/common-utils' +import { getCurrencyAddress, isSellOrder } from '@cowprotocol/common-utils' import { useLocation } from 'react-router-dom' @@ -90,7 +90,7 @@ export function LimitOrdersWidget() { [isSell, inputCurrencyAmount, outputCurrencyAmount], ) - useSetTradeQuoteParams(quoteAmount) + useSetTradeQuoteParams(quoteAmount, inputCurrency && getCurrencyAddress(inputCurrency)) const inputCurrencyInfo: CurrencyInfo = { field: Field.INPUT, diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/index.tsx b/apps/cowswap-frontend/src/modules/swap/updaters/index.tsx index 760c4a54b7..0b8d0b2ad1 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/updaters/index.tsx @@ -1,4 +1,4 @@ -import { isSellOrder, percentToBps } from '@cowprotocol/common-utils' +import { getCurrencyAddress, isSellOrder, percentToBps } from '@cowprotocol/common-utils' import { AppDataUpdater } from 'modules/appData' import { EthFlowDeadlineUpdater } from 'modules/ethFlow' @@ -12,12 +12,16 @@ import { useFillSwapDerivedState, useSwapDerivedState } from '../hooks/useSwapDe import { useSwapDeadlineState } from '../hooks/useSwapSettings' export function SwapUpdaters() { - const { orderKind, inputCurrencyAmount, outputCurrencyAmount, slippage } = useSwapDerivedState() + const { orderKind, inputCurrency, inputCurrencyAmount, outputCurrencyAmount, slippage } = useSwapDerivedState() const isSmartSlippageApplied = useIsSmartSlippageApplied() const swapDeadlineState = useSwapDeadlineState() useFillSwapDerivedState() - useSetTradeQuoteParams(isSellOrder(orderKind) ? inputCurrencyAmount : outputCurrencyAmount, true) + useSetTradeQuoteParams( + isSellOrder(orderKind) ? inputCurrencyAmount : outputCurrencyAmount, + inputCurrency && getCurrencyAddress(inputCurrency), + true, + ) return ( <> diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts index 4f412e8106..c76dd6172e 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts @@ -5,12 +5,20 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Nullish } from 'types' -import { tradeQuoteInputAtom } from '../state/tradeQuoteInputAtom' +import { SellTokenAddress, tradeQuoteInputAtom } from '../state/tradeQuoteInputAtom' -export function useSetTradeQuoteParams(amount: Nullish>, fastQuote?: boolean) { +export function useSetTradeQuoteParams( + amount: Nullish>, + sellTokenAddress: SellTokenAddress | null, + fastQuote?: boolean, +) { const updateState = useSetAtom(tradeQuoteInputAtom) useEffect(() => { - updateState({ amount: amount || null, fastQuote }) - }, [updateState, amount, fastQuote]) + updateState({ + amount: amount || null, + sellTokenAddress: sellTokenAddress ? sellTokenAddress.toLowerCase() : null, + fastQuote, + }) + }, [updateState, amount, sellTokenAddress, fastQuote]) } diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts new file mode 100644 index 0000000000..dec0e80daa --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts @@ -0,0 +1,55 @@ +import { useSetAtom } from 'jotai/index' +import { useMemo } from 'react' + +import { OrderQuoteResponse, PriceQuality } from '@cowprotocol/cow-sdk' + +import QuoteApiError from 'api/cowProtocol/errors/QuoteError' +import { FeeQuoteParams } from 'common/types' + +import { updateTradeQuoteAtom } from '../state/tradeQuoteAtom' +import { SellTokenAddress } from '../state/tradeQuoteInputAtom' + +export interface TradeQuoteManager { + setLoading(hasParamsChanged: boolean): void + reset(): void + onError(error: QuoteApiError): void + onResponse(data: OrderQuoteResponse, requestParams: FeeQuoteParams, fetchStartTimestamp: number): void +} + +export function useTradeQuoteManager(sellTokenAddress: SellTokenAddress | null): TradeQuoteManager | null { + const update = useSetAtom(updateTradeQuoteAtom) + + return useMemo( + () => + sellTokenAddress + ? { + setLoading(hasParamsChanged: boolean) { + update(sellTokenAddress, { + isLoading: true, + hasParamsChanged, + ...(hasParamsChanged ? { response: null } : null), + }) + }, + reset() { + update(sellTokenAddress, { response: null, isLoading: false }) + }, + onError(error: QuoteApiError) { + update(sellTokenAddress, { isLoading: false, error, hasParamsChanged: false }) + }, + onResponse(data: OrderQuoteResponse, requestParams: FeeQuoteParams, fetchStartTimestamp: number) { + const isOptimalQuote = requestParams.priceQuality === PriceQuality.OPTIMAL + + update(sellTokenAddress, { + response: data, + quoteParams: requestParams, + ...(isOptimalQuote ? { isLoading: false } : null), + error: null, + hasParamsChanged: false, + fetchStartTimestamp, + }) + }, + } + : null, + [update, sellTokenAddress], + ) +} diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts index 144f516ce4..4f3102f316 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts @@ -16,7 +16,7 @@ import QuoteApiError, { QuoteApiErrorCodes } from 'api/cowProtocol/errors/QuoteE import { useProcessUnsupportedTokenError } from './useProcessUnsupportedTokenError' import { useQuoteParams } from './useQuoteParams' import { useTradeQuote } from './useTradeQuote' -import { useUpdateTradeQuote } from './useUpdateTradeQuote' +import { useTradeQuoteManager } from './useTradeQuoteManager' import { tradeQuoteInputAtom } from '../state/tradeQuoteInputAtom' import { isQuoteExpired } from '../utils/quoteDeadline' @@ -30,14 +30,14 @@ const getFastQuote = onlyResolvesLast(getQuote) const getOptimalQuote = onlyResolvesLast(getQuote) export function useTradeQuotePolling() { - const { amount, fastQuote } = useAtomValue(tradeQuoteInputAtom) + const { amount, sellTokenAddress, fastQuote } = useAtomValue(tradeQuoteInputAtom) const tradeQuote = useTradeQuote() const tradeQuoteRef = useRef(tradeQuote) tradeQuoteRef.current = tradeQuote const quoteParams = useQuoteParams(amount?.quotient.toString()) - const updateQuoteState = useUpdateTradeQuote() + const tradeQuoteManager = useTradeQuoteManager(sellTokenAddress) const updateCurrencyAmount = useUpdateCurrencyAmount() const getIsUnsupportedTokens = useAreUnsupportedTokens() const processUnsupportedTokenError = useProcessUnsupportedTokenError() @@ -51,8 +51,12 @@ export function useTradeQuotePolling() { isOnlineRef.current = isOnline useLayoutEffect(() => { + if (!tradeQuoteManager) { + return + } + if (!quoteParams || quoteParams.amount === '0') { - updateQuoteState({ response: null, isLoading: false }) + tradeQuoteManager.reset() return } @@ -67,7 +71,7 @@ export function useTradeQuotePolling() { const currentQuoteParams = currentQuote.quoteParams const fetchQuote = (hasParamsChanged: boolean, priceQuality: PriceQuality, fetchStartTimestamp: number) => { - updateQuoteState({ isLoading: true, hasParamsChanged }) + tradeQuoteManager.setLoading(hasParamsChanged) const isOptimalQuote = priceQuality === PriceQuality.OPTIMAL const requestParams = { ...quoteParams, priceQuality } @@ -81,18 +85,11 @@ export function useTradeQuotePolling() { return } - updateQuoteState({ - response: data, - quoteParams: requestParams, - ...(isOptimalQuote ? { isLoading: false } : null), - error: null, - hasParamsChanged: false, - fetchStartTimestamp, - }) + tradeQuoteManager.onResponse(data, requestParams, fetchStartTimestamp) }) .catch((error: QuoteApiError) => { console.log('[useGetQuote]:: fetchQuote error', error) - updateQuoteState({ isLoading: false, error, hasParamsChanged: false }) + tradeQuoteManager.onError(error) if (error.type === QuoteApiErrorCodes.UnsupportedToken) { processUnsupportedTokenError(error, requestParams) @@ -149,7 +146,7 @@ export function useTradeQuotePolling() { /** * Reset the quote state in order to not trigger the quote expiration check again */ - updateQuoteState({ response: null, isLoading: false }) + tradeQuoteManager.reset() fetchAndUpdateQuote(false) } }, QUOTE_EXPIRATION_CHECK_INTERVAL) @@ -161,7 +158,7 @@ export function useTradeQuotePolling() { }, [ fastQuote, quoteParams, - updateQuoteState, + tradeQuoteManager, updateCurrencyAmount, processUnsupportedTokenError, getIsUnsupportedTokens, diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useUpdateTradeQuote.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useUpdateTradeQuote.ts deleted file mode 100644 index e989ae0e1b..0000000000 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useUpdateTradeQuote.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useSetAtom } from 'jotai/index' - -import { updateTradeQuoteAtom } from '../state/tradeQuoteAtom' - -export function useUpdateTradeQuote() { - return useSetAtom(updateTradeQuoteAtom) -} diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/index.ts b/apps/cowswap-frontend/src/modules/tradeQuote/index.ts index 8891dc5ec9..c47b24b0f0 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/index.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/index.ts @@ -4,5 +4,5 @@ export * from './hooks/useTradeQuote' export * from './hooks/useSetTradeQuoteParams' export * from './hooks/useQuoteParams' export * from './hooks/useTradeQuoteFeeFiatAmount' -export * from './hooks/useUpdateTradeQuote' +export * from './hooks/useTradeQuoteManager' export * from './utils/quoteDeadline' diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts index fa69b2dd3c..f36cdc8af7 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts @@ -5,6 +5,10 @@ import { OrderQuoteResponse, PriceQuality } from '@cowprotocol/cow-sdk' import QuoteApiError from 'api/cowProtocol/errors/QuoteError' import { FeeQuoteParams } from 'common/types' +import { tradeQuoteInputAtom } from './tradeQuoteInputAtom' + +type SellTokenAddress = string + export interface TradeQuoteState { response: OrderQuoteResponse | null error: QuoteApiError | null @@ -25,26 +29,43 @@ export const DEFAULT_TRADE_QUOTE_STATE: TradeQuoteState = { localQuoteTimestamp: null, } -export const tradeQuoteAtom = atom(DEFAULT_TRADE_QUOTE_STATE) - -export const updateTradeQuoteAtom = atom(null, (get, set, nextState: Partial) => { - set(tradeQuoteAtom, () => { - const prevState = get(tradeQuoteAtom) - - // Don't update state if Fast quote finished after Optimal quote - if ( - prevState.fetchStartTimestamp === nextState.fetchStartTimestamp && - nextState.response && - nextState.quoteParams?.priceQuality === PriceQuality.FAST - ) { - return { ...prevState } - } - - return { - ...prevState, - ...nextState, - quoteParams: typeof nextState.quoteParams === 'undefined' ? prevState.quoteParams : nextState.quoteParams, - localQuoteTimestamp: nextState.response ? Math.ceil(Date.now() / 1000) : null, - } - }) +const tradeQuotesAtom = atom>({}) + +export const tradeQuoteAtom = atom((get) => { + const { sellTokenAddress } = get(tradeQuoteInputAtom) + const quotes = get(tradeQuotesAtom) + + return (sellTokenAddress && quotes[sellTokenAddress]) || DEFAULT_TRADE_QUOTE_STATE }) + +export const updateTradeQuoteAtom = atom( + null, + (get, set, _sellTokenAddress: SellTokenAddress, nextState: Partial) => { + set(tradeQuotesAtom, () => { + const sellTokenAddress = _sellTokenAddress.toLowerCase() + const prevState = get(tradeQuotesAtom) + const prevQuote = prevState[sellTokenAddress] || DEFAULT_TRADE_QUOTE_STATE + + // Don't update state if Fast quote finished after Optimal quote + if ( + prevQuote.fetchStartTimestamp === nextState.fetchStartTimestamp && + nextState.response && + nextState.quoteParams?.priceQuality === PriceQuality.FAST + ) { + return { ...prevState } + } + + const update: TradeQuoteState = { + ...prevQuote, + ...nextState, + quoteParams: typeof nextState.quoteParams === 'undefined' ? prevQuote.quoteParams : nextState.quoteParams, + localQuoteTimestamp: nextState.response ? Math.ceil(Date.now() / 1000) : null, + } + + return { + ...prevState, + [sellTokenAddress]: update, + } + }) + }, +) diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteInputAtom.ts b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteInputAtom.ts index d93b2c9fa6..2c74a6d018 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteInputAtom.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteInputAtom.ts @@ -2,9 +2,12 @@ import { atom } from 'jotai' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +export type SellTokenAddress = string + export interface TradeQuoteInputState { amount: CurrencyAmount | null + sellTokenAddress: SellTokenAddress | null fastQuote?: boolean } -export const tradeQuoteInputAtom = atom({ amount: null }) +export const tradeQuoteInputAtom = atom({ amount: null, sellTokenAddress: null }) diff --git a/apps/cowswap-frontend/src/modules/twap/updaters/QuoteParamsUpdater.ts b/apps/cowswap-frontend/src/modules/twap/updaters/QuoteParamsUpdater.ts index eb2a680475..e59e5706ef 100644 --- a/apps/cowswap-frontend/src/modules/twap/updaters/QuoteParamsUpdater.ts +++ b/apps/cowswap-frontend/src/modules/twap/updaters/QuoteParamsUpdater.ts @@ -1,5 +1,7 @@ import { useAtomValue } from 'jotai' +import { getCurrencyAddress } from '@cowprotocol/common-utils' + import { useSetTradeQuoteParams } from 'modules/tradeQuote' import { useAdvancedOrdersDerivedState } from '../../advancedOrders' @@ -12,12 +14,12 @@ import { twapOrdersSettingsAtom } from '../state/twapOrdersSettingsAtom' * useSetTradeQuoteParams() just fill the quote amount into `tradeQuoteParamsAtom` that is used by `useQuoteParams`. */ export function QuoteParamsUpdater() { - const { inputCurrencyAmount } = useAdvancedOrdersDerivedState() + const { inputCurrencyAmount, inputCurrency } = useAdvancedOrdersDerivedState() const { numberOfPartsValue } = useAtomValue(twapOrdersSettingsAtom) const inputPartAmount = inputCurrencyAmount?.divide(numberOfPartsValue) - useSetTradeQuoteParams(inputPartAmount) + useSetTradeQuoteParams(inputPartAmount, inputCurrency && getCurrencyAddress(inputCurrency)) return null } diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx b/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx index 1e22910c5e..b14c85bcba 100644 --- a/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx @@ -1,5 +1,5 @@ import { INITIAL_ALLOWED_SLIPPAGE_PERCENT } from '@cowprotocol/common-const' -import { percentToBps } from '@cowprotocol/common-utils' +import { getCurrencyAddress, percentToBps } from '@cowprotocol/common-utils' import { AppDataUpdater } from 'modules/appData' import { useSetTradeQuoteParams } from 'modules/tradeQuote' @@ -10,10 +10,10 @@ import { SetupYieldAmountsFromUrlUpdater } from './SetupYieldAmountsFromUrlUpdat import { useFillYieldDerivedState, useYieldDerivedState } from '../hooks/useYieldDerivedState' export function YieldUpdaters() { - const { inputCurrencyAmount } = useYieldDerivedState() + const { inputCurrencyAmount, inputCurrency } = useYieldDerivedState() useFillYieldDerivedState() - useSetTradeQuoteParams(inputCurrencyAmount, true) + useSetTradeQuoteParams(inputCurrencyAmount, inputCurrency && getCurrencyAddress(inputCurrency), true) return ( <> From 73eb42815e862fbd7a687cfdd8ddeb0b0a18aa98 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 4 Mar 2025 15:38:29 +0500 Subject: [PATCH 05/15] refactor: extract fetchAndProcessQuote logic --- .../tradeQuote/hooks/useTradeQuoteManager.ts | 15 ++++-- .../tradeQuote/hooks/useTradeQuotePolling.ts | 49 +++---------------- .../services/fetchAndProcessQuote.ts | 42 ++++++++++++++++ 3 files changed, 61 insertions(+), 45 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.ts diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts index dec0e80daa..083cec9854 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts @@ -3,21 +3,24 @@ import { useMemo } from 'react' import { OrderQuoteResponse, PriceQuality } from '@cowprotocol/cow-sdk' -import QuoteApiError from 'api/cowProtocol/errors/QuoteError' +import QuoteApiError, { QuoteApiErrorCodes } from 'api/cowProtocol/errors/QuoteError' import { FeeQuoteParams } from 'common/types' +import { useProcessUnsupportedTokenError } from './useProcessUnsupportedTokenError' + import { updateTradeQuoteAtom } from '../state/tradeQuoteAtom' import { SellTokenAddress } from '../state/tradeQuoteInputAtom' export interface TradeQuoteManager { setLoading(hasParamsChanged: boolean): void reset(): void - onError(error: QuoteApiError): void + onError(error: QuoteApiError, requestParams: FeeQuoteParams): void onResponse(data: OrderQuoteResponse, requestParams: FeeQuoteParams, fetchStartTimestamp: number): void } export function useTradeQuoteManager(sellTokenAddress: SellTokenAddress | null): TradeQuoteManager | null { const update = useSetAtom(updateTradeQuoteAtom) + const processUnsupportedTokenError = useProcessUnsupportedTokenError() return useMemo( () => @@ -33,8 +36,12 @@ export function useTradeQuoteManager(sellTokenAddress: SellTokenAddress | null): reset() { update(sellTokenAddress, { response: null, isLoading: false }) }, - onError(error: QuoteApiError) { + onError(error: QuoteApiError, requestParams: FeeQuoteParams) { update(sellTokenAddress, { isLoading: false, error, hasParamsChanged: false }) + + if (error.type === QuoteApiErrorCodes.UnsupportedToken) { + processUnsupportedTokenError(error, requestParams) + } }, onResponse(data: OrderQuoteResponse, requestParams: FeeQuoteParams, fetchStartTimestamp: number) { const isOptimalQuote = requestParams.priceQuality === PriceQuality.OPTIMAL @@ -50,6 +57,6 @@ export function useTradeQuoteManager(sellTokenAddress: SellTokenAddress | null): }, } : null, - [update, sellTokenAddress], + [update, processUnsupportedTokenError, sellTokenAddress], ) } diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts index 4f3102f316..26049220f1 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts @@ -2,22 +2,19 @@ import { useAtomValue } from 'jotai' import { useLayoutEffect, useRef } from 'react' import { useIsOnline, useIsWindowVisible } from '@cowprotocol/common-hooks' -import { onlyResolvesLast } from '@cowprotocol/common-utils' -import { OrderQuoteResponse, PriceQuality } from '@cowprotocol/cow-sdk' +import { PriceQuality } from '@cowprotocol/cow-sdk' import { useAreUnsupportedTokens } from '@cowprotocol/tokens' import ms from 'ms.macro' import { useUpdateCurrencyAmount } from 'modules/trade' -import { getQuote } from 'api/cowProtocol/api' -import QuoteApiError, { QuoteApiErrorCodes } from 'api/cowProtocol/errors/QuoteError' - import { useProcessUnsupportedTokenError } from './useProcessUnsupportedTokenError' import { useQuoteParams } from './useQuoteParams' import { useTradeQuote } from './useTradeQuote' import { useTradeQuoteManager } from './useTradeQuoteManager' +import { fetchAndProcessQuote, TradeQuoteFetchParams } from '../services/fetchAndProcessQuote' import { tradeQuoteInputAtom } from '../state/tradeQuoteInputAtom' import { isQuoteExpired } from '../utils/quoteDeadline' import { quoteUsingSameParameters } from '../utils/quoteUsingSameParameters' @@ -25,10 +22,6 @@ import { quoteUsingSameParameters } from '../utils/quoteUsingSameParameters' export const PRICE_UPDATE_INTERVAL = ms`30s` const QUOTE_EXPIRATION_CHECK_INTERVAL = ms`2s` -// Solves the problem of multiple requests -const getFastQuote = onlyResolvesLast(getQuote) -const getOptimalQuote = onlyResolvesLast(getQuote) - export function useTradeQuotePolling() { const { amount, sellTokenAddress, fastQuote } = useAtomValue(tradeQuoteInputAtom) const tradeQuote = useTradeQuote() @@ -60,44 +53,18 @@ export function useTradeQuotePolling() { return } - const isUnsupportedTokens = getIsUnsupportedTokens(quoteParams) - // Don't fetch quote if token is not supported - if (isUnsupportedTokens) { + if (getIsUnsupportedTokens(quoteParams)) { return } const currentQuote = tradeQuoteRef.current const currentQuoteParams = currentQuote.quoteParams - const fetchQuote = (hasParamsChanged: boolean, priceQuality: PriceQuality, fetchStartTimestamp: number) => { - tradeQuoteManager.setLoading(hasParamsChanged) - - const isOptimalQuote = priceQuality === PriceQuality.OPTIMAL - const requestParams = { ...quoteParams, priceQuality } - const request = isOptimalQuote ? getOptimalQuote(requestParams) : getFastQuote(requestParams) - - return request - .then((response) => { - const { cancelled, data } = response - - if (cancelled) { - return - } - - tradeQuoteManager.onResponse(data, requestParams, fetchStartTimestamp) - }) - .catch((error: QuoteApiError) => { - console.log('[useGetQuote]:: fetchQuote error', error) - tradeQuoteManager.onError(error) - - if (error.type === QuoteApiErrorCodes.UnsupportedToken) { - processUnsupportedTokenError(error, requestParams) - } - }) - } + const fetchQuote = (fetchParams: TradeQuoteFetchParams) => + fetchAndProcessQuote(fetchParams, quoteParams, tradeQuoteManager) - function fetchAndUpdateQuote(paramsChanged: boolean) { + function fetchAndUpdateQuote(hasParamsChanged: boolean) { // Don't fetch quote if the parameters are the same // Also avoid quote refresh when only appData.quote (contains slippage) is changed // Important! We should skip quote updateing only if there is no quote response @@ -116,8 +83,8 @@ export function useTradeQuotePolling() { } const fetchStartTimestamp = Date.now() - if (fastQuote) fetchQuote(paramsChanged, PriceQuality.FAST, fetchStartTimestamp) - fetchQuote(paramsChanged, PriceQuality.OPTIMAL, fetchStartTimestamp) + if (fastQuote) fetchQuote({ hasParamsChanged, priceQuality: PriceQuality.FAST, fetchStartTimestamp }) + fetchQuote({ hasParamsChanged, priceQuality: PriceQuality.OPTIMAL, fetchStartTimestamp }) } /** diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.ts b/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.ts new file mode 100644 index 0000000000..f33d3a4ec7 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tradeQuote/services/fetchAndProcessQuote.ts @@ -0,0 +1,42 @@ +import { onlyResolvesLast } from '@cowprotocol/common-utils' +import { OrderQuoteResponse, PriceQuality } from '@cowprotocol/cow-sdk' + +import { getQuote } from 'api/cowProtocol/api' +import { FeeQuoteParams } from 'common/types' + +import { TradeQuoteManager } from '../hooks/useTradeQuoteManager' + +// Solves the problem of multiple requests +const getFastQuote = onlyResolvesLast(getQuote) +const getOptimalQuote = onlyResolvesLast(getQuote) + +export interface TradeQuoteFetchParams { + hasParamsChanged: boolean + priceQuality: PriceQuality + fetchStartTimestamp: number +} + +export async function fetchAndProcessQuote( + { hasParamsChanged, priceQuality, fetchStartTimestamp }: TradeQuoteFetchParams, + quoteParams: FeeQuoteParams, + tradeQuoteManager: TradeQuoteManager, +) { + tradeQuoteManager.setLoading(hasParamsChanged) + + const isOptimalQuote = priceQuality === PriceQuality.OPTIMAL + const requestParams = { ...quoteParams, priceQuality } + const request = isOptimalQuote ? getOptimalQuote(requestParams) : getFastQuote(requestParams) + + try { + const { cancelled, data } = await request + + if (cancelled) { + return + } + + tradeQuoteManager.onResponse(data, requestParams, fetchStartTimestamp) + } catch (error) { + console.log('[useGetQuote]:: fetchQuote error', error) + tradeQuoteManager.onError(error, requestParams) + } +} From dd09f9e8651f4c6f5bf99528134bedc0af2978a3 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 4 Mar 2025 18:20:07 +0500 Subject: [PATCH 06/15] chore: remove noisy log --- .../src/modules/volumeFee/state/volumeFeeAtom.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts b/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts index 817bd95bfa..2db0f08f9d 100644 --- a/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts +++ b/apps/cowswap-frontend/src/modules/volumeFee/state/volumeFeeAtom.ts @@ -46,7 +46,7 @@ const shouldSkipFeeAtom = atom((get) => { const inputCurrencyAddress = getCurrencyAddress(inputCurrency).toLowerCase() const outputCurrencyAddress = getCurrencyAddress(outputCurrency).toLowerCase() - const isCorrelated = correlatedTokens.some((tokens) => { + return correlatedTokens.some((tokens) => { // If there is only one asset in the list, it means that it is a global correlated token const addresses = Object.keys(tokens) if (addresses.length === 1) { @@ -56,16 +56,6 @@ const shouldSkipFeeAtom = atom((get) => { return tokens[inputCurrencyAddress] && tokens[outputCurrencyAddress] } }) - - if (isCorrelated) { - console.debug('[Volume Fee] Skipping fee for correlated tokens', { - inputCurrencyAddress, - outputCurrencyAddress, - correlatedTokens, - }) - } - - return isCorrelated }) const widgetPartnerFeeAtom = atom((get) => { From dc28060f017bc8abbb3de775dc68a3ce32fd6b09 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 4 Mar 2025 18:50:49 +0500 Subject: [PATCH 07/15] feat: use quote from cache --- .../containers/LimitOrdersWidget/index.tsx | 4 ++-- .../src/modules/swap/updaters/index.tsx | 10 +++------- .../src/modules/trade/state/receiveAmountInfoAtom.ts | 7 ++++--- .../tradeQuote/hooks/useSetTradeQuoteParams.ts | 11 +++-------- .../src/modules/tradeQuote/hooks/useTradeQuote.ts | 10 ++++++---- .../modules/tradeQuote/hooks/useTradeQuoteManager.ts | 2 +- .../modules/tradeQuote/hooks/useTradeQuotePolling.ts | 5 +++-- .../src/modules/tradeQuote/state/tradeQuoteAtom.ts | 11 +---------- .../modules/tradeQuote/state/tradeQuoteInputAtom.ts | 3 +-- .../src/modules/twap/updaters/QuoteParamsUpdater.ts | 6 ++---- .../src/modules/yield/updaters/index.tsx | 6 +++--- .../hooks/tokens/unsupported/useUnsupportedTokens.ts | 6 ++++-- 12 files changed, 33 insertions(+), 48 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx index 724320c845..11f54a64fb 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx @@ -2,7 +2,7 @@ import { useAtomValue } from 'jotai' import React, { useCallback, useEffect, useMemo } from 'react' import { useFeatureFlags } from '@cowprotocol/common-hooks' -import { getCurrencyAddress, isSellOrder } from '@cowprotocol/common-utils' +import { isSellOrder } from '@cowprotocol/common-utils' import { useLocation } from 'react-router-dom' @@ -90,7 +90,7 @@ export function LimitOrdersWidget() { [isSell, inputCurrencyAmount, outputCurrencyAmount], ) - useSetTradeQuoteParams(quoteAmount, inputCurrency && getCurrencyAddress(inputCurrency)) + useSetTradeQuoteParams(quoteAmount) const inputCurrencyInfo: CurrencyInfo = { field: Field.INPUT, diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/index.tsx b/apps/cowswap-frontend/src/modules/swap/updaters/index.tsx index 0b8d0b2ad1..760c4a54b7 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/updaters/index.tsx @@ -1,4 +1,4 @@ -import { getCurrencyAddress, isSellOrder, percentToBps } from '@cowprotocol/common-utils' +import { isSellOrder, percentToBps } from '@cowprotocol/common-utils' import { AppDataUpdater } from 'modules/appData' import { EthFlowDeadlineUpdater } from 'modules/ethFlow' @@ -12,16 +12,12 @@ import { useFillSwapDerivedState, useSwapDerivedState } from '../hooks/useSwapDe import { useSwapDeadlineState } from '../hooks/useSwapSettings' export function SwapUpdaters() { - const { orderKind, inputCurrency, inputCurrencyAmount, outputCurrencyAmount, slippage } = useSwapDerivedState() + const { orderKind, inputCurrencyAmount, outputCurrencyAmount, slippage } = useSwapDerivedState() const isSmartSlippageApplied = useIsSmartSlippageApplied() const swapDeadlineState = useSwapDeadlineState() useFillSwapDerivedState() - useSetTradeQuoteParams( - isSellOrder(orderKind) ? inputCurrencyAmount : outputCurrencyAmount, - inputCurrency && getCurrencyAddress(inputCurrency), - true, - ) + useSetTradeQuoteParams(isSellOrder(orderKind) ? inputCurrencyAmount : outputCurrencyAmount, true) return ( <> diff --git a/apps/cowswap-frontend/src/modules/trade/state/receiveAmountInfoAtom.ts b/apps/cowswap-frontend/src/modules/trade/state/receiveAmountInfoAtom.ts index ff6617c3de..53a5ff44d7 100644 --- a/apps/cowswap-frontend/src/modules/trade/state/receiveAmountInfoAtom.ts +++ b/apps/cowswap-frontend/src/modules/trade/state/receiveAmountInfoAtom.ts @@ -1,8 +1,8 @@ import { atom } from 'jotai' -import { isFractionFalsy } from '@cowprotocol/common-utils' +import { getCurrencyAddress, isFractionFalsy } from '@cowprotocol/common-utils' -import { tradeQuoteAtom } from 'modules/tradeQuote' +import { tradeQuotesAtom } from 'modules/tradeQuote' import { volumeFeeAtom } from 'modules/volumeFee' import { derivedTradeStateAtom } from './derivedTradeStateAtom' @@ -10,10 +10,11 @@ import { derivedTradeStateAtom } from './derivedTradeStateAtom' import { getReceiveAmountInfo } from '../utils/getReceiveAmountInfo' export const receiveAmountInfoAtom = atom((get) => { - const { response: quoteResponse } = get(tradeQuoteAtom) + const tradeQuotes = get(tradeQuotesAtom) const volumeFee = get(volumeFeeAtom) const { inputCurrency, outputCurrency, inputCurrencyAmount, outputCurrencyAmount, slippage, orderKind } = get(derivedTradeStateAtom) || {} + const quoteResponse = inputCurrency && tradeQuotes[getCurrencyAddress(inputCurrency).toLowerCase()]?.response if (isFractionFalsy(inputCurrencyAmount) && isFractionFalsy(outputCurrencyAmount)) return null diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts index c76dd6172e..a19b853d2d 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useSetTradeQuoteParams.ts @@ -5,20 +5,15 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Nullish } from 'types' -import { SellTokenAddress, tradeQuoteInputAtom } from '../state/tradeQuoteInputAtom' +import { tradeQuoteInputAtom } from '../state/tradeQuoteInputAtom' -export function useSetTradeQuoteParams( - amount: Nullish>, - sellTokenAddress: SellTokenAddress | null, - fastQuote?: boolean, -) { +export function useSetTradeQuoteParams(amount: Nullish>, fastQuote?: boolean) { const updateState = useSetAtom(tradeQuoteInputAtom) useEffect(() => { updateState({ amount: amount || null, - sellTokenAddress: sellTokenAddress ? sellTokenAddress.toLowerCase() : null, fastQuote, }) - }, [updateState, amount, sellTokenAddress, fastQuote]) + }, [updateState, amount, fastQuote]) } diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuote.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuote.ts index 7c4914728d..cc68bffce8 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuote.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuote.ts @@ -1,18 +1,20 @@ import { useAtomValue } from 'jotai' import { useMemo } from 'react' +import { getCurrencyAddress } from '@cowprotocol/common-utils' + import { useDerivedTradeState } from 'modules/trade/hooks/useDerivedTradeState' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' -import { tradeQuoteAtom } from '../state/tradeQuoteAtom' +import { tradeQuotesAtom } from '../state/tradeQuoteAtom' import { TradeQuoteState } from '../state/tradeQuoteAtom' import { DEFAULT_TRADE_QUOTE_STATE } from '../state/tradeQuoteAtom' export function useTradeQuote(): TradeQuoteState { const isProviderNetworkUnsupported = useIsProviderNetworkUnsupported() const state = useDerivedTradeState() - const quoteState = useAtomValue(tradeQuoteAtom) + const tradeQuotes = useAtomValue(tradeQuotesAtom) const inputCurrency = state?.inputCurrency const outputCurrency = state?.outputCurrency @@ -22,6 +24,6 @@ export function useTradeQuote(): TradeQuoteState { return DEFAULT_TRADE_QUOTE_STATE } - return quoteState - }, [inputCurrency, outputCurrency, quoteState, isProviderNetworkUnsupported]) + return tradeQuotes[getCurrencyAddress(inputCurrency).toLowerCase()] || DEFAULT_TRADE_QUOTE_STATE + }, [inputCurrency, outputCurrency, tradeQuotes, isProviderNetworkUnsupported]) } diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts index 083cec9854..810fdb5c71 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts @@ -18,7 +18,7 @@ export interface TradeQuoteManager { onResponse(data: OrderQuoteResponse, requestParams: FeeQuoteParams, fetchStartTimestamp: number): void } -export function useTradeQuoteManager(sellTokenAddress: SellTokenAddress | null): TradeQuoteManager | null { +export function useTradeQuoteManager(sellTokenAddress: SellTokenAddress | undefined): TradeQuoteManager | null { const update = useSetAtom(updateTradeQuoteAtom) const processUnsupportedTokenError = useProcessUnsupportedTokenError() diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts index 26049220f1..71ee1a1f4e 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts @@ -23,14 +23,15 @@ export const PRICE_UPDATE_INTERVAL = ms`30s` const QUOTE_EXPIRATION_CHECK_INTERVAL = ms`2s` export function useTradeQuotePolling() { - const { amount, sellTokenAddress, fastQuote } = useAtomValue(tradeQuoteInputAtom) + const { amount, fastQuote } = useAtomValue(tradeQuoteInputAtom) const tradeQuote = useTradeQuote() const tradeQuoteRef = useRef(tradeQuote) tradeQuoteRef.current = tradeQuote const quoteParams = useQuoteParams(amount?.quotient.toString()) + const sellToken = quoteParams?.sellToken - const tradeQuoteManager = useTradeQuoteManager(sellTokenAddress) + const tradeQuoteManager = useTradeQuoteManager(sellToken) const updateCurrencyAmount = useUpdateCurrencyAmount() const getIsUnsupportedTokens = useAreUnsupportedTokens() const processUnsupportedTokenError = useProcessUnsupportedTokenError() diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts index f36cdc8af7..df8effa345 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteAtom.ts @@ -5,8 +5,6 @@ import { OrderQuoteResponse, PriceQuality } from '@cowprotocol/cow-sdk' import QuoteApiError from 'api/cowProtocol/errors/QuoteError' import { FeeQuoteParams } from 'common/types' -import { tradeQuoteInputAtom } from './tradeQuoteInputAtom' - type SellTokenAddress = string export interface TradeQuoteState { @@ -29,14 +27,7 @@ export const DEFAULT_TRADE_QUOTE_STATE: TradeQuoteState = { localQuoteTimestamp: null, } -const tradeQuotesAtom = atom>({}) - -export const tradeQuoteAtom = atom((get) => { - const { sellTokenAddress } = get(tradeQuoteInputAtom) - const quotes = get(tradeQuotesAtom) - - return (sellTokenAddress && quotes[sellTokenAddress]) || DEFAULT_TRADE_QUOTE_STATE -}) +export const tradeQuotesAtom = atom>({}) export const updateTradeQuoteAtom = atom( null, diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteInputAtom.ts b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteInputAtom.ts index 2c74a6d018..fe60bc9efb 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteInputAtom.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/state/tradeQuoteInputAtom.ts @@ -6,8 +6,7 @@ export type SellTokenAddress = string export interface TradeQuoteInputState { amount: CurrencyAmount | null - sellTokenAddress: SellTokenAddress | null fastQuote?: boolean } -export const tradeQuoteInputAtom = atom({ amount: null, sellTokenAddress: null }) +export const tradeQuoteInputAtom = atom({ amount: null }) diff --git a/apps/cowswap-frontend/src/modules/twap/updaters/QuoteParamsUpdater.ts b/apps/cowswap-frontend/src/modules/twap/updaters/QuoteParamsUpdater.ts index e59e5706ef..eb2a680475 100644 --- a/apps/cowswap-frontend/src/modules/twap/updaters/QuoteParamsUpdater.ts +++ b/apps/cowswap-frontend/src/modules/twap/updaters/QuoteParamsUpdater.ts @@ -1,7 +1,5 @@ import { useAtomValue } from 'jotai' -import { getCurrencyAddress } from '@cowprotocol/common-utils' - import { useSetTradeQuoteParams } from 'modules/tradeQuote' import { useAdvancedOrdersDerivedState } from '../../advancedOrders' @@ -14,12 +12,12 @@ import { twapOrdersSettingsAtom } from '../state/twapOrdersSettingsAtom' * useSetTradeQuoteParams() just fill the quote amount into `tradeQuoteParamsAtom` that is used by `useQuoteParams`. */ export function QuoteParamsUpdater() { - const { inputCurrencyAmount, inputCurrency } = useAdvancedOrdersDerivedState() + const { inputCurrencyAmount } = useAdvancedOrdersDerivedState() const { numberOfPartsValue } = useAtomValue(twapOrdersSettingsAtom) const inputPartAmount = inputCurrencyAmount?.divide(numberOfPartsValue) - useSetTradeQuoteParams(inputPartAmount, inputCurrency && getCurrencyAddress(inputCurrency)) + useSetTradeQuoteParams(inputPartAmount) return null } diff --git a/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx b/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx index b14c85bcba..1e22910c5e 100644 --- a/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx +++ b/apps/cowswap-frontend/src/modules/yield/updaters/index.tsx @@ -1,5 +1,5 @@ import { INITIAL_ALLOWED_SLIPPAGE_PERCENT } from '@cowprotocol/common-const' -import { getCurrencyAddress, percentToBps } from '@cowprotocol/common-utils' +import { percentToBps } from '@cowprotocol/common-utils' import { AppDataUpdater } from 'modules/appData' import { useSetTradeQuoteParams } from 'modules/tradeQuote' @@ -10,10 +10,10 @@ import { SetupYieldAmountsFromUrlUpdater } from './SetupYieldAmountsFromUrlUpdat import { useFillYieldDerivedState, useYieldDerivedState } from '../hooks/useYieldDerivedState' export function YieldUpdaters() { - const { inputCurrencyAmount, inputCurrency } = useYieldDerivedState() + const { inputCurrencyAmount } = useYieldDerivedState() useFillYieldDerivedState() - useSetTradeQuoteParams(inputCurrencyAmount, inputCurrency && getCurrencyAddress(inputCurrency), true) + useSetTradeQuoteParams(inputCurrencyAmount, true) return ( <> diff --git a/libs/tokens/src/hooks/tokens/unsupported/useUnsupportedTokens.ts b/libs/tokens/src/hooks/tokens/unsupported/useUnsupportedTokens.ts index 0159c37d06..1d8ec55a4b 100644 --- a/libs/tokens/src/hooks/tokens/unsupported/useUnsupportedTokens.ts +++ b/libs/tokens/src/hooks/tokens/unsupported/useUnsupportedTokens.ts @@ -1,8 +1,10 @@ import { useAtomValue } from 'jotai' import { currentUnsupportedTokensAtom } from '../../../state/tokens/unsupportedTokensAtom' -import { UnsupportedTokensState } from '../../../types'; +import { UnsupportedTokensState } from '../../../types' + +const DEFAULT_UNSUPPORTED_TOKENS_STATE: UnsupportedTokensState = {} export function useUnsupportedTokens(): UnsupportedTokensState { - return useAtomValue(currentUnsupportedTokensAtom) || {} + return useAtomValue(currentUnsupportedTokensAtom) || DEFAULT_UNSUPPORTED_TOKENS_STATE } From 742d09363303d9105e338c703a0205631b7b1492 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Tue, 4 Mar 2025 18:53:17 +0500 Subject: [PATCH 08/15] fix: skip hooks from quote appData comparison --- .../modules/tradeQuote/utils/quoteUsingSameParameters.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts b/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts index 9d993e9580..17410e8f83 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts @@ -1,3 +1,5 @@ +import type { LatestAppDataDocVersion } from '@cowprotocol/app-data' + import { decodeAppData } from 'modules/appData' import { FeeQuoteParams } from 'common/types' @@ -45,7 +47,7 @@ function removeQuoteMetadata(appData: string | undefined): string | undefined { if (!decoded) return - const { metadata: fullMetadata, ...rest } = decoded - const { quote: _, ...metadata } = fullMetadata + const { metadata: fullMetadata, ...rest } = decoded as LatestAppDataDocVersion + const { quote: _, hooks: __, ...metadata } = fullMetadata return JSON.stringify({ ...rest, metadata }) } From d70862d94c91cc44836d6f1396d9cd4c5aaa8e41 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 5 Mar 2025 11:57:27 +0500 Subject: [PATCH 09/15] chore: revert hooks deleting from quote params --- .../modules/tradeQuote/utils/quoteUsingSameParameters.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts b/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts index 17410e8f83..9d993e9580 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts @@ -1,5 +1,3 @@ -import type { LatestAppDataDocVersion } from '@cowprotocol/app-data' - import { decodeAppData } from 'modules/appData' import { FeeQuoteParams } from 'common/types' @@ -47,7 +45,7 @@ function removeQuoteMetadata(appData: string | undefined): string | undefined { if (!decoded) return - const { metadata: fullMetadata, ...rest } = decoded as LatestAppDataDocVersion - const { quote: _, hooks: __, ...metadata } = fullMetadata + const { metadata: fullMetadata, ...rest } = decoded + const { quote: _, ...metadata } = fullMetadata return JSON.stringify({ ...rest, metadata }) } From 1815a2dc9686488c62edc91c186e63c816b7906e Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 5 Mar 2025 12:28:00 +0500 Subject: [PATCH 10/15] fix: fix quote updating --- .../src/modules/tradeQuote/hooks/useTradeQuotePolling.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts index 71ee1a1f4e..8761e998d2 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts @@ -59,13 +59,13 @@ export function useTradeQuotePolling() { return } - const currentQuote = tradeQuoteRef.current - const currentQuoteParams = currentQuote.quoteParams - const fetchQuote = (fetchParams: TradeQuoteFetchParams) => fetchAndProcessQuote(fetchParams, quoteParams, tradeQuoteManager) function fetchAndUpdateQuote(hasParamsChanged: boolean) { + const currentQuote = tradeQuoteRef.current + const currentQuoteParams = currentQuote.quoteParams + // Don't fetch quote if the parameters are the same // Also avoid quote refresh when only appData.quote (contains slippage) is changed // Important! We should skip quote updateing only if there is no quote response From 9c99e279f4b1755e1d2acfc97b0bf81bfce6fec7 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 5 Mar 2025 13:08:18 +0500 Subject: [PATCH 11/15] fix: cache quote errors --- .../modules/swap/updaters/QuoteObserverUpdater/index.tsx | 6 ++++-- .../src/modules/tradeQuote/hooks/useTradeQuoteManager.ts | 2 +- .../src/modules/tradeQuote/hooks/useTradeQuotePolling.ts | 9 +++------ .../modules/tradeQuote/utils/quoteUsingSameParameters.ts | 9 ++++++++- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/swap/updaters/QuoteObserverUpdater/index.tsx b/apps/cowswap-frontend/src/modules/swap/updaters/QuoteObserverUpdater/index.tsx index b763712c56..af96cadea1 100644 --- a/apps/cowswap-frontend/src/modules/swap/updaters/QuoteObserverUpdater/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/updaters/QuoteObserverUpdater/index.tsx @@ -18,6 +18,7 @@ export function QuoteObserverUpdater() { /** * Only when quote update because some params (input amount) changed */ + const hasQuoteError = !!tradeQuote.error const isQuoteUpdating = tradeQuote.isLoading && tradeQuote.hasParamsChanged const { beforeNetworkCosts, isSell } = receiveAmountInfo || {} @@ -41,14 +42,15 @@ export function QuoteObserverUpdater() { * Reset the opposite field when the quote is updating */ useEffect(() => { - if (!isQuoteUpdating || !orderKind) return + // Reset the opposite field when the quote is updating or has an error + if ((!hasQuoteError && !isQuoteUpdating) || !orderKind) return const fieldToReset = isSellOrder(orderKind) ? 'outputCurrencyAmount' : 'inputCurrencyAmount' updateSwapState({ [fieldToReset]: null, }) - }, [isQuoteUpdating, updateSwapState, orderKind]) + }, [isQuoteUpdating, hasQuoteError, updateSwapState, orderKind]) return null } diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts index 810fdb5c71..5a582aed9f 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuoteManager.ts @@ -37,7 +37,7 @@ export function useTradeQuoteManager(sellTokenAddress: SellTokenAddress | undefi update(sellTokenAddress, { response: null, isLoading: false }) }, onError(error: QuoteApiError, requestParams: FeeQuoteParams) { - update(sellTokenAddress, { isLoading: false, error, hasParamsChanged: false }) + update(sellTokenAddress, { error, quoteParams: requestParams, isLoading: false, hasParamsChanged: false }) if (error.type === QuoteApiErrorCodes.UnsupportedToken) { processUnsupportedTokenError(error, requestParams) diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts index 8761e998d2..c6e9cdc55e 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts @@ -65,16 +65,13 @@ export function useTradeQuotePolling() { function fetchAndUpdateQuote(hasParamsChanged: boolean) { const currentQuote = tradeQuoteRef.current const currentQuoteParams = currentQuote.quoteParams + const hasCachedResponse = !!currentQuote.response + const hasCachedError = !!currentQuote.error // Don't fetch quote if the parameters are the same // Also avoid quote refresh when only appData.quote (contains slippage) is changed // Important! We should skip quote updateing only if there is no quote response - if ( - currentQuote.response && - quoteParams && - currentQuoteParams && - quoteUsingSameParameters(currentQuoteParams, quoteParams) - ) { + if ((hasCachedResponse || hasCachedError) && quoteUsingSameParameters(currentQuoteParams, quoteParams)) { return } diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts b/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts index 9d993e9580..46f8199f22 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/utils/quoteUsingSameParameters.ts @@ -1,3 +1,5 @@ +import { Nullish } from 'types' + import { decodeAppData } from 'modules/appData' import { FeeQuoteParams } from 'common/types' @@ -7,7 +9,12 @@ import { FeeQuoteParams } from 'common/types' * * 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, nextParams: FeeQuoteParams): boolean { +export function quoteUsingSameParameters( + currentParams: Nullish, + nextParams: Nullish, +): boolean { + if (!currentParams || !nextParams) return false + const hasSameAppData = compareAppDataWithoutQuoteData(currentParams.appData, nextParams.appData) return ( From 20cd5b6298d257722818575c9d193ca79087df00 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 5 Mar 2025 13:35:36 +0500 Subject: [PATCH 12/15] fix: fix native token caching in quote --- .../src/modules/tradeQuote/hooks/useQuoteParams.ts | 9 ++++++--- .../src/modules/tradeQuote/hooks/useTradeQuotePolling.ts | 6 +++--- .../src/modules/twap/updaters/FullAmountQuoteUpdater.tsx | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts index 7b1be648c6..c0c0378ff6 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useQuoteParams.ts @@ -2,6 +2,7 @@ import { useDebounce } from '@cowprotocol/common-hooks' import { getCurrencyAddress, getIsNativeToken, getWrappedToken } from '@cowprotocol/common-utils' import { PriceQuality } from '@cowprotocol/cow-sdk' import { useWalletInfo } from '@cowprotocol/wallet' +import { Currency } from '@uniswap/sdk-core' import ms from 'ms.macro' import { Nullish } from 'types' @@ -17,7 +18,9 @@ import { FeeQuoteParams } from 'common/types' const DEFAULT_QUOTE_TTL = ms`30m` / 1000 const AMOUNT_CHANGE_DEBOUNCE_TIME = ms`350ms` -export function useQuoteParams(amount: Nullish): FeeQuoteParams | undefined { +export function useQuoteParams( + amount: Nullish, +): { quoteParams: FeeQuoteParams; inputCurrency: Currency } | undefined { const { chainId, account } = useWalletInfo() const appData = useAppData() const isWrapOrUnwrap = useIsWrapOrUnwrap() @@ -36,7 +39,7 @@ export function useQuoteParams(amount: Nullish): FeeQuoteParams | undefi const fromDecimals = inputCurrency.decimals const toDecimals = outputCurrency.decimals - const params: FeeQuoteParams = { + const quoteParams: FeeQuoteParams = { sellToken, buyToken, amount, @@ -53,7 +56,7 @@ export function useQuoteParams(amount: Nullish): FeeQuoteParams | undefi validFor: DEFAULT_QUOTE_TTL, } - return params + return { quoteParams, inputCurrency } }, [ inputCurrency, outputCurrency, diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts index c6e9cdc55e..70804e7c28 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts @@ -2,6 +2,7 @@ import { useAtomValue } from 'jotai' import { useLayoutEffect, useRef } from 'react' import { useIsOnline, useIsWindowVisible } from '@cowprotocol/common-hooks' +import { getCurrencyAddress } from '@cowprotocol/common-utils' import { PriceQuality } from '@cowprotocol/cow-sdk' import { useAreUnsupportedTokens } from '@cowprotocol/tokens' @@ -28,10 +29,9 @@ export function useTradeQuotePolling() { const tradeQuoteRef = useRef(tradeQuote) tradeQuoteRef.current = tradeQuote - const quoteParams = useQuoteParams(amount?.quotient.toString()) - const sellToken = quoteParams?.sellToken + const { quoteParams, inputCurrency } = useQuoteParams(amount?.quotient.toString()) || {} - const tradeQuoteManager = useTradeQuoteManager(sellToken) + const tradeQuoteManager = useTradeQuoteManager(inputCurrency && getCurrencyAddress(inputCurrency)) const updateCurrencyAmount = useUpdateCurrencyAmount() const getIsUnsupportedTokens = useAreUnsupportedTokens() const processUnsupportedTokenError = useProcessUnsupportedTokenError() diff --git a/apps/cowswap-frontend/src/modules/twap/updaters/FullAmountQuoteUpdater.tsx b/apps/cowswap-frontend/src/modules/twap/updaters/FullAmountQuoteUpdater.tsx index f6c5f98269..07851e0ada 100644 --- a/apps/cowswap-frontend/src/modules/twap/updaters/FullAmountQuoteUpdater.tsx +++ b/apps/cowswap-frontend/src/modules/twap/updaters/FullAmountQuoteUpdater.tsx @@ -20,7 +20,7 @@ export function FullAmountQuoteUpdater() { const fullQuoteAmount = inputCurrencyAmount?.quotient.toString() || null const partQuoteAmount = response?.quote.buyAmount - const quoteParams = useQuoteParams(fullQuoteAmount) + const quoteParams = useQuoteParams(fullQuoteAmount)?.quoteParams const updateQuoteState = useSetAtom(fullAmountQuoteAtom) useEffect(() => { From ba35dbb1492d801c4ce2633c904c28ec336924e2 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 5 Mar 2025 15:18:56 +0500 Subject: [PATCH 13/15] chore: add force updating for quote --- .../tradeQuote/hooks/useTradeQuotePolling.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts index 70804e7c28..2f7f6182f0 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts @@ -62,7 +62,7 @@ export function useTradeQuotePolling() { const fetchQuote = (fetchParams: TradeQuoteFetchParams) => fetchAndProcessQuote(fetchParams, quoteParams, tradeQuoteManager) - function fetchAndUpdateQuote(hasParamsChanged: boolean) { + function fetchAndUpdateQuote(hasParamsChanged: boolean, forceUpdate = false) { const currentQuote = tradeQuoteRef.current const currentQuoteParams = currentQuote.quoteParams const hasCachedResponse = !!currentQuote.response @@ -71,12 +71,16 @@ export function useTradeQuotePolling() { // Don't fetch quote if the parameters are the same // Also avoid quote refresh when only appData.quote (contains slippage) is changed // Important! We should skip quote updateing only if there is no quote response - if ((hasCachedResponse || hasCachedError) && quoteUsingSameParameters(currentQuoteParams, quoteParams)) { + if ( + !forceUpdate && + (hasCachedResponse || hasCachedError) && + quoteUsingSameParameters(currentQuoteParams, quoteParams) + ) { return } // When browser is offline or the tab is not active do no fetch - if (!isOnlineRef.current || !isWindowVisibleRef.current) { + if (!forceUpdate && (!isOnlineRef.current || !isWindowVisibleRef.current)) { return } @@ -94,7 +98,7 @@ export function useTradeQuotePolling() { * Start polling for the quote */ const pollingIntervalId = setInterval(() => { - fetchAndUpdateQuote(false) + fetchAndUpdateQuote(false, true) }, PRICE_UPDATE_INTERVAL) /** @@ -112,7 +116,7 @@ export function useTradeQuotePolling() { * Reset the quote state in order to not trigger the quote expiration check again */ tradeQuoteManager.reset() - fetchAndUpdateQuote(false) + fetchAndUpdateQuote(false, true) } }, QUOTE_EXPIRATION_CHECK_INTERVAL) From f26a1af4adbebb0619b74c88a4d1fd78f0b6f08b Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 5 Mar 2025 16:17:45 +0500 Subject: [PATCH 14/15] chore: update quote on window visibility change --- .../tradeQuote/hooks/useTradeQuotePolling.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts index 2f7f6182f0..d7612180ce 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts @@ -37,8 +37,6 @@ export function useTradeQuotePolling() { const processUnsupportedTokenError = useProcessUnsupportedTokenError() const isWindowVisible = useIsWindowVisible() - const isWindowVisibleRef = useRef(isWindowVisible) - isWindowVisibleRef.current = isWindowVisible const isOnline = useIsOnline() const isOnlineRef = useRef(isOnline) @@ -62,7 +60,7 @@ export function useTradeQuotePolling() { const fetchQuote = (fetchParams: TradeQuoteFetchParams) => fetchAndProcessQuote(fetchParams, quoteParams, tradeQuoteManager) - function fetchAndUpdateQuote(hasParamsChanged: boolean, forceUpdate = false) { + function fetchAndUpdateQuote(hasParamsChanged: boolean) { const currentQuote = tradeQuoteRef.current const currentQuoteParams = currentQuote.quoteParams const hasCachedResponse = !!currentQuote.response @@ -71,16 +69,12 @@ export function useTradeQuotePolling() { // Don't fetch quote if the parameters are the same // Also avoid quote refresh when only appData.quote (contains slippage) is changed // Important! We should skip quote updateing only if there is no quote response - if ( - !forceUpdate && - (hasCachedResponse || hasCachedError) && - quoteUsingSameParameters(currentQuoteParams, quoteParams) - ) { + if ((hasCachedResponse || hasCachedError) && quoteUsingSameParameters(currentQuoteParams, quoteParams)) { return } // When browser is offline or the tab is not active do no fetch - if (!forceUpdate && (!isOnlineRef.current || !isWindowVisibleRef.current)) { + if (!isOnlineRef.current || !isWindowVisible) { return } @@ -98,7 +92,7 @@ export function useTradeQuotePolling() { * Start polling for the quote */ const pollingIntervalId = setInterval(() => { - fetchAndUpdateQuote(false, true) + fetchAndUpdateQuote(false) }, PRICE_UPDATE_INTERVAL) /** @@ -116,7 +110,7 @@ export function useTradeQuotePolling() { * Reset the quote state in order to not trigger the quote expiration check again */ tradeQuoteManager.reset() - fetchAndUpdateQuote(false, true) + fetchAndUpdateQuote(false) } }, QUOTE_EXPIRATION_CHECK_INTERVAL) @@ -131,6 +125,7 @@ export function useTradeQuotePolling() { updateCurrencyAmount, processUnsupportedTokenError, getIsUnsupportedTokens, + isWindowVisible, ]) return null From 925478b28b9352f363a6933f24059fe2a3b10459 Mon Sep 17 00:00:00 2001 From: Alexandr Kazachenko Date: Wed, 5 Mar 2025 16:19:16 +0500 Subject: [PATCH 15/15] chore: update quote on window visibility change --- .../src/modules/tradeQuote/hooks/useTradeQuotePolling.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts index d7612180ce..b5fb16aee5 100644 --- a/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts +++ b/apps/cowswap-frontend/src/modules/tradeQuote/hooks/useTradeQuotePolling.ts @@ -1,5 +1,5 @@ import { useAtomValue } from 'jotai' -import { useLayoutEffect, useRef } from 'react' +import { useEffect, useLayoutEffect, useRef } from 'react' import { useIsOnline, useIsWindowVisible } from '@cowprotocol/common-hooks' import { getCurrencyAddress } from '@cowprotocol/common-utils' @@ -42,6 +42,12 @@ export function useTradeQuotePolling() { const isOnlineRef = useRef(isOnline) isOnlineRef.current = isOnline + useEffect(() => { + if (!isWindowVisible && tradeQuoteManager) { + tradeQuoteManager.reset() + } + }, [isWindowVisible, tradeQuoteManager]) + useLayoutEffect(() => { if (!tradeQuoteManager) { return