feat: drop the demo balance/faucet from the showcase deposit flow
All checks were successful
deploy / deploy (push) Successful in 1m17s

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) <noreply@anthropic.com>
This commit is contained in:
maxgort 2026-07-12 20:47:05 +03:00
parent b147d7a766
commit 27f01990f0
3 changed files with 69 additions and 82 deletions

View file

@ -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({
<div className="ticks rounded-none border border-edge bg-surface p-4">
<div className="flex items-center justify-between text-[11px] uppercase tracking-wider text-fog-dim">
<span>Stock leg</span>
<button
type="button"
className="num cursor-pointer text-fog transition-colors hover:text-mint"
onClick={() => setStockInput((Number(stockBalance) / 1e18).toString())}
>
Balance {(Number(stockBalance) / 1e18).toFixed(4)}
</button>
{HAS_LIVE_VAULT && (
<button
type="button"
className="num cursor-pointer text-fog transition-colors hover:text-mint"
onClick={() => setStockInput((Number(stockBalance) / 1e18).toString())}
>
Balance {(Number(stockBalance) / 1e18).toFixed(4)}
</button>
)}
</div>
<div className="mt-2 flex items-center gap-3">
<input
@ -313,13 +312,15 @@ export function DepositPanel({
<div className="ticks rounded-none border border-edge bg-surface p-4">
<div className="flex items-center justify-between text-[11px] uppercase tracking-wider text-fog-dim">
<span>{dual ? "Stable leg" : "You deposit"}</span>
<button
type="button"
className="num cursor-pointer text-fog transition-colors hover:text-mint"
onClick={() => setInput((Number(balance) / 10 ** USD_DECIMALS).toString())}
>
Balance {fmtUsd(Number(balance) / 10 ** USD_DECIMALS)}
</button>
{HAS_LIVE_VAULT && (
<button
type="button"
className="num cursor-pointer text-fog transition-colors hover:text-mint"
onClick={() => setInput((Number(balance) / 10 ** USD_DECIMALS).toString())}
>
Balance {fmtUsd(Number(balance) / 10 ** USD_DECIMALS)}
</button>
)}
</div>
<div className="mt-2 flex items-center gap-3">
<input

View file

@ -2,11 +2,11 @@ import { useSyncExternalStore } from "react";
/**
* Client-side vault used when no on-chain vault is reachable (the showcase
* build). It mirrors the shape and timing of the real faucet -> 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<number, SimPosition>;
}
/** 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 },

View file

@ -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() {
<div className="mt-5 space-y-2.5">
<div className="flex justify-between font-mono text-[10px] uppercase tracking-[0.08em]">
<span className="text-fog">Amount</span>
<button
className="num cursor-pointer text-fog-dim transition-colors hover:text-mint"
onClick={() => setInput(balanceUsd > 0 ? String(balanceUsd) : "")}
>
Balance: {fmtUsd(balanceUsd)}
</button>
{HAS_LIVE_VAULT && (
<button
className="num cursor-pointer text-fog-dim transition-colors hover:text-mint"
onClick={() => setInput(balanceUsd > 0 ? String(balanceUsd) : "")}
>
Balance: {fmtUsd(balanceUsd)}
</button>
)}
</div>
<div className="flex items-center gap-2 border border-edge-bright bg-surface py-1 pl-1 pr-3.5">
<input
@ -483,12 +475,14 @@ export default function AppBoard() {
{v.toLocaleString("en-US")}
</button>
))}
<button
onClick={() => setInput(balanceUsd > 0 ? String(balanceUsd) : "")}
className="flex-1 cursor-pointer border border-edge bg-transparent py-1.5 font-mono text-[11px] text-fog transition-colors hover:border-mint hover:text-mint"
>
MAX
</button>
{HAS_LIVE_VAULT && (
<button
onClick={() => setInput(balanceUsd > 0 ? String(balanceUsd) : "")}
className="flex-1 cursor-pointer border border-edge bg-transparent py-1.5 font-mono text-[11px] text-fog transition-colors hover:border-mint hover:text-mint"
>
MAX
</button>
)}
</div>
{isConnected && belowMin && (
<p className="num font-mono text-[10px] tracking-[0.04em] text-danger">
@ -564,7 +558,11 @@ export default function AppBoard() {
<Stat label="Total deposited" value={fmtUsd(totalDeposited)} accent />
<Stat label="Claimable rewards" value={fmtUsd(totalPending)} />
<Stat label="Open positions" value={String(openPositions.length)} />
<Stat label="Wallet balance" value={fmtUsd(balanceUsd)} />
{HAS_LIVE_VAULT ? (
<Stat label="Wallet balance" value={fmtUsd(balanceUsd)} />
) : (
<Stat label="Rewards claimed" value={fmtUsd(totalClaimed)} />
)}
</div>
{openPositions.length > 0 ? (