src/core.test.ts
v0.1.0 · 16.8 KB
import { randomBytes } from 'node:crypto';
import { describe, expect, it } from 'vitest';
import * as kit from './index.js';
import {
delegate,
exchangeKeys,
globalBytes,
headerBytes,
market,
memoryPolicy,
owner,
pda,
restPolicy,
state,
token,
view,
} from './testing/fixtures.js';
describe('public API', () => {
it('exposes each namespace without silent collisions', () => {
for (const n of [
'account',
'address',
'binding',
'ids',
'margin',
'markets',
'numbers',
'onchain',
'ops',
'orders',
'pending',
'rest',
'signer',
] as const) {
for (const [k, v] of Object.entries(kit[n])) expect((kit as Record<string, unknown>)[k]).toBe(v);
}
});
it('keeps protocol limits visible', () => {
expect(kit.MAX_TX_BYTES).toBe(1232);
expect(kit.MAX_CANCEL_IDS).toBe(30);
expect(kit.MAX_LIMIT_ORDERS_PER_SIDE).toBe(64);
});
});
describe('numbers and addresses', () => {
it.each(['0', '42', (1n << 63n).toString(), kit.U64_MAX.toString()])('parses exact u64 %s', (s) =>
expect(kit.parseU64(s)).toBe(BigInt(s)),
);
it.each([1, 1n, '-1', '01', '1.1', (1n << 64n).toString(), null])('rejects noncanonical u64', (s) =>
expect(kit.parseU64(s)).toBeNull(),
);
it('parses signed bounds and rejects overflowing signed strings', () => {
expect(kit.parseI64(kit.I64_MIN.toString())).toBe(kit.I64_MIN);
expect(kit.parseI64(kit.I64_MAX.toString())).toBe(kit.I64_MAX);
expect(kit.parseI64((kit.I64_MAX + 1n).toString())).toBeNull();
expect(kit.parseI64('-0')).toBeNull();
expect(kit.parseI64(5)).toBeNull();
});
it('round-trips random byte arrays including leading zeroes', () => {
for (let i = 0; i < 100; i++) {
const bytes = randomBytes((i % 65) + 1);
bytes[0] = 0;
expect(kit.base58Decode(kit.base58Encode(bytes))).toEqual(new Uint8Array(bytes));
}
});
it('validates canonical keys and signatures by byte length', () => {
expect(kit.isBase58Pubkey(owner)).toBe(true);
expect(kit.isBase58Pubkey('1' + owner)).toBe(false);
expect(kit.isBase58Pubkey(kit.base58Encode(randomBytes(31)))).toBe(false);
expect(kit.isBase58Pubkey(kit.base58Encode(randomBytes(33)))).toBe(false);
expect(kit.base58Decode('0')).toBeNull();
expect(kit.isSignatureString(kit.base58Encode(randomBytes(64)))).toBe(true);
});
it('quantizes ticks and lots without floating truncation', () => {
const q = kit.createQuant({ tickSize: 100n, baseLotsDecimals: 4 });
expect(q.tickUsd).toBe(1);
expect(q.pxToTicks(801.5, 'nearest')).toBe(802n);
expect(q.pxToTicks(801.5, 'floor')).toBe(801n);
expect(q.pxToTicks(801.5, 'ceil')).toBe(802n);
expect(q.lotsToStr(-5n)).toBe('-0.0005');
expect(q.sizeToLots(1, 'floor')).toBe(10000n);
expect(q.pxToStr(NaN)).toBe('0');
});
it('applies epsilon to calculated sizes and supports negative lot decimals', () => {
const q = kit.createQuant({ tickSize: 10n, baseLotsDecimals: 2 });
expect(q.sizeToLots(0.29, 'floor')).toBe(29n);
expect(q.sizeToLots(0.30000000000000004, 'ceil')).toBe(30n);
expect(q.sizeToLots(0.29999999999999993, 'floor')).toBe(30n);
const bulk = kit.createQuant({ tickSize: 10n, baseLotsDecimals: -2 });
expect(bulk.lot).toBe(100);
expect(bulk.ticksToStr(1n)).toBe('0.0000001');
expect(bulk.lotsToStr(2n)).toBe('200');
});
it('rejects invalid quantizer parameters', () => {
expect(() => kit.createQuant({ tickSize: 0n, baseLotsDecimals: 2 })).toThrow();
expect(() => kit.createQuant({ tickSize: 1n, baseLotsDecimals: 19 })).toThrow();
});
it('preserves compound order identity above double precision', () => {
const seq = ((1n << 63n) + 7n).toString();
expect(kit.isValidOrderId({ symbol: 'BTC', priceTicks: '800', seq })).toBe(true);
expect(kit.isBidSeq(seq)).toBe(true);
expect(kit.orderKey('BTC', '800', seq)).not.toBe(kit.orderKey('ETH', '800', seq));
expect(kit.isValidOrderId({ symbol: 'BTC', priceTicks: '0', seq })).toBe(false);
});
});
describe('markets', () => {
it('accepts both payload forms and negative lot decimals', () => {
expect(kit.decodeMarkets([market()]).size).toBe(1);
const m = { ...market('SYNTH', 2), baseLotsDecimals: -2 };
expect(kit.decodeMarkets({ markets: [m] }).get('SYNTH')?.baseLotsDecimals).toBe(-2);
});
it.each([
{ tickSize: 0 },
{ baseLotsDecimals: 99 },
{ isolatedOnly: undefined },
{ leverageTiers: [] },
{ assetId: -1 },
{ marketStatus: '' },
])('fails the whole invalid payload', (patch) =>
expect(() => kit.decodeMarkets([{ ...market(), ...patch }])).toThrow(),
);
it('rejects duplicate symbols and ids', () => {
expect(() => kit.decodeMarkets([market(), market()])).toThrow();
expect(() => kit.decodeMarkets([market(), market('ETH', 1)])).toThrow();
});
it('distinguishes paused from delisted and retains isolated restrictions', () => {
const m = kit.decodeMarkets([market()]).get('BTC')!;
expect(kit.tradability({ ...m, status: 'paused' })).toBe('unknown');
expect(kit.tradability({ ...m, status: 'closed' })).toBe('not_tradable');
expect(kit.tradability({ ...m, status: 'postOnly' })).toBe('tradable');
expect(kit.decodeMarkets([{ ...market(), isolatedOnly: true }]).get('BTC')?.isolatedOnly).toBe(true);
});
it('uses only fresh, unexpired after-hours bands', () => {
const m = kit
.decodeMarkets([
{
...market(),
commodityMetadata: {
isAfterHours: true,
executionPriceBand: { min: { ui: '10' }, max: { ui: '20' } },
lastIndexExpiryTimestamp: 2000000000,
},
metadata: { calendar: { nextMarketTransitionUtc: '2030-01-01T00:00:00Z' } },
},
])
.get('BTC')!;
expect(kit.afterHoursBand(m, 1000, true)).toEqual({ min: 10, max: 20 }); // value takes precedence over ui in protocol TokenAmount
expect(kit.afterHoursBand(m, 1000, false)).toBeNull();
expect(kit.afterHoursBand({ ...m, afterHours: false }, 1000, true)).toBeNull();
expect(kit.afterHoursBand({ ...m, nextTransitionAt: 10 }, 10, true)).toBeNull();
});
it('keeps broken optional bands separate from core metadata', () => {
expect(
kit
.decodeMarkets([
{
...market(),
commodityMetadata: { isAfterHours: true, executionPriceBand: { min: 'broken', max: 'broken' } },
},
])
.get('BTC')?.executionBand,
).toBeNull();
});
it('freezes an update changing a market in use and retries on caller policy', async () => {
let now = 0;
let raw = market();
const c = kit.createMarketCache({
fetchMarkets: async () => [raw],
ttlMs: 10,
freshForMs: 20,
failedBackoffMs: 5,
now: () => now,
});
c.noteInterest('BTC');
await c.get();
now = 15;
raw = { ...raw, tickSize: 200 };
expect((await c.get()).get('BTC')?.tickSize).toBe(100n);
expect(c.fresh()).toBe(true);
now = 25;
expect(c.fresh()).toBe(false);
});
it('validates exchange keys and refuses TOFU reticks', () => {
const raw = market();
const ex = kit.decodeExchangeView({ keys: exchangeKeys, markets: [raw] });
expect(ex.skipped).toEqual([]);
const pins = new Map();
expect(kit.pinMarkets(pins, ex.markets)).toEqual([]);
expect(kit.pinMarkets(pins, new Map([['BTC', { ...ex.markets.get('BTC')!, tickSize: '200' }]]))).toHaveLength(1);
});
});
describe('account reads', () => {
it('decodes lots exactly and respects address case', () => {
expect(kit.decodeTraderState(state(), { authority: owner, traderPdaIndex: 0 }).positions[0]?.lots).toBe(3n);
expect(() => kit.decodeTraderState(state(), { authority: delegate, traderPdaIndex: 0 })).toThrow();
});
it('accepts omitted empty arrays while refusing malformed arrays', () => {
const s = state('0');
delete (s.snapshot.subaccounts[0] as { positions?: unknown }).positions;
expect(kit.decodeTraderState(s, { authority: owner, traderPdaIndex: 0 }).positions).toEqual([]);
const bad = state();
(bad.snapshot.subaccounts[0] as { positions: unknown }).positions = {};
expect(() => kit.decodeTraderState(bad, { authority: owner, traderPdaIndex: 0 })).toThrow();
});
it('rejects numeric lots and missing cross subaccount', () => {
const s = state();
(s.snapshot.subaccounts[0]!.positions[0] as unknown as { basePositionLots: unknown }).basePositionLots = 3;
expect(() => kit.decodeTraderState(s, { authority: owner, traderPdaIndex: 0 })).toThrow();
const z = state();
z.snapshot.subaccounts[0]!.subaccountIndex = 1;
expect(() => kit.decodeTraderState(z, { authority: owner, traderPdaIndex: 0 })).toThrow();
});
it('rejects closed snapshot rows and side/sequence disagreement', () => {
for (const patch of [
{ change: 'closed' },
{ orderSequenceNumber: '8' },
{ reduceOnly: 'true' },
{ orderSequenceNumber: 8 },
]) {
const s = state();
(s.snapshot.subaccounts[0] as { orders: unknown }).orders = [
{
symbol: 'BTC',
orders: [
{
side: 'bid',
orderSequenceNumber: ((1n << 63n) + 8n).toString(),
priceTicks: '900',
sizeRemainingLots: '3',
reduceOnly: false,
...patch,
},
],
},
];
expect(() => kit.decodeTraderState(s, { authority: owner, traderPdaIndex: 0 })).toThrow();
}
});
it('does not accept numeric TokenAmount ui', () => {
const v = view();
(v.portfolioValue as { ui: unknown }).ui = 72;
expect(() => kit.decodeTraderView(v, { authority: owner, traderPda: pda })).toThrow();
expect(() => kit.decodeTraderView(view(), { authority: owner, traderPda: delegate })).toThrow();
});
it('rejects nonfinite required UI amounts and ignores nonfinite optional margin', () => {
const v = view();
v.portfolioValue.ui = '9'.repeat(400);
expect(() => kit.decodeTraderView(v, { authority: owner, traderPda: pda })).toThrow();
const optional = view();
optional.effectiveCollateral.ui = '9'.repeat(400);
expect(kit.decodeTraderView(optional, { authority: owner, traderPda: pda }).collateralUsd).toBeNull();
const s = state();
s.snapshot.subaccounts[0]!.positions[0]!.entryPriceUsd = '9'.repeat(400);
expect(() => kit.decodeTraderState(s, { authority: owner, traderPdaIndex: 0 })).toThrow();
});
it('rejects duplicate position rows rather than summing or overwriting', () => {
const s = state();
s.snapshot.subaccounts[0]!.positions.push({ ...s.snapshot.subaccounts[0]!.positions[0]! });
expect(() => kit.decodeTraderState(s, { authority: owner, traderPdaIndex: 0 })).toThrow();
const v = view();
v.positions.push({ ...v.positions[0]! });
expect(() => kit.decodeTraderView(v, { authority: owner, traderPda: pda })).toThrow();
});
it('detects a flat snapshot contradicting the intervening equity view', () => {
const flat = kit.decodeTraderState(state('0'), { authority: owner, traderPdaIndex: 0 });
const v = kit.decodeTraderView(view(), { authority: owner, traderPda: pda });
expect(kit.viewContradictsState(flat, flat, v)).not.toBeNull();
expect(kit.viewContradictsState(flat, flat, { ...v, slot: 101 })).toBeNull();
});
it('stable reads fence state, view and state without hidden margin coupling', async () => {
const memory = kit.createWriteMemory(memoryPolicy);
const r = await kit.readStableAccount({
authority: owner,
traderPda: pda,
memory,
catchUpMs: 0,
catchUpGapMs: 1,
rest: { traderState: async () => state(), traderView: async () => view() },
});
expect(r.trusted).toBe(true);
expect(r.equityAvailable).toBe(true);
});
it('retains independently failed equity as explicit missing information', async () => {
const r = await kit.readStableAccount({
authority: owner,
traderPda: pda,
memory: kit.createWriteMemory(memoryPolicy),
catchUpMs: 0,
catchUpGapMs: 1,
rest: {
traderState: async () => state(),
traderView: async () => {
throw new Error('read failed');
},
},
});
expect(r.trusted).toBe(true);
expect(r.equityAvailable).toBe(false);
expect(r.viewError).toBe('read failed');
});
it('converts exact order ids and position sizes without numeric surrogates', () => {
const s = kit.decodeTraderState(state(), { authority: owner, traderPdaIndex: 0 });
expect(kit.positionsToSizes(s, kit.decodeMarkets([market()]))[0]?.sizeString).toBe('0.0003');
const seq = ((1n << 63n) + 5n).toString();
const r = kit.ordersToRows(
[{ symbol: 'BTC', side: 'bid', priceTicks: '900', seq, lots: 3n, reduceOnly: false }],
kit.decodeMarkets([market()]),
);
expect(r[0]?.id.seq).toBe(seq);
});
});
describe('on-chain and binding checks', () => {
it('checks program ownership, discriminator and header length', () => {
expect(kit.parseTraderHeader(headerBytes(), kit.PROGRAM_ADDRESS).ok).toBe(true);
expect(kit.parseTraderHeader(headerBytes(), owner).ok).toBe(false);
expect(kit.parseTraderHeader(new Uint8Array(12), kit.PROGRAM_ADDRESS).ok).toBe(false);
const bad = headerBytes();
bad[0] = bad[0]! ^ 1;
expect(kit.parseTraderHeader(bad, kit.PROGRAM_ADDRESS).ok).toBe(false);
});
it('cold and hot flags do not imply delegation loss', () => {
for (const flags of [62, 63]) {
const parsed = kit.parseTraderHeader(headerBytes(flags), kit.PROGRAM_ADDRESS);
if (!parsed.ok) throw new Error(parsed.why);
expect(kit.headerProblems(parsed.header, { authority: owner, delegate, traderPda: pda })).toEqual([]);
expect(kit.traderRestrictions(kit.describeCapabilities(flags))).toEqual([]);
}
});
it('frozen state preserves a valid binding and reports restrictions', () => {
const parsed = kit.parseTraderHeader(headerBytes(4), kit.PROGRAM_ADDRESS);
if (!parsed.ok) throw new Error(parsed.why);
expect(kit.headerDelegationProblems(parsed.header, { authority: owner, delegate, traderPda: pda })).toEqual([]);
expect(kit.isFrozen(4)).toBe(true);
expect(kit.traderRestrictions(kit.describeCapabilities(4))).toContain('Account frozen');
});
it('decodes global config and matches REST keys', () => {
const gc = kit.parseGlobalConfig(globalBytes(), kit.PROGRAM_ADDRESS);
expect(gc.ok).toBe(true);
expect(kit.checkViewKeysAgainstChain(exchangeKeys, gc)).toEqual([]);
expect(kit.keyRiskOf(kit.describePreferences(2), gc).positionAuthoritySwap).toBe('closed_by_trader');
expect(kit.keyRiskOf(kit.describePreferences(0), gc).positionAuthoritySwap).toBe('open');
});
it('unknown observations preserve a proof and initial unknown blocks writes', () => {
const g = kit.createBindingGate();
expect(g.writeBlock()).not.toBeNull();
g.note('ok');
g.note('unknown');
expect(g.writeBlock()).toBeNull();
g.note('lost', ['Delegate revoked']);
g.note('unknown');
expect(g.writeBlock()).toContain('Delegate revoked');
g.note('ok');
expect(g.writeBlock()).toBeNull();
});
it('restart hold permits cancels and full exits and releases on zero counts', () => {
const g = kit.createStartupHold();
g.note(1, 0);
expect(g.allows('place')).toBe(false);
expect(g.allows('cancel')).toBe(true);
expect(g.allows('fullClose')).toBe(true);
g.note(0, 0);
expect(g.placementsHeld()).toBe(false);
});
it('reports venue margin rejection and explicit free margin', () => {
expect(kit.isMarginReject('InsufficientFunds')).toBe(true);
expect(kit.freeMarginUsd({ collateralUsd: 70, initialMarginUsd: 4 })).toBe(66);
expect(kit.freeMarginUsd({ collateralUsd: null, initialMarginUsd: 4 })).toBeNull();
});
});
describe('read transport', () => {
it('never retries other 4xx', async () => {
let n = 0;
const c = kit.createRestClient({
...restPolicy,
fetch: async () => {
n++;
return new Response('{}', { status: 403 });
},
});
await expect(c.markets()).rejects.toMatchObject({ status: 403 });
expect(n).toBe(1);
});
it('honors shared Retry-After and retries a read', async () => {
let n = 0;
let now = 0;
const c = kit.createRestClient({
...restPolicy,
now: () => now,
sleep: async (ms) => {
now += ms;
},
fetch: async () =>
++n === 1 ? new Response('{}', { status: 429, headers: { 'retry-after': '2' } }) : new Response('[]'),
});
expect(await c.markets()).toEqual([]);
expect(now).toBe(2000);
});
it('uses a mark fallback only after an invalid orderbook mid', async () => {
const c = kit.createRestClient({
...restPolicy,
fetch: async (u) =>
String(u).includes('orderbook')
? new Response('{"mid":null}')
: new Response('{"markets":[{"symbol":"BTC","mark_price":"900"}]}'),
});
expect(await c.mid('BTC')).toBe(900);
});
});