UNPKG

@kinetiq-research/markets-assets

Version:

Asset metadata (descriptions, oracle references) for Kinetiq markets

128 lines (105 loc) • 3.23 kB
#!/usr/bin/env tsx // Downloads SVG icons for every hip-3 asset listed in the picker, saving // to public/icons/<ASSET>.svg. Skips assets that already have a local // icon. import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import { isOurDex, SUPPORTED_DEXES, } from '@kinetiq-research/markets-shared/dexes' const HL_INFO = 'https://api.hyperliquid.xyz/info' const HL_COINS = 'https://app.hyperliquid.xyz/coins' const scriptDir = path.dirname(fileURLToPath(import.meta.url)) const ICONS_DIR = path.join(scriptDir, '..', 'public', 'icons') const RATE_LIMIT_DELAY_MS = 75 const sleep = (ms: number): Promise<void> => new Promise((resolve) => { setTimeout(resolve, ms) }) type Outcome = 'downloaded' | 'missing' | 'skipped' interface UniverseEntry { name: string } interface DexMeta { universe: Array<UniverseEntry> } const fetchDexMeta = async (dex: string): Promise<DexMeta> => { const response = await fetch(HL_INFO, { body: JSON.stringify({ dex, type: 'metaAndAssetCtxs' }), headers: { 'content-type': 'application/json' }, method: 'POST', }) if (!response.ok) { throw new Error( `metaAndAssetCtxs({ dex: ${dex} }) failed: ${response.status}`, ) } const [meta] = (await response.json()) as [DexMeta, unknown] return meta } const downloadIcon = async ({ assetName, fullCoin, }: { assetName: string fullCoin: string }): Promise<Outcome> => { const target = path.join(ICONS_DIR, `${assetName}.svg`) if (fs.existsSync(target)) { return 'skipped' } const response = await fetch(`${HL_COINS}/${fullCoin}.svg`) if (!response.ok) { console.warn(` ✗ ${fullCoin} (HTTP ${response.status})`) return 'missing' } const body = await response.text() if (!body.includes('<svg')) { console.warn(` ✗ ${fullCoin} (not an SVG)`) return 'missing' } fs.writeFileSync(target, body) console.log(` ✓ ${assetName}`) return 'downloaded' } const isExcluded = ({ dex }: { dex: string }): boolean => !isOurDex(dex) const main = async (): Promise<void> => { fs.mkdirSync(ICONS_DIR, { recursive: true }) const counts: Record<Outcome, number> = { downloaded: 0, missing: 0, skipped: 0, } for (const dex of SUPPORTED_DEXES) { console.log(`\nFetching ${dex} universe…`) // oxlint-disable-next-line no-await-in-loop const meta = await fetchDexMeta(dex) const coins = meta.universe.map(({ name }) => name) for (const fullCoin of coins) { const segments = fullCoin.split(':') const assetName = segments[segments.length - 1] if (isExcluded({ dex })) { continue } // oxlint-disable-next-line no-await-in-loop const outcome = await downloadIcon({ assetName, fullCoin }) counts[outcome] += 1 if (outcome === 'downloaded') { // Rate-limit to stay polite to the CDN. // oxlint-disable-next-line no-await-in-loop await sleep(RATE_LIMIT_DELAY_MS) } } } console.log( `\nDone. downloaded=${counts.downloaded} skipped=${counts.skipped} missing=${counts.missing}`, ) } try { await main() } catch (error) { console.error(error) process.exit(1) }