Skip to content
markpaper

src/rest/account.test.ts

v0.2.1 · 4.4 KB

Download file
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);
  });
});
All files