Skip to content
Merged
28 changes: 16 additions & 12 deletions src/app/[locale]/vaults/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {
} from "../../../lib/api/vaults";
import { useFlag } from "../../../lib/flags";

const ABSENT = "—";

/** Metrics where a smaller value is the better one, so the first click sorts ascending. */
const SORT_DEFAULT_DIR: Record<VaultSortKey, "asc" | "desc"> = {
apy: "desc",
Expand Down Expand Up @@ -66,6 +68,7 @@ const TH_STYLE = {
const TD_STYLE = { padding: "10px", fontSize: 12, color: "var(--text-mid)", borderBottom: "1px solid var(--border-subtle)", whiteSpace: "nowrap" } as const;

function VaultsIndex() {
const vaultsEnabled = useFlag("vaults");
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
Expand All @@ -74,9 +77,9 @@ function VaultsIndex() {
const [error, setError] = useState<string | null>(null);
const [attempt, setAttempt] = useState(0);
const [apyTooltip, setApyTooltip] = useState<string | null>(null);
const vaultsEnabled = useFlag("vaults");

useEffect(() => {
if (!vaultsEnabled) return;
let cancelled = false;
setError(null);
fetchVaults({ enabled: vaultsEnabled })
Expand Down Expand Up @@ -120,7 +123,7 @@ function VaultsIndex() {
};

const hasFilters = Boolean(query.strategy || query.underlying || query.status || query.risk) || query.tenor !== null;
const loading = vaults === null && error === null;
const loading = vaultsEnabled && vaults === null && error === null;

return (
<div style={{ display: "flex", flexDirection: "column", height: "100vh", background: "var(--bg)", overflow: "hidden", fontFamily: "var(--font-sans)" }}>
Expand All @@ -142,16 +145,17 @@ function VaultsIndex() {
<p style={{ fontSize: 13, color: "var(--text-mid)", maxWidth: 620 }}>
Earn passive yield by providing liquidity to automated option-writing strategies.
Premiums are collected each epoch and compounded into your share price.
{process.env.NEXT_PUBLIC_VAULTS !== "live" && (
<span style={{ marginLeft: 8, fontSize: 11, color: "var(--atm)", fontStyle: "italic" }}>
(mock data — contracts not yet deployed)
</span>
)}
</p>
</div>

{!vaultsEnabled && (
<div style={{ padding: "12px 16px", border: "1px solid var(--border-default)", background: "var(--bg-raised)", color: "var(--text-mid)", fontSize: 12 }}>
Vaults are not available on this deployment.
</div>
)}

{/* Filters */}
{vaults && (
{vaultsEnabled && vaults && (
<div role="group" aria-label="Filter vaults" style={{ display: "flex", flexWrap: "wrap", gap: 12, alignItems: "flex-end", marginBottom: 16 }}>
<FilterSelect label="Strategy" value={query.strategy ?? ""} onChange={v => update({ strategy: (v || null) as VaultStrategy | null })}
options={Object.entries(VAULT_STRATEGY_LABELS).map(([value, label]) => ({ value, label }))} />
Expand All @@ -174,7 +178,7 @@ function VaultsIndex() {
)}

{/* Compare bar */}
{vaults && (
{vaultsEnabled && vaults && (
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 12, padding: "10px 12px", marginBottom: 12, border: "1px solid var(--border-subtle)", background: "var(--bg-raised)" }}>
<span style={{ fontSize: 12, color: "var(--text-mid)" }}>
{query.ids.length === 0
Expand Down Expand Up @@ -203,7 +207,7 @@ function VaultsIndex() {
)}

{/* Error with retry */}
{error && (
{vaultsEnabled && error && (
<div role="alert" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap", padding: "12px 16px", border: "1px solid var(--put)", background: "var(--put-dim)", color: "var(--put)", fontSize: 12 }}>
<span>Failed to load vaults: {error}</span>
<button type="button" onClick={() => setAttempt(a => a + 1)} style={{
Expand Down Expand Up @@ -246,7 +250,7 @@ function VaultsIndex() {
)}

{/* Empty */}
{vaults && rows.length === 0 && (
{vaultsEnabled && vaults && rows.length === 0 && (
<div style={{ padding: "32px 16px", border: "1px dashed var(--border-default)", textAlign: "center", fontSize: 13, color: "var(--text-mid)" }}>
{vaults.length === 0 ? "No vaults are available yet." : "No vaults match these filters."}
{hasFilters && (
Expand All @@ -262,7 +266,7 @@ function VaultsIndex() {
)}

{/* Vault table */}
{vaults && rows.length > 0 && (
{vaultsEnabled && vaults && rows.length > 0 && (
<div style={{ overflowX: "auto", border: "1px solid var(--border-default)", background: "var(--bg-raised)" }}>
<table style={{ width: "100%", borderCollapse: "collapse", minWidth: 960 }}>
<caption className="sr-only">
Expand Down
23 changes: 11 additions & 12 deletions src/lib/api/vaults.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { z } from "zod";
import { env } from "../../env";
import { bffFetch } from "./client";
import { apiGet, apiPost, ApiError, bffFetch, type RequestOptions } from "./client";
import { ContractError, reportContractError } from "./contractError";
import { toCsv, type CsvColumn } from "../csv";
import freighterApi from "@stellar/freighter-api";
import { apiPost, type RequestOptions } from "./client";
import { submitTransactionXdr } from "./stellar";

/**
Expand Down Expand Up @@ -416,7 +415,10 @@ export interface VaultRoleGrant {
grantedAt: string;
}

/** Summary shown on the /vaults listing page. */
/**
* Summary shown on the /vaults listing page. Fields the backend does not
* serve yet are `null` and must render as absent, never as zero.
*/
export interface VaultSummary {
id: string;
name: string;
Expand All @@ -426,17 +428,17 @@ export interface VaultSummary {
mode: VaultMode;
status: VaultStatus;
/** Trailing 12-month APY estimate (0–1, e.g. 0.18 = 18%). */
apy: number;
apy: number | null;
/** APY methodology note shown in tooltip. */
apyMethodology: string;
/** Total value locked in USD. */
tvl: number;
tvl: number | null;
/** Maximum TVL before the vault is full. */
capacity: number;
capacity: number | null;
/** tvl / capacity (0–1). */
utilisation: number;
currentStrike: number;
currentExpiry: string; // ISO date
currentStrike: number | null;
currentExpiry: string | null; // ISO date
riskRating: RiskRating;
description: string;
/** Epoch length (tenor) in days. */
Expand Down Expand Up @@ -777,7 +779,4 @@ export interface NavPoint {
}

/** Full detail for a single vault. */
export interface VaultDetail extends VaultSummary {
currentEpoch: number;
currentPhase: EpochPhase;
phaseEndsAt: string; // ISO — when the current phase
export interface VaultDetail extends
Loading