src/account/read.ts
v0.1.0 · 4.9 KB
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 };