Skip to content
markpaper

src/assets/registry.test.ts

v0.3.0 · 29.9 KB

Download file
import { describe, expect, it, vi } from 'vitest';
import type { InfoCallOptions, InfoRequest, InfoRequester } from '../transport/types.js';
import { AssetRegistryError, createAssetRegistry, type AssetRegistryOptions } from './registry.js';
import type { AssetLookup, AssetRegistryEvent } from './types.js';

const TTL = 300_000;
const COOLDOWN = 30_000;

const MAIN = [
  { name: 'BTC', szDecimals: 5, maxLeverage: 40 },
  { name: 'ETH', szDecimals: 4, maxLeverage: 25 },
  { name: 'OLD', szDecimals: 1, maxLeverage: 3, isDelisted: true },
  { name: 'ISO', szDecimals: 0, maxLeverage: 5, onlyIsolated: true },
];

/** xyz universe laid out so that the mainnet smoke ids hold: TSLA=1, MU=15, CL=29, SP500=52. */
function xyzUniverse(bare = false) {
  const names = Array.from({ length: 53 }, (_, i) => `F${i}`);
  names[0] = 'XYZ100';
  names[1] = 'TSLA';
  names[15] = 'MU';
  names[29] = 'CL';
  names[52] = 'SP500';
  return names.map((n) => ({
    name: bare ? n : `xyz:${n}`,
    szDecimals: 2,
    maxLeverage: n === 'SP500' ? 50 : 10,
    ...(n === 'MU' ? { onlyIsolated: false } : {}),
  }));
}

const USDC = { name: 'USDC', index: 0, tokenId: '0x6d1e7cde53ba9467b783cb7c530ce054', szDecimals: 8, weiDecimals: 8, isCanonical: true };
const PURR = { name: 'PURR', index: 1, tokenId: `0x${'1'.repeat(32)}`, szDecimals: 0, weiDecimals: 5, isCanonical: true };
const HYPE = { name: 'HYPE', index: 150, tokenId: `0x${'2'.repeat(32)}`, szDecimals: 2, weiDecimals: 8, isCanonical: false };

function makeEnv(options: AssetRegistryOptions = {}) {
  const state = {
    main: { universe: structuredClone(MAIN) } as { universe: unknown[] },
    dexMeta: { xyz: { universe: xyzUniverse() } } as Record<string, { universe: unknown[] }>,
    ctxOverride: {} as Record<string, unknown>,
    perpDexs: [
      null,
      { name: 'xyz', assetToStreamingOiCap: [['xyz:TSLA', '100'], ['xyz:SP500', 5]] },
      { name: 'flx', assetToStreamingOiCap: [['flx:ABC', '1']] },
    ] as unknown,
    spot: {
      tokens: [USDC, PURR, HYPE],
      universe: [
        { tokens: [1, 0], name: 'PURR/USDC', index: 0, isCanonical: true },
        { tokens: [150, 0], name: '@107', index: 107, isCanonical: false },
      ],
    } as { tokens: unknown[]; universe: unknown[] },
    fail: new Set<string>(),
    gate: null as Promise<void> | null,
  };
  const calls: { body: InfoRequest; opts: InfoCallOptions | undefined }[] = [];
  const keyOf = (b: InfoRequest) => (typeof b.dex === 'string' ? `${b.type}:${b.dex}` : b.type);
  const count = (key: string) => calls.filter((c) => keyOf(c.body) === key).length;

  const info = (async (body: InfoRequest, opts?: InfoCallOptions) => {
    calls.push({ body, opts });
    const key = keyOf(body);
    await Promise.resolve();
    if (key === 'meta:xyz' && state.gate) await state.gate;
    if (state.fail.has(key)) throw Object.assign(new Error(`HL info HTTP 500 for ${key}`), { status: 500 });
    const metaFor = (dex: unknown) => {
      if (typeof dex !== 'string') return structuredClone(state.main);
      const m = state.dexMeta[dex];
      if (!m) throw Object.assign(new Error('HTTP 422'), { status: 422 });
      return structuredClone(m);
    };
    switch (body.type) {
      case 'meta':
        return metaFor(body.dex);
      case 'metaAndAssetCtxs': {
        if (key in state.ctxOverride) return state.ctxOverride[key];
        const m = metaFor(body.dex);
        return [m, m.universe.map(() => ({}))];
      }
      case 'perpDexs':
        return structuredClone(state.perpDexs);
      case 'spotMeta':
        return structuredClone(state.spot);
      default:
        throw new Error(`unexpected ${body.type}`);
    }
  }) as InfoRequester;

  let t = 1_000_000;
  const events: AssetRegistryEvent[] = [];
  const reg = createAssetRegistry(info, { now: () => t, onEvent: (e) => events.push(e), ...options });
  return {
    state,
    calls,
    count,
    reg,
    events,
    advance: (ms: number) => {
      t += ms;
    },
  };
}

