@kinetiq-research/markets-assets
Version:
Asset metadata (descriptions, oracle references) for Kinetiq markets
128 lines (105 loc) • 3.23 kB
text/typescript
// 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)
}