Compare commits
2
Commits
95cc3bd23f
...
c109aff3e5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c109aff3e5 | ||
|
|
a0e7c5b7c2 |
@@ -29,7 +29,7 @@ export async function GET(req: Request) {
|
||||
const open = t.closedAt === null && t.closeReason === null && t.closedBy === null;
|
||||
const closedBy = open ? null
|
||||
: t.closedBy === 'bot' ? 'bot'
|
||||
: t.closeReason === 'user_requested' ? (tracked(t.closedAt ?? t.openedAt) ? 'outside' : 'user_requested')
|
||||
: t.closeReason === 'user_requested' ? (t.closedBy === 'outside' || tracked(t.closedAt ?? t.openedAt) ? 'outside' : 'user_requested')
|
||||
: t.closeReason ?? 'unknown';
|
||||
const net = open || t.pnl === null ? null
|
||||
: Math.round((t.pnl - Math.abs(t.fee ?? 0) + (t.funding ?? 0)) * 100) / 100;
|
||||
@@ -48,7 +48,7 @@ export async function GET(req: Request) {
|
||||
net,
|
||||
openedAt: t.openedAt,
|
||||
closedAt: t.closedAt,
|
||||
openedBy: t.openedBy === 'bot' ? 'bot' : tracked(t.openedAt) ? 'outside' : null,
|
||||
openedBy: t.openedBy === 'bot' ? 'bot' : t.openedBy === 'outside' || tracked(t.openedAt) ? 'outside' : null,
|
||||
closedBy,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -95,6 +95,11 @@ dashboard POST /api/vest/session holds it in memory for the firm's Vest cl
|
||||
- The hook only reads the Vest app's own `/v3/auth/refresh` and `/register`
|
||||
answers. It renews nothing and sends nothing; the session stays the app's,
|
||||
so the bot never logs the tab out.
|
||||
- It takes the login's token only. The app renews each trading account's own
|
||||
token through the same `/v3/auth/refresh` (with that account's refresh token
|
||||
in the body), and one of those sees a single account. Before 0.17.2 the hook
|
||||
relayed those too, and the dashboard dropped every other account; the
|
||||
dashboard now refuses such a token and falls back to the login's.
|
||||
- The token lives ~15 minutes and the app renews it by itself while the tab is
|
||||
open. The tab is marked not auto-discardable, since a discarded tab stops
|
||||
renewing.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "AutoFirmer Capture",
|
||||
"version": "0.17.1",
|
||||
"version": "0.17.2",
|
||||
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxdbOMRR+58a9dHlbIKtlALtS3E4eltBGqfTnUc0lzJuWj5ZA4+5YFSdUnxv5N1ogxwpJUGkUV3MnKJuZnTekCmsNwjCy41a20Xtc7jQLHrIBZkesB6cB+d0l9Vo8M6EC3ZMD8Iv6MdAalU3GZh1Hn9vm24NjJ+xUT1qNIVHEcNUIb9evYobR/XHv+JAp6bOFnaLM+3wpyYKe4efMhsz0F86u9thRNhQnraTqUWfnKNpsHPK2zivaYHrziEEWX7Y3EsMHkf2FRhkUe7IDoxeJM49DBToTo9sAJUkcgLeGCGZLo7H3mcxyn/zERYR+rD3k5xdeBtjoZHXFx2+FanLb9wIDAQAB",
|
||||
"update_url": "https://ext.juicerroom.com/updates.xml",
|
||||
|
||||
+22
-7
@@ -14,6 +14,12 @@
|
||||
* It sends nothing anywhere, changes no request and never renews anything
|
||||
* itself: the session stays the app's, refreshed by the app with its own
|
||||
* device key, so the tab is never logged out by the bot.
|
||||
*
|
||||
* The app also renews each trading account's own token through the same
|
||||
* /v3/auth/refresh, sending that account's refresh token in the body (the
|
||||
* login's rides in a cookie) and getting it back renewed. Those tokens see
|
||||
* one account only, so they are skipped: relaying one would leave the
|
||||
* dashboard with a single account.
|
||||
*/
|
||||
|
||||
(() => {
|
||||
@@ -22,11 +28,17 @@
|
||||
|
||||
const AUTH = /^https:\/\/api-gateway\.hz\.vestmarkets\.com\/v3\/auth\/(refresh|register)(\?|$)/;
|
||||
|
||||
function take(url, status, text) {
|
||||
if (status < 200 || status >= 300 || !AUTH.test(url)) return;
|
||||
/** A request body that carries a refresh token: an account's renewal. */
|
||||
function accountRenewal(reqBody) {
|
||||
return typeof reqBody === 'string' && /"refreshToken"\s*:\s*"[^"]/.test(reqBody);
|
||||
}
|
||||
|
||||
function take(url, status, text, accountRequest) {
|
||||
if (accountRequest || status < 200 || status >= 300 || !AUTH.test(url)) return;
|
||||
let body;
|
||||
try { body = JSON.parse(text); } catch { return; }
|
||||
if (!body || typeof body.accessToken !== 'string' || !body.accessToken) return;
|
||||
if (typeof body.refreshToken === 'string' && body.refreshToken) return; // an account's token, renewed
|
||||
const expiresAt = Number(body.accessExpiresAtMs);
|
||||
if (!Number.isFinite(expiresAt)) return;
|
||||
window.__afVestSession = {
|
||||
@@ -44,12 +56,14 @@
|
||||
try { this.__afVestUrl = String(url); } catch { /* ignore */ }
|
||||
return open.apply(this, arguments);
|
||||
};
|
||||
XMLHttpRequest.prototype.send = function () {
|
||||
XMLHttpRequest.prototype.send = function (reqBody) {
|
||||
if (this.__afVestUrl && AUTH.test(this.__afVestUrl)) {
|
||||
let acct = false;
|
||||
try { acct = accountRenewal(reqBody); } catch { /* ignore */ }
|
||||
this.addEventListener('loadend', () => {
|
||||
try {
|
||||
if (this.responseType === '' || this.responseType === 'text') take(this.__afVestUrl, this.status, this.responseText);
|
||||
else if (this.responseType === 'json') take(this.__afVestUrl, this.status, JSON.stringify(this.response));
|
||||
if (this.responseType === '' || this.responseType === 'text') take(this.__afVestUrl, this.status, this.responseText, acct);
|
||||
else if (this.responseType === 'json') take(this.__afVestUrl, this.status, JSON.stringify(this.response), acct);
|
||||
} catch { /* never break the app */ }
|
||||
});
|
||||
}
|
||||
@@ -57,11 +71,12 @@
|
||||
};
|
||||
|
||||
const fetch0 = window.fetch;
|
||||
window.fetch = async function (input) {
|
||||
window.fetch = async function (input, init) {
|
||||
const res = await fetch0.apply(this, arguments);
|
||||
try {
|
||||
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input && input.url;
|
||||
if (url && AUTH.test(url)) res.clone().text().then((t) => take(url, res.status, t)).catch(() => {});
|
||||
const acct = accountRenewal(init && init.body);
|
||||
if (url && AUTH.test(url)) res.clone().text().then((t) => take(url, res.status, t, acct)).catch(() => {});
|
||||
} catch { /* never break the app */ }
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -582,7 +582,9 @@ export function wipeAccountData(accountId: number): void {
|
||||
// Every position a platform's history shows — the bot's and anyone else's
|
||||
// (Vest: GET /v3/positions). The platform can't tell an API order from a
|
||||
// click, so opened_by / closed_by record what this instance did itself:
|
||||
// 'bot' when its own order opened or closed the position, NULL otherwise.
|
||||
// 'bot' when its own order opened or closed the position, NULL otherwise —
|
||||
// or 'outside', set by hand for a trade from before marking began whose
|
||||
// origin the bot's own log settles.
|
||||
// A wipe leaves these alone: they're refetched, but the marks aren't.
|
||||
|
||||
db.exec(`
|
||||
|
||||
+29
-4
@@ -49,11 +49,11 @@ import { firmInstruments } from './instruments';
|
||||
import type { PlatformClient, PlatformAccount, PlatformContract, ClientHealth, PickContext } from './platform-client';
|
||||
import { OrderNotPlacedError, isOrderNotPlaced, errorMessage } from './platform-client';
|
||||
import { encodeContractName, decodeContractName, roundToStep, scoreCandidate } from './dxtrade-helpers';
|
||||
import { vestSessionFor, dropVestSession, listVestSessions } from './vest-session';
|
||||
import { vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from './vest-session';
|
||||
import {
|
||||
vestAccountId, vestAccountName, vestDayOf, lastVestResetMs, nextVestResetMs, parseCapitalAccount, parsePosition, parseOpenOrder,
|
||||
parseTxn, bookTxns, awaitingLegs, unrealizedPnl, hourlyVolatility, marketOpen, num, toMs, formatDecimal, resetSafeStop,
|
||||
parseHistoryPosition, VEST_STATUS_ACTIVE, VEST_TXN,
|
||||
parseHistoryPosition, jwtClaimStrings, accountScopedToken, VEST_STATUS_ACTIVE, VEST_TXN,
|
||||
} from './vest-helpers';
|
||||
import type { VestCapitalAccount, VestPosition, VestOpenOrder, VestTxn, VestTrade } from './vest-helpers';
|
||||
|
||||
@@ -218,6 +218,8 @@ export class VestClient implements PlatformClient {
|
||||
/** Open positions already in the trades table, by id → size stored. */
|
||||
private recordedOpen = new Map<string, number>();
|
||||
private flatAmounts = new Map<number, number>();
|
||||
/** When a relayed token last turned out to be one account's own. */
|
||||
private scopedRefusedAt = 0;
|
||||
|
||||
private gateNext: Record<Bucket, number> = { read: 0, trade: 0, history: 0 };
|
||||
private suspectAccounts = new Map<number, number>();
|
||||
@@ -261,7 +263,9 @@ export class VestClient implements PlatformClient {
|
||||
const other = listVestSessions().find((s) => s.primaryAddr !== this.wallet && s.expiresAt > Date.now());
|
||||
const msg = other
|
||||
? `no Vest session for ${this.wallet} — the Vest tab here is signed in as ${other.primaryAddr}; sign in as ${this.wallet} there, or remove this firm's credential on the dashboard to switch to the tab's login`
|
||||
: `no Vest session for ${this.wallet} — keep a Vest tab signed in to that wallet open in Chrome with the AutoFirmer extension`;
|
||||
: Date.now() - this.scopedRefusedAt < 30 * 60_000
|
||||
? `no Vest session for ${this.wallet} — the extension relayed one account's own token, not the login's; reload the Vest tab (or update the extension) to relay the login's`
|
||||
: `no Vest session for ${this.wallet} — keep a Vest tab signed in to that wallet open in Chrome with the AutoFirmer extension`;
|
||||
if (this.lastFetchErrors['login'] !== msg) console.warn(`${this.tag} ${msg}`);
|
||||
this.lastAuthError = this.lastFetchErrors['login'] = msg;
|
||||
return null;
|
||||
@@ -472,7 +476,8 @@ export class VestClient implements PlatformClient {
|
||||
*/
|
||||
private async requestSync(): Promise<void> {
|
||||
if (this.intentionalDisconnect) return;
|
||||
if (!this.userToken()) return;
|
||||
const token = this.userToken();
|
||||
if (!token) return;
|
||||
const startedAt = Date.now();
|
||||
if (this.syncInFlight) {
|
||||
const runningMs = startedAt - this.syncInFlightSince;
|
||||
@@ -499,6 +504,16 @@ export class VestClient implements PlatformClient {
|
||||
delete this.lastFetchErrors['login'];
|
||||
|
||||
const accounts = (cap.data?.accounts ?? []).map(parseCapitalAccount).filter((a): a is VestCapitalAccount => a !== null);
|
||||
// One account's own token (the Vest app renews those through the
|
||||
// same endpoint the extension watches) sees that account alone:
|
||||
// syncing on it would drop every other account from the list.
|
||||
const hasPrimary = (bal.data?.accounts ?? []).some((b) => num(b.account_type) === 1);
|
||||
if (accountScopedToken({ capitalIds: accounts.map((a) => a.id), hasPrimary, claims: jwtClaimStrings(token) })) {
|
||||
refuseVestSession(this.wallet, token);
|
||||
this.scopedRefusedAt = Date.now();
|
||||
console.warn(`${this.tag} the relayed token is one Vest account's own (it sees ${accounts.length} account, no primary), not the login's — refused; keeping the last account list until the login's token comes`);
|
||||
return;
|
||||
}
|
||||
const free = new Map<string, number>();
|
||||
for (const b of bal.data?.accounts ?? []) {
|
||||
const amount = num(b.amount);
|
||||
@@ -565,6 +580,16 @@ export class VestClient implements PlatformClient {
|
||||
+ `${a.targetEquity ? `, target ${a.targetEquity}` : ''}`;
|
||||
}
|
||||
this.accountList = list;
|
||||
// An account gone from Vest's list (breached, closed) takes its
|
||||
// position with it — a stale one here would hold the lane.
|
||||
for (const key of Object.keys(this.positions)) {
|
||||
const id = Number(key);
|
||||
if (!list.some((a) => a.id === id)) {
|
||||
delete this.positions[id];
|
||||
this.positionsDetail.delete(id);
|
||||
this.seenOpenAt.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
this.settleSuspects();
|
||||
this.checkNakedPositions();
|
||||
|
||||
@@ -334,6 +334,40 @@ export function parseHistoryPosition(raw: unknown): VestTrade | null {
|
||||
};
|
||||
}
|
||||
|
||||
/** Every string value in a JWT's payload, lower-case ([] when it isn't
|
||||
* one). Used only to compare against account ids — never logged. */
|
||||
export function jwtClaimStrings(token: string): string[] {
|
||||
const part = token.split('.')[1];
|
||||
if (!part) return [];
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = JSON.parse(Buffer.from(part.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const out: string[] = [];
|
||||
const walk = (v: unknown, depth: number): void => {
|
||||
if (typeof v === 'string') out.push(v.toLowerCase());
|
||||
else if (v && typeof v === 'object' && depth < 4) for (const x of Object.values(v)) walk(x, depth + 1);
|
||||
};
|
||||
walk(payload, 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a token the extension relayed is one Vest account's own token, not
|
||||
* the login's. The Vest app renews both through POST /v3/auth/refresh — the
|
||||
* login's every ~15 minutes, an account's while someone trades in it — and
|
||||
* an account's token sees only that account: /v3/capital/accounts/active
|
||||
* lists it alone, and /v3/accounts has no primary (wallet) account. The
|
||||
* login sees every account and its primary one.
|
||||
*/
|
||||
export function accountScopedToken(o: { capitalIds: string[]; hasPrimary: boolean; claims: string[] }): boolean {
|
||||
if (o.capitalIds.length > 1) return false;
|
||||
if (!o.hasPrimary) return true;
|
||||
return o.capitalIds.some((id) => o.claims.includes(id.toLowerCase()));
|
||||
}
|
||||
|
||||
/** Exit legs still waiting to trigger. */
|
||||
export function awaitingLegs(p: VestPosition): VestLeg[] {
|
||||
return [...p.stopLosses, ...p.takeProfits].filter((l) => l.status === 'awaiting');
|
||||
|
||||
+36
-1
@@ -32,12 +32,30 @@ export interface VestSessionInfo {
|
||||
receivedAt: number;
|
||||
}
|
||||
|
||||
const g = globalThis as typeof globalThis & { __vestSessions?: Map<string, VestSession> };
|
||||
const g = globalThis as typeof globalThis & {
|
||||
__vestSessions?: Map<string, VestSession>;
|
||||
__vestPrevious?: Map<string, VestSession>;
|
||||
__vestRefused?: Map<string, number>;
|
||||
};
|
||||
|
||||
function store(): Map<string, VestSession> {
|
||||
return (g.__vestSessions ??= new Map());
|
||||
}
|
||||
|
||||
/** The session each wallet's current one replaced — what a refused token
|
||||
* falls back to. */
|
||||
function previous(): Map<string, VestSession> {
|
||||
return (g.__vestPrevious ??= new Map());
|
||||
}
|
||||
|
||||
/** Tokens the client found to be one account's, not the login's (by their
|
||||
* tail — enough to tell them apart, never the whole token), with when. */
|
||||
function refused(): Map<string, number> {
|
||||
return (g.__vestRefused ??= new Map());
|
||||
}
|
||||
|
||||
const REFUSED_KEEP = 50;
|
||||
|
||||
/** Shortest life a relayed token must have left to be taken. */
|
||||
const MIN_REMAINING_MS = 30_000;
|
||||
/** Longest a relayed token may claim to live — a Vest access token is
|
||||
@@ -54,8 +72,10 @@ export function putVestSession(input: { accessToken: unknown; expiresAt: unknown
|
||||
if (!/^0x[0-9a-f]{40}$/.test(addr)) return 'primaryAddr is not a wallet address';
|
||||
if (!Number.isFinite(expiresAt) || expiresAt - now < MIN_REMAINING_MS) return 'token already expired';
|
||||
if (expiresAt - now > MAX_REMAINING_MS) return 'expiresAt is implausibly far away';
|
||||
if (refused().has(token.slice(-32))) return "that is one Vest account's token, not the login's — reload the Vest tab, or update the extension";
|
||||
const prev = store().get(addr);
|
||||
if (prev && prev.expiresAt > expiresAt && prev.accessToken !== token) return null; // keep the fresher one
|
||||
if (prev && prev.accessToken !== token) previous().set(addr, prev);
|
||||
store().set(addr, {
|
||||
accessToken: token,
|
||||
expiresAt,
|
||||
@@ -73,6 +93,21 @@ export function vestSessionFor(primaryAddr: string, now = Date.now(), minRemaini
|
||||
return s && s.expiresAt - now >= minRemainingMs ? s : null;
|
||||
}
|
||||
|
||||
/** The client found the wallet's token to be one account's own (see
|
||||
* accountScopedToken): drop it, and take it no more if it's relayed again —
|
||||
* the login's own token, when the Vest app next renews it, takes its place. */
|
||||
export function refuseVestSession(primaryAddr: string, token: string, now = Date.now()): void {
|
||||
const key = primaryAddr.trim().toLowerCase();
|
||||
refused().set(token.slice(-32), now);
|
||||
while (refused().size > REFUSED_KEEP) refused().delete(refused().keys().next().value as string);
|
||||
if (store().get(key)?.accessToken !== token) return;
|
||||
store().delete(key);
|
||||
// Back to the token it replaced, if that one is still good.
|
||||
const before = previous().get(key);
|
||||
previous().delete(key);
|
||||
if (before && !refused().has(before.accessToken.slice(-32)) && before.expiresAt - now >= MIN_REMAINING_MS) store().set(key, before);
|
||||
}
|
||||
|
||||
/** Drop a wallet's session — the gateway refused it. */
|
||||
export function dropVestSession(primaryAddr: string, token: string): void {
|
||||
const key = primaryAddr.trim().toLowerCase();
|
||||
|
||||
+50
-2
@@ -11,9 +11,10 @@ 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,
|
||||
hourlyVolatility, marketOpen, MIN_HOURLY_RETURNS, nextVestResetMs, resetSafeStop, parseHistoryPosition, jwtClaimStrings,
|
||||
accountScopedToken,
|
||||
} from '../lib/vest-helpers.ts';
|
||||
import { putVestSession, vestSessionFor, dropVestSession, listVestSessions } from '../lib/vest-session.ts';
|
||||
import { putVestSession, vestSessionFor, dropVestSession, listVestSessions, refuseVestSession } from '../lib/vest-session.ts';
|
||||
|
||||
const iso = (s) => Date.parse(s);
|
||||
|
||||
@@ -296,3 +297,50 @@ test('parseHistoryPosition: open, snake_case, seconds, and junk', () => {
|
||||
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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user