A claim's stage time is now taken before the request goes out (sentAt): Vest dates it no earlier, so a claim answered just after 20:00 ET no longer puts the fresh Vest day on stage 2 before the history read shows its WITHDRAW. One Vest dates after the turn reads as the day before until then, the cautious way round. Claims saved without it count from their answer. Completions count one per payout on both paths (completionPlace): the catch-up's stage no longer counts a Vest day's two claims as one, and a cycle a day's second claim started carries cycleSeq, so it can be told from that day's first. The master ignores it until it keys on it; old masters and old instances are unchanged. bookHistory's claim bookkeeping (WITHDRAWs, lapse, fund rows, the balance the books check expects) is now the pure readClaims, tested on the case that held an account for good: a claim that never went in, read an hour on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
510 lines
34 KiB
JavaScript
510 lines
34 KiB
JavaScript
// Checks for the Vest client's pure helpers (lib/vest-helpers.ts) and the
|
||
// relayed-session store (lib/vest-session.ts).
|
||
//
|
||
// npm run check
|
||
//
|
||
// Both files import only Node built-ins, so Node strips their types and loads
|
||
// them directly; .mjs keeps this file out of Next's type check. The samples are
|
||
// shaped like the gateway's real answers (captured 2026-10-26).
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {
|
||
isVestWalletAddress, vestAccountId, num, toMs, formatDecimal, vestDayOf, lastVestResetMs, parseCapitalAccount,
|
||
vestAccountName, parsePosition, awaitingLegs, parseOpenOrder, unrealizedPnl, parseTxn, bookTxns, txnKind, VEST_TXN,
|
||
hourlyVolatility, marketOpen, MIN_HOURLY_RETURNS, nextVestResetMs, resetSafeStop, parseHistoryPosition, jwtClaimStrings,
|
||
accountScopedToken, VestApiError, VestNotSentError, nothingWentIn, tokenForRequest, claimWithdrawal, settleClaims,
|
||
claimFundRows, readClaims, payoutDaysOf, payoutsSince,
|
||
} from '../lib/vest-helpers.ts';
|
||
import { putVestSession, vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from '../lib/vest-session.ts';
|
||
|
||
const iso = (s) => Date.parse(s);
|
||
|
||
test('wallet addresses and account ids', () => {
|
||
assert.equal(isVestWalletAddress('0x7ce10771819f80130e204ddb5ca1522e48a9b451'), true);
|
||
assert.equal(isVestWalletAddress('0x7CE10771819F80130E204DDB5CA1522E48A9B451'), true);
|
||
assert.equal(isVestWalletAddress('0x7ce1'), false);
|
||
assert.equal(isVestWalletAddress(''), false);
|
||
const id = vestAccountId('843f60e2-e013-5a0e-8b6c-face84bc5509');
|
||
assert.equal(id, vestAccountId('843F60E2-E013-5A0E-8B6C-FACE84BC5509'));
|
||
assert.notEqual(id, vestAccountId('102566b2-515a-51c5-9ad7-4a7eae0a6a2a'));
|
||
assert.ok(id >= 2 ** 50 && Number.isSafeInteger(id));
|
||
});
|
||
|
||
test('numbers, times and decimals as the gateway writes them', () => {
|
||
assert.equal(num('49968.511115'), 49968.511115);
|
||
assert.equal(num(''), undefined);
|
||
assert.equal(num(null), undefined);
|
||
assert.equal(toMs(1793035089), 1793035089000);
|
||
assert.equal(toMs(1793035089277), 1793035089277);
|
||
assert.equal(toMs('2026-10-26T18:41:02.886Z'), iso('2026-10-26T18:41:02.886Z'));
|
||
assert.ok(Number.isNaN(toMs(undefined)));
|
||
assert.equal(formatDecimal(29554.5, 2), '29554.5');
|
||
assert.equal(formatDecimal(50, 2), '50');
|
||
assert.equal(formatDecimal(3 * 0.0001, 4), '0.0003');
|
||
assert.equal(formatDecimal(42.65449999, 4), '42.6545');
|
||
assert.equal(formatDecimal(1e-7, 4), '0');
|
||
});
|
||
|
||
test("Vest's day turns at 20:00 New York, summer and winter", () => {
|
||
// EDT (UTC−4): 20:00 ET is 00:00 UTC.
|
||
assert.equal(vestDayOf(iso('2026-10-26T23:59:00Z')), '2026-10-26');
|
||
assert.equal(vestDayOf(iso('2026-10-27T00:00:00Z')), '2026-10-27');
|
||
assert.equal(vestDayOf(iso('2026-10-26T22:05:00Z')), '2026-10-26'); // the bots' 6:05 PM ET open
|
||
// EST (UTC−5): 20:00 ET is 01:00 UTC.
|
||
assert.equal(vestDayOf(iso('2026-12-01T00:30:00Z')), '2026-11-30');
|
||
assert.equal(vestDayOf(iso('2026-12-01T01:00:00Z')), '2026-12-01');
|
||
// Month and year ends.
|
||
assert.equal(vestDayOf(iso('2026-12-31T23:00:00Z')), '2026-12-31');
|
||
assert.equal(vestDayOf(iso('2027-01-01T01:30:00Z')), '2027-01-01');
|
||
|
||
assert.equal(lastVestResetMs(iso('2026-10-26T18:00:00Z')), iso('2026-10-26T00:00:00Z'));
|
||
assert.equal(lastVestResetMs(iso('2026-10-27T00:00:00Z')), iso('2026-10-27T00:00:00Z'));
|
||
assert.equal(lastVestResetMs(iso('2026-12-01T00:30:00Z')), iso('2026-11-30T01:00:00Z'));
|
||
assert.equal(lastVestResetMs(iso('2026-12-01T01:00:01Z')), iso('2026-12-01T01:00:00Z'));
|
||
// Across the November switch back (Nov 1 2026, 2 AM ET).
|
||
assert.equal(lastVestResetMs(iso('2026-11-01T12:00:00Z')), iso('2026-11-01T00:00:00Z'));
|
||
assert.equal(lastVestResetMs(iso('2026-11-02T12:00:00Z')), iso('2026-11-02T01:00:00Z'));
|
||
});
|
||
|
||
const evalAccount = {
|
||
payment_id: 'tailoredpay:vest-markets:production:12727557177',
|
||
id: 'e7f69717-bd18-5cb7-9466-673cb283d0b2',
|
||
plan_id: '86bb03f9-7422-513c-8d5c-c546cb8d221d',
|
||
account_type: 2, stage: 1, status: 2,
|
||
initial_capital: '50000.00000000', max_drawdown_pct: '0.060000', max_leverage: '5.0000', max_profit_split_pct: '0.8000',
|
||
target_equity: '55000.00000000', max_drawdown_limit: '47000.00000000', max_daily_loss_pct: '0.030000',
|
||
daily_reset_balance: '50000.00000000000000', daily_reset_equity: '50000.00000000000000',
|
||
daily_reset_base_limit: '1500.000000000000000', daily_loss_floor: '48500.00000000000000', daily_reset_at: 1793035043842,
|
||
origin: 'eval_purchase', plan_name: 'Vest Capital Evaluation Fee | 10% Goal | 3% Daily Loss | 80/20 | Emerald',
|
||
plan_product_type: 'eval', step: 1, steps_total: 1, root_account_id: 'e7f69717-bd18-5cb7-9466-673cb283d0b2',
|
||
stages: [
|
||
{ ordinal: 1, kind: 'eval', initial_capital: '50000.00000000', profit_target: '5000.00000000', max_drawdown_pct: '0.060000' },
|
||
{ ordinal: 2, kind: 'funded', initial_capital: '50000.00000000', profit_target: '', max_drawdown_pct: '0.060000' },
|
||
],
|
||
attempt_index: 34, activated_at: 1793035043834, created_at: 1793035043756, updated_at: 1793035043843,
|
||
};
|
||
|
||
test('capital accounts parse, and are named for their rules', () => {
|
||
const a = parseCapitalAccount(evalAccount);
|
||
assert.equal(a.id, evalAccount.id);
|
||
assert.equal(a.kind, 'eval');
|
||
assert.equal(a.status, 2);
|
||
assert.equal(a.initialCapital, 50000);
|
||
assert.equal(a.profitTarget, 5000);
|
||
assert.equal(a.maxDrawdownLimit, 47000);
|
||
assert.equal(a.dailyLossFloor, 48500);
|
||
assert.equal(a.maxDailyLossPct, 0.03);
|
||
assert.equal(a.dailyResetAt, 1793035043842);
|
||
assert.equal(vestAccountName(a), 'V50K-E-D3-T10-34');
|
||
|
||
assert.equal(vestAccountName(parseCapitalAccount({ ...evalAccount, stage: 2 })), 'V50K-F-D3-34');
|
||
assert.equal(vestAccountName(parseCapitalAccount({ ...evalAccount, max_daily_loss_pct: '0' })), 'V50K-E-D0-T10-34');
|
||
assert.equal(vestAccountName(parseCapitalAccount({
|
||
...evalAccount, initial_capital: '5000.00000000', attempt_index: 12, stage: 2, steps_total: 2,
|
||
stages: [
|
||
{ ordinal: 1, kind: 'eval', profit_target: '500.00000000' },
|
||
{ ordinal: 2, kind: 'eval', profit_target: '250.00000000' },
|
||
{ ordinal: 3, kind: 'funded', profit_target: '' },
|
||
],
|
||
})), 'V5K-E-D3-T5-12');
|
||
assert.equal(vestAccountName(parseCapitalAccount({
|
||
...evalAccount, plan_product_type: 'instant_funded', initial_capital: '25000.00000000', max_daily_loss_pct: '0', attempt_index: 7,
|
||
stages: [{ ordinal: 1, kind: 'funded', profit_target: '' }],
|
||
})), 'V25K-I-7');
|
||
assert.equal(vestAccountName(parseCapitalAccount({
|
||
...evalAccount, plan_product_type: 'instant_funded', initial_capital: '500.00000000', attempt_index: 1,
|
||
stages: [{ ordinal: 1, kind: 'funded', profit_target: '' }],
|
||
})), 'V500-I-1');
|
||
assert.equal(parseCapitalAccount({ nope: true }), null);
|
||
});
|
||
|
||
const openPosition = {
|
||
positionId: 'afe96d7e-f5ac-4b8a-aae8-f5b63b74e508', accountId: '843f60e2-e013-5a0e-8b6c-face84bc5509', accountType: 2,
|
||
symbol: 'NDX-USD-PERP', side: 'short', quantity: '42.6545', openPrice: '29529.250000000', closePrice: '290.250000000',
|
||
openDate: 1793035089277, pnl: '0', fee: '31.488885', funding: '-5.423271000000000000', collateral: '24944.027426', leverage: '50',
|
||
createdAt: 1793035089244, updatedAt: 1793037621104,
|
||
stopLosses: [{ id: 'e22311d2-fbd9-4ab8-b65f-c2921f58fefc', triggerPrice: '29554.500000000', sizeMode: 'full_position', executionType: 'market', status: 'awaiting' }],
|
||
takeProfits: [],
|
||
};
|
||
|
||
test('positions, their legs and unrealized P&L', () => {
|
||
const p = parsePosition(openPosition);
|
||
assert.equal(p.side, 'short');
|
||
assert.equal(p.quantity, 42.6545);
|
||
assert.equal(p.collateral, 24944.027426);
|
||
assert.equal(p.openedAt, 1793035089277);
|
||
assert.equal(awaitingLegs(p).length, 1);
|
||
assert.equal(awaitingLegs({ ...p, stopLosses: [{ ...p.stopLosses[0], status: 'triggered' }] }).length, 0);
|
||
// Short 42.6545 from 29529.25, marked at 29477.50: +51.75 points each.
|
||
assert.ok(Math.abs(unrealizedPnl(p, 29477.5) - 42.6545 * 51.75) < 1e-6);
|
||
assert.equal(unrealizedPnl(p, NaN), 0);
|
||
assert.equal(parsePosition({ ...openPosition, quantity: '0' }), null);
|
||
assert.equal(parseOpenOrder({ order_id: 'o1', account_id: 'a1', position_id: 'p1', symbol: 'GC-PERP', created_at: 1 }).positionId, 'p1');
|
||
assert.equal(parseOpenOrder({ symbol: 'x' }), null);
|
||
});
|
||
|
||
test('transactions book into daily P&L and fund rows by Vest day', () => {
|
||
assert.equal(txnKind(VEST_TXN.FEE), 'trade');
|
||
assert.equal(txnKind(VEST_TXN.REALIZED_PNL), 'trade');
|
||
assert.equal(txnKind(VEST_TXN.FUNDING), 'trade');
|
||
assert.equal(txnKind(VEST_TXN.WITHDRAW), 'fund');
|
||
assert.equal(txnKind(VEST_TXN.FILL_MARGIN), 'margin');
|
||
assert.equal(txnKind(VEST_TXN.ORDER_MARGIN_RELEASE), 'margin');
|
||
|
||
const w = parseTxn({ account_type: 3, action_id: 'a9', amount: '1200', created_at: iso('2026-10-26T15:00:00Z'), transaction_type: 2 });
|
||
assert.equal(w.amount, -1200);
|
||
assert.equal(w.kind, 'fund');
|
||
assert.equal(parseTxn({ amount: 'x', transaction_type: 9, created_at: 1 }), null);
|
||
|
||
const rows = [
|
||
{ action_id: 'd', amount: '50000', created_at: iso('2026-10-20T14:00:00Z'), transaction_type: 1 },
|
||
{ action_id: 'o', amount: '-24944.02', created_at: iso('2026-10-26T17:18:00Z'), transaction_type: 6 },
|
||
{ action_id: 'f1', amount: '-31.49', created_at: iso('2026-10-26T17:18:00Z'), transaction_type: 8 },
|
||
// After 20:00 ET on the 26th: the 27th's day.
|
||
{ action_id: 'r1', amount: '812.5', created_at: iso('2026-10-27T00:30:00Z'), transaction_type: 9 },
|
||
{ action_id: 'f2', amount: '-31.2', created_at: iso('2026-10-27T00:30:00Z'), transaction_type: 8 },
|
||
{ action_id: 'u1', amount: '-5.42', created_at: iso('2026-10-27T01:00:00Z'), transaction_type: 10 },
|
||
].map(parseTxn);
|
||
const { daily, funds } = bookTxns(rows);
|
||
assert.deepEqual([...funds.entries()], [['2026-10-20', 50000]]);
|
||
assert.ok(Math.abs(daily.get('2026-10-26').pnl - -31.49) < 1e-9);
|
||
assert.ok(Math.abs(daily.get('2026-10-27').pnl - (812.5 - 31.2 - 5.42)) < 1e-9);
|
||
assert.ok(Math.abs(daily.get('2026-10-27').fees - 31.2) < 1e-9);
|
||
});
|
||
|
||
const withdrawalsOf = (txns) => txns.filter((t) => t.type === VEST_TXN.WITHDRAW).map((t) => ({ at: t.time, amount: t.amount }));
|
||
const deposit = { action_id: 'd', amount: '50000', created_at: iso('2026-10-20T14:00:00Z'), transaction_type: 1 };
|
||
|
||
test('a claim of unknown outcome that never shows lapses an hour on; one Vest accepted never does', () => {
|
||
const at = iso('2026-10-27T15:00:00Z');
|
||
const lost = { at, amount: 2250, base: 50000, unknown: true };
|
||
// Read just after the request failed: it may yet show, so it holds.
|
||
assert.deepEqual(settleClaims([lost], [], at + 60_000), { pending: [lost], lapsed: [] });
|
||
// Still not shown by a read an hour on: it never went in.
|
||
assert.deepEqual(settleClaims([lost], [], at + 61 * 60_000), { pending: [], lapsed: [lost] });
|
||
// It went in after all: a WITHDRAW of its amount near it settles it.
|
||
assert.deepEqual(settleClaims([lost], [{ at: at + 3_000, amount: -2250 }], at + 61 * 60_000), { pending: [], lapsed: [] });
|
||
assert.equal(claimWithdrawal([{ at: at + 3_000, amount: -2249 }], lost), undefined, 'another amount');
|
||
assert.equal(claimWithdrawal([{ at: at + 31 * 60_000, amount: -2250 }], lost), undefined, 'too far off');
|
||
assert.deepEqual(claimWithdrawal([{ at: at + 20 * 60_000, amount: -2250 }, { at: at - 2_000, amount: -2250 }], lost), { at: at - 2_000, amount: -2250 });
|
||
// A claim Vest accepted holds for as long as it doesn't show.
|
||
const accepted = { at, amount: 2250, base: 50000 };
|
||
assert.deepEqual(settleClaims([accepted], [], at + 48 * 3_600_000), { pending: [accepted], lapsed: [] });
|
||
});
|
||
|
||
test('the books stop counting a claim that never went in once it lapses, so it holds the account no longer', () => {
|
||
const at = iso('2026-10-27T15:00:00Z');
|
||
const lost = { at, sentAt: at - 20_000, amount: 2250, base: 50150, unknown: true };
|
||
const txns = [deposit, { action_id: 'r1', amount: '2400', created_at: at - 3_600_000, transaction_type: 9 }].map(parseTxn);
|
||
const read = (claims, readAt) => readClaims({ txns, funds: bookTxns(txns).funds, claims, stored: [], readAt, opening: 0 });
|
||
// It never went in: the balance is what the transactions add up to.
|
||
const cash = 52400;
|
||
// Half an hour on it may yet show, so the books still take it out (and hold).
|
||
const early = read([lost], at + 30 * 60_000);
|
||
assert.deepEqual([early.lapsed, early.outstanding], [[], [lost]]);
|
||
assert.equal(cash - early.expected, 2250);
|
||
// A read an hour on lapses it, and the books agree again.
|
||
const late = read([lost], at + 61 * 60_000);
|
||
assert.deepEqual([late.lapsed, late.outstanding], [[lost], []]);
|
||
assert.equal(late.expected, cash);
|
||
assert.deepEqual(late.rows, [{ date: '2026-10-20', amount: 50000 }]);
|
||
// One Vest accepted that doesn't show yet: out of the balance and on its
|
||
// day's row, however long it takes.
|
||
const accepted = { at, sentAt: at - 20_000, amount: 2250, base: 50150 };
|
||
const waiting = read([accepted], at + 48 * 3_600_000);
|
||
assert.deepEqual([waiting.lapsed, waiting.outstanding], [[], [accepted]]);
|
||
assert.equal(waiting.expected, cash - 2250);
|
||
assert.deepEqual(waiting.rows, [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-27', amount: -2250 }]);
|
||
// Once it shows, the WITHDRAW itself is in the sum.
|
||
const shown = [...txns, parseTxn({ action_id: 'w1', amount: '2250', created_at: at + 2_000, transaction_type: 2 })];
|
||
const settled = readClaims({ txns: shown, funds: bookTxns(shown).funds, claims: [accepted], stored: [], readAt: at + 3_600_000, opening: 0 });
|
||
assert.deepEqual(settled.withdrawals, [{ at: at + 2_000, amount: -2250 }]);
|
||
assert.deepEqual(settled.outstanding, []);
|
||
assert.equal(settled.expected, cash - 2250);
|
||
// An account Vest funded without a deposit row: its capital counts in.
|
||
assert.equal(readClaims({ txns: txns.slice(1), funds: new Map(), claims: [], stored: [], readAt: at, opening: 50000 }).expected, cash);
|
||
});
|
||
|
||
test('an account token that can\'t be minted means the order or claim was never sent', async () => {
|
||
const path = '/v3/capital/withdraw';
|
||
assert.equal(await tokenForRequest(path, async () => 'tok'), 'tok');
|
||
const failures = [
|
||
new Error('/v3/auth/account-token timed out after 20s'),
|
||
new VestApiError('/v3/auth/account-token: HTTP 502 (HTTP 502)', 502, undefined),
|
||
new Error('/v3/auth/account-token answered without a token'),
|
||
];
|
||
for (const failure of failures) {
|
||
// As the request's own answer each could have gone in...
|
||
assert.equal(nothingWentIn(failure), false);
|
||
// ...but from the mint, the request never went out.
|
||
await assert.rejects(tokenForRequest(path, async () => { throw failure; }), (err) =>
|
||
err instanceof VestNotSentError && nothingWentIn(err) && err.message.startsWith(`${path} not sent`));
|
||
}
|
||
// The request's own answer: only a refusal proves nothing went in.
|
||
assert.equal(nothingWentIn(new VestApiError('x (HTTP 400, code 1001)', 400, '1001')), true);
|
||
assert.equal(nothingWentIn(new VestApiError('x: no Vest session', 401, 'NO_SESSION')), true);
|
||
assert.equal(nothingWentIn(new VestApiError('x (HTTP 408)', 408, undefined)), false);
|
||
assert.equal(nothingWentIn(new VestApiError('x (HTTP 503)', 503, undefined)), false);
|
||
assert.equal(nothingWentIn(new Error('x: socket hang up')), false);
|
||
});
|
||
|
||
test('a claim Vest dates on the other side of 20:00 ET keeps no fund row of its own', () => {
|
||
// Sent at 19:59:59.8 ET on the 26th and answered after 20:00: Vest dates
|
||
// its WITHDRAW on the 26th's day, the client booked it on the 27th's.
|
||
const claim = { at: iso('2026-10-27T00:00:00.300Z'), sentAt: iso('2026-10-26T23:59:59.800Z'), amount: 2250, base: 50000 };
|
||
const txns = [deposit, { action_id: 'w1', amount: '2250', created_at: iso('2026-10-26T23:59:59.900Z'), transaction_type: 2 }].map(parseTxn);
|
||
const stored = [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-27', amount: -2250 }];
|
||
// Before Vest shows it, it stands where it was booked.
|
||
assert.deepEqual(claimFundRows({ funds: bookTxns(txns.slice(0, 1)).funds, claims: [claim], withdrawals: [], stored }), {
|
||
rows: [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-27', amount: -2250 }], dropped: [],
|
||
});
|
||
// Once it shows, it's on Vest's day, and the 27th's row goes.
|
||
assert.deepEqual(claimFundRows({ funds: bookTxns(txns).funds, claims: [claim], withdrawals: withdrawalsOf(txns), stored }), {
|
||
rows: [{ date: '2026-10-20', amount: 50000 }, { date: '2026-10-26', amount: -2250 }], dropped: ['2026-10-27'],
|
||
});
|
||
// The 27th starts with no payout on it, before Vest shows the claim too:
|
||
// it went out on the 26th, and Vest can't date it earlier.
|
||
const reset = lastVestResetMs(iso('2026-10-27T15:00:00Z'));
|
||
assert.equal(payoutsSince(withdrawalsOf(txns), [claim], reset), 0);
|
||
assert.equal(payoutsSince([], [claim], reset), 0);
|
||
// Dated after the turn instead, it's the 27th's once Vest shows it.
|
||
const after = [deposit, { action_id: 'w3', amount: '2250', created_at: iso('2026-10-27T00:00:00.100Z'), transaction_type: 2 }].map(parseTxn);
|
||
assert.equal(payoutsSince(withdrawalsOf(after), [claim], reset), 1);
|
||
// One saved before the send time was kept counts from its answer.
|
||
assert.equal(payoutsSince([], [{ at: claim.at, amount: claim.amount, base: claim.base }], reset), 1);
|
||
|
||
// Dated the same day: its row stays. A day Vest has other fund rows on
|
||
// is Vest's, with a claim it doesn't show yet added.
|
||
const sameDay = { at: iso('2026-10-26T18:00:00Z'), amount: 1000, base: 49000 };
|
||
const shown = [deposit, { action_id: 'w2', amount: '1000', created_at: sameDay.at + 2_000, transaction_type: 2 }].map(parseTxn);
|
||
assert.deepEqual(claimFundRows({ funds: bookTxns(shown).funds, claims: [sameDay], withdrawals: withdrawalsOf(shown), stored: [{ date: '2026-10-26' }] }).dropped, []);
|
||
const credit = parseTxn({ action_id: 'c1', amount: '40', created_at: sameDay.at - 3_600_000, transaction_type: 22 });
|
||
assert.deepEqual(claimFundRows({ funds: bookTxns([credit]).funds, claims: [sameDay], withdrawals: [], stored: [] }).rows, [{ date: '2026-10-26', amount: -960 }]);
|
||
// One of unknown outcome was never booked, so it's no day's.
|
||
assert.deepEqual(claimFundRows({ funds: new Map(), claims: [{ ...claim, unknown: true }], withdrawals: [], stored }), { rows: [], dropped: [] });
|
||
});
|
||
|
||
test('two claims on one Vest day share a fund row but count as two payouts', () => {
|
||
const first = { at: iso('2026-10-27T14:00:00Z'), amount: 2250, base: 50000 };
|
||
const second = { at: iso('2026-10-27T18:30:00Z'), amount: 1500, base: 50000 };
|
||
const txns = [
|
||
deposit,
|
||
{ action_id: 'w1', amount: '2250', created_at: first.at + 2_000, transaction_type: 2 },
|
||
{ action_id: 'w2', amount: '1500', created_at: second.at + 2_000, transaction_type: 2 },
|
||
].map(parseTxn);
|
||
const withdrawals = withdrawalsOf(txns);
|
||
const { rows } = claimFundRows({ funds: bookTxns(txns).funds, claims: [first, second], withdrawals, stored: [] });
|
||
assert.deepEqual(rows.filter((f) => f.amount < 0), [{ date: '2026-10-27', amount: -3750 }]);
|
||
assert.deepEqual(payoutDaysOf(withdrawals, [first, second], rows), ['2026-10-27', '2026-10-27']);
|
||
// The second not shown yet: still two, on the day it was booked.
|
||
assert.deepEqual(payoutDaysOf(withdrawals.slice(0, 1), [first, second], rows), ['2026-10-27', '2026-10-27']);
|
||
// Made by hand, they show only as WITHDRAWs.
|
||
assert.equal(payoutDaysOf(withdrawals, [], rows).length, 2);
|
||
// Of unknown outcome, one counts only once it shows.
|
||
assert.deepEqual(payoutDaysOf([], [{ ...first, unknown: true }], []), []);
|
||
assert.equal(payoutDaysOf(withdrawals.slice(0, 1), [{ ...first, unknown: true }], rows).length, 1);
|
||
// A day that nets negative with no WITHDRAW still counts, as it did.
|
||
assert.deepEqual(payoutDaysOf([], [], [{ date: '2026-10-21', amount: -75 }, { date: '2026-10-22', amount: 20 }]), ['2026-10-21']);
|
||
assert.deepEqual(payoutDaysOf(withdrawals, [], [...rows, { date: '2026-10-21', amount: -75 }]), ['2026-10-21', '2026-10-27', '2026-10-27']);
|
||
assert.equal(payoutsSince(withdrawals, [first, second], lastVestResetMs(second.at)), 2);
|
||
});
|
||
|
||
test('hourly volatility leaves out the gaps', () => {
|
||
const start = iso('2026-10-19T00:00:00Z');
|
||
const kl = [];
|
||
let price = 29000;
|
||
for (let h = 0; h < 60; h++) {
|
||
price *= h % 2 === 0 ? 1.001 : 1 / 1.001;
|
||
kl.push([start + h * 3_600_000, '0', '0', '0', String(price), '1']);
|
||
}
|
||
const base = hourlyVolatility(kl);
|
||
assert.ok(base > 0.0009 && base < 0.0011, `σ ${base}`);
|
||
// A weekend gap with a 3% jump: not an hour's move, so σ doesn't change.
|
||
const gapped = [...kl, [start + 120 * 3_600_000, '0', '0', '0', String(price * 1.03), '1']];
|
||
assert.ok(Math.abs(hourlyVolatility(gapped) - base) < 1e-12);
|
||
// Any order in, the same σ out.
|
||
assert.equal(hourlyVolatility([...kl].reverse()), base);
|
||
assert.equal(hourlyVolatility(kl.slice(0, MIN_HOURLY_RETURNS)), null);
|
||
});
|
||
|
||
test('market hours: open stretches, closures and crypto', () => {
|
||
const periods = [
|
||
{ fromMs: 0, toMs: 100, state: { session: 'unspecified', closure: 'weekly', tradingStatus: 'CLOSE', price: { allow: false } } },
|
||
{ fromMs: 100, toMs: 200, state: { session: 'overnight', closure: 'unspecified', tradingStatus: 'OPEN', price: { allow: true } } },
|
||
{ fromMs: 200, toMs: 300, state: { session: 'regular', closure: 'unspecified', tradingStatus: 'OPEN', price: { allow: true } } },
|
||
{ fromMs: 300, toMs: 400, state: { session: 'unspecified', closure: 'maintenance', tradingStatus: 'OPEN', price: { allow: true } } },
|
||
{ fromMs: 400, toMs: 500, state: { session: 'overnight', closure: 'unspecified', tradingStatus: 'OPEN', price: { allow: true } } },
|
||
];
|
||
assert.deepEqual(marketOpen(periods, 50), { open: false, closesAt: null });
|
||
assert.deepEqual(marketOpen(periods, 150), { open: true, closesAt: 300 });
|
||
assert.deepEqual(marketOpen(periods, 350), { open: false, closesAt: null });
|
||
assert.deepEqual(marketOpen(periods, 450), { open: true, closesAt: 500 });
|
||
assert.deepEqual(marketOpen(periods, 600), { open: false, closesAt: null });
|
||
assert.deepEqual(marketOpen([], 600), { open: true, closesAt: null });
|
||
});
|
||
|
||
test('relayed sessions: taken, kept fresher, dropped, never listed with tokens', () => {
|
||
const now = iso('2026-10-26T18:40:00Z');
|
||
const jwt = (n) => `eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIke${n}In0.sig${n}`;
|
||
const wallet = '0x7CE10771819F80130E204DDB5CA1522E48A9B451';
|
||
assert.equal(putVestSession({ accessToken: 'nope', expiresAt: now + 900_000, primaryAddr: wallet, userId: 'u' }, now), 'accessToken is not a JWT');
|
||
assert.equal(putVestSession({ accessToken: jwt(1), expiresAt: now + 10_000, primaryAddr: wallet, userId: 'u' }, now), 'token already expired');
|
||
assert.equal(putVestSession({ accessToken: jwt(1), expiresAt: now + 900_000, primaryAddr: '0x12', userId: 'u' }, now), 'primaryAddr is not a wallet address');
|
||
assert.equal(putVestSession({ accessToken: jwt(1), expiresAt: now + 900_000, primaryAddr: wallet, userId: 'u' }, now), null);
|
||
// An older token relayed late doesn't replace the fresher one.
|
||
assert.equal(putVestSession({ accessToken: jwt(0), expiresAt: now + 300_000, primaryAddr: wallet, userId: 'u' }, now), null);
|
||
assert.equal(vestSessionFor(wallet.toLowerCase(), now).accessToken, jwt(1));
|
||
assert.equal(vestSessionFor(wallet, now + 890_000), null); // under 30s left
|
||
const listed = listVestSessions();
|
||
assert.equal(listed.length, 1);
|
||
assert.equal(listed[0].primaryAddr, wallet.toLowerCase());
|
||
assert.equal('accessToken' in listed[0], false);
|
||
dropVestSession(wallet, jwt(0)); // not the held one: kept
|
||
assert.ok(vestSessionFor(wallet, now));
|
||
dropVestSession(wallet, jwt(1));
|
||
assert.equal(vestSessionFor(wallet, now), null);
|
||
});
|
||
|
||
test('the next 8 PM ET reset, across both time changes', () => {
|
||
assert.equal(nextVestResetMs(iso('2026-10-26T18:00:00Z')), iso('2026-10-27T00:00:00Z'));
|
||
assert.equal(nextVestResetMs(iso('2026-10-27T00:00:00Z')), iso('2026-10-28T00:00:00Z'));
|
||
assert.equal(nextVestResetMs(iso('2026-10-31T23:59:00Z')), iso('2026-11-01T00:00:00Z'));
|
||
// Nov 1 2026, the clocks go back: that evening's reset is 01:00 UTC.
|
||
assert.equal(nextVestResetMs(iso('2026-11-01T12:00:00Z')), iso('2026-11-02T01:00:00Z'));
|
||
assert.equal(nextVestResetMs(iso('2026-12-01T00:30:00Z')), iso('2026-12-01T01:00:00Z'));
|
||
});
|
||
|
||
test('resetSafeStop: fit an open position into the room the reset will leave', () => {
|
||
// A stage-2 leg after a claim: balance $50,000, 3% → the reset floor is
|
||
// $48,500 (above the $47k drawdown floor). $150 reserve, a $3 fee: $1,347 to lose.
|
||
const base = {
|
||
side: 'long', quantity: 4, openPrice: 29500, mark: 29480, balance: 50000, dailyLossPct: 0.03,
|
||
maxDrawdownFloor: 47000, reserve: 150, exitFeeRate: 0.000025, tick: 0.25,
|
||
};
|
||
// No stop at all (a last stand): one comes in at open − 1347/4, rounded up to the tick.
|
||
const fee = 0.000025 * 4 * 29480;
|
||
const allowed = 50000 - 48500 - 150 - fee;
|
||
const want = Math.ceil((29500 - allowed / 4) / 0.25) * 0.25;
|
||
assert.deepEqual(resetSafeStop({ ...base, currentStop: null }), { action: 'stop', price: want });
|
||
// A wider stop is pulled in; one already tighter is left alone.
|
||
assert.deepEqual(resetSafeStop({ ...base, currentStop: 29000 }), { action: 'stop', price: want });
|
||
assert.deepEqual(resetSafeStop({ ...base, currentStop: 29300 }), { action: 'none' });
|
||
// The mark already past where the stop must go: close.
|
||
assert.deepEqual(resetSafeStop({ ...base, mark: 29100, currentStop: null }), { action: 'close' });
|
||
// Short: mirrored, rounded down.
|
||
const short = resetSafeStop({ ...base, side: 'short', mark: 29510, currentStop: null });
|
||
assert.equal(short.action, 'stop');
|
||
assert.equal(short.price, Math.floor((29500 + (50000 - 48500 - 150 - 0.000025 * 4 * 29510) / 4) / 0.25) * 0.25);
|
||
// No room past the reset at all: close.
|
||
assert.deepEqual(resetSafeStop({ ...base, balance: 48600, maxDrawdownFloor: 48600 }), { action: 'close' });
|
||
// The drawdown floor binds when it's above the daily one.
|
||
const dd = resetSafeStop({ ...base, maxDrawdownFloor: 49000, currentStop: null });
|
||
assert.equal(dd.action, 'stop');
|
||
assert.ok(dd.price > want);
|
||
// A second position on the account gets what the first leaves: $500
|
||
// already committed at the first one's stop, from the same $48,500 floor.
|
||
const second = resetSafeStop({ ...base, balance: 49500, floorBalance: 50000, currentStop: null });
|
||
assert.equal(second.action, 'stop');
|
||
assert.equal(second.price, Math.ceil((29500 - (allowed - 500) / 4) / 0.25) * 0.25);
|
||
assert.ok(second.price > want);
|
||
// ...and nothing when the first takes it all.
|
||
assert.deepEqual(resetSafeStop({ ...base, balance: 48600, floorBalance: 50000, currentStop: null }), { action: 'close' });
|
||
// With no room left, a position already up keeps going on a stop that
|
||
// locks in the shortfall: one already locking in more is left alone...
|
||
const up = { ...base, mark: 29560, balance: 48600, floorBalance: 50000 };
|
||
assert.deepEqual(resetSafeStop({ ...up, currentStop: 29520 }), { action: 'none' });
|
||
// ...one with none gets it, on the profit side, still under the mark...
|
||
const shortfall = 48600 - 48500 - 150 - 0.000025 * 4 * 29560;
|
||
assert.deepEqual(resetSafeStop({ ...up, currentStop: null }), { action: 'stop', price: Math.ceil((29500 - shortfall / 4) / 0.25) * 0.25 });
|
||
assert.ok(resetSafeStop({ ...up, currentStop: null }).price > 29500);
|
||
// ...and with no mark to tell, it closes.
|
||
assert.deepEqual(resetSafeStop({ ...up, mark: NaN, currentStop: null }), { action: 'close' });
|
||
});
|
||
|
||
test('parseHistoryPosition: a closed position from GET /v3/positions', () => {
|
||
// Shaped like the app's History tab reads it (camelCase, decimal strings).
|
||
const t = parseHistoryPosition({
|
||
positionId: 'bdda9024-63a9-4abc-a5a8-f83df8ba030a', accountId: '843f60e2-e013-5a0e-8b6c-face84bc5509', symbol: 'NDX-USD-PERP',
|
||
side: 'short', quantity: '4.2712', leverage: '50', openPrice: '29529.75', closePrice: '29556.50',
|
||
pnl: '-114.25', fee: '6.31', funding: '-0.12', collateral: '2522.54', takeProfits: [], stopLosses: [],
|
||
openDate: 1793065590123, closeDate: 1793066346456, createdAt: 1793065590123, updatedAt: 1793066346456, closeReason: 'user_requested',
|
||
});
|
||
assert.deepEqual(t, {
|
||
positionId: 'bdda9024-63a9-4abc-a5a8-f83df8ba030a', symbol: 'NDX-USD-PERP', side: 'short', quantity: 4.2712, leverage: 50,
|
||
openPrice: 29529.75, closePrice: 29556.5, pnl: -114.25, fee: 6.31, funding: -0.12,
|
||
openedAt: 1793065590123, closedAt: 1793066346456, closeReason: 'user_requested',
|
||
});
|
||
});
|
||
|
||
test('parseHistoryPosition: open, snake_case, seconds, and junk', () => {
|
||
const open = parseHistoryPosition({
|
||
position_id: 'p1', symbol: 'GC-PERP', side: 'LONG', quantity: '0', open_price: '4208.9', close_price: '',
|
||
pnl: '', open_date: 1793065590, close_date: null, close_reason: '',
|
||
});
|
||
assert.equal(open.side, 'long');
|
||
assert.equal(open.quantity, 0, 'a size Vest leaves out reads 0 — the store keeps the one it knows');
|
||
assert.equal(open.openPrice, 4208.9);
|
||
assert.equal(open.closePrice, null);
|
||
assert.equal(open.pnl, null);
|
||
assert.equal(open.openedAt, 1793065590000);
|
||
assert.equal(open.closedAt, null);
|
||
assert.equal(open.closeReason, null);
|
||
const held = parseHistoryPosition({
|
||
positionId: 'p3', symbol: 'NDX-USD-PERP', side: 'short', quantity: '85.7363', openPrice: '29538.96', closePrice: '290.25',
|
||
pnl: '0.0', fee: '63.314059', funding: '0.0', openDate: 1793065422000, closeDate: null,
|
||
});
|
||
assert.equal(held.closePrice, null, "an open position's placeholder close price is dropped");
|
||
assert.equal(held.pnl, null, 'and its $0 P&L');
|
||
assert.equal(held.fee, 63.314059, 'its entry fee is real');
|
||
assert.equal(parseHistoryPosition({ symbol: 'GC-PERP', openDate: 1 }), null, 'no id');
|
||
assert.equal(parseHistoryPosition({ positionId: 'p2', symbol: 'GC-PERP' }), null, 'no open time');
|
||
assert.equal(parseHistoryPosition(null), null);
|
||
});
|
||
|
||
test('an account\'s own token is told from the login\'s', () => {
|
||
const b64 = (o) => Buffer.from(JSON.stringify(o)).toString('base64url');
|
||
const acct = '275e765a-2d3d-5de3-8386-71aaa847c0e8';
|
||
const accountToken = `${b64({ alg: 'ES256' })}.${b64({ sub: '3cab1794-47bc-523b-91e6-64a9ad26dcbf', acc: { id: acct.toUpperCase() } })}.sig`;
|
||
assert.ok(jwtClaimStrings(accountToken).includes(acct));
|
||
assert.deepEqual(jwtClaimStrings('not.a-jwt.at-all'), []);
|
||
assert.deepEqual(jwtClaimStrings('nodots'), []);
|
||
// The login sees every account and its primary one.
|
||
assert.equal(accountScopedToken({ capitalIds: [acct, 'b', 'c'], hasPrimary: true, claims: [] }), false);
|
||
// One account and no primary: an account's token, whatever its claims.
|
||
assert.equal(accountScopedToken({ capitalIds: [acct], hasPrimary: false, claims: [] }), true);
|
||
// One account with a primary: only when the token names that account.
|
||
assert.equal(accountScopedToken({ capitalIds: [acct], hasPrimary: true, claims: jwtClaimStrings(accountToken) }), true);
|
||
assert.equal(accountScopedToken({ capitalIds: [acct], hasPrimary: true, claims: ['3cab1794-47bc-523b-91e6-64a9ad26dcbf'] }), false);
|
||
// A login with no capital accounts yet.
|
||
assert.equal(accountScopedToken({ capitalIds: [], hasPrimary: true, claims: [] }), false);
|
||
});
|
||
|
||
test('a refused account token falls back to the login\'s and stays refused', () => {
|
||
const now = iso('2026-10-27T01:00:00Z');
|
||
const wallet = '0x00000000000000000000000000000000000000aa';
|
||
const login = 'eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJsb2dpbiJ9.loginsignature-loginsignature-loginsig';
|
||
const account = 'eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJhY2N0In0.accountsignature-accountsignature-acct';
|
||
assert.equal(putVestSession({ accessToken: login, expiresAt: now + 600_000, primaryAddr: wallet, userId: 'u' }, now), null);
|
||
// The account's token lives longer, so it replaces the login's...
|
||
assert.equal(putVestSession({ accessToken: account, expiresAt: now + 900_000, primaryAddr: wallet, userId: 'u' }, now), null);
|
||
assert.equal(vestSessionFor(wallet, now).accessToken, account);
|
||
// ...until the client finds it sees one account: back to the login's.
|
||
refuseVestSession(wallet, account, now);
|
||
assert.equal(vestSessionFor(wallet, now).accessToken, login);
|
||
// Relayed again (the old extension posts it every minute): refused.
|
||
assert.match(putVestSession({ accessToken: account, expiresAt: now + 900_000, primaryAddr: wallet, userId: 'u' }, now), /one Vest account's token/);
|
||
assert.equal(vestSessionFor(wallet, now).accessToken, login);
|
||
// The login's next renewal is taken as usual.
|
||
const renewed = 'eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJsb2dpbiJ9.renewedsignature-renewedsignature-rene';
|
||
assert.equal(putVestSession({ accessToken: renewed, expiresAt: now + 1_200_000, primaryAddr: wallet, userId: 'u' }, now), null);
|
||
assert.equal(vestSessionFor(wallet, now).accessToken, renewed);
|
||
// Refusing a token that isn't held changes nothing held.
|
||
refuseVestSession(wallet, 'x.y.zzz', now);
|
||
assert.equal(vestSessionFor(wallet, now).accessToken, renewed);
|
||
// No good token to fall back to: none at all.
|
||
const wallet2 = '0x00000000000000000000000000000000000000bb';
|
||
assert.equal(putVestSession({ accessToken: account.replace('acct', 'accu'), expiresAt: now + 900_000, primaryAddr: wallet2, userId: 'u' }, now), null);
|
||
refuseVestSession(wallet2, account.replace('acct', 'accu'), now);
|
||
assert.equal(vestSessionFor(wallet2, now), null);
|
||
});
|