Full build: Vite/React acid-terminal frontend (BEARER design system, Panchang/Sentient/Fragment Mono, neon layer, palette audit script), HoodquidityVaults Foundry contracts (single+dual deposits via Chainlink-style feeds, hybrid exit, 10% platform fee, boost, Genesis LP), Reown AppKit wallet connect with Robinhood Chain testnet (46630), live data from Dexscreener and onchain reads, partner portal, design-sync setup for claude.ai/design. Deploy: static via Forgejo Actions platform, BUILD_DIR=apps/web/dist (.env). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
163 lines
5.3 KiB
TypeScript
163 lines
5.3 KiB
TypeScript
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 { fmtUsd, fmtCountdown, fmtNum } from "@/lib/format";
|
|
import { campaignPoints, isGenesisLp } from "@/lib/points";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { CountUp } from "@/components/CountUp";
|
|
import type { CampaignState } from "@/hooks/useCampaigns";
|
|
import type { usePosition } from "@/hooks/useCampaigns";
|
|
|
|
type Position = NonNullable<ReturnType<typeof usePosition>["position"]>;
|
|
|
|
export function PositionPanel({
|
|
campaign,
|
|
position,
|
|
onSettled,
|
|
}: {
|
|
campaign: CampaignState;
|
|
position: Position;
|
|
onSettled: () => void;
|
|
}) {
|
|
const deployment = getActiveDeployment();
|
|
const { writeContract, data: txHash, isPending, reset } = useWriteContract();
|
|
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
|
|
hash: txHash,
|
|
});
|
|
|
|
if (isSuccess) {
|
|
reset();
|
|
onSettled();
|
|
}
|
|
|
|
const busy = isPending || isConfirming;
|
|
const ended = campaign.status === "ended";
|
|
const id = BigInt(campaign.meta.campaignId);
|
|
|
|
const exit = () =>
|
|
writeContract({
|
|
address: deployment.vaults,
|
|
abi: hoodquidityVaultsAbi,
|
|
functionName: "withdraw",
|
|
args: [id, parseUnits(position.amountUsd.toString(), USD_DECIMALS)],
|
|
chainId: ACTIVE_CHAIN_ID,
|
|
});
|
|
|
|
const claim = () =>
|
|
writeContract({
|
|
address: deployment.vaults,
|
|
abi: hoodquidityVaultsAbi,
|
|
functionName: "claim",
|
|
args: [id],
|
|
chainId: ACTIVE_CHAIN_ID,
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="slabel">LP receipt</div>
|
|
<div className="flex gap-2">
|
|
{isGenesisLp(position.lpIndex) && <Badge tone="mint">Genesis LP</Badge>}
|
|
{position.boosted && <Badge tone="lime">1.5x boosted</Badge>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2.5 text-sm">
|
|
<div className="leader">
|
|
<span className="text-fog">Deposited</span>
|
|
<span className="num font-semibold">{fmtUsd(position.amountUsd, false)}</span>
|
|
</div>
|
|
<div className="leader">
|
|
<span className="text-fog">Vault share</span>
|
|
<span className="num font-semibold">
|
|
{campaign.tvlUsd > 0
|
|
? `${((position.amountUsd / campaign.tvlUsd) * 100).toFixed(1)}%`
|
|
: "0%"}
|
|
</span>
|
|
</div>
|
|
<div className="leader">
|
|
<span className="text-fog">Rewards accrued</span>
|
|
<span className="num font-semibold text-mint">
|
|
{fmtUsd(position.pendingUsd, false)}
|
|
</span>
|
|
</div>
|
|
<div className="leader">
|
|
<span className="text-fog">Rewards claimed</span>
|
|
<span className="num font-semibold">{fmtUsd(position.claimedUsd, false)}</span>
|
|
</div>
|
|
{position.stockAmount > 0 && (
|
|
<div className="leader">
|
|
<span className="text-fog">Stock leg</span>
|
|
<span className="num font-semibold">
|
|
{position.stockAmount.toFixed(4)} {campaign.meta.symbol}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<div className="leader">
|
|
<span className="text-fog">Points</span>
|
|
<span className="num font-semibold text-lime">
|
|
<CountUp
|
|
value={campaignPoints({
|
|
amountUsd: position.amountUsd,
|
|
depositedAt: position.depositedAt,
|
|
boosted: position.boosted,
|
|
})}
|
|
format={(n) => fmtNum(Math.floor(n))}
|
|
/>
|
|
</span>
|
|
</div>
|
|
<div className="leader">
|
|
<span className="text-fog">Full rewards unlock</span>
|
|
<span className="num font-semibold">
|
|
{ended ? "Unlocked" : fmtCountdown(campaign.end)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-3 pt-1">
|
|
{ended ? (
|
|
<>
|
|
<Button
|
|
className="flex-1"
|
|
disabled={busy || position.pendingUsd === 0}
|
|
onClick={claim}
|
|
>
|
|
{busy ? "Confirming..." : "Claim rewards"}
|
|
</Button>
|
|
<Button
|
|
className="flex-1"
|
|
variant="outline"
|
|
disabled={busy || position.amountUsd === 0}
|
|
onClick={exit}
|
|
>
|
|
Withdraw all
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<Button
|
|
className="flex-1"
|
|
variant="danger"
|
|
disabled={busy || position.amountUsd === 0}
|
|
onClick={exit}
|
|
>
|
|
{busy ? "Confirming..." : "Exit early"}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{!ended ? (
|
|
<p className="text-[11px] leading-relaxed text-fog-dim">
|
|
Early exit returns your principal minus a 0.1% fee. Accrued campaign
|
|
rewards are forfeited and redistributed to remaining LPs.
|
|
</p>
|
|
) : (
|
|
<p className="text-[11px] leading-relaxed text-fog-dim">
|
|
Reward payouts carry a 10% platform fee. Points have no monetary
|
|
value and feed future programs.
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|