const idOf = (r: AssetLookup) => (r.status === 'listed' ? r.asset.assetId : null);

describe('createAssetRegistry: resolution and asset ids', () => {
  it('resolves main perps: id = universe index, request without a dex key, weight 20', async () => {
    const env = makeEnv({ requestTimeoutMs: 8000 });
    const btc = await env.reg.resolve('BTC');
    expect(btc).toMatchObject({ coin: 'BTC', assetId: 0, szDecimals: 5, maxPxDecimals: 1, maxLeverage: 40, dex: '', dexIndex: 0, market: 'perp', onlyIsolated: false, isDelisted: false });
    expect((await env.reg.resolve('ISO')).onlyIsolated).toBe(true);
    const metaCall = env.calls.find((c) => c.body.type === 'meta');
    expect(metaCall?.body).toEqual({ type: 'meta' });
    expect(Object.prototype.hasOwnProperty.call(metaCall?.body, 'dex')).toBe(false);
    expect(metaCall?.opts).toEqual({ weight: 20, timeoutMs: 8000 });
    expect(Object.isFrozen(btc)).toBe(true);
  });

  it('resolves HIP-3 perps: xyz:TSLA = 110001, xyz:SP500 = 110052 with dexIndex from perpDexs', async () => {
    const env = makeEnv();
    const tsla = await env.reg.resolve('xyz:TSLA');
    expect(tsla).toMatchObject({ coin: 'xyz:TSLA', assetId: 110001, dex: 'xyz', dexIndex: 1, onlyIsolated: true });
    expect((await env.reg.resolve('xyz:SP500')).assetId).toBe(110052);
    expect((await env.reg.resolve('xyz:SP500')).maxLeverage).toBe(50);
    expect((await env.reg.resolve('xyz:CL')).assetId).toBe(110029);
    expect((await env.reg.resolve('xyz:MU')).onlyIsolated).toBe(false);
    expect(env.calls.find((c) => c.body.type === 'meta')?.body).toEqual({ type: 'meta', dex: 'xyz' });
  });

  it('accepts the old bare-name meta format without producing xyz:xyz:', async () => {
    const env = makeEnv();
    env.state.dexMeta.xyz = { universe: xyzUniverse(true) };
    expect((await env.reg.resolve('xyz:TSLA')).assetId).toBe(110001);
    expect((await env.reg.lookup('xyz:xyz:TSLA')).status).toBe('unlisted');
  });

  it('uses the dex index by name, not a hardcoded 110000 offset', async () => {
    const env = makeEnv();
    env.state.perpDexs = [null, { name: 'flx', assetToStreamingOiCap: [['flx:ABC', '1']] }, { name: 'xyz', assetToStreamingOiCap: [['xyz:TSLA', '1']] }];
    expect((await env.reg.resolve('xyz:TSLA')).assetId).toBe(120001);
  });

  it('delisted coins resolve with isDelisted; rejectDelisted throws', async () => {
    const env = makeEnv();
    expect((await env.reg.resolve('OLD')).isDelisted).toBe(true);
    await expect(env.reg.resolve('OLD', { rejectDelisted: true })).rejects.toMatchObject({ name: 'AssetRegistryError', status: 'delisted' });
  });

  it('unlisted coins: fresh universe miss, malformed names', async () => {
    const env = makeEnv();
    expect(await env.reg.lookup('NOPE')).toEqual({ status: 'unlisted', coin: 'NOPE', reason: 'not_in_universe' });
    await expect(env.reg.resolve('NOPE')).rejects.toBeInstanceOf(AssetRegistryError);
    expect(await env.reg.lookup('xyz:xyz:TSLA')).toMatchObject({ status: 'unlisted', reason: 'invalid' });
    expect(await env.reg.lookup('')).toMatchObject({ status: 'unlisted', reason: 'invalid' });
  });

  it('an unknown dex in a fresh perpDexs is unlisted without loading its meta', async () => {
    const env = makeEnv();
    expect(await env.reg.lookup('abc:FOO')).toMatchObject({ status: 'unlisted', reason: 'dex_not_found' });
    expect(env.count('meta:abc')).toBe(0);
  });

  it('dexes option restricts HIP-3 dexes without network calls', async () => {
    const env = makeEnv({ dexes: ['xyz'] });
    expect(await env.reg.lookup('flx:ABC')).toMatchObject({ status: 'unknown', reason: 'dex_not_configured' });
    expect(env.calls).toHaveLength(0);
    expect((await env.reg.resolve('xyz:TSLA')).assetId).toBe(110001);
  });

  it('validates options', () => {
    const info = (async () => ({})) as InfoRequester;
    expect(() => createAssetRegistry(info, { ttlMs: 0 })).toThrow(RangeError);
    expect(() => createAssetRegistry(info, { failCooldownMs: -1 })).toThrow(RangeError);
    expect(() => createAssetRegistry(info, { dexes: ['a:b'] })).toThrow(RangeError);
  });
});

describe('createAssetRegistry: cache, single-flight, stale-on-error, cooldown', () => {
  it('single-flight: concurrent resolves share one request per source (no stampede)', async () => {
    const env = makeEnv();
    const coins = Array.from({ length: 25 }, (_, i) => (i % 2 ? 'BTC' : 'xyz:TSLA'));
    const res = await Promise.all(coins.map((c) => env.reg.resolve(c)));
    expect(res.every((a) => a.assetId === 0 || a.assetId === 110001)).toBe(true);
    expect(env.count('meta')).toBe(1);
    expect(env.count('metaAndAssetCtxs')).toBe(1);
    expect(env.count('meta:xyz')).toBe(1);
    expect(env.count('metaAndAssetCtxs:xyz')).toBe(1);
    expect(env.count('perpDexs')).toBe(1);
  });

  it('serves from cache within TTL and refetches once after it', async () => {
    const env = makeEnv();
    await env.reg.resolve('BTC');
    env.advance(TTL - 1);
    await env.reg.resolve('ETH');
    expect(env.count('meta')).toBe(1);
    env.advance(2);
    await Promise.all([env.reg.resolve('BTC'), env.reg.resolve('ETH')]);
    expect(env.count('meta')).toBe(2);
  });

  it('stale-on-error keeps known assets; misses become unknown, not unlisted', async () => {
    const env = makeEnv();
    await env.reg.resolve('BTC');
    env.advance(TTL + 1);
    env.state.fail.add('meta');
    expect(idOf(await env.reg.lookup('BTC'))).toBe(0);
    expect(await env.reg.lookup('NEWCOIN')).toMatchObject({ status: 'unknown', reason: 'load_failed' });
    expect(env.reg.status().find((s) => s.source === 'meta')).toMatchObject({ loaded: true, healthy: false, lastErrorReason: 'load_failed' });
    expect(env.events.some((e) => e.type === 'load_failed' && e.servingStale)).toBe(true);
  });

  it('fail-cooldown: no refetch for 30 s after a failure, immediate pickup after it', async () => {
    const env = makeEnv();
    await env.reg.resolve('BTC');
    env.advance(TTL + 1);
    env.state.fail.add('meta');
    await env.reg.lookup('BTC');
    expect(env.count('meta')).toBe(2);
    env.advance(COOLDOWN - 1);
    await env.reg.lookup('BTC');
    await env.reg.lookup('ETH');
    expect(env.count('meta')).toBe(2);
    env.state.fail.delete('meta');
    env.advance(2);
    await env.reg.lookup('BTC');
    expect(env.count('meta')).toBe(3);
    expect(env.reg.status().find((s) => s.source === 'meta')).toMatchObject({ healthy: true, fresh: true });
  });

  it('memoizes a failed attempt even without any cache', async () => {
    const env = makeEnv();
    env.state.fail.add('meta');
    expect(await env.reg.lookup('BTC')).toMatchObject({ status: 'unknown', reason: 'load_failed' });
    await expect(env.reg.resolve('BTC')).rejects.toMatchObject({ status: 'unknown', reason: 'load_failed' });
    expect(env.count('meta')).toBe(1);
    env.state.fail.delete('meta');
    env.advance(COOLDOWN);
    expect(idOf(await env.reg.lookup('BTC'))).toBe(0);
  });

  it('refresh() forces a reload regardless of TTL', async () => {
    const env = makeEnv();
    await env.reg.resolve('BTC');
    await env.reg.resolve('@107');
    await env.reg.refresh();
    expect(env.count('meta')).toBe(2);
    expect(env.count('spotMeta')).toBe(2);
  });

  it('peek is synchronous, starts one background load and never stampedes', async () => {
    const env = makeEnv();
    const results = Array.from({ length: 50 }, () => env.reg.peek('BTC'));
    expect(results[0]).toMatchObject({ status: 'unknown', reason: 'not_loaded' });
    await vi.waitFor(() => expect(env.reg.peek('BTC').status).toBe('listed'));
    expect(env.count('meta')).toBe(1);
    expect(env.reg.peek('NOPE')).toMatchObject({ status: 'unlisted' });
    expect(env.reg.peek('bad:')).toMatchObject({ status: 'unlisted', reason: 'invalid' });
  });

  it('a throwing onEvent handler does not break resolution', async () => {
    const info = (async (b: InfoRequest) => (b.type === 'meta' ? { universe: MAIN } : [{ universe: MAIN }, MAIN.map(() => ({}))])) as InfoRequester;
    const reg = createAssetRegistry(info, {
      onEvent: () => {
        throw new Error('boom');
      },
    });
    expect((await reg.resolve('BTC')).assetId).toBe(0);
  });
});

