@kinetiq-research/markets-assets
Version:
Asset metadata (descriptions, oracle references) for Kinetiq markets
116 lines (106 loc) • 4.41 kB
text/typescript
import type { AllPerpMetasResponse } from '@nktkas/hyperliquid'
import type { PerpsAsset } from './asset'
import {
HIP3_ASSET_INDEX_BASE,
HIP3_DEX_INDEX_MULTIPLIER,
} from './asset-indices'
import { getDexNameFromMeta } from './get-dex-name-from-meta'
import { parsePerpsHyperliquidSymbol } from './parse-perps-hyperliquid-symbol'
import { toDisplaySymbol } from './to-display-symbol'
import { toTradeUrlSymbol } from './to-trade-url-symbol'
// Collateral token index -> quote asset symbol. Indices come from the
// Hyperliquid spot meta and are stable, so they are hardcoded here to avoid
// threading spot meta through perps normalization.
//
// This is a DISPLAY LABEL, not a key. It disagrees with the token names
// Hyperliquid publishes (index 268 is named `USDT0` in the spot meta), and an
// index absent from this map is labelled `USDC` by the fallback below. Anything
// that has to identify the settlement token — matching a balance, a fee, or a
// collateral figure — keys on `collateralTokenIndex` instead.
const COLLATERAL_TOKEN_SYMBOLS: Record<number, PerpsAsset['quoteAssetSymbol']> =
{
0: 'USDC',
235: 'USDE',
268: 'USDT',
360: 'USDH',
}
const getQuoteAssetSymbol = (
collateralToken: number,
): PerpsAsset['quoteAssetSymbol'] =>
COLLATERAL_TOKEN_SYMBOLS[collateralToken] ?? 'USDC'
type NormalizedPerpsMetaUniverseItem =
AllPerpMetasResponse[number]['universe'][number] & {
asset: PerpsAsset
}
type NormalizedPerpsMetaDex = Omit<AllPerpMetasResponse[number], 'universe'> & {
universe: Array<NormalizedPerpsMetaUniverseItem>
}
export type NormalizedPerpsMeta = Array<NormalizedPerpsMetaDex>
export const normalizePerpsMeta = ({
perpsAnnotations,
perpsMeta,
}: {
perpsAnnotations?: Record<string, string>
perpsMeta: AllPerpMetasResponse
}): NormalizedPerpsMeta =>
perpsMeta.map((dexMeta, dexIndex) => {
const dex = getDexNameFromMeta({ meta: dexMeta })
const quoteAssetSymbol = getQuoteAssetSymbol(dexMeta.collateralToken)
const universe = dexMeta.universe.map((universeItem, positionInDex) => {
const { symbol } = parsePerpsHyperliquidSymbol({
hyperliquidSymbol: universeItem.name,
})
// Curated override → live annotation → the raw cleaned name. `symbol`
// stays the raw cleaned name. The precedence lives in `toDisplaySymbol`
// because Pulse's `buildTickerUniverse` resolves the same question from
// the same payloads, and when the two disagreed the trade app called one
// listing `QQQ` while Pulse briefs called it `USTECH`.
const preferredSymbol = toDisplaySymbol({
// `Object.hasOwn`, not a bare index: `perpsAnnotations` is a plain
// object literal typed `Record<string, string>`, so a listing named
// `constructor` would read a FUNCTION off the prototype chain, and the
// typechecker types the read as `string`. Same guard, same reason as
// `toOverride` in `to-display-symbol.ts`.
annotationDisplayName:
perpsAnnotations !== undefined &&
Object.hasOwn(perpsAnnotations, universeItem.name)
? perpsAnnotations[universeItem.name]
: undefined,
hyperliquidSymbol: universeItem.name,
symbol,
})
const display = preferredSymbol
// `dex` is passed because a listing whose ticker is live on a second dex
// we surface keeps the prefix in its url — see `PREFIXED_URL_SYMBOLS`.
const tradeUrlSymbol = toTradeUrlSymbol({ dex, symbol: preferredSymbol })
// Main HL (dex index 0) uses the position directly; HIP-3 dexes use the
// computed offset formula.
const assetIndex =
dexIndex === 0
? positionInDex
: HIP3_ASSET_INDEX_BASE +
dexIndex * HIP3_DEX_INDEX_MULTIPLIER +
positionInDex
return {
...universeItem,
asset: {
assetIndex,
collateralTokenIndex: dexMeta.collateralToken,
dex,
display,
hyperliquidSymbol: universeItem.name,
maxLeverage: Number(universeItem.maxLeverage),
onlyIsolated: !!universeItem.onlyIsolated,
quoteAssetSymbol,
symbol,
szDecimals: universeItem.szDecimals,
tradeUrlSymbol,
type: 'perps' as const,
},
}
})
return {
...dexMeta,
universe,
}
})