Vest: a claim that never reached Vest stops holding the account, and each claim counts once
A claim of unknown outcome now lapses where history is read (bookHistory), not only in claimPending: its callers wait on historyReady, and the books check held the account while it counted that claim, so one that never went in held the account for good. The lapse is the same (no WITHDRAW within 30 min of it, a read an hour on) and is saved; an accepted claim still holds. An account token that can't be minted means the request never went out: an order, stop or take-profit reads as not placed, and a claim records no unknown claim. Only the request's own failure may be "unknown". A claim Vest dates on the other side of 20:00 ET than it was booked here no longer leaves a second fund row: the history read takes the local day back (and the latest fund date with it), and sameDayClaims counts the claim on Vest's day. A claim Vest doesn't show yet stays on its day. The report counts payouts one per claim (Vest's WITHDRAWs and the claims they don't show yet, never fewer than the negative fund rows) for the account list, payout states and the completion stage, where two claims on one Vest day were one row. Same-day cycles still share a cycle key; that needs the master. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
beddbdcf6c
commit
9fe7fc0ea3
+99
-1
@@ -12,7 +12,8 @@ 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,
|
||||
accountScopedToken, VestApiError, VestNotSentError, nothingWentIn, tokenForRequest, claimWithdrawal, settleClaims,
|
||||
claimFundRows, payoutCount, payoutsSince,
|
||||
} from '../lib/vest-helpers.ts';
|
||||
import { putVestSession, vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from '../lib/vest-session.ts';
|
||||
|
||||
@@ -171,6 +172,103 @@ test('transactions book into daily P&L and fund rows by Vest day', () => {
|
||||
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('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.9 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'), 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 — once Vest says so.
|
||||
const reset = lastVestResetMs(iso('2026-10-27T15:00:00Z'));
|
||||
assert.equal(payoutsSince(withdrawalsOf(txns), [claim], reset), 0);
|
||||
assert.equal(payoutsSince([], [claim], 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.equal(payoutCount(withdrawals, [first, second], rows), 2);
|
||||
// The second not shown yet: still two.
|
||||
assert.equal(payoutCount(withdrawals.slice(0, 1), [first, second], rows), 2);
|
||||
// Made by hand, they show only as WITHDRAWs.
|
||||
assert.equal(payoutCount(withdrawals, [], rows), 2);
|
||||
// Of unknown outcome, one counts only once it shows.
|
||||
assert.equal(payoutCount([], [{ ...first, unknown: true }], []), 0);
|
||||
assert.equal(payoutCount(withdrawals.slice(0, 1), [{ ...first, unknown: true }], rows), 1);
|
||||
// A day that nets negative with no WITHDRAW still counts, as it did.
|
||||
assert.equal(payoutCount([], [], [{ date: '2026-10-21', amount: -75 }, { date: '2026-10-22', amount: 20 }]), 1);
|
||||
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 = [];
|
||||
|
||||
Reference in New Issue
Block a user