describe('createAssetRegistry: HIP-3 isolation and topology', () => {
  it('a HIP-3 failure does not break main', async () => {
    const env = makeEnv();
    env.state.fail.add('meta:xyz');
    expect(await env.reg.lookup('xyz:TSLA')).toMatchObject({ status: 'unknown', reason: 'load_failed' });
    expect((await env.reg.resolve('BTC')).assetId).toBe(0);
  });

  it('cold start without perpDexs: HIP-3 closed (fail-closed), no xyz meta request', async () => {
    const env = makeEnv();
    env.state.fail.add('perpDexs');
    expect(await env.reg.lookup('xyz:TSLA')).toMatchObject({ status: 'unknown', reason: 'dex_unverified' });
    expect(env.count('meta:xyz')).toBe(0);
    expect((await env.reg.resolve('ETH')).assetId).toBe(1);
  });

  it('reuses a dex index this process verified when perpDexs later fails', async () => {
    const env = makeEnv();
    await env.reg.resolve('xyz:TSLA');
    env.advance(TTL + 1);
    env.state.fail.add('perpDexs');
    expect(idOf(await env.reg.lookup('xyz:TSLA'))).toBe(110001);
    expect(env.count('meta:xyz')).toBe(2);
  });

  it('dex index change drops the old ids, alerts, then rebuilds with the new index', async () => {
    const env = makeEnv();
    await env.reg.resolve('xyz:TSLA');
    env.advance(TTL + 1);
    env.state.perpDexs = [null, { name: 'flx', assetToStreamingOiCap: [['flx:ABC', '1']] }, { name: 'xyz', assetToStreamingOiCap: [['xyz:TSLA', '1']] }];
    const first = await env.reg.lookup('xyz:TSLA');
    expect(idOf(first)).not.toBe(110001);
    expect(first).toMatchObject({ status: 'unknown', reason: 'topology_unsafe' });
    expect(env.events).toContainEqual({ type: 'topology_changed', dex: 'xyz', previousIndex: 1, currentIndex: 2, duplicated: false });
    expect(idOf(await env.reg.lookup('xyz:TSLA'))).toBe(120001);
  });

  it('duplicated dex name closes the dex and alerts once', async () => {
    const env = makeEnv();
    await env.reg.resolve('xyz:TSLA');
    env.advance(TTL + 1);
    env.state.perpDexs = [null, { name: 'xyz', assetToStreamingOiCap: [['xyz:TSLA', '1']] }, { name: 'xyz', assetToStreamingOiCap: [] }];
    expect(await env.reg.lookup('xyz:TSLA')).toMatchObject({ status: 'unknown', reason: 'topology_unsafe' });
    env.advance(TTL + COOLDOWN);
    expect(await env.reg.lookup('xyz:SP500')).toMatchObject({ status: 'unknown', reason: 'topology_unsafe' });
    expect(env.events.filter((e) => e.type === 'topology_changed')).toEqual([
      { type: 'topology_changed', dex: 'xyz', previousIndex: 1, currentIndex: null, duplicated: true },
    ]);
  });

  it('a verified dex that vanishes from perpDexs is unknown, not unlisted', async () => {
    const env = makeEnv();
    await env.reg.resolve('xyz:TSLA');
    env.advance(TTL + 1);
    env.state.perpDexs = [null, { name: 'flx', assetToStreamingOiCap: [['flx:ABC', '1']] }];
    expect(await env.reg.lookup('xyz:TSLA')).toMatchObject({ status: 'unknown', reason: 'topology_unsafe' });
  });

  it('discards an in-flight load whose dex index was invalidated meanwhile', async () => {
    const env = makeEnv();
    await env.reg.resolve('xyz:TSLA');
    env.advance(TTL + 1);
    let release!: () => void;
    env.state.gate = new Promise<void>((r) => (release = r));
    const pending = env.reg.lookup('xyz:TSLA');
    await vi.waitFor(() => expect(env.count('meta:xyz')).toBe(2));
    env.state.perpDexs = [null, { name: 'flx', assetToStreamingOiCap: [['flx:ABC', '1']] }, { name: 'xyz', assetToStreamingOiCap: [['xyz:TSLA', '1']] }];
    const refreshing = env.reg.refresh();
    release();
    env.state.gate = null;
    const [r] = await Promise.all([pending, refreshing]);
    expect(idOf(r)).not.toBe(110001);
    expect(idOf(await env.reg.lookup('xyz:TSLA'))).toBe(120001);
  });

  it('rejects a first load if perpDexs moved the dex while meta was in flight', async () => {
    const env = makeEnv();
    let release!: () => void;
    env.state.gate = new Promise<void>((r) => (release = r));
    const pending = env.reg.lookup('xyz:TSLA');
    await vi.waitFor(() => expect(env.count('meta:xyz')).toBe(1));
    env.state.perpDexs = [null, { name: 'flx', assetToStreamingOiCap: [['flx:ABC', '1']] }, { name: 'xyz', assetToStreamingOiCap: [['xyz:TSLA', '1']] }];
    const refreshing = env.reg.refresh();
    release();
    env.state.gate = null;
    const [r] = await Promise.all([pending, refreshing]);
    expect(r).toMatchObject({ status: 'unknown', reason: 'topology_unsafe' });
    env.advance(COOLDOWN);
    expect(idOf(await env.reg.lookup('xyz:TSLA'))).toBe(120001);
  });
});

