feat: full client-side vault flow in the showcase build
All checks were successful
deploy / deploy (push) Successful in 46s
All checks were successful
deploy / deploy (push) Successful in 46s
When no on-chain vault is reachable (the production/showcase build), route faucet -> approve -> deposit -> withdraw/claim through a client-side vault (lib/simVault) instead of firing an on-chain tx that has no contract to hit. This removes the ContractFunctionExecutionError and makes the desk behave exactly like the live app: a funded wallet, realistic confirm delays, positions that show in the portfolio, campaign depth that grows and rewards that accrue over time (tuned to the displayed APRs). No funds move. Wired through AppBoard (deposit/claim/withdraw), DepositPanel and PositionPanel, plus useCampaigns/usePositions/usePosition so TVL, LP counts and positions all reflect the simulated deposits. On-chain path (dev/anvil, or any future real deployment) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
70ed11c7dd
commit
b147d7a766
5 changed files with 344 additions and 17 deletions
|
|
@ -11,11 +11,13 @@ import { hoodquidityVaultsAbi } from "@/lib/abi/HoodquidityVaults";
|
|||
import { mockERC20Abi } from "@/lib/abi/MockERC20";
|
||||
import {
|
||||
ACTIVE_CHAIN_ID,
|
||||
HAS_LIVE_VAULT,
|
||||
getActiveDeployment,
|
||||
USD_DECIMALS,
|
||||
MIN_DEPOSIT_USD,
|
||||
MIN_DEPOSIT_UNITS,
|
||||
} from "@/lib/contracts";
|
||||
import { useSimVault, simFaucet, simApprove, simDeposit } from "@/lib/simVault";
|
||||
import { slippageImpact } from "@/lib/math";
|
||||
import { fmtPct, fmtUsd } from "@/lib/format";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -40,9 +42,11 @@ export function DepositPanel({
|
|||
const { address, isConnected } = useAccount();
|
||||
const { open: openConnectModal } = useAppKit();
|
||||
const deployment = getActiveDeployment();
|
||||
const sim = useSimVault();
|
||||
const [mode, setMode] = useState<Mode>("single");
|
||||
const [input, setInput] = useState("");
|
||||
const [stockInput, setStockInput] = useState("");
|
||||
const [simBusy, setSimBusy] = useState(false);
|
||||
|
||||
const stockToken = campaign.meta.stockToken;
|
||||
const symbol = campaign.meta.symbol;
|
||||
|
|
@ -105,17 +109,26 @@ export function DepositPanel({
|
|||
chainId: ACTIVE_CHAIN_ID,
|
||||
},
|
||||
],
|
||||
query: { enabled: !!address },
|
||||
query: { enabled: HAS_LIVE_VAULT && !!address },
|
||||
});
|
||||
|
||||
const balance = (walletData?.[0]?.result as bigint | undefined) ?? 0n;
|
||||
const allowance = (walletData?.[1]?.result as bigint | undefined) ?? 0n;
|
||||
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);
|
||||
const allowance = HAS_LIVE_VAULT
|
||||
? ((walletData?.[1]?.result as bigint | undefined) ?? 0n)
|
||||
: parseUnits((isConnected ? sim.allowanceUsd : 0).toFixed(USD_DECIMALS), USD_DECIMALS);
|
||||
const stockBalance = (walletData?.[2]?.result as bigint | undefined) ?? 0n;
|
||||
const stockAllowance = (walletData?.[3]?.result as bigint | undefined) ?? 0n;
|
||||
const stockQuote = (walletData?.[4]?.result as bigint | undefined) ?? 0n;
|
||||
|
||||
/** USD value of the entered stock leg (or unit price when input empty). */
|
||||
const stockValueUsd = Number(stockQuote) / 10 ** USD_DECIMALS;
|
||||
const stockValueUsd = HAS_LIVE_VAULT
|
||||
? Number(stockQuote) / 10 ** USD_DECIMALS
|
||||
: (pair?.priceUsd ?? 0) * (stockAmount > 0n ? stockInputNum : 1);
|
||||
|
||||
const { writeContract, data: txHash, isPending, reset } = useWriteContract();
|
||||
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
|
||||
|
|
@ -128,18 +141,25 @@ export function DepositPanel({
|
|||
onSettled();
|
||||
}
|
||||
|
||||
const busy = isPending || isConfirming;
|
||||
const busy = isPending || isConfirming || simBusy;
|
||||
const dual = mode === "dual";
|
||||
|
||||
// combined credit (stable leg + valued stock leg) in stable base units
|
||||
const creditUnits = dual ? amount + (stockAmount > 0n ? stockQuote : 0n) : amount;
|
||||
const stockCreditUnits =
|
||||
stockAmount > 0n
|
||||
? HAS_LIVE_VAULT
|
||||
? stockQuote
|
||||
: parseUnits(stockValueUsd.toFixed(USD_DECIMALS), USD_DECIMALS)
|
||||
: 0n;
|
||||
const creditUnits = dual ? amount + stockCreditUnits : amount;
|
||||
const belowMin = creditUnits > 0n && creditUnits < MIN_DEPOSIT_UNITS;
|
||||
|
||||
const needsUsdFaucet = amount > 0n && balance < amount;
|
||||
const needsUsdApprove = !needsUsdFaucet && amount > 0n && allowance < amount;
|
||||
const needsStockFaucet = dual && stockAmount > 0n && stockBalance < stockAmount;
|
||||
// stock leg faucet/approve only apply on-chain; sim values it directly
|
||||
const needsStockFaucet = HAS_LIVE_VAULT && dual && stockAmount > 0n && stockBalance < stockAmount;
|
||||
const needsStockApprove =
|
||||
dual && !needsStockFaucet && stockAmount > 0n && stockAllowance < stockAmount;
|
||||
HAS_LIVE_VAULT && dual && !needsStockFaucet && stockAmount > 0n && stockAllowance < stockAmount;
|
||||
|
||||
const ready = dual ? stockAmount > 0n : amount > 0n;
|
||||
|
||||
|
|
@ -148,9 +168,28 @@ export function DepositPanel({
|
|||
? slippageImpact(PREVIEW_TRADE, pair.liquidityUsd + campaign.tvlUsd, depositUsd)
|
||||
: null;
|
||||
|
||||
const runSim = async () => {
|
||||
setSimBusy(true);
|
||||
try {
|
||||
if (needsUsdFaucet) {
|
||||
await simFaucet();
|
||||
} else if (needsUsdApprove) {
|
||||
await simApprove(Number(input) || 0);
|
||||
} else {
|
||||
await simDeposit(campaign.meta.campaignId, depositUsd, campaign.status === "boost");
|
||||
setInput("");
|
||||
setStockInput("");
|
||||
onSettled();
|
||||
}
|
||||
} finally {
|
||||
setSimBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const act = () => {
|
||||
if (!isConnected) return openConnectModal?.();
|
||||
if (belowMin) return; // guarded; the button is disabled below the minimum
|
||||
if (!HAS_LIVE_VAULT) return void runSim();
|
||||
if (needsUsdFaucet) {
|
||||
writeContract({
|
||||
address: deployment.usd,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { useState } from "react";
|
||||
import { parseUnits } from "viem";
|
||||
import { useWaitForTransactionReceipt, useWriteContract } from "wagmi";
|
||||
import { hoodquidityVaultsAbi } from "@/lib/abi/HoodquidityVaults";
|
||||
import { ACTIVE_CHAIN_ID, getActiveDeployment, USD_DECIMALS } from "@/lib/contracts";
|
||||
import { ACTIVE_CHAIN_ID, HAS_LIVE_VAULT, getActiveDeployment, USD_DECIMALS } from "@/lib/contracts";
|
||||
import { simWithdraw, simClaim } from "@/lib/simVault";
|
||||
import { fmtUsd, fmtCountdown, fmtNum } from "@/lib/format";
|
||||
import { campaignPoints, isGenesisLp } from "@/lib/points";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -22,6 +24,7 @@ export function PositionPanel({
|
|||
onSettled: () => void;
|
||||
}) {
|
||||
const deployment = getActiveDeployment();
|
||||
const [simBusy, setSimBusy] = useState(false);
|
||||
const { writeContract, data: txHash, isPending, reset } = useWriteContract();
|
||||
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
|
||||
hash: txHash,
|
||||
|
|
@ -32,11 +35,19 @@ export function PositionPanel({
|
|||
onSettled();
|
||||
}
|
||||
|
||||
const busy = isPending || isConfirming;
|
||||
const busy = isPending || isConfirming || simBusy;
|
||||
const ended = campaign.status === "ended";
|
||||
const id = BigInt(campaign.meta.campaignId);
|
||||
const campaignId = campaign.meta.campaignId;
|
||||
|
||||
const exit = () =>
|
||||
const exit = () => {
|
||||
if (!HAS_LIVE_VAULT) {
|
||||
setSimBusy(true);
|
||||
simWithdraw(campaignId)
|
||||
.then(onSettled)
|
||||
.finally(() => setSimBusy(false));
|
||||
return;
|
||||
}
|
||||
writeContract({
|
||||
address: deployment.vaults,
|
||||
abi: hoodquidityVaultsAbi,
|
||||
|
|
@ -44,8 +55,16 @@ export function PositionPanel({
|
|||
args: [id, parseUnits(position.amountUsd.toString(), USD_DECIMALS)],
|
||||
chainId: ACTIVE_CHAIN_ID,
|
||||
});
|
||||
};
|
||||
|
||||
const claim = () =>
|
||||
const claim = () => {
|
||||
if (!HAS_LIVE_VAULT) {
|
||||
setSimBusy(true);
|
||||
simClaim(campaignId)
|
||||
.then(onSettled)
|
||||
.finally(() => setSimBusy(false));
|
||||
return;
|
||||
}
|
||||
writeContract({
|
||||
address: deployment.vaults,
|
||||
abi: hoodquidityVaultsAbi,
|
||||
|
|
@ -53,6 +72,7 @@ export function PositionPanel({
|
|||
args: [id],
|
||||
chainId: ACTIVE_CHAIN_ID,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
USD_DECIMALS,
|
||||
type MarketMeta,
|
||||
} from "@/lib/contracts";
|
||||
import { useSimVault } from "@/lib/simVault";
|
||||
|
||||
export interface CampaignState {
|
||||
meta: MarketMeta;
|
||||
|
|
@ -63,6 +64,7 @@ function seedCampaign(meta: MarketMeta, now: number): CampaignState {
|
|||
|
||||
export function useCampaigns() {
|
||||
const deployment = getActiveDeployment();
|
||||
const sim = useSimVault();
|
||||
|
||||
const { data, isLoading, error } = useReadContracts({
|
||||
contracts: deployment.markets.map((m) => ({
|
||||
|
|
@ -77,10 +79,19 @@ export function useCampaigns() {
|
|||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Showcase build with no reachable vault: render the seeded launch set.
|
||||
// Showcase build with no reachable vault: render the seeded launch set,
|
||||
// folding in any simulated deposits so depth and LP counts move live.
|
||||
if (!HAS_LIVE_VAULT) {
|
||||
return {
|
||||
campaigns: deployment.markets.map((m) => seedCampaign(m, now)),
|
||||
campaigns: deployment.markets.map((m) => {
|
||||
const c = seedCampaign(m, now);
|
||||
const sp = sim.positions[m.campaignId];
|
||||
if (sp && sp.amountUsd > 0) {
|
||||
c.tvlUsd += sp.amountUsd;
|
||||
c.activeLPs += 1;
|
||||
}
|
||||
return c;
|
||||
}),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
deployment,
|
||||
|
|
@ -145,6 +156,7 @@ export function useCampaigns() {
|
|||
export function usePositions() {
|
||||
const { address } = useAccount();
|
||||
const deployment = getActiveDeployment();
|
||||
const sim = useSimVault();
|
||||
const zero = "0x0000000000000000000000000000000000000000" as const;
|
||||
|
||||
const { data, isLoading, refetch } = useReadContracts({
|
||||
|
|
@ -167,6 +179,23 @@ export function usePositions() {
|
|||
query: { enabled: HAS_LIVE_VAULT && !!address, refetchInterval: 15_000 },
|
||||
});
|
||||
|
||||
if (!HAS_LIVE_VAULT) {
|
||||
const positions = deployment.markets.map((meta) => {
|
||||
const sp = address ? sim.positions[meta.campaignId] : undefined;
|
||||
return {
|
||||
meta,
|
||||
amountUsd: sp?.amountUsd ?? 0,
|
||||
claimedUsd: sp?.claimedUsd ?? 0,
|
||||
pendingUsd: sp?.pendingUsd ?? 0,
|
||||
boosted: sp?.boosted ?? false,
|
||||
depositedAt: sp?.depositedAt ?? 0,
|
||||
lpIndex: sp && sp.amountUsd > 0 ? 1 : 0,
|
||||
stockAmount: 0,
|
||||
};
|
||||
});
|
||||
return { positions, isLoading: false, refetch };
|
||||
}
|
||||
|
||||
const positions = deployment.markets.map((meta, i) => {
|
||||
const raw = data?.[i * 2]?.result as
|
||||
| {
|
||||
|
|
@ -198,6 +227,7 @@ export function usePositions() {
|
|||
export function usePosition(campaignId: number) {
|
||||
const { address } = useAccount();
|
||||
const deployment = getActiveDeployment();
|
||||
const sim = useSimVault();
|
||||
|
||||
const { data, isLoading, refetch } = useReadContracts({
|
||||
contracts: [
|
||||
|
|
@ -219,6 +249,28 @@ export function usePosition(campaignId: number) {
|
|||
query: { enabled: HAS_LIVE_VAULT && !!address, refetchInterval: 15_000 },
|
||||
});
|
||||
|
||||
if (!HAS_LIVE_VAULT) {
|
||||
const sp = address ? sim.positions[campaignId] : undefined;
|
||||
return {
|
||||
isLoading: false,
|
||||
refetch,
|
||||
position:
|
||||
sp && sp.amountUsd > 0
|
||||
? {
|
||||
amountUsd: sp.amountUsd,
|
||||
weighted: sp.weightedUsd,
|
||||
claimedUsd: sp.claimedUsd,
|
||||
depositedAt: sp.depositedAt,
|
||||
lpIndex: 1,
|
||||
stockAmount: 0,
|
||||
stockCreditUsd: 0,
|
||||
pendingUsd: sp.pendingUsd,
|
||||
boosted: sp.boosted,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const raw = data?.[0]?.result as
|
||||
| {
|
||||
amount: bigint;
|
||||
|
|
|
|||
165
apps/web/src/lib/simVault.ts
Normal file
165
apps/web/src/lib/simVault.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
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.
|
||||
*/
|
||||
|
||||
export interface SimPosition {
|
||||
amountUsd: number;
|
||||
weightedUsd: number;
|
||||
claimedUsd: number;
|
||||
pendingUsd: number;
|
||||
depositedAt: number; // unix seconds
|
||||
boosted: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
/** 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";
|
||||
|
||||
function initial(): SimState {
|
||||
return { balanceUsd: START_BALANCE_USD, allowanceUsd: 0, positions: {} };
|
||||
}
|
||||
|
||||
function load(): SimState {
|
||||
try {
|
||||
const raw = typeof localStorage !== "undefined" && localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw) as SimState;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return initial();
|
||||
}
|
||||
|
||||
let state: SimState = load();
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function persist() {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function set(next: SimState) {
|
||||
state = next;
|
||||
persist();
|
||||
listeners.forEach((l) => l());
|
||||
}
|
||||
|
||||
const wait = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
||||
|
||||
// ------------------------------------------------------------ reward accrual
|
||||
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
function ensureAccrual() {
|
||||
if (timer || typeof setInterval === "undefined") return;
|
||||
timer = setInterval(() => {
|
||||
const ids = Object.keys(state.positions);
|
||||
if (ids.length === 0) return;
|
||||
let changed = false;
|
||||
const positions: Record<number, SimPosition> = { ...state.positions };
|
||||
for (const key of ids) {
|
||||
const id = Number(key);
|
||||
const p = positions[id];
|
||||
if (p.amountUsd <= 0) continue;
|
||||
const rate = (DEMO_APR / SECONDS_PER_YEAR) * (p.boosted ? 1.5 : 1);
|
||||
positions[id] = { ...p, pendingUsd: p.pendingUsd + p.amountUsd * rate * (TICK_MS / 1000) };
|
||||
changed = true;
|
||||
}
|
||||
if (changed) set({ ...state, positions });
|
||||
}, TICK_MS);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- actions
|
||||
|
||||
export async function simFaucet() {
|
||||
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 });
|
||||
}
|
||||
|
||||
export async function simDeposit(campaignId: number, amountUsd: number, boosted: boolean) {
|
||||
await wait(CONFIRM_MS);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const prev = state.positions[campaignId];
|
||||
const weight = amountUsd * (boosted ? 1.5 : 1);
|
||||
const pos: SimPosition = prev
|
||||
? {
|
||||
...prev,
|
||||
amountUsd: prev.amountUsd + amountUsd,
|
||||
weightedUsd: prev.weightedUsd + weight,
|
||||
depositedAt: now,
|
||||
boosted: prev.boosted || 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 },
|
||||
});
|
||||
}
|
||||
|
||||
export async function simWithdraw(campaignId: number) {
|
||||
await wait(CONFIRM_MS);
|
||||
const prev = state.positions[campaignId];
|
||||
if (!prev) 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 });
|
||||
}
|
||||
|
||||
export async function simClaim(campaignId: number) {
|
||||
await wait(CONFIRM_MS);
|
||||
const prev = state.positions[campaignId];
|
||||
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 },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------- hook
|
||||
|
||||
function subscribe(cb: () => void) {
|
||||
listeners.add(cb);
|
||||
ensureAccrual();
|
||||
return () => listeners.delete(cb);
|
||||
}
|
||||
function getSnapshot() {
|
||||
return state;
|
||||
}
|
||||
|
||||
export function useSimVault(): SimState {
|
||||
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
||||
}
|
||||
|
|
@ -19,6 +19,14 @@ 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 { feeApr, rewardApr } from "@/lib/math";
|
||||
import { fmtUsd, fmtAprPct, fmtCountdown } from "@/lib/format";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -47,8 +55,10 @@ export default function AppBoard() {
|
|||
const { campaigns, isLoading } = useCampaigns();
|
||||
const { data: pairs } = useAllMarketData(deployment.markets);
|
||||
const { positions, refetch: refetchPositions } = usePositions();
|
||||
const sim = useSimVault();
|
||||
|
||||
const [tab, setTab] = useState<"campaigns" | "portfolio">("campaigns");
|
||||
const [simBusy, setSimBusy] = useState(false);
|
||||
const [risk, setRisk] = useState<(typeof RISKS)[number]>("All");
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const [input, setInput] = useState("");
|
||||
|
|
@ -100,8 +110,13 @@ export default function AppBoard() {
|
|||
],
|
||||
query: { enabled: HAS_LIVE_VAULT && !!address },
|
||||
});
|
||||
const balance = (walletData?.[0]?.result as bigint | undefined) ?? 0n;
|
||||
const allowance = (walletData?.[1]?.result as bigint | undefined) ?? 0n;
|
||||
// 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);
|
||||
const allowance = HAS_LIVE_VAULT
|
||||
? ((walletData?.[1]?.result as bigint | undefined) ?? 0n)
|
||||
: parseUnits((isConnected ? sim.allowanceUsd : 0).toFixed(USD_DECIMALS), USD_DECIMALS);
|
||||
const balanceUsd = Number(balance) / 10 ** USD_DECIMALS;
|
||||
|
||||
const { writeContract, data: txHash, isPending, reset } = useWriteContract({
|
||||
|
|
@ -120,7 +135,7 @@ export default function AppBoard() {
|
|||
lastAction.current = "";
|
||||
}, [isSuccess, reset, refetchWallet, refetchPositions]);
|
||||
|
||||
const busy = isPending || isConfirming;
|
||||
const busy = isPending || isConfirming || simBusy;
|
||||
const belowMin = amount > 0n && amount < MIN_DEPOSIT_UNITS;
|
||||
const needsFaucet = amount > 0n && balance < amount;
|
||||
const needsApprove = !needsFaucet && amount > 0n && allowance < amount;
|
||||
|
|
@ -132,11 +147,33 @@ export default function AppBoard() {
|
|||
: 0;
|
||||
const selApr = selFee + selReward;
|
||||
|
||||
const runSim = async () => {
|
||||
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`);
|
||||
} else {
|
||||
const boosted = selected.status === "boost";
|
||||
setInput("");
|
||||
await simDeposit(selected.meta.campaignId, amountUsd, boosted);
|
||||
say(`deposited ${fmtUsd(amountUsd)} -> ${selected.meta.symbol} / hUSD`);
|
||||
}
|
||||
} finally {
|
||||
setSimBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const act = () => {
|
||||
if (!selected) return;
|
||||
if (!isConnected) return openConnectModal?.();
|
||||
if (amount === 0n) return say("enter an amount to deposit");
|
||||
if (belowMin) return say(`minimum deposit is $${MIN_DEPOSIT_USD}`);
|
||||
if (!HAS_LIVE_VAULT) return void runSim();
|
||||
if (needsFaucet) {
|
||||
lastAction.current = "hUSD minted to wallet";
|
||||
writeContract({
|
||||
|
|
@ -168,6 +205,13 @@ export default function AppBoard() {
|
|||
};
|
||||
|
||||
const claim = (campaignId: number) => {
|
||||
if (!HAS_LIVE_VAULT) {
|
||||
setSimBusy(true);
|
||||
simClaim(campaignId)
|
||||
.then(() => say("rewards claimed -> wallet"))
|
||||
.finally(() => setSimBusy(false));
|
||||
return;
|
||||
}
|
||||
lastAction.current = "rewards claimed -> wallet";
|
||||
writeContract({
|
||||
address: deployment.vaults,
|
||||
|
|
@ -179,6 +223,13 @@ export default function AppBoard() {
|
|||
};
|
||||
|
||||
const withdraw = (campaignId: number, posAmountUsd: number) => {
|
||||
if (!HAS_LIVE_VAULT) {
|
||||
setSimBusy(true);
|
||||
simWithdraw(campaignId)
|
||||
.then(() => say("position closed · principal returned"))
|
||||
.finally(() => setSimBusy(false));
|
||||
return;
|
||||
}
|
||||
lastAction.current = "position closed · principal returned";
|
||||
writeContract({
|
||||
address: deployment.vaults,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue