Skip to content
This repository was archived by the owner on Jun 24, 2022. It is now read-only.

[USD Price] Add coingecko api and hooks #1356

Merged
merged 16 commits into from
Sep 7, 2021
98 changes: 98 additions & 0 deletions src/custom/api/coingecko/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { SupportedChainId as ChainId } from 'constants/chains'
import { PriceInformation } from 'utils/price'

function getApiUrl(): string {
// it's all the same base url
return 'https://api.coingecko.com/api'
}

// https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=0x33e18a092a93ff21ad04746c7da12e35d34dc7c4&vs_currencies=usd
// Defaults
const API_NAME = 'Coingecko'
const API_BASE_URL = getApiUrl()
const API_VERSION = 'v3'
const DEFAULT_HEADERS = {
'Content-Type': 'application/json',
}

function _getApiBaseUrl(chainId: ChainId): string {
const baseUrl = API_BASE_URL

if (!baseUrl) {
throw new Error(`Unsupported Network. The ${API_NAME} API is not deployed in the Network ${chainId}`)
} else {
return baseUrl + '/' + API_VERSION
}
}

function _getCoinGeckoAssetPlatform(chainId: ChainId) {
switch (chainId) {
// Use of asset platforms - supports ethereum and xdai
// https://api.coingecko.com/api/v3/asset_platforms
case ChainId.MAINNET:
return 'ethereum'
case ChainId.XDAI:
return 'xdai'
default:
return null
}
}

function _fetch(chainId: ChainId, url: string, method: 'GET' | 'POST' | 'DELETE', data?: any): Promise<Response> {
const baseUrl = _getApiBaseUrl(chainId)
return fetch(baseUrl + url, {
headers: DEFAULT_HEADERS,
method,
body: data !== undefined ? JSON.stringify(data) : data,
})
}

// TODO: consider making these _get/_delete/_post etc reusable across apis
function _get(chainId: ChainId, url: string): Promise<Response> {
return _fetch(chainId, url, 'GET')
}

export interface CoinGeckoUsdPriceParams {
chainId: ChainId
tokenAddress: string
}

interface CoinGeckoUsdQuote {
[address: string]: {
usd: number
}
}

export async function getUSDPriceQuote(params: CoinGeckoUsdPriceParams): Promise<CoinGeckoUsdQuote | null> {
const { chainId, tokenAddress } = params
// ethereum/xdai (chains)
const assetPlatform = _getCoinGeckoAssetPlatform(chainId)
if (assetPlatform == null) {
// Unsupported
return null
}

console.log(`[api:${API_NAME}] Get USD price from ${API_NAME}`, params)

const response = await _get(
chainId,
`/simple/token_price/${assetPlatform}?contract_addresses=${tokenAddress}&vs_currencies=usd`
).catch((error) => {
console.error(`Error getting ${API_NAME} USD price quote:`, error)
throw new Error(error)
})

return response.json()
}

export function toPriceInformation(priceRaw: CoinGeckoUsdQuote | null): PriceInformation | null {
// We only receive/want the first key/value pair in the return object
const token = priceRaw ? Object.keys(priceRaw)[0] : null

if (!token || !priceRaw?.[token].usd) {
return null
}

const { usd } = priceRaw[token]
return { amount: usd.toString(), token }
}
5 changes: 3 additions & 2 deletions src/custom/components/swap/FeeInformationTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'
import TradeGp from 'state/swap/TradeGp'
import QuestionHelper from 'components/QuestionHelper'
import styled from 'styled-components'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { useHigherUSDValue /* , useUSDCValue */ } from 'hooks/useUSDCPrice'
import { formatSmart } from 'utils/format'
import useTheme from 'hooks/useTheme'
import { FIAT_PRECISION } from 'constants/index'
Expand Down Expand Up @@ -76,7 +76,8 @@ export default function FeeInformationTooltip(props: FeeInformationTooltipProps)
const { trade, label, amountBeforeFees, amountAfterFees, feeAmount, type, showHelper, showFiat = false } = props

