@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
136 lines • 5.36 kB
JavaScript
import { assert, objKeys, rootLogger, sleep, } from '@hyperlane-xyz/utils';
const COINGECKO_PRICE_API = 'https://api.coingecko.com/api/v3/simple/price';
const COINGECKO_COIN_API = 'https://api.coingecko.com/api/v3/coins';
class TokenPriceCache {
cache;
freshSeconds;
evictionSeconds;
constructor(freshSeconds = 60, evictionSeconds = 3 * 60 * 60) {
this.cache = new Map();
this.freshSeconds = freshSeconds;
this.evictionSeconds = evictionSeconds;
}
put(id, price) {
const now = new Date();
this.cache.set(id, { timestamp: now, price });
}
isFresh(id) {
const entry = this.cache.get(id);
if (!entry)
return false;
const expiryTime = new Date(entry.timestamp.getTime() + 1000 * this.freshSeconds);
const now = new Date();
return now < expiryTime;
}
fetch(id) {
const entry = this.cache.get(id);
if (!entry) {
throw new Error(`no entry found for ${id} in token price cache`);
}
const evictionTime = new Date(entry.timestamp.getTime() + 1000 * this.evictionSeconds);
const now = new Date();
if (now > evictionTime) {
throw new Error(`evicted entry found for ${id} in token price cache`);
}
return entry.price;
}
}
export class CoinGeckoTokenPriceGetter {
cache;
apiKey;
sleepMsBetweenRequests;
metadata;
constructor({ chainMetadata, apiKey, expirySeconds, sleepMsBetweenRequests = 5000, }) {
this.apiKey = apiKey;
this.cache = new TokenPriceCache(expirySeconds);
this.metadata = chainMetadata;
this.sleepMsBetweenRequests = sleepMsBetweenRequests;
}
async getTokenPrice(chain, currency = 'usd') {
const [price] = await this.getTokenPrices([chain], currency);
return price;
}
async getAllTokenPrices(currency = 'usd') {
const chains = objKeys(this.metadata);
const prices = await this.getTokenPrices(chains, currency);
return chains.reduce((agg, chain, i) => ({ ...agg, [chain]: prices[i] }), {});
}
async getTokenExchangeRate(base, quote, currency = 'usd') {
const [basePrice, quotePrice] = await this.getTokenPrices([base, quote], currency);
return basePrice / quotePrice;
}
async getTokenPrices(chains, currency = 'usd') {
const isMainnet = chains.map((c) => !this.metadata[c].isTestnet);
const allMainnets = isMainnet.every((v) => v === true);
const allTestnets = isMainnet.every((v) => v === false);
if (allTestnets) {
// Testnet tokens are all artificially priced at 1.0 USD.
return chains.map(() => 1);
}
if (!allMainnets) {
throw new Error('Cannot mix testnets and mainnets when fetching token prices');
}
const ids = chains.map((chain) => this.metadata[chain].gasCurrencyCoinGeckoId || chain);
await this.getTokenPriceByIds(ids, currency);
return chains.map((chain) => this.cache.fetch(this.metadata[chain].gasCurrencyCoinGeckoId || chain));
}
async getTokenPriceByIds(ids, currency = 'usd') {
const toQuery = ids.filter((id) => !this.cache.isFresh(id));
await sleep(this.sleepMsBetweenRequests);
if (toQuery.length > 0) {
try {
const prices = await this.fetchPriceData(toQuery, currency);
prices.forEach((price, i) => this.cache.put(toQuery[i], price));
}
catch (err) {
rootLogger.warn(err, 'Failed to fetch token prices');
return undefined;
}
}
return ids.map((id) => this.cache.fetch(id));
}
async fetchPriceDataByContractAddress(chain, contractAddress) {
const tokenPrice = await this.get(`${COINGECKO_COIN_API}/${chain}/contract/${contractAddress}`);
const price = tokenPrice?.market_data?.current_price?.usd;
assert(price, `USD price not found for token at address "${contractAddress}" and chain ${chain}`);
return price;
}
async fetchPriceData(ids, currency) {
const tokenIds = ids.join(',');
const idPrices = await this.get(`${COINGECKO_PRICE_API}?ids=${tokenIds}&vs_currencies=${currency}`);
return ids.map((id) => {
const price = idPrices[id]?.[currency];
if (!price)
throw new Error(`No price found for ${id}`);
return Number(price);
});
}
async get(endpoint) {
const url = new URL(endpoint);
if (this.apiKey) {
url.searchParams.append('x-cg-pro-api-key', this.apiKey);
}
const resp = await fetch(url);
let idPrices = {};
let jsonError;
try {
idPrices = await resp.json();
}
catch (err) {
jsonError = err;
idPrices = {};
}
if (!resp.ok) {
rootLogger.warn({
status: resp.status,
statusText: resp.statusText,
url,
}, `Failed to fetch token prices: ${idPrices?.error}`);
}
if (jsonError) {
rootLogger.warn(jsonError, 'Failed to parse token prices');
}
return idPrices;
}
}
//# sourceMappingURL=token-prices.js.map