Skip to content
markpaper

src/account/snapshot.ts

v0.2.0 · 4 KB

Download file
// Fenced own-account snapshot: positions -> orders -> positions.
//
// If any non-zero amount changed between the two `subaccount_info` reads, something filled during the
// read and order sizes computed from the snapshot are untrustworthy (`stable = false`).
// Positions and account value come from the SECOND read. On Nado the fence costs weight 2 per read,
// there is no reason to skip it.

import type { PerpProduct } from '../markets/types.js';
import type { Hex } from '../signing/types.js';
import type { QueryRequester } from '../transport/types.js';
import { type NadoOpenOrder, readOpenOrders, type SweepClock } from './orders.js';
import { amountsUnchanged, nonZeroAmounts, parseSubaccountInfo, type SubaccountInfo } from './parse.js';
import { type NadoPosition, positionsFromInfo } from './positions.js';

/** Options of {@link readAccountSnapshot}. */
export interface AccountSnapshotOptions {
  query: QueryRequester;
  /** bytes32 subaccount. */
  sender: Hex;
  byProductId: ReadonlyMap<number, PerpProduct>;
  /**
   * Working set of products: traded markets, markets with open orders, non-zero balances.
   * Non-zero balances from the first read are always added.
   */
  interestProductIds?: Iterable<number>;
  /**
   * When given and `due(sender)` is true, the read covers the whole universe and `ordersComplete`
   * becomes true; the window is spent (`mark`) only when the snapshot is usable (stable and positions ok).
   * Pass it only from the caller that CONSUMES completeness.
   */
  sweepClock?: SweepClock;
  /** Force a full sweep regardless of the clock. */
  fullSweep?: boolean;
  /** Nonce tag. */
  tag?: number;
  /** Caller-selected products per `orders` request. */
  chunkSize: number;
}

/** Fenced snapshot. */
export interface AccountSnapshot {
  /** Second `subaccount_info` read. */
  readonly info: SubaccountInfo;
  readonly accountValue: number;
  readonly positions: readonly NadoPosition[];
  /** False when a non-zero balance could not be decoded (unknown product / no oracle). */
  readonly positionsOk: boolean;
  readonly orders: readonly NadoOpenOrder[];
  readonly ownOrders: readonly NadoOpenOrder[];
  readonly foreignOrders: readonly NadoOpenOrder[];
  /** No amount changed across the order read AND positions decoded. */
  readonly stable: boolean;
  /** The order read covered every product of the universe. */
  readonly ordersComplete: boolean;
  readonly productIds: readonly number[];
}

/** Reads `subaccount_info`, `orders` (chunked), `subaccount_info` again and applies the stability fence. */
export async function readAccountSnapshot(options: AccountSnapshotOptions): Promise<AccountSnapshot> {
  const { query, sender, byProductId } = options;
  const info1 = parseSubaccountInfo(
    await query({ type: 'subaccount_info', subaccount: sender }, { label: 'subaccount_info' }),
  );

  const sweep = options.fullSweep === true || options.sweepClock?.due(sender) === true;
  const ids = new Set<number>(options.interestProductIds ?? []);
  for (const pid of nonZeroAmounts(info1).keys()) ids.add(pid);
  for (const id of [...ids]) if (!byProductId.has(id)) ids.delete(id); // a balance on an unknown product is reported by positionsFromInfo
  const read = await readOpenOrders({
    query,
    sender,
    byProductId,
    productIds: sweep ? 'all' : ids,
    tag: options.tag,
    chunkSize: options.chunkSize,
  });

  const info2 = parseSubaccountInfo(
    await query({ type: 'subaccount_info', subaccount: sender }, { label: 'subaccount_info' }),
  );
  const unchanged = amountsUnchanged(info1, info2);
  const { positions, ok: positionsOk } = positionsFromInfo(info2, byProductId);
  const stable = unchanged && positionsOk;
  if (sweep && options.sweepClock && stable) options.sweepClock.mark(sender);

  return {
    info: info2,
    accountValue: info2.accountValue,
    positions,
    positionsOk,
    orders: read.orders,
    ownOrders: read.ours,
    foreignOrders: read.foreign,
    stable,
    ordersComplete: read.ordersComplete,
    productIds: read.productIds,
  };
}
All files