-
Notifications
You must be signed in to change notification settings - Fork 54
[USD Price] Add coingecko api and hooks #1356
Changes from 5 commits
5ec18b5
b07b95a
6778b5c
b9178af
1034fef
70565af
37c2604
0cbcffa
4e6b850
fe1e867
25176af
59da08b
835420c
8381aa0
d0c2b22
30c8219
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
|
||
|
@@ -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 | ||
|
||
|
@@ -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) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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) { | ||
W3stside marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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, | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
higher or highest?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whatever