describe('createAssetRegistry: meta integrity checks', () => {
  it('registry coverage is inclusion: extra meta assets are a new listing, reported once', async () => {
    const env = makeEnv();
    await env.reg.resolve('xyz:TSLA');
    const listings = env.events.filter((e) => e.type === 'new_listings');
    expect(listings).toHaveLength(1);
    expect(listings[0]).toMatchObject({ dex: 'xyz' });
    env.advance(TTL + 1);
    await env.reg.resolve('xyz:TSLA');
    expect(env.events.filter((e) => e.type === 'new_listings')).toHaveLength(1);
  });

  it('a registry asset missing from meta rejects the universe', async () => {
    const env = makeEnv();
    env.state.perpDexs = [null, { name: 'xyz', assetToStreamingOiCap: [['xyz:TSLA', '1'], ['xyz:GHOST', '1']] }];
    expect(await env.reg.lookup('xyz:TSLA')).toMatchObject({ status: 'unknown', reason: 'coverage_failed' });
    expect(env.events).toContainEqual({ type: 'coverage_missing', dex: 'xyz', missing: ['xyz:GHOST'] });
  });

  it('empty dex registry: refused by default, allowed with requireDexRegistry=false', async () => {
    const env = makeEnv();
    env.state.perpDexs = [null, { name: 'xyz', assetToStreamingOiCap: [] }];
    expect(await env.reg.lookup('xyz:TSLA')).toMatchObject({ status: 'unknown', reason: 'registry_empty' });
    expect(env.events).toContainEqual({ type: 'registry_empty', dex: 'xyz' });

    const lax = makeEnv({ requireDexRegistry: false });
    lax.state.perpDexs = [null, { name: 'xyz', assetToStreamingOiCap: [] }];
    expect(idOf(await lax.reg.lookup('xyz:TSLA'))).toBe(110001);
  });

  it('double read: a truncated metaAndAssetCtxs rejects the universe', async () => {
    const env = makeEnv();
    env.state.ctxOverride.metaAndAssetCtxs = [{ universe: MAIN.slice(0, 3) }, [{}, {}, {}]];
    expect(await env.reg.lookup('BTC')).toMatchObject({ status: 'unknown', reason: 'cross_check_failed' });
  });

  it('double read: universe/assetCtxs length mismatch rejects the universe', async () => {
    const env = makeEnv();
    env.state.ctxOverride.metaAndAssetCtxs = [{ universe: MAIN }, [{}]];
    expect(await env.reg.lookup('BTC')).toMatchObject({ status: 'unknown', reason: 'cross_check_failed' });
  });

  it('crossCheck=false skips metaAndAssetCtxs', async () => {
    const env = makeEnv({ crossCheck: false });
    expect((await env.reg.resolve('BTC')).assetId).toBe(0);
    expect(env.count('metaAndAssetCtxs')).toBe(0);
  });

  it('an invalid element rejects the whole universe', async () => {
    const env = makeEnv();
    env.state.main.universe[1] = { name: 'ETH', szDecimals: 12, maxLeverage: 25 };
    expect(await env.reg.lookup('BTC')).toMatchObject({ status: 'unknown', reason: 'invalid_response' });
  });

  it('append-only: a refresh that moves or drops verified assets is rejected, last verified map kept', async () => {
    const env = makeEnv();
    expect((await env.reg.resolve('ETH')).assetId).toBe(1);
    env.advance(TTL + 1);
    env.state.main.universe = [MAIN[1], MAIN[0], MAIN[2], MAIN[3], { name: 'NEW', szDecimals: 1, maxLeverage: 3 }];
    expect(idOf(await env.reg.lookup('ETH'))).toBe(1);
    expect(await env.reg.lookup('NEW')).toMatchObject({ status: 'unknown', reason: 'append_only_violation' });
    expect(env.events.some((e) => e.type === 'append_only_violation')).toBe(true);
  });

  it('append-only accepts pure additions', async () => {
    const env = makeEnv();
    await env.reg.resolve('BTC');
    env.advance(TTL + 1);
    env.state.main.universe.push({ name: 'NEW', szDecimals: 1, maxLeverage: 3 });
    expect(idOf(await env.reg.lookup('NEW'))).toBe(4);
  });
});

