Skip to content
markpaper

src/markets/exchange.ts

v0.1.0 · 3.9 KB

Download file
import { isBase58Pubkey } from '../address/base58.js';
import { GLOBAL_CONFIGURATION } from '../onchain/global.js';
export interface ExchangeMarket {
  symbol: string;
  assetId: number;
  status: string;
  marketPubkey: string;
  splinePubkey: string;
  tickSize: string;
  baseLotsDecimals: number;
  isolatedOnly: boolean;
}
function reqAddr(v: unknown, name: string): string {
  const a = typeof v === 'string' && isBase58Pubkey(v) ? v : null;
  if (!a) throw new Error(`${name}: expected a canonical 32-byte base58 public key`);
  return a;
}
export function decodeExchangeView(json: unknown) {
  if (!json || typeof json !== 'object' || Array.isArray(json))
    throw new Error('Exchange view must be an object with a nonempty market list');
  const payload = json as Record<string, any>;
  const k = payload.keys;
  if (!k || typeof k !== 'object') throw new Error('Exchange view keys missing');
  const globalConfig = reqAddr(k.globalConfig, 'keys.globalConfig');
  if (globalConfig !== GLOBAL_CONFIGURATION)
    throw new Error(
      `REST keys.globalConfig ${globalConfig} differs from the Phoenix mainnet configuration ${GLOBAL_CONFIGURATION}`,
    );
  const perpAssetMap = reqAddr(k.perpAssetMap, 'keys.perpAssetMap');
  const arr = (v: unknown, name: string): string[] => {
    if (!Array.isArray(v) || v.length === 0 || v.length > 16)
      throw new Error(`${name}: expected between 1 and 16 public keys`);
    return v.map((x: unknown, i: number) => reqAddr(x, `${name}[${i}]`));
  };
  const keys = {
    globalConfig,
    perpAssetMap,
    globalTraderIndex: arr(k.globalTraderIndex, 'keys.globalTraderIndex'),
    activeTraderBuffer: arr(k.activeTraderBuffer, 'keys.activeTraderBuffer'),
  };
  if (!Array.isArray(payload.markets) || payload.markets.length === 0)
    throw new Error('Exchange view must be an object with a nonempty market list');
  const markets = new Map<string, ExchangeMarket>();
  const skipped: Array<{
    symbol: string;
    why: string;
  }> = [];
  for (const m of payload.markets) {
    const symbol = m?.symbol;
    if (typeof symbol !== 'string' || !/^[A-Za-z0-9._-]{1,32}$/.test(symbol)) {
      skipped.push({ symbol: String(symbol).slice(0, 40), why: 'Invalid market symbol' });
      continue;
    }
    if (markets.has(symbol)) throw new Error('Duplicate exchange market symbol');
    try {
      const tick = m.tickSize;
      const tickOk =
        (typeof tick === 'number' && Number.isSafeInteger(tick) && tick > 0) ||
        (typeof tick === 'string' && /^[1-9]\d{0,19}$/.test(tick));
      if (!tickOk) throw new Error(`tickSize ${tick}`);
      const bld = m.baseLotsDecimals;
      if (!Number.isInteger(bld) || bld < -12 || bld > 18) throw new Error(`baseLotsDecimals ${bld}`);
      if (typeof m.isolatedOnly !== 'boolean') throw new Error('isolatedOnly');
      if (typeof m.marketStatus !== 'string') throw new Error('marketStatus');
      markets.set(symbol, {
        symbol,
        assetId: m.assetId,
        status: m.marketStatus,
        marketPubkey: reqAddr(m.marketPubkey, 'marketPubkey'),
        splinePubkey: reqAddr(m.splinePubkey, 'splinePubkey'),
        tickSize: String(tick),
        baseLotsDecimals: bld,
        isolatedOnly: m.isolatedOnly,
      });
    } catch (e) {
      skipped.push({ symbol, why: (e as Error).message });
    }
  }
  if (markets.size === 0) throw new Error('Exchange view must be an object with a nonempty market list');
  return { keys, markets, skipped };
}
export function pinMarkets(pins: Map<string, ExchangeMarket>, markets: Map<string, ExchangeMarket>): string[] {
  const p: string[] = [];
  for (const [symbol, m] of markets) {
    const old = pins.get(symbol);
    if (
      old &&
      (old.marketPubkey !== m.marketPubkey ||
        old.splinePubkey !== m.splinePubkey ||
        old.tickSize !== m.tickSize ||
        old.baseLotsDecimals !== m.baseLotsDecimals)
    )
      p.push('Market pin conflict: ' + symbol);
    else if (!old) pins.set(symbol, { ...m });
  }
  return p;
}
All files