"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { claimOperatorFees, fetchOperatorPendingFees, type ClaimFeesResponse, type PendingFeesResponse, } from "@/lib/api"; import { arcscanTxUrl } from "@/lib/utils"; import { Coins, ExternalLink, Loader2, CheckCircle2, XCircle } from "lucide-react"; /** * Operator-facing button to withdraw accumulated 90% builder fees from * ``BuilderFeeRouter.claimFees``. Renders three visual states: * * 1. Loading pending balance — disabled "Claim Fees" with spinner. * 2. Pending balance == 0 — disabled with tooltip "No fees accumulated yet". * 3. Pending balance > 0 — enabled "Claim Fees ($X.XX)"; on click POSTs * to ``/api/operators/{addr}/claim-fees`` and shows the resulting tx * with an arcscan link (when the hash is real, not synthetic). * * Defaults to ``mode="mock"`` so the demo path never burns real gas. The * containing /operators page toggles ``mode`` based on the user's mode * selection elsewhere in the UI (TBD; for now mock is the safe default). */ interface ClaimFeesButtonProps { /** Operator wallet address (0x-prefixed, 42 chars). */ address: string; /** Demo mode — "mock" returns 0xsim_ hash, "live" attempts real RPC. */ mode?: "mock" | "live"; /** * Optional initial pending value (e.g. from a parent leaderboard fetch). * The component still fetches its own pending balance to stay accurate * after a claim, but having an initial value avoids a flash of "Loading…". */ initialPendingUsdc?: number; /** Optional callback invoked after a successful claim (parent can refresh). */ onClaimed?: (result: ClaimFeesResponse) => void; /** Compact size for inline table/card use. */ size?: "sm" | "default"; } export function ClaimFeesButton({ address, mode = "mock", initialPendingUsdc, onClaimed, size = "sm", }: ClaimFeesButtonProps) { const [pending, setPending] = useState( typeof initialPendingUsdc === "number" ? initialPendingUsdc : null, ); const [isLoadingPending, setIsLoadingPending] = useState( typeof initialPendingUsdc !== "number", ); const [pendingError, setPendingError] = useState(null); const [isClaiming, setIsClaiming] = useState(false); const [lastResult, setLastResult] = useState(null); const [claimError, setClaimError] = useState(null); const refreshPending = useCallback(async () => { setIsLoadingPending(true); setPendingError(null); try { const res: PendingFeesResponse = await fetchOperatorPendingFees(address); setPending(res.pending_usdc); } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; setPendingError(message); } finally { setIsLoadingPending(false); } }, [address]); useEffect(() => { void refreshPending(); }, [refreshPending]); const handleClaim = useCallback(async () => { setIsClaiming(true); setClaimError(null); setLastResult(null); try { const result = await claimOperatorFees(address, mode); setLastResult(result); if (result.success) { // Refresh the pending balance — it should drop to 0 after a settle. await refreshPending(); onClaimed?.(result); } else { setClaimError( mode === "live" ? "Chain call failed — local balance preserved for retry." : "Nothing to claim.", ); } } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; setClaimError(message); } finally { setIsClaiming(false); } }, [address, mode, onClaimed, refreshPending]); const pendingLabel = useMemo(() => { if (isLoadingPending) return "Loading…"; if (pending === null) return "—"; return pending.toLocaleString("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 4, }); }, [isLoadingPending, pending]); const isDisabled = isLoadingPending || isClaiming || pending === null || pending <= 0 || pendingError !== null; const tooltipText = pendingError !== null ? `Failed to load pending fees: ${pendingError}` : isLoadingPending ? "Loading pending balance…" : pending !== null && pending <= 0 ? "No fees accumulated yet" : undefined; const txUrl = lastResult?.tx_hash ? arcscanTxUrl(lastResult.tx_hash) : null; return (
{lastResult && lastResult.success ? (
Claimed{" "} ${lastResult.amount_claimed_usdc.toFixed(4)} {lastResult.is_simulated ? " (simulated)" : ""} {txUrl ? ( {lastResult.tx_hash?.slice(0, 10)}… ) : lastResult.tx_hash ? ( {lastResult.tx_hash} ) : null}
) : null} {claimError ? (
{claimError}
) : null}
); }