describe('createAssetRegistry: spot (@experimental)', () => {
  it('resolves @index and canonical pair names; asset id = 10000 + index', async () => {
    const env = makeEnv();
    expect(await env.reg.resolve('@107')).toMatchObject({ coin: '@107', assetId: 10107, market: 'spot', base: 'HYPE', quote: 'USDC', szDecimals: 2, maxPxDecimals: 6, maxLeverage: 1, dexIndex: null });
    expect(await env.reg.resolve('PURR/USDC')).toMatchObject({ coin: 'PURR/USDC', assetId: 10000 });
    expect((await env.reg.resolve('@0')).coin).toBe('PURR/USDC');
    expect(env.count('spotMeta')).toBe(1);
  });

  it('maps BASE/QUOTE to a non-canonical @index pair through token names', async () => {
    const env = makeEnv();
    const r = await env.reg.lookup('HYPE/USDC');
    expect(r).toMatchObject({ status: 'listed', coin: 'HYPE/USDC', asset: { coin: '@107', assetId: 10107 } });
  });

  it('ambiguous token-name pairs are unknown, not guessed', async () => {
    const env = makeEnv();
    env.state.spot.universe.push({ tokens: [150, 0], name: '@200', index: 200, isCanonical: false });
    expect(await env.reg.lookup('HYPE/USDC')).toMatchObject({ status: 'unknown', reason: 'ambiguous' });
  });

  it('unlisted spot and token wire ids', async () => {
    const env = makeEnv();
    expect(await env.reg.lookup('@999')).toMatchObject({ status: 'unlisted', reason: 'not_in_universe' });
    expect((await env.reg.spotToken('USDC')).wireId).toBe('USDC:0x6d1e7cde53ba9467b783cb7c530ce054');
    await expect(env.reg.spotToken('NOPE')).rejects.toMatchObject({ status: 'unlisted' });
    expect(await env.reg.listSpotAssets()).toHaveLength(2);
  });
});

