From 27f01990f08e80abf92c0fc1a6811ee8c30ed280 Mon Sep 17 00:00:00 2001 From: maxgort Date: Sun, 12 Jul 2026 20:47:05 +0300 Subject: [PATCH] feat: drop the demo balance/faucet from the showcase deposit flow Remove the pre-funded wallet ($25k hUSD) and the faucet step from the client-side vault so the showcase reads like a real, already-funded mainnet wallet: no balance readout, no MAX, no "Get hUSD". The flow is simply approve -> deposit. Only the vault allowance is tracked (a single approve covers later deposits). Portfolio swaps the "Wallet balance" tile for "Rewards claimed". Storage key bumped to v2 so any stale $25k state from a prior visit is discarded. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/src/components/DepositPanel.tsx | 51 +++++++++---------- apps/web/src/lib/simVault.ts | 38 +++++---------- apps/web/src/screens/AppBoard.tsx | 62 ++++++++++++------------ 3 files changed, 69 insertions(+), 82 deletions(-) diff --git a/apps/web/src/components/DepositPanel.tsx b/apps/web/src/components/DepositPanel.tsx index a72121e..c0da62a 100644 --- a/apps/web/src/components/DepositPanel.tsx +++ b/apps/web/src/components/DepositPanel.tsx @@ -17,7 +17,7 @@ import { MIN_DEPOSIT_USD, MIN_DEPOSIT_UNITS, } from "@/lib/contracts"; -import { useSimVault, simFaucet, simApprove, simDeposit } from "@/lib/simVault"; +import { useSimVault, simApprove, simDeposit } from "@/lib/simVault"; import { slippageImpact } from "@/lib/math"; import { fmtPct, fmtUsd } from "@/lib/format"; import { Button } from "@/components/ui/button"; @@ -113,11 +113,10 @@ export function DepositPanel({ }); const stockInputNum = Number(stockInput) || 0; - // No live vault: the funded demo wallet stands in, and the stock leg is - // valued off the live Dexscreener price instead of the on-chain feed. - const balance = HAS_LIVE_VAULT - ? ((walletData?.[0]?.result as bigint | undefined) ?? 0n) - : parseUnits((isConnected ? sim.balanceUsd : 0).toFixed(USD_DECIMALS), USD_DECIMALS); + // No live vault: a funded mainnet wallet is assumed (no faucet, no balance + // gate); the stock leg is valued off the live Dexscreener price, and only + // the vault allowance is tracked so approve -> deposit stays real. + const balance = (walletData?.[0]?.result as bigint | undefined) ?? 0n; const allowance = HAS_LIVE_VAULT ? ((walletData?.[1]?.result as bigint | undefined) ?? 0n) : parseUnits((isConnected ? sim.allowanceUsd : 0).toFixed(USD_DECIMALS), USD_DECIMALS); @@ -154,7 +153,7 @@ export function DepositPanel({ const creditUnits = dual ? amount + stockCreditUnits : amount; const belowMin = creditUnits > 0n && creditUnits < MIN_DEPOSIT_UNITS; - const needsUsdFaucet = amount > 0n && balance < amount; + const needsUsdFaucet = HAS_LIVE_VAULT && amount > 0n && balance < amount; const needsUsdApprove = !needsUsdFaucet && amount > 0n && allowance < amount; // stock leg faucet/approve only apply on-chain; sim values it directly const needsStockFaucet = HAS_LIVE_VAULT && dual && stockAmount > 0n && stockBalance < stockAmount; @@ -171,10 +170,8 @@ export function DepositPanel({ const runSim = async () => { setSimBusy(true); try { - if (needsUsdFaucet) { - await simFaucet(); - } else if (needsUsdApprove) { - await simApprove(Number(input) || 0); + if (needsUsdApprove) { + await simApprove(); } else { await simDeposit(campaign.meta.campaignId, depositUsd, campaign.status === "boost"); setInput(""); @@ -284,13 +281,15 @@ export function DepositPanel({
Stock leg - + {HAS_LIVE_VAULT && ( + + )}
{dual ? "Stable leg" : "You deposit"} - + {HAS_LIVE_VAULT && ( + + )}
approve -> - * deposit -> claim/withdraw flow so the app behaves exactly like the live - * version: balances move, positions appear in the portfolio, campaign depth - * grows and rewards accrue over time. No funds move; this is presentation - * state only. + * build). It mirrors the shape and timing of the real approve -> deposit -> + * claim/withdraw flow so the app behaves exactly like the live version: + * positions appear in the portfolio, campaign depth grows and rewards accrue + * over time. There is no wallet balance or faucet — you approve once, then + * deposit, exactly like a funded mainnet wallet. No funds move. */ export interface SimPosition { @@ -19,25 +19,22 @@ export interface SimPosition { } interface SimState { - balanceUsd: number; allowanceUsd: number; positions: Record; } -/** A funded demo wallet, so the flow starts at approve -> deposit. */ -const START_BALANCE_USD = 25_000; -/** Matches MockERC20's faucet drip, in case a deposit outruns the balance. */ -const FAUCET_USD = 10_000; +/** A single approve covers any later deposit, like an infinite allowance. */ +const APPROVE_MAX = 1e15; /** Block-confirmation feel for each action. */ const CONFIRM_MS = 1_100; /** Reward accrual tuned to the campaign APRs the UI advertises (~5,000%). */ const DEMO_APR = 50; const SECONDS_PER_YEAR = 31_536_000; const TICK_MS = 2_000; -const STORAGE_KEY = "hq-vault-state-v1"; +const STORAGE_KEY = "hq-vault-state-v2"; function initial(): SimState { - return { balanceUsd: START_BALANCE_USD, allowanceUsd: 0, positions: {} }; + return { allowanceUsd: 0, positions: {} }; } function load(): SimState { @@ -93,14 +90,9 @@ function ensureAccrual() { // ------------------------------------------------------------------- actions -export async function simFaucet() { +export async function simApprove() { await wait(CONFIRM_MS); - set({ ...state, balanceUsd: state.balanceUsd + FAUCET_USD }); -} - -export async function simApprove(amountUsd: number) { - await wait(CONFIRM_MS); - set({ ...state, allowanceUsd: amountUsd }); + set({ ...state, allowanceUsd: APPROVE_MAX }); } export async function simDeposit(campaignId: number, amountUsd: number, boosted: boolean) { @@ -119,7 +111,6 @@ export async function simDeposit(campaignId: number, amountUsd: number, boosted: : { amountUsd, weightedUsd: weight, claimedUsd: 0, pendingUsd: 0, depositedAt: now, boosted }; set({ ...state, - balanceUsd: Math.max(0, state.balanceUsd - amountUsd), allowanceUsd: Math.max(0, state.allowanceUsd - amountUsd), positions: { ...state.positions, [campaignId]: pos }, }); @@ -127,12 +118,10 @@ export async function simDeposit(campaignId: number, amountUsd: number, boosted: export async function simWithdraw(campaignId: number) { await wait(CONFIRM_MS); - const prev = state.positions[campaignId]; - if (!prev) return; + if (!state.positions[campaignId]) return; const positions = { ...state.positions }; delete positions[campaignId]; - // principal returns in full; accrued (unclaimed) rewards are forfeited early - set({ ...state, balanceUsd: state.balanceUsd + prev.amountUsd, positions }); + set({ ...state, positions }); } export async function simClaim(campaignId: number) { @@ -141,7 +130,6 @@ export async function simClaim(campaignId: number) { if (!prev || prev.pendingUsd <= 0) return; set({ ...state, - balanceUsd: state.balanceUsd + prev.pendingUsd, positions: { ...state.positions, [campaignId]: { ...prev, claimedUsd: prev.claimedUsd + prev.pendingUsd, pendingUsd: 0 }, diff --git a/apps/web/src/screens/AppBoard.tsx b/apps/web/src/screens/AppBoard.tsx index c301034..d89b9e3 100644 --- a/apps/web/src/screens/AppBoard.tsx +++ b/apps/web/src/screens/AppBoard.tsx @@ -19,14 +19,7 @@ import { } from "@/lib/contracts"; import { useCampaigns, usePositions, type CampaignState } from "@/hooks/useCampaigns"; import { useAllMarketData } from "@/hooks/useMarketData"; -import { - useSimVault, - simFaucet, - simApprove, - simDeposit, - simWithdraw, - simClaim, -} from "@/lib/simVault"; +import { useSimVault, simApprove, simDeposit, simWithdraw, simClaim } from "@/lib/simVault"; import { feeApr, rewardApr } from "@/lib/math"; import { fmtUsd, fmtAprPct, fmtCountdown } from "@/lib/format"; import { cn } from "@/lib/utils"; @@ -110,10 +103,9 @@ export default function AppBoard() { ], query: { enabled: HAS_LIVE_VAULT && !!address }, }); - // With no live vault, the funded demo wallet drives the same state machine. - const balance = HAS_LIVE_VAULT - ? ((walletData?.[0]?.result as bigint | undefined) ?? 0n) - : parseUnits((isConnected ? sim.balanceUsd : 0).toFixed(USD_DECIMALS), USD_DECIMALS); + // No live vault: a funded mainnet wallet is assumed (no faucet, no balance + // gate); only the vault allowance is tracked so approve -> deposit is real. + const balance = (walletData?.[0]?.result as bigint | undefined) ?? 0n; const allowance = HAS_LIVE_VAULT ? ((walletData?.[1]?.result as bigint | undefined) ?? 0n) : parseUnits((isConnected ? sim.allowanceUsd : 0).toFixed(USD_DECIMALS), USD_DECIMALS); @@ -137,7 +129,7 @@ export default function AppBoard() { const busy = isPending || isConfirming || simBusy; const belowMin = amount > 0n && amount < MIN_DEPOSIT_UNITS; - const needsFaucet = amount > 0n && balance < amount; + const needsFaucet = HAS_LIVE_VAULT && amount > 0n && balance < amount; const needsApprove = !needsFaucet && amount > 0n && allowance < amount; const selPair = selected ? pairs?.[selected.meta.symbol] : undefined; @@ -151,12 +143,9 @@ export default function AppBoard() { if (!selected) return; setSimBusy(true); try { - if (needsFaucet) { - await simFaucet(); - say("hUSD minted to wallet"); - } else if (needsApprove) { - await simApprove(amountUsd); - say(`approved ${fmtUsd(amountUsd)} hUSD`); + if (needsApprove) { + await simApprove(); + say("hUSD spending approved"); } else { const boosted = selected.status === "boost"; setInput(""); @@ -243,6 +232,7 @@ export default function AppBoard() { const openPositions = positions.filter((p) => p.amountUsd > 0); const totalDeposited = openPositions.reduce((s, p) => s + p.amountUsd, 0); const totalPending = openPositions.reduce((s, p) => s + p.pendingUsd, 0); + const totalClaimed = openPositions.reduce((s, p) => s + p.claimedUsd, 0); const primaryLabel = !isConnected ? "Connect wallet" @@ -456,12 +446,14 @@ export default function AppBoard() {
Amount - + {HAS_LIVE_VAULT && ( + + )}
))} - + {HAS_LIVE_VAULT && ( + + )}
{isConnected && belowMin && (

@@ -564,7 +558,11 @@ export default function AppBoard() { - + {HAS_LIVE_VAULT ? ( + + ) : ( + + )}

{openPositions.length > 0 ? (