src/assets/assetId.ts
v0.3.0 · 3.2 KB
// Asset id encoding. A wrong id sends the order to a DIFFERENT market, so every function validates its input.
/** Spot asset id = 10000 + pair index. */
export const SPOT_ASSET_OFFSET = 10_000;
/** HIP-3 asset id = 100000 + perpDexIndex × 10000 + index in that dex's universe. */
export const HIP3_ASSET_OFFSET = 100_000;
/** Each builder dex owns a block of exactly 10000 ids. */
export const HIP3_DEX_BLOCK = 10_000;
function assertIndex(value: number, what: string, maxExclusive: number): void {
if (!Number.isSafeInteger(value) || value < 0 || value >= maxExclusive) {
throw new RangeError(`${what} must be an integer in [0, ${maxExclusive}), got ${String(value)}`);
}
}
/**
* Main perp dex: asset id = index in `meta.universe` (e.g. HYPE = 159 at the time of writing).
* Indices at or above 10000 would collide with the spot id range and are rejected.
*/
export function perpAssetId(index: number): number {
assertIndex(index, 'perp universe index', SPOT_ASSET_OFFSET);
return index;
}
/** Spot: asset id = `10000 + pairIndex` (pair `@85` → `10085`). */
export function spotAssetId(pairIndex: number): number {
assertIndex(pairIndex, 'spot pair index', HIP3_ASSET_OFFSET - SPOT_ASSET_OFFSET);
return SPOT_ASSET_OFFSET + pairIndex;
}
/**
* HIP-3 perp: `100000 + dexIndex × 10000 + index`. Observed on mainnet: `xyz` has dexIndex 1, so
* `xyz:TSLA = 110001`, `xyz:SP500 = 110052`.
*
* Never hardcode `dexIndex` (or the `110000` offset): resolve it by name from `perpDexs`. If HL inserts a dex
* before yours, a hardcoded offset routes orders into someone else's builder dex.
* @experimental For dexes other than `xyz` the formula comes from HL docs and was not verified with live orders
* (the knowledge base only smoke-tested xyz = 110000 + index). Risk: a wrong id routes an order to another market;
* smoke-test ids of a new dex before trading it.
*/
export function hip3AssetId(dexIndex: number, index: number): number {
if (!Number.isSafeInteger(dexIndex) || dexIndex < 1) {
throw new RangeError(`HIP-3 dexIndex must be an integer >= 1, got ${String(dexIndex)}`);
}
assertIndex(index, 'HIP-3 universe index', HIP3_DEX_BLOCK);
const id = HIP3_ASSET_OFFSET + dexIndex * HIP3_DEX_BLOCK + index;
if (!Number.isSafeInteger(id)) throw new RangeError(`HIP-3 asset id overflow for dexIndex ${dexIndex}`);
return id;
}
export type DecodedAssetId =
| { readonly market: 'perp'; readonly dexIndex: number; readonly index: number }
| { readonly market: 'spot'; readonly index: number };
/** Inverse of the three encoders. Throws `RangeError` for negative or non-integer ids. */
export function decodeAssetId(assetId: number): DecodedAssetId {
if (!Number.isSafeInteger(assetId) || assetId < 0) {
throw new RangeError(`asset id must be a non-negative integer, got ${String(assetId)}`);
}
if (assetId < SPOT_ASSET_OFFSET) return { market: 'perp', dexIndex: 0, index: assetId };
if (assetId < HIP3_ASSET_OFFSET) return { market: 'spot', index: assetId - SPOT_ASSET_OFFSET };
const rel = assetId - HIP3_ASSET_OFFSET;
const dexIndex = Math.floor(rel / HIP3_DEX_BLOCK);
const index = rel % HIP3_DEX_BLOCK;
if (dexIndex < 1) throw new RangeError(`asset id ${assetId} lies in the unused HIP-3 block of dex 0`);
return { market: 'perp', dexIndex, index };
}