describe('createAssetRegistry: reverse lookup, listing, startup checks', () => {
  it('lookupByAssetId covers main, HIP-3 and spot', async () => {
    const env = makeEnv();
    expect(await env.reg.lookupByAssetId(0)).toMatchObject({ status: 'listed', asset: { coin: 'BTC' } });
    expect(await env.reg.lookupByAssetId(110052)).toMatchObject({ status: 'listed', asset: { coin: 'xyz:SP500' } });
    expect(await env.reg.lookupByAssetId(10107)).toMatchObject({ status: 'listed', asset: { coin: '@107' } });
    expect(await env.reg.lookupByAssetId(110053)).toMatchObject({ status: 'unlisted', reason: 'not_in_universe' });
    expect(await env.reg.lookupByAssetId(130000)).toMatchObject({ status: 'unlisted', reason: 'dex_not_found' });
    expect(await env.reg.lookupByAssetId(-1)).toMatchObject({ status: 'unlisted', reason: 'invalid' });
  });

  it('lookupByAssetId respects the dexes option', async () => {
    const env = makeEnv({ dexes: ['flx'] });
    expect(await env.reg.lookupByAssetId(110001)).toMatchObject({ status: 'unknown', reason: 'dex_not_configured' });
  });

  it('listDexes / listAssets / status', async () => {
    const env = makeEnv();
    expect(await env.reg.listDexes()).toEqual([
      { name: 'xyz', index: 1 },
      { name: 'flx', index: 2 },
    ]);
    const xyz = await env.reg.listAssets('xyz');
    expect(xyz).toHaveLength(53);
    expect(xyz[52]?.coin).toBe('xyz:SP500');
    await expect(env.reg.listAssets('bad:')).rejects.toMatchObject({ status: 'unlisted', reason: 'invalid' });
    await expect(env.reg.listAssets('abc')).rejects.toMatchObject({ status: 'unlisted', reason: 'dex_not_found' });
    const rows = env.reg.status();
    expect(rows.find((r) => r.source === 'meta:xyz')).toMatchObject({ loaded: true, fresh: true, count: 53 });
    expect(rows.find((r) => r.source === 'perpDexs')).toMatchObject({ loaded: true, count: 2 });
  });

  it('checkMarkets flags unlisted, delisted and leverage above maxLeverage', async () => {
    const env = makeEnv();
    const res = await env.reg.checkMarkets([
      { coin: 'BTC', leverage: 20 },
      { coin: 'xyz:TSLA', leverage: 20 },
      { coin: 'OLD' },
      { coin: 'NOPE' },
      { coin: 'ETH', leverage: 2.5 },
    ]);
    expect(res.map((r) => [r.coin, r.ok, r.problem])).toEqual([
      ['BTC', true, undefined],
      ['xyz:TSLA', false, 'leverage_above_max'],
      ['OLD', false, 'delisted'],
      ['NOPE', false, 'unlisted'],
      ['ETH', false, 'invalid_leverage'],
    ]);
  });

  it('preload loads main, configured dexes and spot', async () => {
    const env = makeEnv({ dexes: ['xyz'] });
    const rows = await env.reg.preload({ spot: true });
    expect(rows.map((r) => r.source).sort()).toEqual(['meta', 'meta:xyz', 'perpDexs', 'spotMeta']);
    expect(env.reg.peek('xyz:TSLA')).toMatchObject({ status: 'listed' });
  });
});