const theme = useTheme()
const fiatValue = useUSDCValue(type === 'From' ? trade?.inputAmount : trade?.outputAmount)
// const fiatValue = useUSDCValue(type === 'From' ? trade?.inputAmount : trade?.outputAmount)
const fiatValue = useHigherUSDValue(type === 'From' ? trade?.inputAmount : trade?.outputAmount)

const [symbol, fullFeeAmount] = useMemo(() => {
const amount = trade?.[type === 'From' ? 'inputAmount' : 'outputAmount']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React, { useState, useContext, useMemo } from 'react'
import { ArrowDown, AlertTriangle } from 'react-feather'
import { Text } from 'rebass'
import styled, { ThemeContext } from 'styled-components'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { useHigherUSDValue /* , useUSDCValue */ } from 'hooks/useUSDCPrice'
import { TYPE } from 'theme'
import { ButtonPrimary } from 'components/Button'
import { isAddress, shortenAddress } from 'utils'
Expand Down Expand Up @@ -89,8 +89,10 @@ SwapModalHeaderProps) {
const [showInverted, setShowInverted] = useState<boolean>(false)

// show fiatValue for unadjusted trade amounts!
const fiatValueInput = useUSDCValue(trade.inputAmountWithoutFee)
const fiatValueOutput = useUSDCValue(trade.outputAmountWithoutFee)
// const fiatValueInput = useUSDCValue(trade.inputAmountWithoutFee)
// const fiatValueOutput = useUSDCValue(trade.outputAmountWithoutFee)
const fiatValueInput = useHigherUSDValue(trade.inputAmountWithoutFee)
Copy link
Contributor

Choose a reason for hiding this comment

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

higher or highest?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

well technically "highest" would mean just that, the "highest" whereas "higher" implies of the choices given, which is "highest"

but yes it's very nitpicky here. happy to change it to whatever we like best @alfetopito @Anxo @nenadV91

Copy link
Contributor

Choose a reason for hiding this comment

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

whatever

const fiatValueOutput = useHigherUSDValue(trade.outputAmountWithoutFee)

const [slippageIn, slippageOut] = useMemo(
() => [slippageAdjustedAmounts[Field.INPUT], slippageAdjustedAmounts[Field.OUTPUT]],
Expand Down
5 changes: 3 additions & 2 deletions src/custom/components/swap/TradePrice/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react'
import TradePriceMod, { TradePriceProps } from './TradePriceMod'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { useHigherUSDValue /* useUSDCValue */ } from 'hooks/useUSDCPrice'
import { formatSmart } from 'utils/format'
import { tryParseAmount } from 'state/swap/hooks'
import { FIAT_PRECISION } from 'constants/index'
Expand All @@ -17,7 +17,8 @@ export default function TradePrice(props: Omit<TradePriceProps, 'fiatValue'>) {
: tryParseAmount(price.toFixed(price.quoteCurrency.decimals), price.quoteCurrency),
[price, showInverted]
)
const amount = useUSDCValue(priceSide)
// const amount = useUSDCValue(priceSide)
const amount = useHigherUSDValue(priceSide)
const fiatValueFormatted = formatSmart(amount, FIAT_PRECISION)

return <TradePriceMod {...props} fiatValue={fiatValueFormatted} />
Expand Down
5 changes: 3 additions & 2 deletions src/custom/components/swap/TradeSummary/RowFee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { RowBetween, RowFixed } from 'components/Row'
import { MouseoverTooltipContent } from 'components/Tooltip'
import { AMOUNT_PRECISION, FIAT_PRECISION } from 'constants/index'
import { LightGreyText } from 'pages/Swap'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { useHigherUSDValue /* , useUSDCValue */ } from 'hooks/useUSDCPrice'

export const GASLESS_FEE_TOOLTIP_MSG =
'On CowSwap you sign your order (hence no gas costs!). The fees are covering your gas costs already.'
Expand Down Expand Up @@ -61,7 +61,8 @@ export function RowFee({
// trades are null when there is a fee quote error e.g
// so we can take both
const feeAmount = trade?.fee.feeAsCurrency || fee
const feeFiatValue = useUSDCValue(feeAmount)
// const feeFiatValue = useUSDCValue(feeAmount)
const feeFiatValue = useHigherUSDValue(feeAmount)
const feeFiatDisplay = `(≈$${formatSmart(feeFiatValue, FIAT_PRECISION)})`

const displayFee = realizedFee || fee
Expand Down
121 changes: 115 additions & 6 deletions src/custom/hooks/useUSDCPrice/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import { stringToCurrency } from 'state/swap/extension'
import { SupportedChainId } from 'constants/chains'
import { USDC_XDAI } from 'utils/xdai/constants'
import { OrderKind } from 'state/orders/actions'
import { CoinGeckoUsdPriceParams, getUSDPriceQuote, toPriceInformation } from 'api/coingecko'
import { tryParseAmount } from 'state/swap/hooks'
import { DEFAULT_NETWORK_FOR_LISTS } from 'constants/lists'
import { useToken } from 'hooks/Tokens'
import { currencyId } from 'utils/currencyId'
import { parseUnits } from 'ethers/lib/utils'

export * from '@src/hooks/useUSDCPrice'

Expand Down Expand Up @@ -83,13 +89,14 @@ export default function useUSDCPrice(currency?: Currency) {
return { price: bestUsdPrice, error }
}

/**
* Returns the price in USDC of the input currency from price APIs
* @param currencyAmount currency to compute the USDC price of
*/
export function useUSDCValue(currencyAmount?: CurrencyAmount<Currency>) {
const { price, error } = useUSDCPrice(currencyAmount?.currency)
interface GetPriceQuoteParams {
currencyAmount?: CurrencyAmount<Currency>
error: Error | null
price: Price<Token, Currency> | null
}

// common logic for returning price quotes
function useGetPriceQuote({ price, error, currencyAmount }: GetPriceQuoteParams) {
return useMemo(() => {
if (!price || error || !currencyAmount) return null

Expand All @@ -100,3 +107,105 @@ export function useUSDCValue(currencyAmount?: CurrencyAmount<Currency>) {
}
}, [currencyAmount, error, price])
}

/**
* Returns the price in USDC of the input currency from price APIs
* @param currencyAmount currency to compute the USDC price of
*/
export function useUSDCValue(currencyAmount?: CurrencyAmount<Currency>) {
const usdcPrice = useUSDCPrice(currencyAmount?.currency)

return useGetPriceQuote({ ...usdcPrice, currencyAmount })
}

export function useCoingeckoUsdPrice({ tokenAddress }: Pick<CoinGeckoUsdPriceParams, 'tokenAddress'>) {
// default to MAINNET (if disconnected e.g)
const { chainId = DEFAULT_NETWORK_FOR_LISTS } = useActiveWeb3React()
const [price, setPrice] = useState<Price<Token, Currency> | null>(null)
const [error, setError] = useState<Error | null>(null)

const currency = useToken(tokenAddress)

useEffect(() => {
getUSDPriceQuote({
chainId,
tokenAddress,
})
.then(toPriceInformation)
.then((response) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

just curious, why do you expose the original data instead of just the, priceInformation directly?

Copy link
Contributor

Choose a reason for hiding this comment

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

This response i think is to generic name. maybe priceInfo?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i think it was because in the other APIs it's done similarly ( i think ) - and for paraswap there are other useful props that could posisbly want to be used elsewhere before the priceInformation schema mapping. In the case of coingecko there isn't any useful info so i could change the fn directly 👍

if (!currency || !response?.amount) return

const { amount } = response
// api returns base units, we want atoms and CurrencyAmount
const usdParsed = tryParseAmount(amount.toString(), currency)
// parse failure is unlikely - type safe
if (!usdParsed) return

// create a new Price object
const usdPrice = new Price({
quoteAmount: CurrencyAmount.fromRawAmount(
usdParsed.currency,
// we use 1 as the denominator
parseUnits('1', usdParsed.currency.decimals).toString()
),
baseAmount: usdParsed,
})

console.debug(
'[useCoingeckoUsdPrice] Best Coingecko USD price amount',
usdPrice.toSignificant(12),
usdPrice.invert().toSignificant(12)
)

setPrice(usdPrice)
})
.catch((error) => {
console.error(
'[useUSDCPrice::useCoingeckoUsdPrice]::Error getting USD price from Coingecko for token',
tokenAddress,
error
)
return batchedUpdate(() => {
setError(new Error(error))
setPrice(null)
})
})
}, [chainId, currency, tokenAddress])

return { price, error }
}

type CoinGeckoUsdValueParams = Pick<CoinGeckoUsdPriceParams, 'tokenAddress'> & {
currencyAmount?: CurrencyAmount<Currency>
}

export function useCoingeckoUSDValue(params: CoinGeckoUsdValueParams) {
const { currencyAmount } = params
const coingeckoUsdPrice = useCoingeckoUsdPrice(params)

return useGetPriceQuote({ ...coingeckoUsdPrice, currencyAmount })
}

export function useHigherUSDValue(currencyAmount: CurrencyAmount<Currency> | undefined) {
const usdcValue = useUSDCValue(currencyAmount)
const coingeckoUsdPrice = useCoingeckoUSDValue({
tokenAddress: currencyAmount ? currencyId(currencyAmount.currency) : '',
currencyAmount,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have any kind of timeout logic? probably not for this PR, also less important than other things, but maybe good to track it.


return useMemo(() => {
// USDC PRICE UNAVAILABLE
if (!usdcValue && coingeckoUsdPrice) {
return coingeckoUsdPrice
// COINGECKO PRICE UNAVAILABLE
} else if (usdcValue && !coingeckoUsdPrice) {
return usdcValue
// BOTH AVAILABLE
} else if (usdcValue && coingeckoUsdPrice) {
// take the greater of the 2 values
return usdcValue.greaterThan(coingeckoUsdPrice) ? usdcValue : coingeckoUsdPrice
} else {
return null
}
}, [usdcValue, coingeckoUsdPrice])
}
8 changes: 5 additions & 3 deletions src/custom/pages/Swap/SwapMod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import useIsArgentWallet from 'hooks/useIsArgentWallet'
import { useIsSwapUnsupported } from 'hooks/useIsSwapUnsupported'
import { useSwapCallback } from 'hooks/useSwapCallback'
import { /* useToggledVersion, */ Version } from 'hooks/useToggledVersion'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { useHigherUSDValue /* , useUSDCValue */ } from 'hooks/useUSDCPrice'
import useWrapCallback, { WrapType } from 'hooks/useWrapCallback'
import { useActiveWeb3React } from 'hooks/web3'
import { useWalletModalToggle /*, useToggleSettingsMenu */ } from 'state/application/hooks'
Expand Down Expand Up @@ -228,8 +228,10 @@ export default function Swap({
[independentField, parsedAmount, showWrap, trade]
)

const fiatValueInput = useUSDCValue(parsedAmounts[Field.INPUT])
const fiatValueOutput = useUSDCValue(parsedAmounts[Field.OUTPUT])
// const fiatValueInput = useUSDCValue(parsedAmounts[Field.INPUT])
// const fiatValueOutput = useUSDCValue(parsedAmounts[Field.OUTPUT])
const fiatValueInput = useHigherUSDValue(parsedAmounts[Field.INPUT])
const fiatValueOutput = useHigherUSDValue(parsedAmounts[Field.OUTPUT])
const priceImpact = computeFiatValuePriceImpact(fiatValueInput, fiatValueOutput)

const { onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient } = useSwapActionHandlers()
Expand Down