Skip to content

Commit

Permalink
feat(usd): use bff prices (#5279)
Browse files Browse the repository at this point in the history
* feat: add getBffUsdPrice

* feat: replace Coingecko price source with BFF

Keep the existing fallbacks in case BFF is down

* feat: remove getCoingeckoUsdPrice

* chore: remove redundant variable

* refactor: use try/catch instead of promise chainning
  • Loading branch information
alfetopito authored Jan 13, 2025
1 parent 7d3242c commit 2bd17bf
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 135 deletions.
62 changes: 62 additions & 0 deletions apps/cowswap-frontend/src/modules/usdAmount/apis/getBffUsdPrice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { BFF_BASE_URL } from '@cowprotocol/common-const'
import { FractionUtils } from '@cowprotocol/common-utils'
import { Fraction, Token } from '@uniswap/sdk-core'

import ms from 'ms.macro'

import { fetchWithRateLimit } from 'common/utils/fetch'

import { UnknownCurrencyError } from './errors'

type BffUsdPriceResponse = {
price: number
}
type BffUsdErrorResponse = { message: string }
type BffResponse = BffUsdPriceResponse | BffUsdErrorResponse

const fetchRateLimited = fetchWithRateLimit({
// Allow 5 requests per second
rateLimit: {
tokensPerInterval: 5,
interval: 'second',
},
// 2 retry attempts with 100ms delay
backoff: {
maxDelay: ms`0.1s`,
numOfAttempts: 2,
},
})

export async function getBffUsdPrice(currency: Token): Promise<Fraction | null> {
const url = `${BFF_BASE_URL}/${currency.chainId}/tokens/${currency.address}/usdPrice`

try {
const res = await fetchRateLimited(url)

// Token not found
if (res.status === 404) {
throw new UnknownCurrencyError({
cause: `BFF did not return a price for '${currency.address}' on chain '${currency.chainId}'`,
})
// Unknown error case
} else if (res.status !== 200) {
throw new Error(`Unexpected response from BFF: ${res.status}`)
}

const data: BffResponse = await res.json()

// 200 response with error message
if (isErrorResponse(data)) {
throw new Error(`Unexpected response from BFF: ${JSON.stringify(data)}`)
}

// Happy path
return FractionUtils.fromNumber(data.price)
} catch (error) {
return Promise.reject(error)
}
}

function isErrorResponse(response: BffResponse): response is BffUsdErrorResponse {
return 'message' in response && !('price' in response)
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,17 @@ import { PersistentStateByChain } from '@cowprotocol/types'
import { Fraction, Token } from '@uniswap/sdk-core'

import { RateLimitError, UnknownCurrencyError } from '../apis/errors'
import { COINGECKO_PLATFORMS, COINGECKO_RATE_LIMIT_TIMEOUT, getCoingeckoUsdPrice } from '../apis/getCoingeckoUsdPrice'
import { getBffUsdPrice } from '../apis/getBffUsdPrice'
import { getCowProtocolUsdPrice } from '../apis/getCowProtocolUsdPrice'
import { DEFILLAMA_PLATFORMS, DEFILLAMA_RATE_LIMIT_TIMEOUT, getDefillamaUsdPrice } from '../apis/getDefillamaUsdPrice'

type UnknownCurrencies = { [address: string]: true }
type UnknownCurrenciesMap = PersistentStateByChain<UnknownCurrencies>

let coingeckoRateLimitHitTimestamp: null | number = null
let defillamaRateLimitHitTimestamp: null | number = null

const coingeckoUnknownCurrencies: UnknownCurrenciesMap = mapSupportedNetworks({})
const defillamaUnknownCurrencies: UnknownCurrenciesMap = mapSupportedNetworks({})

function getShouldSkipCoingecko(currency: Token): boolean {
return getShouldSkipPriceSource(
currency,
COINGECKO_PLATFORMS,
coingeckoUnknownCurrencies,
coingeckoRateLimitHitTimestamp,
COINGECKO_RATE_LIMIT_TIMEOUT,
)
}
const bffUnknownCurrencies: UnknownCurrenciesMap = mapSupportedNetworks({})

function getShouldSkipDefillama(currency: Token): boolean {
return getShouldSkipPriceSource(
Expand All @@ -38,37 +27,36 @@ function getShouldSkipDefillama(currency: Token): boolean {

function getShouldSkipPriceSource(
currency: Token,
platforms: Record<SupportedChainId, string | null>,
platforms: Record<SupportedChainId, string | null> | null,
unknownCurrenciesMap: UnknownCurrenciesMap,
rateLimitTimestamp: null | number,
timeout: number,
): boolean {
const chainId = currency.chainId as SupportedChainId
const unknownCurrenciesForChain = unknownCurrenciesMap[chainId] || {}

if (!platforms[chainId]) return true
if (platforms && !platforms[chainId]) return true

if (unknownCurrenciesForChain[currency.address.toLowerCase()]) return true

return !!rateLimitTimestamp && Date.now() - rateLimitTimestamp < timeout
}

function getShouldSkipBff(currency: Token): boolean {
return getShouldSkipPriceSource(currency, null, bffUnknownCurrencies, null, 0)
}

/**
* Fetches USD price for a given currency from coingecko or CowProtocol
* CoW Protocol Orderbook API is used as a fallback
* When Coingecko rate limit is hit, CowProtocol will be used for 1 minute
* Fetches USD price for a given currency from BFF, Defillama, or CowProtocol
* Tries sources in that order
*/
export function fetchCurrencyUsdPrice(
currency: Token,
getUsdcPrice: () => Promise<Fraction | null>,
): Promise<Fraction | null> {
const shouldSkipCoingecko = getShouldSkipCoingecko(currency)
const shouldSkipBff = getShouldSkipBff(currency)
const shouldSkipDefillama = getShouldSkipDefillama(currency)

if (coingeckoRateLimitHitTimestamp && !shouldSkipCoingecko) {
coingeckoRateLimitHitTimestamp = null
}

if (defillamaRateLimitHitTimestamp && !shouldSkipDefillama) {
defillamaRateLimitHitTimestamp = null
}
Expand All @@ -80,24 +68,22 @@ export function fetchCurrencyUsdPrice(
})
}

// No coingecko. Try Defillama, then cow
if (shouldSkipCoingecko) {
// Try BFF first, then fall back to Defillama, then CoW
if (!shouldSkipBff) {
return getBffUsdPrice(currency)
.catch(handleErrorFactory(currency, null, bffUnknownCurrencies, getDefillamaUsdPrice))
.catch(handleErrorFactory(currency, defillamaRateLimitHitTimestamp, defillamaUnknownCurrencies, getCowPrice))
}

// If BFF is skipped, try Defillama
if (!shouldSkipDefillama) {
return getDefillamaUsdPrice(currency).catch(
handleErrorFactory(currency, defillamaRateLimitHitTimestamp, defillamaUnknownCurrencies, getCowPrice),
)
}
// No Defillama. Try coingecko, then cow
if (shouldSkipDefillama) {
return getCoingeckoUsdPrice(currency).catch(
handleErrorFactory(currency, coingeckoRateLimitHitTimestamp, coingeckoUnknownCurrencies, getCowPrice),
)
}
// Both coingecko and defillama available. Try coingecko, then defillama, then cow
return getCoingeckoUsdPrice(currency)
.catch(
handleErrorFactory(currency, coingeckoRateLimitHitTimestamp, coingeckoUnknownCurrencies, getDefillamaUsdPrice),
)
.catch(handleErrorFactory(currency, defillamaRateLimitHitTimestamp, defillamaUnknownCurrencies, getCowPrice))

// If all other sources are skipped, use CoW as last resort
return getCowPrice(currency)
}

function handleErrorFactory(
Expand Down

0 comments on commit 2bd17bf

Please sign in to comment.