Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(trade): cache quotes #5473

Merged
merged 15 commits into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -21,25 +20,16 @@ export function useAdvancedOrdersActions() {

const naviageOnCurrencySelection = useNavigateOnCurrencySelection()
const updateCurrencyAmount = useUpdateCurrencyAmount()
const resetTradeQuote = useResetTradeQuote()
const cowAnalytics = useCowAnalytics()
const debouncedTradeAmountAnalytics = useMemo(() => createDebouncedTradeAmountAnalytics(cowAnalytics), [cowAnalytics])

const updateAdvancedOrdersState = useUpdateAdvancedOrdersRawState()

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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an execessive amount reset which caused sell amount resetting to 1 when you change sell token

updateCurrencyAmount({
amount: { isTyped: false, value: '' },
field: Field.OUTPUT,
currency,
})
naviageOnCurrencySelection(field, currency)
resetTradeQuote()
},
[naviageOnCurrencySelection, updateCurrencyAmount, resetTradeQuote],
[naviageOnCurrencySelection],
)

const onUserInput = useCallback(
Expand All @@ -61,12 +51,7 @@ export function useAdvancedOrdersActions() {
[updateAdvancedOrdersState],
)

const onSwitchTokensDefault = useSwitchTokensPlaces(onSwitchTradeOverride)

const onSwitchTokens = useCallback(() => {
onSwitchTokensDefault()
resetTradeQuote()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also an excessive quote resetting, this is already handled in useTradeQuotePolling

}, [resetTradeQuote, onSwitchTokensDefault])
const onSwitchTokens = useSwitchTokensPlaces(onSwitchTradeOverride)

return useMemo(
() => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {}

Expand All @@ -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
}
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tradeQuoteStateOverride was not in use, just deleted it

updateQuoteState(tradeQuoteStateOverride)
}
}, [tradeQuoteStateOverride, disableQuotePolling, updateQuoteState])

useResetRecipient(onChangeRecipient)

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -22,7 +17,6 @@ export function TradeWidget(props: TradeWidgetProps) {
<TradeWidgetUpdaters
disableQuotePolling={disableQuotePolling}
disableNativeSelling={disableNativeSelling}
tradeQuoteStateOverride={tradeQuoteStateOverride}
enableSmartSlippage={enableSmartSlippage}
onChangeRecipient={props.actions.onChangeRecipient}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -22,7 +20,6 @@ interface TradeWidgetParams {
isTradePriceUpdating: boolean
isSellingEthSupported?: boolean
priceImpact: PriceImpact
tradeQuoteStateOverride?: TradeQuoteState | null
disableQuotePolling?: boolean
disableNativeSelling?: boolean
disablePriceImpact?: boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
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'

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<string>): FeeQuoteParams | undefined {
export function useQuoteParams(
amount: Nullish<string>,
): { quoteParams: FeeQuoteParams; inputCurrency: Currency } | undefined {
const { chainId, account } = useWalletInfo()
const appData = useAppData()
const isWrapOrUnwrap = useIsWrapOrUnwrap()
Expand All @@ -36,7 +39,7 @@ export function useQuoteParams(amount: Nullish<string>): FeeQuoteParams | undefi
const fromDecimals = inputCurrency.decimals
const toDecimals = outputCurrency.decimals

const params: FeeQuoteParams = {
const quoteParams: FeeQuoteParams = {
sellToken,
buyToken,
amount,
Expand All @@ -53,7 +56,7 @@ export function useQuoteParams(amount: Nullish<string>): FeeQuoteParams | undefi
validFor: DEFAULT_QUOTE_TTL,
}

return params
return { quoteParams, inputCurrency }
}, [
inputCurrency,
outputCurrency,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export function useSetTradeQuoteParams(amount: Nullish<CurrencyAmount<Currency>>
const updateState = useSetAtom(tradeQuoteInputAtom)

useEffect(() => {
updateState({ amount: amount || null, fastQuote })
updateState({
amount: amount || null,
fastQuote,
})
}, [updateState, amount, fastQuote])
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useSetAtom } from 'jotai/index'
import { useMemo } from 'react'

import { OrderQuoteResponse, PriceQuality } from '@cowprotocol/cow-sdk'

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, requestParams: FeeQuoteParams): void
onResponse(data: OrderQuoteResponse, requestParams: FeeQuoteParams, fetchStartTimestamp: number): void
}

export function useTradeQuoteManager(sellTokenAddress: SellTokenAddress | undefined): TradeQuoteManager | null {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted the tradeQuotesAtom state updates logic in TradeQuoteManager in order to make it more clear what are the updates doing.

const update = useSetAtom(updateTradeQuoteAtom)
const processUnsupportedTokenError = useProcessUnsupportedTokenError()

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, requestParams: FeeQuoteParams) {
update(sellTokenAddress, { error, quoteParams: requestParams, isLoading: false, hasParamsChanged: false })

if (error.type === QuoteApiErrorCodes.UnsupportedToken) {
processUnsupportedTokenError(error, requestParams)
}
},
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, processUnsupportedTokenError, sellTokenAddress],
)
}
Loading
Loading