From c2514a1e8f57da97700fef256024d4a89cfb93ac Mon Sep 17 00:00:00 2001 From: Melisa Guevara Date: Fri, 15 Nov 2024 16:43:01 -0500 Subject: [PATCH] feat: new CoinGecko fn getCurrentPriceById (#771) Co-authored-by: James Morris, MS <96435344+james-a-morris@users.noreply.github.com> --- package.json | 2 +- src/coingecko/Coingecko.ts | 141 ++++++++++++++++++++++++------------- 2 files changed, 95 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 12ccfc1a7..6ccaa0440 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "3.2.13", + "version": "3.2.14", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/coingecko/Coingecko.ts b/src/coingecko/Coingecko.ts index 7ec3806b8..17187b369 100644 --- a/src/coingecko/Coingecko.ts +++ b/src/coingecko/Coingecko.ts @@ -29,6 +29,11 @@ type PriceCache = { }; }; +type CGTokenPrice = { + [currency: string]: number; + last_updated_at: number; +}; + // Singleton Coingecko class. export class Coingecko { private static instance: Coingecko | undefined; @@ -118,38 +123,53 @@ export class Coingecko { getContractDetails(contract_address: string, platform_id = "ethereum") { return this.call(`coins/${platform_id}/contract/${contract_address.toLowerCase()}`); } + async getCurrentPriceByContract( - contract_address: string, + contractAddress: string, currency = "usd", platform_id = "ethereum" ): Promise<[string, number]> { const priceCache: { [addr: string]: CoinGeckoPrice } = this.getPriceCache(currency, platform_id); - const now: number = msToS(Date.now()); - let tokenPrice: CoinGeckoPrice | undefined = priceCache[contract_address]; + let tokenPrice = this.getCachedAddressPrice(contractAddress, currency, platform_id); + if (tokenPrice === undefined) { + await this.getContractPrices([contractAddress], currency, platform_id); + tokenPrice = priceCache[contractAddress]; + } - if (tokenPrice === undefined || tokenPrice.timestamp + this.maxPriceAge <= now) { - if (this.maxPriceAge > 0) { + assert(tokenPrice !== undefined); + return [tokenPrice.timestamp.toString(), tokenPrice.price]; + } + + async getCurrentPriceById( + contractAddress: string, + currency = "usd", + platform_id = "ethereum" + ): Promise<[string, number]> { + const priceCache: { [addr: string]: CoinGeckoPrice } = this.getPriceCache(currency, platform_id); + let tokenPrice = this.getCachedAddressPrice(contractAddress, currency, platform_id); + if (tokenPrice === undefined) { + const coingeckoId = getCoingeckoTokenIdByAddress(contractAddress); + // Build the path for the Coingecko API request + const result = await this.call( + `simple/price?ids=${coingeckoId}&vs_currencies=${currency}&include_last_updated_at=true` + ); + const cgPrice = result?.[coingeckoId]; + if (cgPrice === undefined || !cgPrice?.[currency]) { + const errMsg = `No price found for ${coingeckoId}`; this.logger.debug({ - at: "Coingecko#getCurrentPriceByContract", - message: `Cache miss on ${platform_id}/${currency} for ${contract_address}`, - maxPriceAge: this.maxPriceAge, - tokenPrice: tokenPrice, + at: "Coingecko#getCurrentPriceById", + message: errMsg, }); + throw new Error(errMsg); + } else { + this.updatePriceCache(cgPrice, contractAddress, currency, platform_id); } - - await this.getContractPrices([contract_address], currency, platform_id); - tokenPrice = priceCache[contract_address]; - } else { - this.logger.debug({ - at: "Coingecko#getCurrentPriceByContract", - message: `Cache hit on token ${contract_address} (age ${now - tokenPrice.timestamp} S).`, - price: tokenPrice, - }); } - + tokenPrice = priceCache[contractAddress]; assert(tokenPrice !== undefined); return [tokenPrice.timestamp.toString(), tokenPrice.price]; } + // Return an array of spot prices for an array of collateral addresses in one async call. Note we might in future // This was adapted from packages/merkle-distributor/kpi-options-helpers/calculate-uma-tvl.ts async getContractPrices( @@ -176,10 +196,6 @@ export class Coingecko { }); // annoying, but have to type this to iterate over entries - type CGTokenPrice = { - [currency: string]: number; - last_updated_at: number; - }; type Result = { [address: string]: CGTokenPrice; }; @@ -195,7 +211,7 @@ export class Coingecko { } catch (err) { const errMsg = `Failed to retrieve ${platform_id}/${currency} prices (${err})`; this.logger.debug({ - at: "Coingecko#getCurrentPriceByContract", + at: "Coingecko#getContractPrices", message: errMsg, tokens: contract_addresses, }); @@ -204,38 +220,17 @@ export class Coingecko { // Note: contract_addresses is a reliable reference for the price lookup. // priceCache might have been updated subsequently by concurrent price requests. - const updated: string[] = []; contract_addresses.forEach((addr) => { const cgPrice: CGTokenPrice | undefined = result[addr.toLowerCase()]; - if (cgPrice === undefined) { this.logger.debug({ at: "Coingecko#getContractPrices", message: `Token ${addr} not included in CoinGecko response.`, }); - } else if (cgPrice.last_updated_at > priceCache[addr].timestamp) { - priceCache[addr] = { - address: addr, - price: cgPrice[currency], - timestamp: cgPrice.last_updated_at, - }; - updated.push(addr); - } else if (cgPrice.last_updated_at === priceCache[addr].timestamp) { - this.logger.debug({ - at: "Coingecko#getContractPrices", - message: `No new price available for token ${addr}.`, - token: cgPrice, - }); + } else { + this.updatePriceCache(cgPrice, addr, currency, platform_id); } }); - - if (updated.length > 0) { - this.logger.debug({ - at: "Coingecko#updatePriceCache", - message: `Updated ${platform_id}/${currency} token price cache.`, - tokens: updated, - }); - } return addresses.map((addr: string) => priceCache[addr]); } @@ -275,6 +270,58 @@ export class Coingecko { return this.prices[platform_id][currency]; } + protected getCachedAddressPrice( + contractAddress: string, + currency: string, + platform_id: string + ): CoinGeckoPrice | undefined { + const priceCache = this.getPriceCache(currency, platform_id); + const now: number = msToS(Date.now()); + const tokenPrice: CoinGeckoPrice | undefined = priceCache[contractAddress]; + if (tokenPrice === undefined || tokenPrice.timestamp + this.maxPriceAge <= now) { + if (this.maxPriceAge > 0) { + this.logger.debug({ + at: "Coingecko#getCachedAddressPrice", + message: `Cache miss on ${platform_id}/${currency} for ${contractAddress}`, + maxPriceAge: this.maxPriceAge, + tokenPrice: tokenPrice, + }); + } + return undefined; + } else { + this.logger.debug({ + at: "Coingecko#getCachedAddressPrice", + message: `Cache hit on token ${contractAddress} (age ${now - tokenPrice.timestamp} S).`, + price: tokenPrice, + }); + return tokenPrice; + } + } + + protected updatePriceCache(cgPrice: CGTokenPrice, contractAddress: string, currency: string, platform_id: string) { + const priceCache = this.getPriceCache(currency, platform_id); + if (priceCache[contractAddress] === undefined) { + priceCache[contractAddress] = { address: contractAddress, price: 0, timestamp: 0 }; + } + if (cgPrice.last_updated_at > priceCache[contractAddress].timestamp) { + priceCache[contractAddress] = { + address: contractAddress, + price: cgPrice[currency], + timestamp: cgPrice.last_updated_at, + }; + this.logger.debug({ + at: "Coingecko#updatePriceCache", + message: `Updated ${platform_id}/${currency}/${contractAddress} token price cache.`, + }); + } else { + this.logger.debug({ + at: "Coingecko#updatePriceCache", + message: `No new price available for token ${contractAddress}.`, + token: cgPrice, + }); + } + } + private async _callBasic(path: string, timeout?: number) { const url = `${this.host}/${path}`;