Skip to content

Commit

Permalink
Merge branch 'develop' into feat/limit-ui-upgrade
Browse files Browse the repository at this point in the history
  • Loading branch information
alfetopito committed Jan 16, 2025
2 parents 100f7ab + f2b1881 commit c58bc33
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 168 deletions.
113 changes: 39 additions & 74 deletions apps/cowswap-frontend/src/common/updaters/orders/SpotPricesUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { useSetAtom } from 'jotai'
import { useCallback, useEffect, useRef } from 'react'
import { useEffect, useMemo } from 'react'

import { useIsWindowVisible } from '@cowprotocol/common-hooks'
import { FractionUtils } from '@cowprotocol/common-utils'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { UiOrderType } from '@cowprotocol/types'
import { useWalletInfo } from '@cowprotocol/wallet'
import { Token } from '@uniswap/sdk-core'

import { SPOT_PRICE_CHECK_POLL_INTERVAL } from 'legacy/state/orders/consts'
import { useCombinedPendingOrders } from 'legacy/state/orders/hooks'

import { requestPrice } from 'modules/limitOrders/hooks/useGetInitialPrice'
import { UpdateSpotPriceAtom, updateSpotPricesAtom } from 'modules/orders/state/spotPricesAtom'
import { updateSpotPricesAtom } from 'modules/orders/state/spotPricesAtom'
import { useUsdPrices } from 'modules/usdAmount/hooks/useUsdPrice'

import { getUiOrderType } from 'utils/orderUtils/getUiOrderType'

Expand Down Expand Up @@ -52,93 +50,60 @@ function useMarkets(chainId: SupportedChainId, account: string | undefined): Mar

return acc
},
{}
{},
)
}, [pending])
}

interface UseUpdatePendingProps {
isWindowVisibleRef: React.MutableRefObject<boolean>
isUpdating: React.MutableRefObject<boolean>
markets: MarketRecord
updateSpotPrices: (update: UpdateSpotPriceAtom) => void
}

