From 509b20d564465ab79652eb541ea869dc046bbe85 Mon Sep 17 00:00:00 2001 From: Sikkra <159844544+Sikkra@users.noreply.github.com> Date: Tue, 19 May 2026 11:31:56 -0500 Subject: [PATCH] Add safe max leverage control --- frontend/index.html | 5 ++++ frontend/src/main.ts | 66 ++++++++++++++++++++++++++++++++++++++++++ frontend/src/style.css | 12 ++++++++ 3 files changed, 83 insertions(+) diff --git a/frontend/index.html b/frontend/index.html index f904f23..2adc1f5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -55,6 +55,10 @@

Important Disclaimer

@@ -396,6 +400,7 @@

Open Position

× +
Conservative diff --git a/frontend/src/main.ts b/frontend/src/main.ts index fcc1ceb..3a377ce 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -336,6 +336,26 @@ const fmt = (n: number, d = 2) => const aprToApy = (apr: number) => (Math.exp(apr / 100) - 1) * 100; const fmtAddr = (addr: string) => addr.slice(0, 6) + "…" + addr.slice(-4); +// ── Safe Max settings ───────────────────────────────────────────────────────── + +const SAFE_MAX_HF_FLOOR_KEY = "safeMaxHfFloor"; +const DEFAULT_SAFE_MAX_HF_FLOOR = 1.2; + +function sanitizeSafeMaxFloor(value: number): number { + if (!Number.isFinite(value)) return DEFAULT_SAFE_MAX_HF_FLOOR; + return Math.min(10, Math.max(MIN_HF_NORMAL, Math.round(value * 1000) / 1000)); +} + +let safeMaxHfFloor = sanitizeSafeMaxFloor( + parseFloat(localStorage.getItem(SAFE_MAX_HF_FLOOR_KEY) ?? String(DEFAULT_SAFE_MAX_HF_FLOOR)) +); + +function renderSafeMaxFloorInput() { + const input = $("safe-max-floor") as HTMLInputElement; + input.value = safeMaxHfFloor.toFixed(2); + input.min = MIN_HF_NORMAL.toFixed(2); +} + // ── Skeleton loading (#3) ──────────────────────────────────────────────────── function setSkeleton(id: string) { @@ -610,6 +630,8 @@ function updateLeverageSlider(c: number, l: number = 1) { slider.max = numIn.max = String(leverageable ? maxLev : 1.0); slider.step = numIn.step = "0.1"; slider.disabled = numIn.disabled = !leverageable; + const safeMaxBtn = document.getElementById("safe-max-btn") as HTMLButtonElement | null; + if (safeMaxBtn) safeMaxBtn.disabled = !leverageable; const cur = parseFloat(slider.value); const clamped = Math.min(parseFloat(slider.max), Math.max(1.0, cur)); if (clamped !== cur) { slider.value = String(clamped); numIn.value = String(clamped); } @@ -1167,6 +1189,40 @@ function switchAdjustSubTab(sub: "leverage" | "add-funds") { // ── Leverage preview ────────────────────────────────────────────────────────── +function saveSafeMaxFloorFromInput() { + const input = $("safe-max-floor") as HTMLInputElement; + const parsed = parseFloat(input.value); + const next = sanitizeSafeMaxFloor(parsed); + safeMaxHfFloor = next; + localStorage.setItem(SAFE_MAX_HF_FLOOR_KEY, String(next)); + renderSafeMaxFloorInput(); + updatePreview(); +} + +function applySafeMaxLeverage() { + const slider = $("leverage-slider") as HTMLInputElement; + const numIn = $("leverage-input") as HTMLInputElement; + const rs = reserves.find(r => r.asset.id === selectedAsset.id); + const c = rs ? rs.cFactor : selectedAsset.cFactor; + const l = rs?.lFactor ?? 1; + const floor = Math.max(safeMaxHfFloor, minHF()); + const sliderMax = parseFloat(slider.max) || 1.0; + const rawMax = Math.min(sliderMax, maxLeverageFor(c, l, floor)); + const steppedMax = Math.floor(rawMax * 10) / 10; + const next = Math.max(1.0, steppedMax); + + if (!Number.isFinite(next) || next <= 1.0) { + slider.value = numIn.value = "1.0"; + updatePreview(); + toast(`HF floor ${fmt(floor, 2)} only allows 1.0x leverage.`, "info"); + return; + } + + slider.value = numIn.value = next.toFixed(1); + updatePreview(); + toast(`Safe Max set to ${next.toFixed(1)}x for HF floor ${fmt(floor, 2)}.`, "success"); +} + function updatePreview() { const slider = $("leverage-slider") as HTMLInputElement; const numIn = $("leverage-input") as HTMLInputElement; @@ -2023,12 +2079,21 @@ function toggleTheme() { $("theme-toggle").addEventListener("click", toggleTheme); document.getElementById("mobile-theme-toggle")?.addEventListener("click", toggleTheme); +renderSafeMaxFloorInput(); +$("safe-max-floor").addEventListener("change", saveSafeMaxFloorFromInput); +$("safe-max-floor").addEventListener("keydown", (e) => { + if ((e as KeyboardEvent).key === "Enter") { + (e.target as HTMLInputElement).blur(); + } +}); + // Settings dropdown toggle $("settings-btn").addEventListener("click", (e) => { e.stopPropagation(); $("settings-dropdown").classList.toggle("hidden"); $("pool-dropdown").classList.add("hidden"); }); +$("settings-dropdown").addEventListener("click", (e) => e.stopPropagation()); // Network toggle $("network-toggle").addEventListener("click", () => { @@ -2201,6 +2266,7 @@ async function refreshAddFundsBalance() { } ($("leverage-slider") as HTMLInputElement).addEventListener("input", updatePreview); +$("safe-max-btn").addEventListener("click", applySafeMaxLeverage); // Live preview while typing (no clamping so user can type multi-digit numbers like "10") ($("leverage-input") as HTMLInputElement).addEventListener("input", () => { const numIn = $("leverage-input") as HTMLInputElement; diff --git a/frontend/src/style.css b/frontend/src/style.css index 0d4348f..cce685f 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -217,6 +217,16 @@ body { } .settings-dropdown-item:hover { background: var(--tab-hover-bg); color: var(--text); } .settings-badge { font-size: 10px; padding: 1px 6px; border-radius: 99px; background: var(--surface2); color: var(--text-3); } +.settings-field { + display: flex; align-items: center; justify-content: space-between; gap: 10px; + padding: 8px 12px; color: var(--text-2); font-size: 13px; font-weight: 500; +} +.settings-number { + width: 66px; padding: 4px 6px; + background: var(--input-bg); border: 1px solid var(--border); border-radius: var(--r-xs); + color: var(--text); font-size: 12px; text-align: right; outline: none; +} +.settings-number:focus { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-glow); } /* Legacy sidebar — hidden by default, used for mobile drawer */ .sidebar { display: none; } @@ -606,6 +616,7 @@ main { flex: 1; max-width: 1200px; width: 100%; margin: 0 auto; padding: 20px 24 .slider::-webkit-slider-thumb:hover { background: var(--primary-h); box-shadow: 0 0 0 6px var(--slider-glow-h); } .leverage-num-input { width: 68px; padding: 5px 6px; font-size: 15px; text-align: right; padding-right: 4px; } .slider-value-x { font-family: var(--mono); font-size: 16px; font-weight: 700; color: var(--text-3); margin-left: -4px; } +.safe-max-btn { flex-shrink: 0; padding: 5px 10px; font-size: 12px; } /* Slider risk zones */ .slider-zones { @@ -1192,6 +1203,7 @@ main { flex: 1; max-width: 1200px; width: 100%; margin: 0 auto; padding: 20px 24 .swap-asset-select { width: 100%; } .slider-row { flex-wrap: wrap; } .leverage-num-input { width: 100%; margin-top: 4px; } + .safe-max-btn { width: 100%; justify-content: center; } #wallet-area { flex-wrap: wrap; gap: 6px; } .btn-connect { font-size: 12px; padding: 5px 10px; } .slippage-opt { padding: 2px 6px; font-size: 11px; }