Skip to content
markpaper

src/assets/assetId.ts

v0.3.0 · 3.2 KB

Download file
// 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 };
}
All files