src/account/snapshot.ts
v0.2.0 · 4 KB
// 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,
};
}