Hoodquidity: liquidity campaigns for Robinhood Chain stock token markets
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>
This commit is contained in:
commit
dba2811447
1194 changed files with 206422 additions and 0 deletions
163
apps/web/src/components/PositionPanel.tsx
Normal file
163
apps/web/src/components/PositionPanel.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue