Skip to content
markpaper

src/rest/activeOrders.ts

v0.2.1 · 4.6 KB

Download file
// Fail-closed decoding of `GET /api/v1/accountActiveOrders?account_index=<N>` (knowledge base: orders.md §6).
//
// Feed it the RAW RESPONSE TEXT: `parseJsonExact` keeps `order_index` as a string, so the identity of
// every order is exact even when `order_id` is missing. Rules:
//   - `orders` not an array -> read error ("did not read" is not "empty");
//   - a broken record (no id, price or remaining not a number) -> the whole read is untrusted;
//   - `remaining_base_amount <= 0` -> skipped (nothing live);
//   - an order on a market_index unknown to the meta is KEPT with `symbol: undefined`: the caller must
//     warn, not skip silently - code keyed by symbol cannot cancel what it does not see.

import { exactOrderId, type OrderIdPrecision, orderIdPrecision, parseJsonExact } from '../ids/exact.js';
import { LighterReadError } from './errors.js';

export interface LighterActiveOrder {
  /** Exact id string; the value that goes into a cancel. */
  orderId: string;
  /** `exact` unless the payload was parsed without `parseJsonExact` and `order_id` was missing. */
  orderIdPrecision: OrderIdPrecision;
  /** `client_order_index` as a number, or null when absent. */
  clientOrderIndex: number | null;
  marketIndex: number;
  /** Symbol from the caller's meta map; `undefined` when the market is unknown to it. */
  symbol: string | undefined;
  isAsk: boolean;
  isBuy: boolean;
  /** On-grid price string as returned. Compare prices by this string. */
  priceStr: string;
  price: number;
  /** LIVE remainder (`remaining_base_amount`); compare intended sizes with this, not with the initial size. */
  remaining: number;
  initial: number;
  reduceOnly: boolean;
  status: string;
}

export interface ParseActiveOrdersOptions {
  /** `market_id` -> symbol from `markets.symbolByMarketId`. Without it every `symbol` is `undefined`. */
  symbolByMarketId?: ReadonlyMap<number, string>;
}

const num = (v: unknown): number => {
  if (typeof v === 'number') return v;
  if (typeof v === 'string' && v.trim() !== '') return Number(v);
  return Number.NaN;
};

function isRecord(v: unknown): v is Record<string, unknown> {
  return typeof v === 'object' && v !== null && !Array.isArray(v);
}

function preview(v: unknown): string {
  try {
    return JSON.stringify(v).slice(0, 160);
  } catch {
    return String(v);
  }
}

/**
 * Decodes the active-orders response. Pass the response TEXT whenever you have it; an already parsed
 * object is accepted too, but then ids above 2^53 without a string `order_id` are rejected as broken
 * (they were rounded by `JSON.parse` and cannot be cancelled).
 */
export function parseActiveOrders(
  payloadOrText: string | unknown,
  opts: ParseActiveOrdersOptions = {},
): LighterActiveOrder[] {
  const payload: unknown = typeof payloadOrText === 'string' ? parseJsonExact(payloadOrText) : payloadOrText;
  const orders = isRecord(payload) ? payload.orders : undefined;
  if (!Array.isArray(orders))
    throw new LighterReadError('accountActiveOrders: no orders array (a read error, not an empty book)');
  const out: LighterActiveOrder[] = [];
  for (const o of orders) {
    if (!isRecord(o)) throw new LighterReadError(`accountActiveOrders: non-object order ${preview(o)}`);
    const precision = orderIdPrecision(o);
    const orderId = exactOrderId(o);
    const priceStr =
      typeof o.price === 'string' ? o.price.trim() : Number.isFinite(num(o.price)) ? String(o.price) : '';
    const price = num(priceStr);
    const remaining = num(o.remaining_base_amount);
    const marketIndex = num(o.market_index);
    if (orderId === null || !Number.isFinite(price) || !Number.isFinite(remaining) || !Number.isInteger(marketIndex)) {
      throw new LighterReadError(`accountActiveOrders: broken order (${precision} id) ${preview(o)}`);
    }
    if (remaining <= 0) continue;
    const initial = num(o.initial_base_amount);
    const coi = num(o.client_order_index);
    const isAsk = o.is_ask === true;
    out.push({
      orderId,
      orderIdPrecision: precision,
      clientOrderIndex: Number.isFinite(coi) ? coi : null,
      marketIndex,
      symbol: opts.symbolByMarketId?.get(marketIndex),
      isAsk,
      isBuy: !isAsk,
      priceStr,
      price,
      remaining,
      initial: Number.isFinite(initial) ? initial : remaining,
      reduceOnly: o.reduce_only === true,
      status: typeof o.status === 'string' ? o.status : '',
    });
  }
  return out;
}

/** Orders whose market the meta does not know: warn about them, code keyed by symbol cannot cancel them. */
export function ordersOnUnknownMarkets(orders: readonly LighterActiveOrder[]): LighterActiveOrder[] {
  return orders.filter((o) => o.symbol === undefined);
}
All files