src/assets/registry.test.ts
v0.3.0 · 29.9 KB
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' });
});
});