Skip to content
markpaper

src/account/read.ts

v0.1.0 · 4.9 KB

Download file
import type { PhoenixMarket } from '../markets/parse.js';
import { createQuant } from '../numbers/quant.js';
import type { EchoRow, WriteMemory } from '../pending/memory.js';
import {
  type DecodedState,
  type DecodedView,
  decodeTraderState,
  decodeTraderView,
  PhoenixReadError,
  type StateOrder,
  type StatePosition,
} from './decode.js';
import { viewContradictsState } from './plausibility.js';

export function positionsToSizes(state: DecodedState, markets: Map<string, PhoenixMarket>) {
  return state.positions.map((p) => {
    const m = markets.get(p.symbol);
    if (!m) throw new PhoenixReadError(`Unknown position market: ${p.symbol}`);
    const q = createQuant(m);
    return {
      ...p,
      size: q.lotsToSize(p.lots),
      sizeString: q.lotsToStr(p.lots),
      entryPrice: p.entryPriceUsd ?? Number(q.ticksToStr(p.entryPriceTicks)),
    };
  });
}
export function ordersToRows(rows: Array<StateOrder | EchoRow>, markets: Map<string, PhoenixMarket>) {
  return rows.map((r) => {
    const m = markets.get(r.symbol);
    if (!m) throw new PhoenixReadError(`Unknown order market: ${r.symbol}`);
    const q = createQuant(m);
    return {
      ...r,
      price: Number(q.ticksToStr(BigInt(r.priceTicks))),
      priceString: q.ticksToStr(BigInt(r.priceTicks)),
      size: q.lotsToSize(r.lots),
      sizeString: q.lotsToStr(r.lots),
      id: { symbol: r.symbol, priceTicks: r.priceTicks, seq: r.seq },
    };
  });
}
export interface AccountReadOptions {
  rest: { traderState(authority: string, index?: number): Promise<unknown>; traderView(pda: string): Promise<unknown> };
  memory: WriteMemory;
  authority: string;
  traderPda: string;
  catchUpMs: number;
  catchUpGapMs: number;
  now?: () => number;
  sleep?: (ms: number) => Promise<void>;
  resolveOutstanding?: () => Promise<void>;
}
export async function readGatedState(opts: AccountReadOptions) {
  if (!(opts.catchUpMs >= 0) || !(opts.catchUpGapMs > 0)) throw new TypeError('Explicit catch-up timings required');
  const now = opts.now ?? Date.now;
  const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
  await opts.resolveOutstanding?.();
  const deadline = now() + opts.catchUpMs;
  let startedAt = now();
  let state = decodeTraderState(await opts.rest.traderState(opts.authority, 0), {
    authority: opts.authority,
    traderPdaIndex: 0,
  });
  let verdict = opts.memory.judgeStateSlot(state.slot, now());
  while (
    !verdict.trusted &&
    opts.catchUpMs > 0 &&
    now() < deadline &&
    opts.memory.ownWriteWithin(opts.catchUpMs, now())
  ) {
    await sleep(Math.min(opts.catchUpGapMs, deadline - now()));
    startedAt = now();
    state = decodeTraderState(await opts.rest.traderState(opts.authority, 0), {
      authority: opts.authority,
      traderPdaIndex: 0,
    });
    verdict = opts.memory.judgeStateSlot(state.slot, now());
  }
  if (!verdict.trusted) return { state, trusted: false, reason: verdict.reason, rows: state.orders };
  opts.memory.noteStateSlot(state.slot, startedAt);
  return { state, trusted: true, reason: null, rows: opts.memory.applyEcho(state.orders, state.slot, now()) };
}
export async function readStableAccount(opts: AccountReadOptions) {
  const first = await readGatedState(opts);
  let view: DecodedView | null = null;
  let viewError: string | null = null;
  try {
    view = decodeTraderView(await opts.rest.traderView(opts.traderPda), {
      authority: opts.authority,
      traderPda: opts.traderPda,
    });
  } catch (e) {
    viewError = (e as Error).message;
  }
  const second = await readGatedState(opts);
  const positionKey = (s: DecodedState) =>
    s.positions
      .filter((p) => p.lots !== 0n)
      .map((p) => `${p.subaccountIndex}:${p.symbol}:${p.lots}`)
      .sort()
      .join('|');
  const stable = first.trusted && second.trusted && positionKey(first.state) === positionKey(second.state);
  const contradiction = stable ? viewContradictsState(first.state, second.state, view) : null;
  const trusted = stable && contradiction === null;
  // Equity is independently fallible; callers must check equityFresh before using it.
  return {
    state: second.state,
    view,
    stable: stable && contradiction === null,
    trusted,
    ordersComplete: second.trusted && contradiction === null,
    rows: second.rows,
    equityAvailable: view !== null,
    reason: contradiction ?? second.reason ?? (!stable ? 'Position changed between reads' : null),
    viewError,
  };
}
export async function readPositionLots(
  opts: AccountReadOptions,
  symbol: string,
): Promise<{ lots: bigint; slot: number } | null> {
  try {
    const r = await readGatedState(opts);
    if (!r.trusted) return null;
    const lots = r.state.positions
      .filter((p) => p.symbol === symbol && p.subaccountIndex === 0)
      .reduce((a, p) => a + p.lots, 0n);
    return { lots, slot: r.state.slot };
  } catch {
    return null;
  }
}
export type { StatePosition };
All files