A passed Vest evaluation sits at GOAL_PASSED (status 3) until someone presses
"Go to funded account", which sends POST /v3/capital/activate-funded
{eval_account_id} on the evaluation's own token (read from the Vest app's
bundle, 2026-10-29); Vest then opens the funded account and marks the
evaluation CLAIMED (6). The 5 evaluations that passed on 2026-10-29 waited
for a hand activation.
runActivations now asks for it, the way runClaims claims: from the
scheduler's tick (not awaited, so a slow ask doesn't hold the lane's entry)
and from the claim watcher while the scheduler is off, for evaluations with
an account type only, 15 minutes apart after a failure. Off with the
auto_activate setting at 'off' (dashboard: "Auto-activate funded"). The
client asks for an evaluation once at a time, never again after Vest
answered, and after a timeout not until a list read a minute on says either
way; a refusal frees it at once. The rule is the Vest app's own: an
evaluation at GOAL_PASSED, not blocked, on its plan's last step.
Vest's active balance list leaves a passed evaluation out, and a missing
balance made every sync short of good (15:31-15:34Z), which holds and then
quarantines the lane. Only a live account's missing balance does now. Log
lines name Vest's statuses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
652 lines
44 KiB
JavaScript
652 lines
44 KiB
JavaScript
// Checks for the Vest client's pure helpers (lib/vest-helpers.ts), the
|
||
// relayed-session store (lib/vest-session.ts), and the trade date the engine
|
||
// plans Vest accounts on (lib/trading-logic.ts).
|
||
//
|
||
// npm run check
|
||
//
|
||
// These 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, longClosure, unknownOpenFill, awaitingActivation, vestStatusName,
|
||
VEST_STATUS,
|
||
} from '../lib/vest-helpers.ts';
|
||
import { putVestSession, vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from '../lib/vest-session.ts';
|
||
import { computeDayPlan, currentTradeDateCT, tradeDateFor, cycleCompletion } from '../lib/trading-logic.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);
|
||
});
|
||
|
||
test('a passed evaluation waits on its funded account, as the Vest app decides', () => {
|
||
const passed = { ...evalAccount, status: VEST_STATUS.GOAL_PASSED };
|
||
const a = parseCapitalAccount(passed);
|
||
assert.equal(a.step, 1);
|
||
assert.equal(a.stepsTotal, 1);
|
||
assert.equal(a.accountType, 2);
|
||
assert.equal(a.blocked, false);
|
||
assert.equal(awaitingActivation(a), true);
|
||
|
||
// Still trading, already claimed, failed or expired: nothing to activate.
|
||
for (const status of [1, 2, 4, 5, 6, 7]) assert.equal(awaitingActivation(parseCapitalAccount({ ...evalAccount, status })), false, `status ${status}`);
|
||
// Blocked by Vest.
|
||
assert.equal(awaitingActivation(parseCapitalAccount({ ...passed, blocked_at: 1793287000000 })), false);
|
||
// A funded account that reached its goal isn't an evaluation — on the
|
||
// last step's number too, so only the evaluation test can say so.
|
||
assert.equal(awaitingActivation(parseCapitalAccount({ ...passed, account_type: 3, stage: 2, step: 1 })), false);
|
||
// Either of Vest's marks makes it one.
|
||
assert.equal(awaitingActivation(parseCapitalAccount({ ...passed, account_type: 3 })), true);
|
||
assert.equal(awaitingActivation(parseCapitalAccount({ ...passed, stage: 2, step: 1 })), true);
|
||
// A 2-Step's first step passes on to its second by itself; the last one waits.
|
||
assert.equal(awaitingActivation(parseCapitalAccount({ ...passed, step: 1, steps_total: 2 })), false);
|
||
assert.equal(awaitingActivation(parseCapitalAccount({ ...passed, step: 2, steps_total: 2 })), true);
|
||
|
||
// Without step or steps_total, the Vest app's own fallbacks.
|
||
const { step, steps_total, ...bare } = passed;
|
||
assert.equal(awaitingActivation(parseCapitalAccount(bare)), true);
|
||
assert.equal(parseCapitalAccount({ ...bare, stage: 2 }).step, 2);
|
||
assert.equal(parseCapitalAccount({ ...bare, plan_product_type: 'instant_funded' }).stepsTotal, 0);
|
||
assert.equal(awaitingActivation(parseCapitalAccount({ ...bare, plan_product_type: 'instant_funded' })), false);
|
||
|
||
assert.equal(vestStatusName(3), 'goal passed (status 3)');
|
||
assert.equal(vestStatusName(6), 'claimed (status 6)');
|
||
assert.equal(vestStatusName(9), 'unknown (status 9)');
|
||
});
|
||
|
||
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('an open of unknown outcome is the position on its account, symbol and side opened around it', () => {
|
||
const pos = (positionId, accountId, side, openedAt, symbol = 'NDX-USD', stopLosses = []) => ({
|
||
positionId, accountId, symbol, side, quantity: 0.5, openPrice: 20000, collateral: 500, leverage: 20, openedAt, stopLosses, takeProfits: [],
|
||
});
|
||
// Sent at 12:30:00, its error at 12:30:15: the window is a few seconds of clock either side.
|
||
const sent = { accountId: 'acc-1', symbol: 'NDX-USD', side: 'long', from: iso('2026-10-30T12:29:55Z'), until: iso('2026-10-30T12:30:20Z') };
|
||
const filled = pos('9001', 'acc-1', 'long', iso('2026-10-30T12:30:01Z'));
|
||
assert.deepEqual(unknownOpenFill(sent, [pos('9002', 'acc-2', 'long', iso('2026-10-30T12:30:01Z')), filled]), [filled]);
|
||
// Another side or symbol, a position from before it, or one opened after
|
||
// the window (a hand trade, or an outside copy-trader, half a minute on): not it.
|
||
assert.deepEqual(unknownOpenFill(sent, [pos('9003', 'acc-1', 'short', iso('2026-10-30T12:30:01Z'))]), []);
|
||
assert.deepEqual(unknownOpenFill(sent, [pos('9004', 'acc-1', 'long', iso('2026-10-30T12:30:01Z'), 'SPX-USD')]), []);
|
||
assert.deepEqual(unknownOpenFill(sent, [pos('9005', 'acc-1', 'long', iso('2026-10-30T11:00:00Z'))]), []);
|
||
assert.deepEqual(unknownOpenFill(sent, [pos('9006', 'acc-1', 'long', iso('2026-10-30T12:30:45Z'))]), []);
|
||
// No open time: can't be told from a hand trade.
|
||
assert.deepEqual(unknownOpenFill(sent, [pos('9007', 'acc-1', 'long', NaN)]), []);
|
||
// One with exit legs isn't the bot's open — none went on it.
|
||
const leg = { id: 'sl-1', triggerPrice: 19900, executionType: 'market', status: 'awaiting' };
|
||
assert.deepEqual(unknownOpenFill(sent, [pos('9008', 'acc-1', 'long', iso('2026-10-30T12:30:01Z'), 'NDX-USD', [leg])]), []);
|
||
// Two inside the window: both come back, for the caller to adopt neither.
|
||
const copy = pos('9009', 'acc-1', 'long', iso('2026-10-30T12:30:02Z'));
|
||
assert.deepEqual(unknownOpenFill(sent, [filled, copy]), [filled, copy]);
|
||
// The position id the open's answer named settles it, legs and window aside.
|
||
const named = pos('9010', 'acc-1', 'long', iso('2026-10-30T12:31:00Z'), 'NDX-USD', [leg]);
|
||
assert.deepEqual(unknownOpenFill({ ...sent, positionId: '9010' }, [filled, copy, named]), [named]);
|
||
assert.deepEqual(unknownOpenFill({ ...sent, positionId: '9010' }, [filled, copy]), []);
|
||
assert.deepEqual(unknownOpenFill({ ...sent, positionId: '9010' }, [{ ...named, accountId: 'acc-2' }]), []);
|
||
});
|
||
|
||
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('market hours: a closure past an 8 PM ET reset is a long one, the maintenance hour is not', () => {
|
||
const open = (from, to) => ({ fromMs: iso(from), toMs: iso(to), state: { session: 'overnight', closure: 'unspecified', tradingStatus: 'OPEN', price: { allow: true } } });
|
||
const shut = (from, to, closure) => ({ fromMs: iso(from), toMs: iso(to), state: { session: 'unspecified', closure, tradingStatus: closure === 'weekly' ? 'CLOSE' : 'OPEN', price: { allow: closure !== 'weekly' } } });
|
||
const week = [
|
||
open('2026-10-28T22:00:00Z', '2026-10-29T21:00:00Z'),
|
||
shut('2026-10-29T21:00:00Z', '2026-10-29T22:00:00Z', 'maintenance'),
|
||
open('2026-10-29T22:00:00Z', '2026-10-30T21:00:00Z'),
|
||
shut('2026-10-30T21:00:00Z', '2026-11-01T23:00:00Z', 'weekly'),
|
||
open('2026-11-01T23:00:00Z', '2026-11-02T22:00:00Z'),
|
||
shut('2026-11-02T22:00:00Z', '2026-11-02T23:00:00Z', 'maintenance'),
|
||
open('2026-11-02T23:00:00Z', '2026-11-03T22:00:00Z'),
|
||
];
|
||
// The weekend: shut through Friday's and Saturday's 8 PM ET resets.
|
||
const friday = marketOpen(week, iso('2026-10-30T20:57:00Z'));
|
||
assert.deepEqual(friday, { open: true, closesAt: iso('2026-10-30T21:00:00Z') });
|
||
assert.equal(longClosure(week, friday.closesAt), true);
|
||
// The maintenance hour reopens before the evening's reset, in summer and winter.
|
||
const thursday = marketOpen(week, iso('2026-10-29T20:57:00Z'));
|
||
assert.equal(thursday.closesAt, iso('2026-10-29T21:00:00Z'));
|
||
assert.equal(longClosure(week, thursday.closesAt), false);
|
||
assert.equal(longClosure(week, iso('2026-11-02T22:00:00Z')), false);
|
||
// A gap in the periods is shut too (a holiday), and this one outlasts the reset.
|
||
const early = [open('2026-11-25T14:00:00Z', '2026-11-25T18:00:00Z'), open('2026-11-26T23:00:00Z', '2026-11-27T18:00:00Z')];
|
||
assert.equal(longClosure(early, iso('2026-11-25T18:00:00Z')), true);
|
||
// Shut as far as the periods go: long. Periods that just end: no close known.
|
||
assert.equal(longClosure(week.slice(0, 4), iso('2026-10-30T21:00:00Z')), true);
|
||
assert.equal(longClosure(week.slice(0, 3), iso('2026-10-30T21:00:00Z')), false);
|
||
assert.equal(longClosure(undefined, iso('2026-10-30T21:00:00Z')), false);
|
||
});
|
||
|
||
test("a Vest account plans on Vest's day: the 5 PM CT roll doesn't count today's P&L twice", () => {
|
||
const vest = { tradeDate: vestDayOf };
|
||
// 3:30 and 5:30 PM CT (4:30 / 6:30 PM ET): the Central date rolls at
|
||
// 5 PM CT, Vest's day only at 8 PM ET.
|
||
const before = new Date('2026-10-27T20:30:00Z');
|
||
const after = new Date('2026-10-27T22:30:00Z');
|
||
assert.equal(currentTradeDateCT(after), '2026-10-28');
|
||
assert.equal(tradeDateFor(vest, after), '2026-10-27');
|
||
assert.equal(tradeDateFor(vest, new Date('2026-10-28T00:00:00Z')), '2026-10-28');
|
||
// Every other platform keeps the Central date.
|
||
assert.equal(tradeDateFor(undefined, after), '2026-10-28');
|
||
assert.equal(tradeDateFor({}, after), '2026-10-28');
|
||
// $3,000 to go, 3 min days of $150, $800 made today: Vest's row for its
|
||
// day, and the realized P&L since its reset.
|
||
const rows = [{ date: '2026-10-27', pnl: 800 }];
|
||
const plan = (date) => computeDayPlan(7, date, 3000, 0, rows, 150, 3, 800, 800);
|
||
assert.equal(plan(currentTradeDateCT(before)).remainingToday, 1900);
|
||
assert.deepEqual(plan(tradeDateFor(vest, after)), plan(currentTradeDateCT(before)));
|
||
// On the Central date the $800 is a prior min day and today's P&L both.
|
||
assert.equal(plan(currentTradeDateCT(after)).remainingToday, 2050);
|
||
});
|
||
|
||
test("a Vest account's completion is judged on Vest's day, and waits out Vest's own roll", () => {
|
||
const vest = { tradeDate: vestDayOf };
|
||
// $3,000 in 2 min days: day one made $3,100 and is flat. At 5:30 PM CT
|
||
// its row is booked and its P&L is still today's realized P&L.
|
||
const eff = { profitTarget: 3000, consistency: 0, minTradingDays: 2, minDayPnL: 0, drain: false, dailyLossLimit: 0 };
|
||
const opts = {
|
||
accountId: 7, cycleDailyPnL: [{ date: '2026-10-27', pnl: 3100 }], rawRealizedPnL: 3100, combineCmeHolidays: false,
|
||
tradesEveryDay: true, effective: eff, equityProfit: 3100, splitEnabled: true, client: vest,
|
||
};
|
||
const evening = new Date('2026-10-27T22:30:00Z');
|
||
// The Central date counts the one day twice and reports a pass.
|
||
assert.equal(cycleCompletion({ ...opts, client: undefined, rawTradeDate: currentTradeDateCT(evening), now: evening }), 'today');
|
||
assert.equal(cycleCompletion({ ...opts, rawTradeDate: tradeDateFor(vest, evening), now: evening }), 'none');
|
||
// Two minutes past Vest's 8 PM ET roll the last sync's realized P&L can
|
||
// still be the ended day's: no 'today' until the settle window passes.
|
||
const justRolled = new Date('2026-10-28T00:02:00Z');
|
||
assert.equal(tradeDateFor(vest, justRolled), '2026-10-28');
|
||
assert.equal(cycleCompletion({ ...opts, rawTradeDate: '2026-10-28', now: justRolled }), 'none');
|
||
// A real second day, once settled, finishes it.
|
||
const settled = new Date('2026-10-28T00:10:00Z');
|
||
assert.equal(cycleCompletion({ ...opts, rawTradeDate: '2026-10-28', rawRealizedPnL: 50, equityProfit: 3150, now: settled }), 'today');
|
||
});
|
||
|
||
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);
|
||
});
|