Vest: the relayed login binds itself to an unset Vest firm

A Vest firm's credential is the wallet of the login it trades. It no longer
has to be typed or clicked in: when the extension relays a Vest tab's
session and a Vest firm here has no credential, that wallet is saved as the
credential (lib/vest-wallet.ts), the client starts, and the backup is
pushed.

- A saved wallet is never replaced. When the tab is signed in to another
  login, the firm now says which one and how to switch: sign in as the
  saved wallet, or remove the credential.
- With two logins relayed at once nothing binds itself; the one-click form
  decides.
- Extension 0.17.1 re-posts the session every minute even when it hasn't
  changed. The dashboard holds it in memory only, so a restart there was
  unauthenticated until the tab's next renewal (up to ~15 min); now it is
  back within a minute, and a firm created later binds within a minute too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-26 14:42:01 -05:00
co-authored by Claude Opus 5.5
parent 96fdd9ca23
commit 769d96883f
8 changed files with 79 additions and 15 deletions
+5 -2
View File
@@ -49,7 +49,7 @@ 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 } from './vest-session';
import { vestSessionFor, dropVestSession, listVestSessions } from './vest-session';
import {
vestAccountId, vestAccountName, vestDayOf, lastVestResetMs, parseCapitalAccount, parsePosition, parseOpenOrder,
parseTxn, bookTxns, awaitingLegs, unrealizedPnl, hourlyVolatility, marketOpen, num, toMs, formatDecimal,
@@ -231,7 +231,10 @@ export class VestClient implements PlatformClient {
private userToken(): string | null {
const session = vestSessionFor(this.wallet, Date.now(), USER_TOKEN_MIN_MS);
if (session) return session.accessToken;
const msg = `no Vest session for ${this.wallet} — keep a Vest tab signed in to that wallet open in Chrome with the AutoFirmer extension`;
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`;
if (this.lastFetchErrors['login'] !== msg) console.warn(`${this.tag} ${msg}`);
this.lastAuthError = this.lastFetchErrors['login'] = msg;
return null;
+42
View File
@@ -0,0 +1,42 @@
/**
* Binds the Vest login the extension relays to this machine's Vest firm, so
* nobody has to copy a wallet address into the dashboard.
*
* A Vest firm's only credential here is the wallet of the login whose session
* it trades (lib/vest-session.ts). One person per machine, and Vest allows one
* profile per person, so the Vest tab signed in on this machine is the one to
* trade. It is bound automatically when:
* - the firm has no credential yet. A saved one is never replaced: someone
* chose it, and a different login in the tab is a mistake to flag (the
* client says so), not one to follow;
* - exactly one wallet has a live relayed session. Two logins in two tabs
* can't be told apart, so the dashboard's one-click form decides.
*/
import { getCachedCatalog } from './firm-catalog';
import { getCredentials, setCredentials } from './db';
import { listVestSessions } from './vest-session';
import { reconcileClients } from './clients';
import { scheduleBackupPush } from './backup-sync';
/** Bind the relayed wallet to every Vest firm here that has no credential.
* Returns the names of the firms it bound. */
export function autoBindVestWallet(now = Date.now()): string[] {
const live = [...new Set(listVestSessions().filter((s) => s.expiresAt > now).map((s) => s.primaryAddr))];
if (live.length !== 1) return [];
const wallet = live[0];
const bound: string[] = [];
for (const firm of getCachedCatalog()) {
if (firm.platform !== 'vest') continue;
const creds = getCredentials(firm.id);
if (creds && (creds.apiKey || creds.username || creds.password)) continue;
setCredentials(firm.id, firm.name, '', '', wallet);
bound.push(firm.name);
console.log(`[vest] ${firm.name}: bound to wallet ${wallet}, the Vest login signed in on this machine`);
}
if (bound.length > 0) {
reconcileClients();
scheduleBackupPush();
}
return bound;
}