feat: drop the demo balance/faucet from the showcase deposit flow
All checks were successful
deploy / deploy (push) Successful in 1m17s
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:
parent
b147d7a766
commit
27f01990f0
3 changed files with 69 additions and 82 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue