- 
                Notifications
    You must be signed in to change notification settings 
- Fork 129
Coins Api #187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Coins Api #187
Changes from all commits
8f54ffc
              cc9a920
              28bfd38
              d5b0514
              af1ef76
              244e972
              df94a09
              bc154a2
              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,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)); | ||
| }) | 
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| import axios from "axios"; | ||
| import { getEnvValue } 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 = getEnvValue("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; | ||
| } | ||
|  | ||
| function sleep(ms: number) { | ||
| 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 we already have defined sleep somewhere else? 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 cant find a timeout/sleep anywhere else in the codebase 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. so, there was sleep already here | ||
| return new Promise((resolve) => setTimeout(resolve, ms)); | ||
| } | ||
|  | ||
| async function restCallWrapper( | ||
| request: () => Promise<any>, | ||
| retries: number = 3, | ||
| name: string = "-" | ||
| ) { | ||
| while (retries > 0) { | ||
| try { | ||
| const res = await request(); | ||
| return res; | ||
| } catch { | ||
| await sleep(5_000 + 10_000 * Math.random()); | ||
| restCallWrapper(request, retries--, name); | ||
| 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. missing  | ||
| } | ||
| } | ||
| 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 }; | ||
| 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. you read from the cache, but there is no code that sets to the cache | ||
| return false; | ||
| } | ||
| return true; | ||
| }); | ||
|  | ||
| const bodies = getBodies(readKeys, timestamp); | ||
| const tokenData: CoinsApiData[][] = []; | ||
| await runInPromisePool({ | ||
| 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. lot of code duplication between /prices and /mcaps call | ||
| items: bodies, | ||
| concurrency: 10, | ||
| processor: async (body: string) => { | ||
| const res = await restCallWrapper(() => | ||
| axios.post( | ||
| `https://coins.llama.fi/prices?source=internal${coinsApiKey ? `?apikey=${coinsApiKey}` : "" | ||
| }`, | ||
| body, | ||
| { | ||
| headers: { "Content-Type": "application/json" }, | ||
| params: { source: "internal", apikey: coinsApiKey }, | ||
| }, | ||
| ) | ||
| ); | ||
|  | ||
| const data = (res.data.coins = Object.entries(res.data.coins).map( | ||
| ([PK, value]) => ({ | ||
| ...(value as CoinsApiData), | ||
| PK, | ||
| }) | ||
| )); | ||
|  | ||
| tokenData.push(data); | ||
| }, | ||
| }); | ||
|  | ||
| 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; | ||
| } | ||
|  | ||
| 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({ | ||
| items: bodies, | ||
| concurrency: 10, | ||
| processor: async (body: string) => { | ||
| 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); | ||
| }, | ||
| }); | ||
|  | ||
| 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; | ||
| } | ||
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.
this was test code should have reverted back to 100