From 8f54ffc50af30478076798bed50c0d85c3e442c2 Mon Sep 17 00:00:00 2001 From: wayne Date: Wed, 15 Oct 2025 10:34:08 +0100 Subject: [PATCH 1/7] coins-api in SDK --- src/api2.ts | 1 + src/util/coinsApi.ts | 116 +++++++++++++++++++++++++++++++++++++++++ src/util/computeTVL.ts | 28 ++-------- 3 files changed, 122 insertions(+), 23 deletions(-) create mode 100644 src/util/coinsApi.ts diff --git a/src/api2.ts b/src/api2.ts index c6556888f..a83de894e 100644 --- a/src/api2.ts +++ b/src/api2.ts @@ -3,3 +3,4 @@ export * as eth from "./eth"; export * as erc20 from "./erc20"; export * as abi from "./abi/abi2"; export * as config from "./general"; +export * as coinsApi from "./util/coinsApi"; \ No newline at end of file diff --git a/src/util/coinsApi.ts b/src/util/coinsApi.ts new file mode 100644 index 000000000..d3c3e0cb0 --- /dev/null +++ b/src/util/coinsApi.ts @@ -0,0 +1,116 @@ +import axios from "axios"; +import { ENV_CONSTANTS } from "./env"; +import runInPromisePool from "./promisePool"; + +type CoinsApiData = { + decimals: number; + price: number; + symbol: string; + timestamp: number; + PK?: string; +}; + +type McapsApiData = { + mcap: number; + timestamp: number; +}; + +const coinsApiKey = ENV_CONSTANTS["COINS_API_KEY"]; +const bodySize = 2; // 100; + +function getBodies(readKeys: string[], timestamp: number | "now") { + const bodies: string[] = []; + for (let i = 0; i < readKeys.length; i += bodySize) { + const body = { + coins: readKeys.slice(i, i + bodySize), + } as any; + if (timestamp !== "now") body.timestamp = timestamp; + bodies.push(JSON.stringify(body)); + } + + return bodies; +} + +export async function getPrices( + readKeys: string[], + timestamp: number | "now" +): Promise<{ [address: string]: CoinsApiData }> { + if (!readKeys.length) return {}; + + const bodies = getBodies(readKeys, timestamp); + const tokenData: CoinsApiData[][] = []; + await runInPromisePool({ + items: bodies, + concurrency: 10, + processor: async (body: string) => { + const res = await axios.post( + `https://coins.llama.fi/prices?source=internal${ + coinsApiKey ? `?apikey=${coinsApiKey}` : "" + }`, + body, + { + headers: { "Content-Type": "application/json" }, + } + ); + + const data = (res.data.coins = Object.entries(res.data.coins).map( + ([PK, value]) => ({ + ...(value as CoinsApiData), + PK, + }) + )); + + tokenData.push(data); + }, + }); + + const aggregatedRes: { [address: string]: CoinsApiData } = {}; + const normalizedReadKeys = readKeys.map((k: string) => k.toLowerCase()); + tokenData.map((batch: CoinsApiData[]) => { + batch.map((a: CoinsApiData) => { + if (!a.PK) return; + const i = normalizedReadKeys.indexOf(a.PK.toLowerCase()); + aggregatedRes[readKeys[i]] = a; + }); + }); + + return aggregatedRes; +} + +export async function getMcaps( + readKeys: string[], + timestamp: number | "now" +): Promise<{ [address: string]: McapsApiData }> { + if (!readKeys.length) return {}; + + const bodies = getBodies(readKeys, timestamp); + const tokenData: {[key: string]: McapsApiData}[] = []; + await runInPromisePool({ + items: bodies, + concurrency: 10, + processor: async (body: string) => { + const res = await axios.post( + `https://coins.llama.fi/mcaps${ + coinsApiKey ? `?apikey=${coinsApiKey}` : "" + }`, + body, + { + headers: { "Content-Type": "application/json" }, + } + ); + tokenData.push(res.data as any); + }, + }); + + const aggregatedRes: { [address: string]: McapsApiData } = {}; + const normalizedReadKeys = readKeys.map((k: string) => k.toLowerCase()); + tokenData.map((batch: {[key: string]: McapsApiData}) => { + Object.keys(batch).map((a: string) => { + if (!batch[a].mcap) return; + const i = normalizedReadKeys.indexOf(a.toLowerCase()); + aggregatedRes[readKeys[i]] = batch[a]; + }); + }); + + return aggregatedRes; +} \ No newline at end of file diff --git a/src/util/computeTVL.ts b/src/util/computeTVL.ts index 61f89bab6..623e2d990 100644 --- a/src/util/computeTVL.ts +++ b/src/util/computeTVL.ts @@ -1,8 +1,6 @@ -import { sliceIntoChunks } from "."; import { sumSingleBalance } from "../generalUtil"; import { Balances } from "../types"; -import axios from "axios"; -import { ENV_CONSTANTS } from "./env"; +import { getPrices } from "./coinsApi"; type PricesObject = { // NOTE: the tokens queried might be case sensitive and can be in mixed case, but while storing them in the cache, we convert them to lowercase @@ -85,27 +83,11 @@ async function updatePriceCache(keys: string[], timestamp?: number) { const missingKeys = keys.filter(key => !pricesCache[key.toLowerCase()]) - const chunks = sliceIntoChunks(missingKeys, 100) - for (const chunk of chunks) { - const coins = await getPrices(chunk) - for (const [token, data] of Object.entries(coins)) { - pricesCache[token.toLowerCase()] = data - } - chunk.map(i => i.toLowerCase()).filter(i => !pricesCache[i]).forEach(i => pricesCache[i] = {}) + const coins = await getPrices(missingKeys, timestamp ?? "now") + for (const [token, data] of Object.entries(coins)) { + pricesCache[token.toLowerCase()] = data } - - async function getPrices(keys: string[]) { - if (!timestamp) { - const { coins } = await axios.get(`https://coins.llama.fi/prices/current/${keys.join(',')}`).then((res) => res.data) - return coins - } - - // fetch post with timestamp in body - const coinsApiKey = ENV_CONSTANTS['COINS_API_KEY'] - const { coins } = await axios.post("https://coins.llama.fi/prices?source=internal&apikey=" + coinsApiKey, { coins: keys, timestamp }).then((res) => res.data) - return coins - } - + missingKeys.map(i => i.toLowerCase()).filter(i => !pricesCache[i]).forEach(i => pricesCache[i] = {}) } function getPriceCache(timestamp?: number) { From cc9a9204eb4897769abbde9ad4d6d547ca362d89 Mon Sep 17 00:00:00 2001 From: wayne Date: Wed, 15 Oct 2025 13:10:12 +0100 Subject: [PATCH 2/7] rest call wrapper and key --- src/util/coinsApi.ts | 65 ++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/src/util/coinsApi.ts b/src/util/coinsApi.ts index d3c3e0cb0..50d44941e 100644 --- a/src/util/coinsApi.ts +++ b/src/util/coinsApi.ts @@ -15,7 +15,7 @@ type McapsApiData = { timestamp: number; }; -const coinsApiKey = ENV_CONSTANTS["COINS_API_KEY"]; +const coinsApiKey = process.env.COINS_KEY || ENV_CONSTANTS["COINS_API_KEY"]; const bodySize = 2; // 100; function getBodies(readKeys: string[], timestamp: number | "now") { @@ -31,6 +31,27 @@ function getBodies(readKeys: string[], timestamp: number | "now") { return bodies; } +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function restCallWrapper( + request: () => Promise, + retries: number = 8, + name: string = "-" +) { + while (retries > 0) { + try { + const res = await request(); + return res; + } catch { + await sleep(60000 + 40000 * Math.random()); + restCallWrapper(request, retries--, name); + } + } + throw new Error(`couldnt work ${name} call after retries!`); +} + export async function getPrices( readKeys: string[], timestamp: number | "now" @@ -43,14 +64,16 @@ export async function getPrices( items: bodies, concurrency: 10, processor: async (body: string) => { - const res = await axios.post( - `https://coins.llama.fi/prices?source=internal${ - coinsApiKey ? `?apikey=${coinsApiKey}` : "" - }`, - body, - { - headers: { "Content-Type": "application/json" }, - } + const res = await restCallWrapper(() => + axios.post( + `https://coins.llama.fi/prices?source=internal${ + coinsApiKey ? `?apikey=${coinsApiKey}` : "" + }`, + body, + { + headers: { "Content-Type": "application/json" }, + } + ) ); const data = (res.data.coins = Object.entries(res.data.coins).map( @@ -84,19 +107,21 @@ export async function getMcaps( if (!readKeys.length) return {}; const bodies = getBodies(readKeys, timestamp); - const tokenData: {[key: string]: McapsApiData}[] = []; + const tokenData: { [key: string]: McapsApiData }[] = []; await runInPromisePool({ items: bodies, concurrency: 10, processor: async (body: string) => { - const res = await axios.post( - `https://coins.llama.fi/mcaps${ - coinsApiKey ? `?apikey=${coinsApiKey}` : "" - }`, - body, - { - headers: { "Content-Type": "application/json" }, - } + const res = await restCallWrapper(() => + axios.post( + `https://coins.llama.fi/mcaps${ + coinsApiKey ? `?apikey=${coinsApiKey}` : "" + }`, + body, + { + headers: { "Content-Type": "application/json" }, + } + ) ); tokenData.push(res.data as any); }, @@ -104,7 +129,7 @@ export async function getMcaps( const aggregatedRes: { [address: string]: McapsApiData } = {}; const normalizedReadKeys = readKeys.map((k: string) => k.toLowerCase()); - tokenData.map((batch: {[key: string]: McapsApiData}) => { + tokenData.map((batch: { [key: string]: McapsApiData }) => { Object.keys(batch).map((a: string) => { if (!batch[a].mcap) return; const i = normalizedReadKeys.indexOf(a.toLowerCase()); @@ -113,4 +138,4 @@ export async function getMcaps( }); return aggregatedRes; -} \ No newline at end of file +} From 28bfd389a08e0a259e79d274802e3e2f3ef2de80 Mon Sep 17 00:00:00 2001 From: wayne Date: Thu, 16 Oct 2025 11:26:16 +0100 Subject: [PATCH 3/7] cached queries SDK --- src/util/cache.ts | 113 ++++++++++++++++++++++++++++++++++++++++++++++ src/util/index.ts | 2 +- 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/src/util/cache.ts b/src/util/cache.ts index 110cfd891..a52119c02 100644 --- a/src/util/cache.ts +++ b/src/util/cache.ts @@ -3,6 +3,9 @@ import { storeR2JSONString, getR2JSONString, } from "./r2"; import { constants, brotliCompress, brotliDecompress } from "zlib"; import { promisify } from 'util'; import { getEnvCacheFolder } from "./env"; +import { log } from ".."; +import axios from "axios"; +const Bucket = "tvl-adapter-cache"; const brotliOptions = { [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT, @@ -250,4 +253,114 @@ export async function writeExpiringJsonCache(file: string, data: any, { if (!expiryTimestamp) expiryTimestamp = Math.floor(Date.now() / 1e3) + expireAfter const options: WriteCacheOptions = { skipR2CacheWrite: true, skipCompression: true } await writeCache(file, { data, expiryTimestamp }, options) +} + +function getKey(project: string, chain: string) { + return `cache/${project}/${chain}.json` +} + +function getFileKey(project: string, chain: string) { + return `${Bucket}/${getKey(project, chain)}` +} + +function getLink(project: string, chain: string) { + return `https://${Bucket}.s3.eu-central-1.amazonaws.com/${getKey(project, chain)}` +} + +export async function getCache(project: string, chain: string) { + const Key = getKey(project, chain) + const fileKey = getFileKey(project, chain) + + try { + const json = await readCache(fileKey) + if (!json || Object.keys(json).length === 0) throw new Error('Invalid data') + return json + } catch (e) { + try { + const { data: json } = await axios.get(getLink(project, chain)) + await writeCache(fileKey, json) + return json + } catch (e) { + log('failed to fetch data from s3 bucket:', Key) + // log(e) + return {} + } + } +} + +async function _setCache(project: string, chain: string, cache: any) { + const Key = getKey(project, chain) + + try { + await writeCache(getFileKey(project, chain), cache) + } catch (e) { + log('failed to write data to s3 bucket: ', Key) + log(e) + } +} + +export const configCache: { [key: string]: any } = {} +let lastCacheReset = Date.now() +const cacheResetInterval = 1000 * 30 // 30 seconds + +function resetCache() { + if (Date.now() - lastCacheReset > cacheResetInterval) { + Object.keys(configCache).forEach(key => delete configCache[key]) + lastCacheReset = Date.now() + } +} + +export async function setCache(project: string, chain: string, json: any) { + if (!json || json?.error?.message) return; + const strData = typeof json === 'string' ? json : JSON.stringify(json) + let isValidData = strData.length > 42 + if (isValidData) // sometimes we get bad data/empty object, we dont overwrite cache with it + await _setCache(project, chain, json) +} + +export async function getConfig(project: string, endpoint: string, { fetcher } = { fetcher: () => Promise }) { + resetCache() + if (!project || (!endpoint && !fetcher)) throw new Error('Missing parameters') + const key = 'config-cache' + const cacheKey = getKey(key, project) + if (!configCache[cacheKey]) configCache[cacheKey] = _getConfig() + return configCache[cacheKey] + + async function _getConfig() { + try { + let json + if (endpoint) { + json = (await axios.get(endpoint)).data + } else { + json = await fetcher() + } + if (!json) throw new Error('Invalid data') + await setCache(key, project, json) + return json + } catch (e) { + // log(e) + log(project, 'tryng to fetch from cache, failed to fetch data from endpoint:', endpoint) + return getCache(key, project) + } + } +} + +export async function configPost(project: string, endpoint: string, data: any) { + if (!project || !endpoint) throw new Error('Missing parameters') + const key = 'config-cache' + const cacheKey = getKey(key, project) + if (!configCache[cacheKey]) configCache[cacheKey] = _configPost() + return configCache[cacheKey] + + async function _configPost() { + try { + const { data: json } = await axios.post(endpoint, data) + await setCache(key, project, json) + return json + } catch (e) { + // log(e) + log(project, 'tryng to fetch from cache, failed to fetch data from endpoint:', endpoint) + return getCache(key, project) + } + } } \ No newline at end of file diff --git a/src/util/index.ts b/src/util/index.ts index f2ec97996..bf1145321 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -8,7 +8,7 @@ export { getLatestBlock, getTimestamp, lookupBlock, } from "./blocks"; export { convertToBigInt } from "../generalUtil" import pLimit from 'p-limit'; import { getParallelGetLogsLimit } from "./env"; - +export { getConfig, getCache, setCache, configPost, configCache } from "./cache"; export const runInPromisePool = runInPromisePoolOrig export function sliceIntoChunks(arr: any[], chunkSize = 100) { From d5b05145595731c433633fbaa5d08b3c9fda848b Mon Sep 17 00:00:00 2001 From: wayne Date: Thu, 16 Oct 2025 12:19:02 +0100 Subject: [PATCH 4/7] Revert "cached queries SDK" This reverts commit 28bfd389a08e0a259e79d274802e3e2f3ef2de80. --- src/util/cache.ts | 113 ---------------------------------------------- src/util/index.ts | 2 +- 2 files changed, 1 insertion(+), 114 deletions(-) diff --git a/src/util/cache.ts b/src/util/cache.ts index a52119c02..110cfd891 100644 --- a/src/util/cache.ts +++ b/src/util/cache.ts @@ -3,9 +3,6 @@ import { storeR2JSONString, getR2JSONString, } from "./r2"; import { constants, brotliCompress, brotliDecompress } from "zlib"; import { promisify } from 'util'; import { getEnvCacheFolder } from "./env"; -import { log } from ".."; -import axios from "axios"; -const Bucket = "tvl-adapter-cache"; const brotliOptions = { [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT, @@ -253,114 +250,4 @@ export async function writeExpiringJsonCache(file: string, data: any, { if (!expiryTimestamp) expiryTimestamp = Math.floor(Date.now() / 1e3) + expireAfter const options: WriteCacheOptions = { skipR2CacheWrite: true, skipCompression: true } await writeCache(file, { data, expiryTimestamp }, options) -} - -function getKey(project: string, chain: string) { - return `cache/${project}/${chain}.json` -} - -function getFileKey(project: string, chain: string) { - return `${Bucket}/${getKey(project, chain)}` -} - -function getLink(project: string, chain: string) { - return `https://${Bucket}.s3.eu-central-1.amazonaws.com/${getKey(project, chain)}` -} - -export async function getCache(project: string, chain: string) { - const Key = getKey(project, chain) - const fileKey = getFileKey(project, chain) - - try { - const json = await readCache(fileKey) - if (!json || Object.keys(json).length === 0) throw new Error('Invalid data') - return json - } catch (e) { - try { - const { data: json } = await axios.get(getLink(project, chain)) - await writeCache(fileKey, json) - return json - } catch (e) { - log('failed to fetch data from s3 bucket:', Key) - // log(e) - return {} - } - } -} - -async function _setCache(project: string, chain: string, cache: any) { - const Key = getKey(project, chain) - - try { - await writeCache(getFileKey(project, chain), cache) - } catch (e) { - log('failed to write data to s3 bucket: ', Key) - log(e) - } -} - -export const configCache: { [key: string]: any } = {} -let lastCacheReset = Date.now() -const cacheResetInterval = 1000 * 30 // 30 seconds - -function resetCache() { - if (Date.now() - lastCacheReset > cacheResetInterval) { - Object.keys(configCache).forEach(key => delete configCache[key]) - lastCacheReset = Date.now() - } -} - -export async function setCache(project: string, chain: string, json: any) { - if (!json || json?.error?.message) return; - const strData = typeof json === 'string' ? json : JSON.stringify(json) - let isValidData = strData.length > 42 - if (isValidData) // sometimes we get bad data/empty object, we dont overwrite cache with it - await _setCache(project, chain, json) -} - -export async function getConfig(project: string, endpoint: string, { fetcher } = { fetcher: () => Promise }) { - resetCache() - if (!project || (!endpoint && !fetcher)) throw new Error('Missing parameters') - const key = 'config-cache' - const cacheKey = getKey(key, project) - if (!configCache[cacheKey]) configCache[cacheKey] = _getConfig() - return configCache[cacheKey] - - async function _getConfig() { - try { - let json - if (endpoint) { - json = (await axios.get(endpoint)).data - } else { - json = await fetcher() - } - if (!json) throw new Error('Invalid data') - await setCache(key, project, json) - return json - } catch (e) { - // log(e) - log(project, 'tryng to fetch from cache, failed to fetch data from endpoint:', endpoint) - return getCache(key, project) - } - } -} - -export async function configPost(project: string, endpoint: string, data: any) { - if (!project || !endpoint) throw new Error('Missing parameters') - const key = 'config-cache' - const cacheKey = getKey(key, project) - if (!configCache[cacheKey]) configCache[cacheKey] = _configPost() - return configCache[cacheKey] - - async function _configPost() { - try { - const { data: json } = await axios.post(endpoint, data) - await setCache(key, project, json) - return json - } catch (e) { - // log(e) - log(project, 'tryng to fetch from cache, failed to fetch data from endpoint:', endpoint) - return getCache(key, project) - } - } } \ No newline at end of file diff --git a/src/util/index.ts b/src/util/index.ts index bf1145321..f2ec97996 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -8,7 +8,7 @@ export { getLatestBlock, getTimestamp, lookupBlock, } from "./blocks"; export { convertToBigInt } from "../generalUtil" import pLimit from 'p-limit'; import { getParallelGetLogsLimit } from "./env"; -export { getConfig, getCache, setCache, configPost, configCache } from "./cache"; + export const runInPromisePool = runInPromisePoolOrig export function sliceIntoChunks(arr: any[], chunkSize = 100) { From af1ef7688af8a356cc5657fba39c0b069ed9328e Mon Sep 17 00:00:00 2001 From: wayne Date: Tue, 21 Oct 2025 16:08:28 +0100 Subject: [PATCH 5/7] cached --- src/util/coinsApi.ts | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/util/coinsApi.ts b/src/util/coinsApi.ts index 50d44941e..1ea3262d1 100644 --- a/src/util/coinsApi.ts +++ b/src/util/coinsApi.ts @@ -45,19 +45,39 @@ async function restCallWrapper( const res = await request(); return res; } catch { - await sleep(60000 + 40000 * Math.random()); + await sleep(5_000 + 10_000 * Math.random()); restCallWrapper(request, retries--, name); } } throw new Error(`couldnt work ${name} call after retries!`); } +const priceCache: { [PK: string]: any } = { + "coingecko:tether": { + price: 1, + symbol: "USDT", + timestamp: Math.floor(Date.now() / 1e3 + 3600), // an hour from script start time + }, +}; + export async function getPrices( readKeys: string[], timestamp: number | "now" ): Promise<{ [address: string]: CoinsApiData }> { if (!readKeys.length) return {}; + const aggregatedRes: { [address: string]: CoinsApiData } = {}; + + // read data from cache where possible + readKeys = readKeys.filter((PK: string) => { + if (timestamp !== "now") return true; + if (priceCache[PK]) { + aggregatedRes[PK] = { ...priceCache[PK], PK }; + return false; + } + return true; + }); + const bodies = getBodies(readKeys, timestamp); const tokenData: CoinsApiData[][] = []; await runInPromisePool({ @@ -87,7 +107,6 @@ export async function getPrices( }, }); - const aggregatedRes: { [address: string]: CoinsApiData } = {}; const normalizedReadKeys = readKeys.map((k: string) => k.toLowerCase()); tokenData.map((batch: CoinsApiData[]) => { batch.map((a: CoinsApiData) => { @@ -100,12 +119,26 @@ export async function getPrices( return aggregatedRes; } +const mcapCache: { [PK: string]: any } = {}; + export async function getMcaps( readKeys: string[], timestamp: number | "now" ): Promise<{ [address: string]: McapsApiData }> { if (!readKeys.length) return {}; + const aggregatedRes: { [address: string]: McapsApiData } = {}; + + // read data from cache where possible + readKeys = readKeys.filter((PK: string) => { + if (timestamp !== "now") return true; + if (mcapCache[PK]) { + aggregatedRes[PK] = { ...mcapCache[PK], PK }; + return false; + } + return true; + }); + const bodies = getBodies(readKeys, timestamp); const tokenData: { [key: string]: McapsApiData }[] = []; await runInPromisePool({ @@ -127,7 +160,6 @@ export async function getMcaps( }, }); - const aggregatedRes: { [address: string]: McapsApiData } = {}; const normalizedReadKeys = readKeys.map((k: string) => k.toLowerCase()); tokenData.map((batch: { [key: string]: McapsApiData }) => { Object.keys(batch).map((a: string) => { From 244e972a65a6d119b2e8b91463bf9c6167b48844 Mon Sep 17 00:00:00 2001 From: wayne Date: Tue, 21 Oct 2025 16:08:57 +0100 Subject: [PATCH 6/7] test --- src/index.test.ts | 4 ++++ src/util/coinsApi.test.ts | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/util/coinsApi.test.ts diff --git a/src/index.test.ts b/src/index.test.ts index 579710cfc..89997a437 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -56,6 +56,10 @@ test("imports", async () => { "fetchList": [Function], "multiCall": [Function], }, + "coinsApi": { + "getMcaps": [Function], + "getPrices": [Function], + }, "config": { "ETHER_ADDRESS": "0x0000000000000000000000000000000000000000", "TEN": 10n, diff --git a/src/util/coinsApi.test.ts b/src/util/coinsApi.test.ts new file mode 100644 index 000000000..cf9e6dec5 --- /dev/null +++ b/src/util/coinsApi.test.ts @@ -0,0 +1,18 @@ +import { getPrices, getMcaps } from "./coinsApi"; + +test("coinsApi - mcaps", async () => { + const res = await getMcaps(["coingecko:tether"], "now"); + expect(res["coingecko:tether"].mcap).toBeGreaterThan(100_000); + expect(res["coingecko:tether"].mcap).toBeLessThan(1_000_000_000_000); + expect(res["coingecko:tether"].timestamp).toBeGreaterThan(Math.floor(Date.now() / 1e3 - 3600)); + expect(Object.keys(res).length).toBe(1); +}) + +test("coinsApi - prices", async () => { + const res = await getPrices(["coingecko:tether", "ethereum:0xdac17f958d2ee523a2206206994597c13d831ec7", "solana:So11111111111111111111111111111111111111112"], "now"); + expect(res["coingecko:tether"].price).toBe(1); + expect(res["ethereum:0xdac17f958d2ee523a2206206994597c13d831ec7"].symbol).toBe("USDT"); + expect(res["ethereum:0xdac17f958d2ee523a2206206994597c13d831ec7"].decimals).toBe(6); + expect(Object.keys(res).length).toBe(3); + expect(res["solana:So11111111111111111111111111111111111111112"].timestamp).toBeGreaterThan(Math.floor(Date.now() / 1e3 - 3600)); +}) \ No newline at end of file From bc154a2830cf8ba7503fdfd8abad4687de444fac Mon Sep 17 00:00:00 2001 From: g1nt0ki <99907941+g1nt0ki@users.noreply.github.com> Date: Wed, 22 Oct 2025 15:34:41 +0200 Subject: [PATCH 7/7] rename coins module --- src/api2.ts | 3 +-- src/index.test.ts | 8 ++++---- src/index.ts | 1 + src/util/coinsApi.ts | 15 +++++++-------- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/api2.ts b/src/api2.ts index a83de894e..526cfccb9 100644 --- a/src/api2.ts +++ b/src/api2.ts @@ -2,5 +2,4 @@ export * as util from "./util"; export * as eth from "./eth"; export * as erc20 from "./erc20"; export * as abi from "./abi/abi2"; -export * as config from "./general"; -export * as coinsApi from "./util/coinsApi"; \ No newline at end of file +export * as config from "./general"; \ No newline at end of file diff --git a/src/index.test.ts b/src/index.test.ts index a203d76ca..124b90e6f 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -56,10 +56,6 @@ test("imports", async () => { "fetchList": [Function], "multiCall": [Function], }, - "coinsApi": { - "getMcaps": [Function], - "getPrices": [Function], - }, "config": { "ETHER_ADDRESS": "0x0000000000000000000000000000000000000000", "TEN": 10n, @@ -120,6 +116,10 @@ test("imports", async () => { "writeCache": [Function], "writeExpiringJsonCache": [Function], }, + "coins": { + "getMcaps": [Function], + "getPrices": [Function], + }, "elastic": { "addDebugLog": [Function], "addErrorLog": [Function], diff --git a/src/index.ts b/src/index.ts index 0fd8283b0..64bc6d049 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ export * as indexer from "./util/indexer"; export * as types from "./types"; export * as tron from "./abi/tron"; export * as erc20 from "./erc20"; +export * as coins from "./util/coinsApi"; export const log = debugLog export const logTable = debugTable diff --git a/src/util/coinsApi.ts b/src/util/coinsApi.ts index 1ea3262d1..ea571f6e7 100644 --- a/src/util/coinsApi.ts +++ b/src/util/coinsApi.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import { ENV_CONSTANTS } from "./env"; +import { getEnvValue } from "./env"; import runInPromisePool from "./promisePool"; type CoinsApiData = { @@ -15,7 +15,7 @@ type McapsApiData = { timestamp: number; }; -const coinsApiKey = process.env.COINS_KEY || ENV_CONSTANTS["COINS_API_KEY"]; +const coinsApiKey = getEnvValue("COINS_API_KEY") const bodySize = 2; // 100; function getBodies(readKeys: string[], timestamp: number | "now") { @@ -37,7 +37,7 @@ function sleep(ms: number) { async function restCallWrapper( request: () => Promise, - retries: number = 8, + retries: number = 3, name: string = "-" ) { while (retries > 0) { @@ -86,13 +86,13 @@ export async function getPrices( processor: async (body: string) => { const res = await restCallWrapper(() => axios.post( - `https://coins.llama.fi/prices?source=internal${ - coinsApiKey ? `?apikey=${coinsApiKey}` : "" + `https://coins.llama.fi/prices?source=internal${coinsApiKey ? `?apikey=${coinsApiKey}` : "" }`, body, { headers: { "Content-Type": "application/json" }, - } + params: { source: "internal", apikey: coinsApiKey }, + }, ) ); @@ -147,8 +147,7 @@ export async function getMcaps( processor: async (body: string) => { const res = await restCallWrapper(() => axios.post( - `https://coins.llama.fi/mcaps${ - coinsApiKey ? `?apikey=${coinsApiKey}` : "" + `https://coins.llama.fi/mcaps${coinsApiKey ? `?apikey=${coinsApiKey}` : "" }`, body, {