function useUpdatePending(props: UseUpdatePendingProps) {
const { isWindowVisibleRef, isUpdating, markets, updateSpotPrices } = props

return useCallback(async () => {
if (isUpdating.current) {
return
}

if (!isWindowVisibleRef.current) {
return
}

// Lock updates
isUpdating.current = true

const promises = Object.keys(markets).map((key) => {
const { chainId, inputCurrency, outputCurrency } = markets[key]

return requestPrice(chainId, inputCurrency, outputCurrency)
.then((fraction) => {
if (!fraction) {
return
}

const price = FractionUtils.toPrice(fraction, inputCurrency, outputCurrency)

updateSpotPrices({
chainId,
sellTokenAddress: inputCurrency.address,
buyTokenAddress: outputCurrency.address,
price,
})
})
.catch((e) => {
console.debug(`[SpotPricesUpdater] Failed to get price for ${key}`, e)
})
})

// Wait everything to finish, regardless if failed or not
await Promise.allSettled(promises)

// Release update lock
isUpdating.current = false

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [Object.keys(markets).sort().join(','), updateSpotPrices])
}

/**
* TODO: move this updater to modules/orders
* Spot Prices Updater
*
* Goes over all pending LIMIT orders and aggregates all markets
* Queries the spot price for given markets at every SPOT_PRICE_CHECK_POLL_INTERVAL
* Fetches the spot prices for all markets based on USD prices from usdPricesAtom
*/
export function SpotPricesUpdater(): null {
const { chainId, account } = useWalletInfo()

const isWindowVisible = useIsWindowVisible()
const isWindowVisibleRef = useRef(isWindowVisible)

const updateSpotPrices = useSetAtom(updateSpotPricesAtom)
const markets = useMarkets(chainId, account)
const isUpdating = useRef(false) // TODO: Implement using SWR or retry/cancellable promises
const updatePending = useUpdatePending({ isWindowVisibleRef, isUpdating, markets, updateSpotPrices })

isWindowVisibleRef.current = isWindowVisible
const marketTokens = useMemo(() => {
return Object.values(markets).reduce<Token[]>((acc, { inputCurrency, outputCurrency }) => {
acc.push(inputCurrency)
acc.push(outputCurrency)

useEffect(() => {
updatePending()
return acc
}, [])
}, [markets])

const interval = setInterval(updatePending, SPOT_PRICE_CHECK_POLL_INTERVAL)
const usdPrices = useUsdPrices(marketTokens)

return () => clearInterval(interval)
}, [chainId, isWindowVisible, updatePending])
useEffect(() => {
Object.values(markets).forEach(({ inputCurrency, outputCurrency }) => {
const inputPrice = usdPrices[inputCurrency.address.toLowerCase()]
const outputPrice = usdPrices[outputCurrency.address.toLowerCase()]

if (!inputPrice?.price || !outputPrice?.price || !inputPrice?.isLoading || !outputPrice?.isLoading) {
return
}

const inputFraction = FractionUtils.fractionLikeToFraction(inputPrice.price)
const outputFraction = FractionUtils.fractionLikeToFraction(outputPrice.price)
const fraction = inputFraction.divide(outputFraction)

if (!fraction) {
return
}
const price = FractionUtils.toPrice(fraction, inputCurrency, outputCurrency)

updateSpotPrices({
chainId,
sellTokenAddress: inputCurrency.address,
buyTokenAddress: outputCurrency.address,
price,
})
})
}, [usdPrices, markets, chainId, updateSpotPrices])

return null
}
1 change: 0 additions & 1 deletion apps/cowswap-frontend/src/legacy/state/orders/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export const MARKET_OPERATOR_API_POLL_INTERVAL = ms`2s`
// We can have lots of limit orders and it creates a high load, so we poll them no so ofter as market orders
export const LIMIT_OPERATOR_API_POLL_INTERVAL = ms`15s`
export const PENDING_ORDERS_PRICE_CHECK_POLL_INTERVAL = ms`30s`
export const SPOT_PRICE_CHECK_POLL_INTERVAL = ms`15s`
export const EXPIRED_ORDERS_CHECK_POLL_INTERVAL = ms`15s`

export const OUT_OF_MARKET_PRICE_DELTA_PERCENTAGE = new Percent(1, 100) // 1/100 => 0.01 => 1%
Expand Down
Original file line number Diff line number Diff line change
@@ -1,90 +1,44 @@
import { useEffect, useState } from 'react'

import { useIsWindowVisible } from '@cowprotocol/common-hooks'
import { getWrappedToken } from '@cowprotocol/common-utils'
import { useWalletInfo } from '@cowprotocol/wallet'
import { Currency, Fraction } from '@uniswap/sdk-core'
import { FractionUtils, getWrappedToken } from '@cowprotocol/common-utils'
import { Fraction } from '@uniswap/sdk-core'

import ms from 'ms.macro'
import { useAsyncMemo } from 'use-async-memo'

import { useLimitOrdersDerivedState } from 'modules/limitOrders/hooks/useLimitOrdersDerivedState'

import { useSafeMemo } from 'common/hooks/useSafeMemo'

import { fetchCurrencyUsdPrice, usdcPriceLoader } from '../../usdAmount'

const PRICE_UPDATE_INTERVAL = ms`10sec`

export async function requestPrice(
chainId: number | undefined,
inputCurrency: Currency | null,
outputCurrency: Currency | null,
): Promise<Fraction | null> {
if (!chainId || !inputCurrency || !outputCurrency) {
return null
}

const inputToken = getWrappedToken(inputCurrency)
const outputToken = getWrappedToken(outputCurrency)

// Only needed for the fallback CoW price, which needs to know the USDC price
const getUsdPrice = usdcPriceLoader(chainId)

return Promise.all([
fetchCurrencyUsdPrice(inputToken, getUsdPrice),
fetchCurrencyUsdPrice(outputToken, getUsdPrice),
]).then(([inputPrice, outputPrice]) => {
if (!inputPrice || !outputPrice) {
return null
}

const result = inputPrice.divide(outputPrice)

console.debug('Updated limit orders initial price: ', result.toSignificant(18))

return result
})
}
import { useUsdPrice } from '../../usdAmount'

// Fetches the INPUT and OUTPUT price and calculates initial Active rate
// When return null it means we failed on price loading
export function useGetInitialPrice(): { price: Fraction | null; isLoading: boolean } {
const { chainId } = useWalletInfo()
const { inputCurrency, outputCurrency } = useLimitOrdersDerivedState()
const [isLoading, setIsLoading] = useState(false)
const [updateTimestamp, setUpdateTimestamp] = useState(Date.now())
const isWindowVisible = useIsWindowVisible()

const inputToken = inputCurrency && getWrappedToken(inputCurrency)
const outputToken = outputCurrency && getWrappedToken(outputCurrency)
const inputUsdPrice = useUsdPrice(inputToken)
const outputUsdPrice = useUsdPrice(outputToken)

useEffect(() => {
setIsLoading(!!inputUsdPrice?.isLoading || !!outputUsdPrice?.isLoading)
}, [inputUsdPrice?.isLoading, outputUsdPrice?.isLoading])

const price = useAsyncMemo(
async () => {
setIsLoading(true)

console.debug('[useGetInitialPrice] Fetching price')
try {
return await requestPrice(chainId, inputCurrency, outputCurrency)
} finally {
setIsLoading(false)
if (!inputUsdPrice?.price || !outputUsdPrice?.price) {
return null
}
const inputFraction = FractionUtils.fractionLikeToFraction(inputUsdPrice.price)
const outputFraction = FractionUtils.fractionLikeToFraction(outputUsdPrice.price)
return inputFraction.divide(outputFraction)
},
[chainId, inputCurrency, outputCurrency, updateTimestamp],
[inputUsdPrice?.price, outputUsdPrice?.price],
null,
)

// Update initial price every 10 seconds
useEffect(() => {
if (!isWindowVisible) {
console.debug('[useGetInitialPrice] No need to fetch quotes')
return
}

console.debug('[useGetInitialPrice] Periodically fetch price')
const interval = setInterval(() => {
setUpdateTimestamp(Date.now())
}, PRICE_UPDATE_INTERVAL)

return () => clearInterval(interval)
}, [isWindowVisible])

return useSafeMemo(() => ({ price, isLoading }), [price, isLoading])
}
Original file line number Diff line number Diff line change
@@ -1,49 +1,56 @@
import { useSetAtom } from 'jotai'
import { useEffect } from 'react'
import { useEffect, useMemo } from 'react'

import { FractionUtils } from '@cowprotocol/common-utils'
import { CurrencyAmount, Percent, Price } from '@uniswap/sdk-core'
import { FractionUtils, getWrappedToken } from '@cowprotocol/common-utils'
import { Fraction, Token } from '@uniswap/sdk-core'

import { Nullish } from 'types'

import { updateLimitRateAtom } from 'modules/limitOrders/state/limitRateAtom'
import { useDerivedTradeState } from 'modules/trade/hooks/useDerivedTradeState'
import { useTradeQuote } from 'modules/tradeQuote'

const LIMIT_ORDERS_PRICE_SLIPPAGE = new Percent(1, 10) // 0.1%
import { useUsdPrice } from 'modules/usdAmount/hooks/useUsdPrice'

export function QuoteObserverUpdater() {
const { response } = useTradeQuote()
const state = useDerivedTradeState()

const updateLimitRateState = useSetAtom(updateLimitRateAtom)

const inputCurrency = state?.inputCurrency
const outputCurrency = state?.outputCurrency

useEffect(() => {
if (!outputCurrency || !inputCurrency || !response) {
return
}

const { buyAmount: buyAmountRaw, sellAmount: sellAmountRaw, feeAmount: feeAmountRaw } = response.quote
const inputToken = inputCurrency && getWrappedToken(inputCurrency)
const outputToken = outputCurrency && getWrappedToken(outputCurrency)

const feeAmount = CurrencyAmount.fromRawAmount(inputCurrency, feeAmountRaw)
const sellAmount = CurrencyAmount.fromRawAmount(inputCurrency, sellAmountRaw)
const buyAmount = CurrencyAmount.fromRawAmount(outputCurrency, buyAmountRaw)
const { price, isLoading } = useSpotPrice(inputToken, outputToken)

if (sellAmount.equalTo(0) || buyAmount.equalTo(0)) return
useEffect(() => {
updateLimitRateState({ marketRate: price, isLoadingMarketRate: isLoading })
}, [price, isLoading, updateLimitRateState])

const price = FractionUtils.fractionLikeToFraction(new Price({ baseAmount: sellAmount, quoteAmount: buyAmount }))
const marketRate = price.subtract(price.multiply(LIMIT_ORDERS_PRICE_SLIPPAGE.divide(100)))
return null
}

const biggestDecimal = Math.max(sellAmount.currency.decimals, buyAmount.currency.decimals)
/**
* In case when inputted sell amount is enormously big and the price is very small
* App crashes with "Invariant failed"
*/
const isPriceInvalid = +marketRate.toFixed(biggestDecimal) === 0
function useSpotPrice(
inputCurrency: Nullish<Token>,
outputCurrency: Nullish<Token>,
): {
price: Fraction | null
isLoading: boolean
} {
const inputUsdPrice = useUsdPrice(inputCurrency)
const outputUsdPrice = useUsdPrice(outputCurrency)

return useMemo(() => {
const isLoading = !!inputUsdPrice?.isLoading || !!outputUsdPrice?.isLoading

if (!inputUsdPrice?.price || !outputUsdPrice?.price) {
return { price: null, isLoading }
}
const inputFraction = FractionUtils.fractionLikeToFraction(inputUsdPrice.price)
const outputFraction = FractionUtils.fractionLikeToFraction(outputUsdPrice.price)

updateLimitRateState({ marketRate: isPriceInvalid ? null : marketRate, feeAmount })
}, [response, inputCurrency, outputCurrency, updateLimitRateState])
const price = inputFraction.divide(outputFraction)

return null
return { price, isLoading }
}, [inputUsdPrice?.price, inputUsdPrice?.isLoading, outputUsdPrice?.price, outputUsdPrice?.isLoading])
}
Loading

0 comments on commit c58bc33

Please sign in to comment.