src/rest/activeOrders.ts
v0.2.1 · 4.6 KB
// 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);
}