src/rest/account.test.ts
v0.2.1 · 4.4 KB
import { describe, expect, it } from 'vitest';
import { marginIdentity } from '../numbers/margin.js';
import { accountPayload, FIXTURE_L1_ADDRESS } from '../testing/fixtures.js';
import { accountOwnerMatches, parseAccount, signedPosition } from './account.js';
import { LighterReadError } from './errors.js';
describe('signedPosition', () => {
it('takes the sign from `sign`, never from `position`', () => {
expect(signedPosition(1, '0.6230')).toBe(0.623);
expect(signedPosition(-1, '27.73')).toBe(-27.73);
expect(signedPosition('-1', '5')).toBe(-5);
expect(signedPosition(1, '-5')).toBe(5); // magnitude only
expect(Number.isNaN(signedPosition('x', '1'))).toBe(true);
});
});
describe('parseAccount', () => {
it('decodes equity, owner and signed positions, skipping flat ones', () => {
const a = parseAccount(accountPayload());
expect(a.index).toBe(12345);
expect(a.l1Address).toBe(FIXTURE_L1_ADDRESS);
expect(a.totalAssetValue).toBe(3000);
expect(a.availableBalance).toBe(1920);
expect(a.crossInitialMarginRequirement).toBe(1080);
expect(a.totalOrderCount).toBe(3);
expect([...a.positions.keys()]).toEqual(['ETH', 'CRWV', 'LIT']);
expect(a.positions.get('CRWV')).toMatchObject({
size: -27.73,
sign: -1,
magnitude: 27.73,
marketId: 41,
entryPrice: 90.1,
positionValue: 2500,
unrealizedPnl: -1.4,
initialMarginFractionPercent: '20.00',
leverage: 5,
marginMode: 'cross',
});
expect(a.positions.get('ETH')?.leverage).toBe(2);
expect(a.positions.get('LIT')?.leverage).toBe(10);
});
it('the fixture satisfies the margin identity with the percent scale', () => {
const a = parseAccount(accountPayload());
const r = marginIdentity({
positions: [...a.positions.values()].map((p) => ({
positionValue: p.positionValue,
initialMarginFractionPercent: p.initialMarginFractionPercent,
})),
crossInitialMarginRequirement: a.crossInitialMarginRequirement,
totalAssetValue: a.totalAssetValue,
availableBalance: a.availableBalance,
});
expect(r.holds).toBe(true);
expect(r.expected).toBe(1080);
});
it('fails closed on an empty accounts array (a read error, not an empty account)', () => {
expect(() => parseAccount({ accounts: [] })).toThrow(LighterReadError);
expect(() => parseAccount({})).toThrow(LighterReadError);
expect(() => parseAccount(null)).toThrow(LighterReadError);
});
it('fails closed on NaN equity, a position without symbol, broken sign, duplicate symbol', () => {
const noEquity = accountPayload();
(noEquity.accounts as Array<Record<string, unknown>>)[0] = {
...(noEquity.accounts as Array<Record<string, unknown>>)[0],
total_asset_value: 'n/a',
};
expect(() => parseAccount(noEquity)).toThrow(/total_asset_value/);
const noSymbol = accountPayload();
const acc = (noSymbol.accounts as Array<Record<string, unknown>>)[0] as Record<string, unknown>;
(acc.positions as Array<Record<string, unknown>>).push({ sign: 1, position: '1' });
expect(() => parseAccount(noSymbol)).toThrow(/without symbol/);
const badSign = accountPayload();
const acc2 = (badSign.accounts as Array<Record<string, unknown>>)[0] as Record<string, unknown>;
(acc2.positions as Array<Record<string, unknown>>).push({ symbol: 'Z', sign: 'up', position: '1' });
expect(() => parseAccount(badSign)).toThrow(/broken sign/);
const dup = accountPayload();
const acc3 = (dup.accounts as Array<Record<string, unknown>>)[0] as Record<string, unknown>;
(acc3.positions as Array<Record<string, unknown>>).push({ symbol: 'ETH', sign: 1, position: '1' });
expect(() => parseAccount(dup)).toThrow(/duplicate position/);
});
it('accepts an account without positions', () => {
const a = parseAccount({
accounts: [{ index: 1, l1_address: FIXTURE_L1_ADDRESS.toUpperCase(), total_asset_value: '0' }],
});
expect(a.positions.size).toBe(0);
expect(a.l1Address).toBe(FIXTURE_L1_ADDRESS);
});
it('accountOwnerMatches compares lower-cased and rejects an empty expectation', () => {
const a = parseAccount(accountPayload());
expect(accountOwnerMatches(a, FIXTURE_L1_ADDRESS.toUpperCase())).toBe(true);
expect(accountOwnerMatches(a, '')).toBe(false);
const otherAddress = `${FIXTURE_L1_ADDRESS.slice(0, -1)}1`; // zero address with the last nibble changed
expect(accountOwnerMatches(a, otherAddress)).toBe(false);
});
});