describe('createAssetRegistry: review edge cases', () => {
  it('fail-cooldown also works with a clock that reads 0 (no 0 sentinel)', async () => {
    let n = 0;
    const info = (async () => {
      n++;
      throw new Error('HTTP 500');
    }) as InfoRequester;
    const reg = createAssetRegistry(info, { now: () => 0 });
    expect(await reg.lookup('BTC')).toMatchObject({ status: 'unknown', reason: 'load_failed' });
    expect(await reg.lookup('BTC')).toMatchObject({ status: 'unknown', reason: 'load_failed' });
    expect(reg.peek('ETH')).toMatchObject({ status: 'unknown' });
    await Promise.resolve();
    expect(n).toBe(2); // meta + metaAndAssetCtxs of the single attempt
    expect(reg.status()[0]).toMatchObject({ lastFailAgoMs: 0, healthy: false });
  });

  it('new_listings ignores delisted extras (live: HL drops delisted assets from assetToStreamingOiCap)', async () => {
    const env = makeEnv();
    const registry = xyzUniverse()
      .filter((x) => x.name !== 'xyz:F7' && x.name !== 'xyz:F8')
      .map((x) => [x.name, '25000000.0']);
    env.state.perpDexs = [null, { name: 'xyz', assetToStreamingOiCap: registry }];
    env.state.dexMeta.xyz!.universe[7] = { name: 'xyz:F7', szDecimals: 2, maxLeverage: 10, isDelisted: true };
    expect(idOf(await env.reg.lookup('xyz:TSLA'))).toBe(110001);
    expect(env.events.filter((e) => e.type === 'new_listings')).toEqual([{ type: 'new_listings', dex: 'xyz', coins: ['xyz:F8'] }]);
    // a delisted asset outside the registry still resolves (needed to cancel / close) and is flagged
    expect(await env.reg.resolve('xyz:F7')).toMatchObject({ assetId: 110007, isDelisted: true });
  });

  it('no new_listings event when every extra is delisted', async () => {
    const env = makeEnv();
    env.state.perpDexs = [null, { name: 'xyz', assetToStreamingOiCap: [['xyz:TSLA', '1']] }];
    env.state.dexMeta.xyz!.universe = xyzUniverse().map((x) => (x.name === 'xyz:TSLA' ? x : { ...x, isDelisted: true }));
    await env.reg.resolve('xyz:TSLA');
    expect(env.events.some((e) => e.type === 'new_listings')).toBe(false);
  });

  it('peek on an arbitrary dex name proven absent by a fresh perpDexs creates no cache entry', async () => {
    const env = makeEnv();
    await env.reg.listDexes();
    for (let i = 0; i < 20; i++) {
      expect(env.reg.peek(`junk${i}:BTC`)).toMatchObject({ status: 'unlisted', reason: 'dex_not_found' });
    }
    await Promise.resolve();
    expect(env.reg.status().some((r) => r.source.startsWith('meta:junk'))).toBe(false);
    expect(env.calls.some((c) => typeof c.body.dex === 'string' && c.body.dex.startsWith('junk'))).toBe(false);
  });

  it('live-shaped responses: extra fields, string caps, empty registry dex, noCross/strictIsolated', async () => {
    const env = makeEnv();
    env.state.perpDexs = [
      null,
      {
        name: 'xyz',
        fullName: 'XYZ',
        deployer: '0x0000000000000000000000000000000000000000', // privacy-allow: placeholder address
        oracleUpdater: null,
        assetToStreamingOiCap: [
          ['xyz:TSLA', '200000000.0'],
          ['xyz:SP500', '100000000.0'],
        ],
        subDeployers: [],
        assetToFundingMultiplier: [],
      },
      { name: 'abcd', fullName: 'ABCD', assetToStreamingOiCap: [] },
    ];
    env.state.dexMeta.xyz!.universe = xyzUniverse().map((x) => ({
      ...x,
      marginTableId: 10,
      growthMode: 'enabled',
      lastFeeScaleChangeTime: '2025-11-23T17:37:10.033211662',
      deployerFeeScale: '1.0',
      ...(x.name === 'xyz:MU' ? { onlyIsolated: true, marginMode: 'noCross' } : {}),
    }));
    env.state.dexMeta.abcd = {
      universe: [{ name: 'abcd:ONE', szDecimals: 3, maxLeverage: 25, onlyIsolated: true, marginMode: 'strictIsolated', isDelisted: true }],
    };
    expect(await env.reg.resolve('xyz:MU')).toMatchObject({ assetId: 110015, onlyIsolated: true, maxPxDecimals: 4 });
    expect(await env.reg.lookup('abcd:ONE')).toMatchObject({ status: 'unknown', reason: 'registry_empty' });
    expect(env.count('meta:abcd')).toBe(0);
  });

  it('HIP-3 universe boundary: index 9999 is the last id of the dex block', async () => {
    const env = makeEnv({ crossCheck: false });
    const big = Array.from({ length: 10_000 }, (_, i) => ({ name: `xyz:A${i}`, szDecimals: 0, maxLeverage: 3 }));
    env.state.dexMeta.xyz = { universe: big };
    env.state.perpDexs = [null, { name: 'xyz', assetToStreamingOiCap: [['xyz:A0', '1']] }];
    expect(idOf(await env.reg.lookup('xyz:A9999'))).toBe(119_999);
    expect(await env.reg.lookupByAssetId(119_999)).toMatchObject({ status: 'listed', asset: { coin: 'xyz:A9999' } });
    expect(await env.reg.lookupByAssetId(120_000)).toMatchObject({ status: 'unlisted', reason: 'dex_not_found' });
  });

  it('spot pair and token indices differ from array positions (live spotMeta): ids use the explicit index', async () => {
    const env = makeEnv();
    env.state.spot = {
      tokens: [USDC, PURR, { ...HYPE, index: 478 }],
      universe: [
        { tokens: [1, 0], name: 'PURR/USDC', index: 0, isCanonical: true },
        { tokens: [478, 0], name: '@72', index: 72, isCanonical: false },
      ],
    };
    expect(await env.reg.resolve('@72')).toMatchObject({ assetId: 10072, index: 72, base: 'HYPE' });
    expect(await env.reg.lookupByAssetId(10001)).toMatchObject({ status: 'unlisted', reason: 'not_in_universe' });
  });

  it('empty main universe is valid but lists nothing; a coin miss is unlisted', async () => {
    const env = makeEnv();
    env.state.main.universe = [];
    expect(await env.reg.lookup('BTC')).toMatchObject({ status: 'unlisted', reason: 'not_in_universe' });
    expect(await env.reg.lookupByAssetId(0)).toMatchObject({ status: 'unlisted' });
  });
});
All files