src/account/snapshot.test.ts
v0.2.0 · 5.4 KB
import { describe, expect, it } from 'vitest';
import { decimalToX18 } from '../numbers/x18.js';
import type { Hex } from '../signing/types.js';
import type { QueryRequest, QueryRequester } from '../transport/types.js';
import { ordersData, rawOrder, subaccountInfoPayload, UNIVERSE } from './fixtures.js';
import { createSweepClock } from './orders.js';
import { readAccountSnapshot } from './snapshot.js';
const SENDER = `0x${'0'.repeat(64)}` as Hex;
function stubQuery(infos: Array<Record<string, unknown>>, orders: unknown[] = [rawOrder()]) {
const bodies: QueryRequest[] = [];
let infoCalls = 0;
const query: QueryRequester = async <T>(body: QueryRequest) => {
bodies.push(body);
if (body.type === 'subaccount_info') return infos[Math.min(infoCalls++, infos.length - 1)] as T;
if (body.type === 'orders') return ordersData(orders) as T;
throw new Error(`unexpected ${body.type}`);
};
return { query, bodies };
}
describe('readAccountSnapshot', () => {
it('reads positions -> orders -> positions, takes state from the second read, stable when amounts match', async () => {
const { query, bodies } = stubQuery([subaccountInfoPayload(), subaccountInfoPayload()]);
const snap = await readAccountSnapshot({
query,
sender: SENDER,
byProductId: UNIVERSE.byProductId,
chunkSize: 40,
interestProductIds: [8],
});
expect(bodies.map((b) => b.type)).toEqual(['subaccount_info', 'orders', 'subaccount_info']);
expect(bodies[0]).toMatchObject({ subaccount: SENDER });
// working set = interest (8) + non-zero balances (2); no sweep
expect(new Set(bodies[1]?.product_ids as number[])).toEqual(new Set([2, 8]));
expect(snap.stable).toBe(true);
expect(snap.positionsOk).toBe(true);
expect(snap.ordersComplete).toBe(false);
expect(snap.accountValue).toBe(1234.5);
expect(snap.positions.map((p) => p.coin)).toEqual(['BTC']);
expect(snap.ownOrders).toHaveLength(1);
expect(snap.foreignOrders).toHaveLength(0);
expect(snap.orders).toHaveLength(1);
});
it('is unstable when an amount changed between the reads', async () => {
const after = subaccountInfoPayload({
perp_balances: [{ product_id: 2, balance: { amount: decimalToX18('0.6').toString(), v_quote_balance: '0' } }],
});
const { query } = stubQuery([subaccountInfoPayload(), after]);
const snap = await readAccountSnapshot({ query, sender: SENDER, byProductId: UNIVERSE.byProductId, chunkSize: 40 });
expect(snap.stable).toBe(false);
expect(snap.positions[0]?.size).toBe(0.6); // second read
});
it('is unstable (positionsOk=false) on a degraded position and drops unknown ids from the order read', async () => {
const withUnknown = subaccountInfoPayload({
perp_balances: [{ product_id: 777, balance: { amount: '1000000000000000000', v_quote_balance: '0' } }],
});
const { query, bodies } = stubQuery([withUnknown, withUnknown], []);
const snap = await readAccountSnapshot({
query,
sender: SENDER,
byProductId: UNIVERSE.byProductId,
chunkSize: 40,
interestProductIds: [2],
});
expect(bodies[1]?.product_ids).toEqual([2]);
expect(snap.positionsOk).toBe(false);
expect(snap.stable).toBe(false);
});
it('full sweep: covers the universe, ordersComplete=true, spends the window only on a usable snapshot', async () => {
let t = 0;
const clock = createSweepClock({ now: () => t });
const unstable = stubQuery([
subaccountInfoPayload(),
subaccountInfoPayload({ perp_balances: [{ product_id: 2, balance: { amount: '1', v_quote_balance: '0' } }] }),
]);
const s1 = await readAccountSnapshot({
query: unstable.query,
sender: SENDER,
byProductId: UNIVERSE.byProductId,
chunkSize: 40,
sweepClock: clock,
});
expect(unstable.bodies[1]?.product_ids).toEqual([2, 8, 188]);
expect(s1.ordersComplete).toBe(true);
expect(s1.stable).toBe(false);
expect(clock.due(SENDER)).toBe(true); // window kept for the next tick
const stable = stubQuery([subaccountInfoPayload(), subaccountInfoPayload()]);
const s2 = await readAccountSnapshot({
query: stable.query,
sender: SENDER,
byProductId: UNIVERSE.byProductId,
chunkSize: 40,
sweepClock: clock,
});
expect(s2.ordersComplete).toBe(true);
expect(s2.stable).toBe(true);
expect(clock.due(SENDER)).toBe(false);
const s3 = await readAccountSnapshot({
query: stable.query,
sender: SENDER,
byProductId: UNIVERSE.byProductId,
chunkSize: 40,
sweepClock: clock,
});
expect(s3.ordersComplete).toBe(false);
t = 300_000;
const s4 = await readAccountSnapshot({
query: stable.query,
sender: SENDER,
byProductId: UNIVERSE.byProductId,
chunkSize: 40,
sweepClock: clock,
});
expect(s4.ordersComplete).toBe(true);
const forced = await readAccountSnapshot({
query: stable.query,
sender: SENDER,
byProductId: UNIVERSE.byProductId,
chunkSize: 40,
fullSweep: true,
});
expect(forced.ordersComplete).toBe(true);
});
it('propagates read failures (never an empty snapshot)', async () => {
const query: QueryRequester = async () => {
throw new Error('503');
};
await expect(
readAccountSnapshot({ query, sender: SENDER, byProductId: UNIVERSE.byProductId, chunkSize: 40 }),
).rejects.toThrow('503');
});
});