/* global React, ReactDOM */ /* Vault Control — app shell. */ const { Gate, Sidebar, TopBar } = window; const APPVC = window.VC; // Plain-language subtitles. Vocabulary is fixed across the app: // Strategy → the Asset it trades → the Basket it belongs to → its allocation inside that basket // → the Broker account (API) that executes it → how much of that account goes to each basket. const TITLES = { baskets: ["Basket Studio", "Build baskets, set each strategy's allocation and leverage, and arm/disarm execution — all in one place."], environments: ["Broker Accounts", "Your broker accounts — the API keys that place trades — and how much of each account goes to each basket."], strategies: ["Strategies", "Every strategy: the asset it trades, the basket it belongs to, its health and full record."], analytics: ["Analytics", "Compare and analyze strategies — equity curves, monthly P&L, head-to-head metrics and correlation."], monitor: ["Monitor", "Health scores, lifecycle status, recommendations and the full audit trail across the strategy book."], trades: ["Trade Log", "Open positions, live positions, recent orders and every closed trade."], }; // Official Hyperliquid blob (brand SVG: Hyperliquid_Blob_{Dark,Green,Light}). // Fill follows --hl-blob (dark #011916 on light surfaces / green #97fce4 on dark // theme); tone="ondark" forces the green for surfaces that are dark in BOTH themes // (selected account-type buttons, sidebar). function HLLogo({ size = 24, tone }) { return ( ); } // Every Coinbase broker value the DB can hold. brokerTag() used to compare against // the bare string "coinbase" and so rendered a coinbase_deribit account as "HL // Personal" — the logo was right and the label was wrong. Both read this now. const COINBASE_BROKERS = ["coinbase", "coinbase_perps", "coinbase_deribit"]; const isCoinbaseBroker = (b) => COINBASE_BROKERS.includes(String(b || "").toLowerCase()); function BrokerMark({ broker, size = 22, tone = "light" }) { const isCoinbase = isCoinbaseBroker(broker); if (!isCoinbase) return ; return ; } // Coinbase consumer wordmark (blue). Rendered at small sizes inside broker tags. function CoinbaseLogo({ size = 22 }) { return ( Coinbase ); } function DeploymentEnvironment({ vault }) { const [busy, setBusy] = React.useState(false); const [loading, setLoading] = React.useState(false); const [msg, setMsg] = React.useState(null); const [adding, setAdding] = React.useState(false); const [envs, setEnvs] = React.useState([]); const [studioBaskets, setStudioBaskets] = React.useState([]); // inline "edit which baskets this account funds" — writes deployment_environment_baskets only // (env_update_baskets), no credentials. Separate state from the Add-account form. const [editSlug, setEditSlug] = React.useState(null); const [editBaskets, setEditBaskets] = React.useState([]); const blank = { slug: "", display_name: "", broker_mode: "vault", status: "active", api_key: "", api_secret: "", portfolio_uuid: "", wallet_address: "", vault_address: "", notes: "", // HIP-3 is the builder perp dex that lists the EQUITY perps (xyz:NVDA, xyz:TSLA). // It is NOT a property of "personal account" — a personal wallet trading crypto // perps (BNB, HYPE, SOL) must have this OFF. Default off: turning it on makes the // bridge read equity from the builder dex's own clearinghouse and ignore the // standard-perps balance entirely (execution/brokers/hyperliquid.py get_account), // so a crypto account with this on reads 0 equity and every order is blocked // "non-positive equity". Only the US-equity account should set it. hip3: false, baskets: [{ basket_ref: "", alloc_pct: 50 }], }; const [form, setForm] = React.useState(blank); const accountTypes = { vault: { label: "Hyperliquid Vault", profile: "hyperliquid_vault", broker: "hyperliquid" }, personal: { label: "Hyperliquid Personal Account", profile: "hyperliquid_personal", broker: "hyperliquid" }, coinbase: { label: "Coinbase Perpetuals", profile: "coinbase_deribit", broker: "coinbase_deribit" }, }; const selectedType = accountTypes[form.broker_mode] || accountTypes.vault; // A vault never trades the builder dex, so HIP-3 is only ever offered on personal // accounts — and only honoured there, so a stale flag can't survive a mode switch. const hip3Available = form.broker_mode === "personal"; const hip3On = hip3Available && !!form.hip3; const inputStyle = { padding: 10, border: "1px solid var(--line)", borderRadius: 8, background: "var(--paper)", color: "var(--ink)", width: "100%", boxSizing: "border-box" }; const labelStyle = { display: "flex", flexDirection: "column", gap: 6 }; const smallCaps = { fontSize: 11, textTransform: "uppercase", letterSpacing: ".08em", color: "var(--muted)" }; // Basket allocation offers ONLY the baskets the admin composed in Basket Studio // (showcase/builder baskets — the single source of truth). The legacy vault-plan // library (zhedgedbac_index_strategies: B1 Crypto, B2 Metals, …) and raw deployment // basket_codes are deliberately NOT listed — they duplicated every basket under a // second name and are not admin-managed. Compose baskets in Basket Studio. const basketOptions = React.useMemo(() => { const out = []; const seen = {}; function push(ref, name, showcase = false) { const r = String(ref || "").trim(); if (!r || seen[r]) return; seen[r] = true; out.push({ ref: r, name: String(name || r), showcase }); } // From Basket Studio loaded baskets (showcase) studioBaskets.filter((b) => b.status !== "archived").forEach((b) => push(b.name, "★ " + b.name, true)); // keep any ref an in-flight form row already selected (so an open edit never blanks) (form && form.baskets ? form.baskets : []).forEach((b) => push(b.basket_ref, b.basket_ref, false)); return out; }, [vault, studioBaskets, form]); function setField(key, value) { setForm((cur) => ({ ...cur, [key]: value })); } function slugify(s) { return String(s || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64); } function openAdd() { const seed = selectedType.profile + "-" + (envs.length + 1); setForm({ ...blank, slug: seed, display_name: selectedType.label + " " + (envs.length + 1), baskets: [{ basket_ref: basketOptions[0]?.ref || "", alloc_pct: 50 }] }); setAdding(true); setMsg(null); } function loadEnvs() { setLoading(true); return APPVC.api3("list_environments").then((r) => { setLoading(false); if (r.status === 200 && r.body.ok) { const list = r.body.environments || []; // Name every venue from its own display_name, so a new broker labels itself. if (window.registerVenueLabels) window.registerVenueLabels(list); setEnvs(list); return; } setMsg({ t: "Error " + r.status + ": " + (r.body.error || "could not load environments"), err: true }); }).catch((e) => { setLoading(false); setMsg({ t: "Network error: " + e.message, err: true }); }); } // Load basket studio baskets for the basket selector function loadStudioBaskets() { APPVC.api2("list_baskets").then((r) => { if (r.status === 200 && r.body.ok) setStudioBaskets(r.body.baskets || []); }).catch(() => { }); } React.useEffect(() => { loadEnvs(); loadStudioBaskets(); }, []); function updateBasket(i, key, value) { setForm((cur) => ({ ...cur, baskets: cur.baskets.map((b, idx) => idx === i ? { ...b, [key]: value } : b) })); } function addBasket() { const used = {}; form.baskets.forEach((b) => { used[b.basket_ref] = true; }); const next = basketOptions.find((b) => !used[b.ref]); setForm((cur) => ({ ...cur, baskets: cur.baskets.concat({ basket_ref: next?.ref || "", alloc_pct: 0 }) })); } function removeBasket(i) { setForm((cur) => ({ ...cur, baskets: cur.baskets.filter((_, idx) => idx !== i) })); } // ---- inline basket-allocation editor for a saved account ---- function startEditBaskets(env) { setEditSlug(env.slug); setEditBaskets((env.baskets || []).map((b) => ({ basket_ref: b.basket_ref, alloc_pct: Number(b.alloc_pct) }))); setMsg(null); } function setEB(i, key, value) { setEditBaskets((cur) => cur.map((b, idx) => idx === i ? { ...b, [key]: value } : b)); } function addEB() { const used = {}; editBaskets.forEach((b) => { used[b.basket_ref] = true; }); const next = basketOptions.find((b) => !used[b.ref]); setEditBaskets((cur) => cur.concat({ basket_ref: next?.ref || "", alloc_pct: 0 })); } function delEB(i) { setEditBaskets((cur) => cur.filter((_, idx) => idx !== i)); } const editTotal = editBaskets.reduce((s, b) => s + (Number(b.alloc_pct) || 0), 0); function saveEB() { const clean = editBaskets .map((b, idx) => ({ basket_ref: String(b.basket_ref || "").trim(), alloc_pct: Number(b.alloc_pct), sort_order: idx, enabled: true })) .filter((b) => b.basket_ref && Number.isFinite(b.alloc_pct)); const refs = clean.map((b) => b.basket_ref); if (new Set(refs).size !== refs.length) { setMsg({ t: "Each basket can appear only once", err: true }); return; } if (editTotal > 150.01) { setMsg({ t: "Total allocation is " + editTotal.toFixed(1) + "% — must be ≤ 150%", err: true }); return; } setBusy(true); setMsg({ t: "Saving basket allocation…", err: false }); APPVC.api3("update_baskets", { slug: editSlug, baskets: clean }).then((r) => { setBusy(false); if (r.status === 200 && r.body.ok) { setEditSlug(null); setMsg({ t: "✓ basket allocation saved", err: false }); loadEnvs(); } else setMsg({ t: "Error " + r.status + ": " + (r.body.error || "unknown"), err: true }); }).catch((e) => { setBusy(false); setMsg({ t: "Network error: " + e.message, err: true }); }); } // ---- account Active/Inactive + halt-resurrect (env_set_account_status) ---- // statusPanel: { slug, account, mode: "deactivate" | "reactivate" } | null const [statusPanel, setStatusPanel] = React.useState(null); const [ddPct, setDdPct] = React.useState(""); const [deletePanel, setDeletePanel] = React.useState(null); const [deletePasscode, setDeletePasscode] = React.useState(""); function openStatusPanel(env, mode) { const acct = env.account || { id: env.slug, status: env.status || "active", current_equity: 0, total_dd_limit_pct: 5 }; setStatusPanel({ slug: env.slug, account: acct, mode }); setDdPct(acct && acct.total_dd_limit_pct != null ? String(acct.total_dd_limit_pct) : ""); setMsg(null); } function submitDelete() { if (!deletePanel) return; if (!deletePasscode.trim()) { setMsg({ t: "Enter the Control passcode to delete", err: true }); return; } setBusy(true); setMsg({ t: "Deleting account…", err: false }); APPVC.api3("delete_environment", { slug: deletePanel, passcode: deletePasscode.trim() }).then((r) => { setBusy(false); if (r.status === 200 && r.body.ok) { setDeletePanel(null); setDeletePasscode(""); setMsg({ t: "✓ Account deleted", err: false }); loadEnvs(); return; } setMsg({ t: "Error " + r.status + ": " + (r.body.error || "unknown"), err: true }); }).catch((e) => { setBusy(false); setMsg({ t: "Network error: " + e.message, err: true }); }); } function submitStatus() { if (!statusPanel) return; const { slug, mode } = statusPanel; const payload = { slug, op: mode }; if (mode === "reactivate" && ddPct.trim() !== "") payload.dd_pct = Number(ddPct); setBusy(true); setMsg({ t: mode === "deactivate" ? "Deactivating account…" : "Resurrecting account…", err: false }); APPVC.api3("set_account_status", payload).then((r) => { setBusy(false); if (r.status === 200 && r.body.ok) { setStatusPanel(null); setMsg({ t: "✓ " + (r.body.picked_up || (mode === "deactivate" ? "account deactivated" : "account reactivated")), err: false }); loadEnvs(); return; } setMsg({ t: "Error " + r.status + ": " + (r.body.error || "unknown"), err: true }); }).catch((e) => { setBusy(false); setMsg({ t: "Network error: " + e.message, err: true }); }); } // Real trading state of the linked broker account: OK / HALTED / INACTIVE. function accountBadge(env) { const a = env.account; if (!a) return null; const st = String(a.status || ""); const cfg = st === "active" ? { label: "OK", color: "var(--green)", clickable: false } : st === "inactive" ? { label: "INACTIVE", color: "var(--muted)", clickable: false } : { label: "HALTED", color: "var(--red)", clickable: true }; // halted / killed / anything else return ( { e.stopPropagation(); openStatusPanel(env, "reactivate"); } : undefined} title={cfg.clickable ? "Account halted by the risk gate — click to resurrect" : a.kill_switch ? "kill switch ON" : undefined} style={{ fontFamily: "var(--font-mono)", fontSize: 10, padding: "2px 8px", borderRadius: 6, border: "1px solid var(--line)", color: cfg.color, textTransform: "uppercase", cursor: cfg.clickable ? "pointer" : "default", fontWeight: cfg.clickable ? 700 : 400 }}> {cfg.label} ); } function credentialPayload() { if (form.broker_mode === "coinbase") return { BROKER: "coinbase_deribit", EXECUTION_PROFILE: "coinbase_deribit", api_key: form.api_key.trim(), private_key: form.api_secret.trim(), portfolio_uuid: form.portfolio_uuid.trim(), settlement_currency: "USDC" }; const privateKey = form.api_key.trim(); const payload = { BROKER: "hyperliquid", HYPERLIQUID_ACCOUNT_TYPE: form.broker_mode, EXECUTION_PROFILE: selectedType.profile, HIP3_ENABLED: hip3On, HYPERLIQUID_PRIVATE_KEY: privateKey, HYPERLIQUID_API_KEY: privateKey, }; if (form.api_secret.trim()) payload.HYPERLIQUID_API_SECRET = form.api_secret.trim(); if (form.wallet_address.trim()) payload.HYPERLIQUID_WALLET_ADDRESS = form.wallet_address.trim(); if (form.vault_address.trim()) payload.HYPERLIQUID_VAULT_ADDRESS = form.vault_address.trim(); return payload; } function cleanBaskets() { return form.baskets .map((b, idx) => ({ basket_ref: String(b.basket_ref || "").trim(), alloc_pct: Number(b.alloc_pct), sort_order: idx, enabled: true })) .filter((b) => b.basket_ref && Number.isFinite(b.alloc_pct)); } const baskets = cleanBaskets(); const totalAlloc = baskets.reduce((sum, b) => sum + Number(b.alloc_pct || 0), 0); async function upsert() { if (!form.slug.trim() || !form.display_name.trim()) { setMsg({ t: "Name and slug are required", err: true }); return; } if (!form.api_key.trim() || (form.broker_mode === "coinbase" && !form.api_secret.trim())) { setMsg({ t: "API key / private key is required", err: true }); return; } if (form.broker_mode === "coinbase") { const secret = form.api_secret.trim(); const compact = secret.replace(/\s+/g, ""); const isPem = secret.includes("BEGIN") && secret.includes("PRIVATE KEY"); const isEd25519 = /^[A-Za-z0-9+/]+={0,2}$/.test(compact) && (compact.length === 44 || compact.length === 88); if (!isPem && !isEd25519) { setMsg({ t: "Private key must be a base64 Ed25519 key or a PEM ECDSA key", err: true }); return; } if (!form.api_key.trim().includes("/apiKeys/")) { setMsg({ t: "Use the full CDP key name: organizations//apiKeys/", err: true }); return; } } if (form.broker_mode === "vault" && !form.vault_address.trim()) { setMsg({ t: "Vault address is required for Hyperliquid Vault mode", err: true }); return; } if (form.broker_mode === "personal" && !form.wallet_address.trim()) { setMsg({ t: "Wallet address is required for Personal Account mode", err: true }); return; } if (!baskets.length) { setMsg({ t: "Select at least one basket for this account", err: true }); return; } if (baskets.some((b) => !(b.alloc_pct > 0 && b.alloc_pct <= 150))) { setMsg({ t: "Each basket allocation must be > 0 and ≤ 150", err: true }); return; } if (totalAlloc > 150) { setMsg({ t: "Total basket allocation cannot exceed 150%", err: true }); return; } setBusy(true); setMsg({ t: "Storing deployment environment…", err: false }); // Credentials go over TLS and are encrypted SERVER-SIDE by vault_deploy. // // This used to AES-encrypt here in the browser using a hardcoded master string. That bought // nothing: the Cloud Run bridge must decrypt to sign orders, so the server holds the key // regardless — TLS already protects the wire. All the browser step achieved was publishing // the key, because control/app.jsx is served unauthenticated (curl it and read the source). // The same string is also the monitor's GATE_PASS. See // docs/HANDOFF-CREDENTIAL-KEY-RISK-2026-07-15.md — it was never deployed, so nothing to rotate. // // Pairs with the vault_deploy change (accept `credentials`, encrypt with the // CREDENTIALS_MASTER_KEY secret). SHIPPED 2026-07-15 as vault_deploy v19 — server-side // encryption verified round-trip against the bridge; saving an environment now succeeds. APPVC.api3("upsert_environment", { slug: slugify(form.slug), display_name: form.display_name.trim(), broker: selectedType.broker, broker_mode: form.broker_mode === "coinbase" ? "custom" : form.broker_mode, network: "mainnet", reserve_pct: 0, status: form.status, notes: form.notes.trim() || null, credentials: credentialPayload(), baskets, }).then((r) => { setBusy(false); if (r.status === 200 && r.body.ok) { setAdding(false); setForm(blank); setMsg({ t: "✓ Deployment environment saved with encrypted credentials", err: false }); loadEnvs(); return; } setMsg({ t: "Error " + r.status + ": " + (r.body.error || "unknown"), err: true }); }).catch((e) => { setBusy(false); setMsg({ t: "Network error: " + e.message, err: true }); }); } const brokerTag = (env) => { const broker = String(env.broker || env.credentials?.BROKER || "hyperliquid").toLowerCase(); if (isCoinbaseBroker(broker)) return ( {broker === "coinbase_perps" ? "Coinbase INTX" : "Coinbase"} ); const mode = env.broker_mode === "vault" ? "Vault" : "Personal"; return ( HL HL {mode} ); }; const activeEnvCount = envs.filter((env) => String(env.account?.status || env.status || "").toLowerCase() === "active").length; const attentionEnvCount = envs.filter((env) => ["halted", "killed"].includes(String(env.account?.status || "").toLowerCase())).length; const fundedBasketLinks = envs.reduce((sum, env) => sum + (env.baskets || []).filter((b) => b.enabled !== false).length, 0); return (
Connected{envs.length}broker accounts
Trading{activeEnvCount}runtime active
Attention{attentionEnvCount}halted or killed
Basket links{fundedBasketLinks}funding assignments
{/* Add form */} {adding && (

New broker account

{/* Broker type selector */}
{/* Switching mode resets HIP-3 to off, so the default-off promise holds on every mode change and can never be inherited from a previous pick. */} {Object.entries(accountTypes).map(([key, t]) => ( ))}