revert: restore the d6e38d2 showcase, keep only the $500 minimum deposit
All checks were successful
deploy / deploy (push) Successful in 1m18s
All checks were successful
deploy / deploy (push) Successful in 1m18s
Remove the client-side simulated vault and all its wiring (simVault.ts, AppBoard/DepositPanel/PositionPanel/useCampaigns sim paths, the balance/ faucet changes) and restore the original local dev addresses. The only change retained overd6e38d2is the $500 minimum-deposit rule: the MIN_DEPOSIT/BelowMinimum contract enforcement plus the "Min. deposit $500" frontend validation. Working tree now equalsd6e38d2+ that feature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
27f01990f0
commit
399610bcc8
6 changed files with 56 additions and 370 deletions
|
|
@ -11,13 +11,11 @@ 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, simApprove, simDeposit } from "@/lib/simVault";
|
||||
import { slippageImpact } from "@/lib/math";
|
||||
import { fmtPct, fmtUsd } from "@/lib/format";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -42,11 +40,9 @@ 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;
|
||||
|
|
@ -109,25 +105,17 @@ export function DepositPanel({
|
|||
chainId: ACTIVE_CHAIN_ID,
|
||||
},
|
||||
],
|
||||
query: { enabled: HAS_LIVE_VAULT && !!address },
|
||||
query: { enabled: !!address },
|
||||
});
|
||||
|
||||
const stockInputNum = Number(stockInput) || 0;
|
||||
// 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);
|
||||
const allowance = (walletData?.[1]?.result as bigint | undefined) ?? 0n;
|
||||
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 = HAS_LIVE_VAULT
|
||||
? Number(stockQuote) / 10 ** USD_DECIMALS
|
||||
: (pair?.priceUsd ?? 0) * (stockAmount > 0n ? stockInputNum : 1);
|
||||
const stockValueUsd = Number(stockQuote) / 10 ** USD_DECIMALS;
|
||||
|
||||
const { writeContract, data: txHash, isPending, reset } = useWriteContract();
|
||||
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
|
||||
|
|
@ -140,25 +128,18 @@ export function DepositPanel({
|
|||
onSettled();
|
||||
}
|
||||
|
||||
const busy = isPending || isConfirming || simBusy;
|
||||
const busy = isPending || isConfirming;
|
||||
const dual = mode === "dual";
|
||||
|
||||
// combined credit (stable leg + valued stock leg) in stable base units
|
||||
const stockCreditUnits =
|
||||
stockAmount > 0n
|
||||
? HAS_LIVE_VAULT
|
||||
? stockQuote
|
||||
: parseUnits(stockValueUsd.toFixed(USD_DECIMALS), USD_DECIMALS)
|
||||
: 0n;
|
||||
const creditUnits = dual ? amount + stockCreditUnits : amount;
|
||||
const creditUnits = dual ? amount + (stockAmount > 0n ? stockQuote : 0n) : amount;
|
||||
const belowMin = creditUnits > 0n && creditUnits < MIN_DEPOSIT_UNITS;
|
||||
|
||||
const needsUsdFaucet = HAS_LIVE_VAULT && amount > 0n && balance < amount;
|
||||
const needsUsdFaucet = 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;
|
||||
const needsStockFaucet = dual && stockAmount > 0n && stockBalance < stockAmount;
|
||||
const needsStockApprove =
|
||||
HAS_LIVE_VAULT && dual && !needsStockFaucet && stockAmount > 0n && stockAllowance < stockAmount;
|
||||
dual && !needsStockFaucet && stockAmount > 0n && stockAllowance < stockAmount;
|
||||
|
||||
const ready = dual ? stockAmount > 0n : amount > 0n;
|
||||
|
||||
|
|
@ -167,26 +148,9 @@ export function DepositPanel({
|
|||
? slippageImpact(PREVIEW_TRADE, pair.liquidityUsd + campaign.tvlUsd, depositUsd)
|
||||
: null;
|
||||
|
||||
const runSim = async () => {
|
||||
setSimBusy(true);
|
||||
try {
|
||||
if (needsUsdApprove) {
|
||||
await simApprove();
|
||||
} 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,
|
||||
|
|
@ -281,15 +245,13 @@ 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>
|
||||
{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>
|
||||
)}
|
||||
<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
|
||||
|
|
@ -312,15 +274,13 @@ 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>
|
||||
{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>
|
||||
)}
|
||||
<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
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { useState } from "react";
|
||||
import { parseUnits } from "viem";
|
||||
import { useWaitForTransactionReceipt, useWriteContract } from "wagmi";
|
||||
import { hoodquidityVaultsAbi } from "@/lib/abi/HoodquidityVaults";
|
||||
import { ACTIVE_CHAIN_ID, HAS_LIVE_VAULT, getActiveDeployment, USD_DECIMALS } from "@/lib/contracts";
|
||||
import { simWithdraw, simClaim } from "@/lib/simVault";
|
||||
import { ACTIVE_CHAIN_ID, getActiveDeployment, USD_DECIMALS } from "@/lib/contracts";
|
||||
import { fmtUsd, fmtCountdown, fmtNum } from "@/lib/format";
|
||||
import { campaignPoints, isGenesisLp } from "@/lib/points";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -24,7 +22,6 @@ 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,
|
||||
|
|
@ -35,19 +32,11 @@ export function PositionPanel({
|
|||
onSettled();
|
||||
}
|
||||
|
||||
const busy = isPending || isConfirming || simBusy;
|
||||
const busy = isPending || isConfirming;
|
||||
const ended = campaign.status === "ended";
|
||||
const id = BigInt(campaign.meta.campaignId);
|
||||
const campaignId = campaign.meta.campaignId;
|
||||
|
||||
const exit = () => {
|
||||
if (!HAS_LIVE_VAULT) {
|
||||
setSimBusy(true);
|
||||
simWithdraw(campaignId)
|
||||
.then(onSettled)
|
||||
.finally(() => setSimBusy(false));
|
||||
return;
|
||||
}
|
||||
const exit = () =>
|
||||
writeContract({
|
||||
address: deployment.vaults,
|
||||
abi: hoodquidityVaultsAbi,
|
||||
|
|
@ -55,16 +44,8 @@ export function PositionPanel({
|
|||
args: [id, parseUnits(position.amountUsd.toString(), USD_DECIMALS)],
|
||||
chainId: ACTIVE_CHAIN_ID,
|
||||
});
|
||||
};
|
||||
|
||||
const claim = () => {
|
||||
if (!HAS_LIVE_VAULT) {
|
||||
setSimBusy(true);
|
||||
simClaim(campaignId)
|
||||
.then(onSettled)
|
||||
.finally(() => setSimBusy(false));
|
||||
return;
|
||||
}
|
||||
const claim = () =>
|
||||
writeContract({
|
||||
address: deployment.vaults,
|
||||
abi: hoodquidityVaultsAbi,
|
||||
|
|
@ -72,7 +53,6 @@ export function PositionPanel({
|
|||
args: [id],
|
||||
chainId: ACTIVE_CHAIN_ID,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
USD_DECIMALS,
|
||||
type MarketMeta,
|
||||
} from "@/lib/contracts";
|
||||
import { useSimVault } from "@/lib/simVault";
|
||||
|
||||
export interface CampaignState {
|
||||
meta: MarketMeta;
|
||||
|
|
@ -64,7 +63,6 @@ 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) => ({
|
||||
|
|
@ -79,19 +77,10 @@ export function useCampaigns() {
|
|||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Showcase build with no reachable vault: render the seeded launch set,
|
||||
// folding in any simulated deposits so depth and LP counts move live.
|
||||
// Showcase build with no reachable vault: render the seeded launch set.
|
||||
if (!HAS_LIVE_VAULT) {
|
||||
return {
|
||||
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;
|
||||
}),
|
||||
campaigns: deployment.markets.map((m) => seedCampaign(m, now)),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
deployment,
|
||||
|
|
@ -156,7 +145,6 @@ 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({
|
||||
|
|
@ -179,23 +167,6 @@ 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
|
||||
| {
|
||||
|
|
@ -227,7 +198,6 @@ export function usePositions() {
|
|||
export function usePosition(campaignId: number) {
|
||||
const { address } = useAccount();
|
||||
const deployment = getActiveDeployment();
|
||||
const sim = useSimVault();
|
||||
|
||||
const { data, isLoading, refetch } = useReadContracts({
|
||||
contracts: [
|
||||
|
|
@ -249,28 +219,6 @@ 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;
|
||||
|
|
|
|||
|
|
@ -33,15 +33,15 @@ interface Deployment {
|
|||
|
||||
/** anvil (local dev) deployment, chainId 31337 */
|
||||
const local: Deployment = {
|
||||
vaults: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
|
||||
usd: "0x5FbDB2315678afecb367f032d93F642f64180aa3",
|
||||
vaults: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
|
||||
usd: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
|
||||
markets: [
|
||||
{
|
||||
symbol: "NVDA",
|
||||
name: "NVIDIA Stock Token",
|
||||
campaignId: 0,
|
||||
stockToken: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9",
|
||||
priceFeed: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707",
|
||||
stockToken: "0x59b670e9fA9D0A427751Af201D676719a970857b",
|
||||
priceFeed: "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1",
|
||||
dexToken: XSTOCK.NVDA,
|
||||
risk: "Medium",
|
||||
},
|
||||
|
|
@ -49,8 +49,8 @@ const local: Deployment = {
|
|||
symbol: "TSLA",
|
||||
name: "Tesla Stock Token",
|
||||
campaignId: 1,
|
||||
stockToken: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
|
||||
priceFeed: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6",
|
||||
stockToken: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
|
||||
priceFeed: "0x4A679253410272dd5232B3Ff7cF5dbB88f295319",
|
||||
dexToken: XSTOCK.TSLA,
|
||||
risk: "Medium",
|
||||
},
|
||||
|
|
@ -58,8 +58,8 @@ const local: Deployment = {
|
|||
symbol: "AAPL",
|
||||
name: "Apple Stock Token",
|
||||
campaignId: 2,
|
||||
stockToken: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
|
||||
priceFeed: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
|
||||
stockToken: "0x09635F643e140090A9A8Dcd712eD6285858ceBef",
|
||||
priceFeed: "0xc5a5C42992dECbae36851359345FE25997F5C42d",
|
||||
dexToken: XSTOCK.AAPL,
|
||||
risk: "Low",
|
||||
},
|
||||
|
|
@ -67,8 +67,8 @@ const local: Deployment = {
|
|||
symbol: "SPY",
|
||||
name: "SPDR S&P 500 Token",
|
||||
campaignId: 3,
|
||||
stockToken: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
|
||||
priceFeed: "0x9A676e781A523b5d0C0e43731313A708CB607508",
|
||||
stockToken: "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E",
|
||||
priceFeed: "0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690",
|
||||
dexToken: XSTOCK.SPY,
|
||||
risk: "Low",
|
||||
},
|
||||
|
|
@ -76,8 +76,8 @@ const local: Deployment = {
|
|||
symbol: "AIB",
|
||||
name: "AI Basket Token",
|
||||
campaignId: 4,
|
||||
stockToken: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
|
||||
priceFeed: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
|
||||
stockToken: "0x9E545E3C0baAB3E08CdfD552C960A1050f373042",
|
||||
priceFeed: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
|
||||
dexBasket: [XSTOCK.NVDA, XSTOCK.META, XSTOCK.GOOGL, XSTOCK.AMZN],
|
||||
risk: "High",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,153 +0,0 @@
|
|||
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 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 {
|
||||
amountUsd: number;
|
||||
weightedUsd: number;
|
||||
claimedUsd: number;
|
||||
pendingUsd: number;
|
||||
depositedAt: number; // unix seconds
|
||||
boosted: boolean;
|
||||
}
|
||||
|
||||
interface SimState {
|
||||
allowanceUsd: number;
|
||||
positions: Record<number, SimPosition>;
|
||||
}
|
||||
|
||||
/** 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-v2";
|
||||
|
||||
function initial(): SimState {
|
||||
return { 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 simApprove() {
|
||||
await wait(CONFIRM_MS);
|
||||
set({ ...state, allowanceUsd: APPROVE_MAX });
|
||||
}
|
||||
|
||||
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,
|
||||
allowanceUsd: Math.max(0, state.allowanceUsd - amountUsd),
|
||||
positions: { ...state.positions, [campaignId]: pos },
|
||||
});
|
||||
}
|
||||
|
||||
export async function simWithdraw(campaignId: number) {
|
||||
await wait(CONFIRM_MS);
|
||||
if (!state.positions[campaignId]) return;
|
||||
const positions = { ...state.positions };
|
||||
delete positions[campaignId];
|
||||
set({ ...state, positions });
|
||||
}
|
||||
|
||||
export async function simClaim(campaignId: number) {
|
||||
await wait(CONFIRM_MS);
|
||||
const prev = state.positions[campaignId];
|
||||
if (!prev || prev.pendingUsd <= 0) return;
|
||||
set({
|
||||
...state,
|
||||
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,7 +19,6 @@ import {
|
|||
} from "@/lib/contracts";
|
||||
import { useCampaigns, usePositions, type CampaignState } from "@/hooks/useCampaigns";
|
||||
import { useAllMarketData } from "@/hooks/useMarketData";
|
||||
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";
|
||||
|
|
@ -48,10 +47,8 @@ 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("");
|
||||
|
|
@ -103,12 +100,8 @@ export default function AppBoard() {
|
|||
],
|
||||
query: { enabled: HAS_LIVE_VAULT && !!address },
|
||||
});
|
||||
// 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);
|
||||
const allowance = (walletData?.[1]?.result as bigint | undefined) ?? 0n;
|
||||
const balanceUsd = Number(balance) / 10 ** USD_DECIMALS;
|
||||
|
||||
const { writeContract, data: txHash, isPending, reset } = useWriteContract({
|
||||
|
|
@ -127,9 +120,9 @@ export default function AppBoard() {
|
|||
lastAction.current = "";
|
||||
}, [isSuccess, reset, refetchWallet, refetchPositions]);
|
||||
|
||||
const busy = isPending || isConfirming || simBusy;
|
||||
const busy = isPending || isConfirming;
|
||||
const belowMin = amount > 0n && amount < MIN_DEPOSIT_UNITS;
|
||||
const needsFaucet = HAS_LIVE_VAULT && amount > 0n && balance < amount;
|
||||
const needsFaucet = amount > 0n && balance < amount;
|
||||
const needsApprove = !needsFaucet && amount > 0n && allowance < amount;
|
||||
|
||||
const selPair = selected ? pairs?.[selected.meta.symbol] : undefined;
|
||||
|
|
@ -139,30 +132,11 @@ export default function AppBoard() {
|
|||
: 0;
|
||||
const selApr = selFee + selReward;
|
||||
|
||||
const runSim = async () => {
|
||||
if (!selected) return;
|
||||
setSimBusy(true);
|
||||
try {
|
||||
if (needsApprove) {
|
||||
await simApprove();
|
||||
say("hUSD spending approved");
|
||||
} 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({
|
||||
|
|
@ -194,13 +168,6 @@ 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,
|
||||
|
|
@ -212,13 +179,6 @@ 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,
|
||||
|
|
@ -232,7 +192,6 @@ 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"
|
||||
|
|
@ -446,14 +405,12 @@ 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>
|
||||
{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>
|
||||
)}
|
||||
<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
|
||||
|
|
@ -475,14 +432,12 @@ export default function AppBoard() {
|
|||
{v.toLocaleString("en-US")}
|
||||
</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>
|
||||
)}
|
||||
<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">
|
||||
|
|
@ -558,11 +513,7 @@ 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)} />
|
||||
{HAS_LIVE_VAULT ? (
|
||||
<Stat label="Wallet balance" value={fmtUsd(balanceUsd)} />
|
||||
) : (
|
||||
<Stat label="Rewards claimed" value={fmtUsd(totalClaimed)} />
|
||||
)}
|
||||
<Stat label="Wallet balance" value={fmtUsd(balanceUsd)} />
|
||||
</div>
|
||||
|
||||
{openPositions.length > 0 ? (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue