Skip to content

Commit

Permalink
feat: new CoinGecko fn getCurrentPriceById (#771)
Browse files Browse the repository at this point in the history
Co-authored-by: James Morris, MS <[email protected]>
  • Loading branch information
melisaguevara and james-a-morris authored Nov 15, 2024
1 parent 29af3e7 commit c2514a1
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 48 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
141 changes: 94 additions & 47 deletions src/coingecko/Coingecko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -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;
};
Expand All @@ -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,
});
Expand All @@ -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]);
}

Expand Down Expand Up @@ -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}`;

Expand Down

0 comments on commit c2514a1

Please sign in to comment.