"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { fetchOperatorStakeStatus, withdrawOperatorStake, type StakeStatusResponse, type WithdrawStakeResponse, } from "@/lib/api"; import { arcscanTxUrl } from "@/lib/utils"; import { ArrowDownToLine, ExternalLink, Loader2, CheckCircle2, XCircle, } from "lucide-react"; /** * Operator-facing button to withdraw the 5 USDC auction-contract stake * (``TranslationAuction.withdrawStake()``). Mirrors ``ClaimFeesButton`` in * shape and state machine; the two sit side-by-side on the OperatorCard. * * Renders four visual states: * * 1. Loading stake status — disabled "Withdraw Stake" with spinner. * 2. ``staked=false`` — disabled with tooltip "No active stake". * 3. ``staked=true`` but ``can_withdraw=false`` (lock window) — disabled * with tooltip "Stake locked until block N". * 4. ``can_withdraw=true`` — enabled "Withdraw Stake ($X.XX)"; on click * POSTs to ``/api/operators/{addr}/withdraw-stake`` and shows the * resulting tx with an arcscan link (real hash) or muted text * (synthetic ``0xsim_…``). * * 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; for now mock is the safe default. */ interface WithdrawStakeButtonProps { /** Operator wallet address (0x-prefixed, 42 chars). */ address: string; /** Demo mode — "mock" returns a 0xsim_ hash, "live" attempts real RPC. */ mode?: "mock" | "live"; /** Optional callback invoked after a successful withdraw (parent refreshes). */ onWithdrawn?: (result: WithdrawStakeResponse) => void; /** Compact size for inline card use. */ size?: "sm" | "default"; } export function WithdrawStakeButton({ address, mode = "mock", onWithdrawn, size = "sm", }: WithdrawStakeButtonProps) { const [stakeStatus, setStakeStatus] = useState( null, ); const [isLoadingStatus, setIsLoadingStatus] = useState(true); const [statusError, setStatusError] = useState(null); const [isWithdrawing, setIsWithdrawing] = useState(false); const [lastResult, setLastResult] = useState( null, ); const [withdrawError, setWithdrawError] = useState(null); const refreshStatus = useCallback(async () => { setIsLoadingStatus(true); setStatusError(null); try { const res = await fetchOperatorStakeStatus(address); setStakeStatus(res); } catch (err) { const message = err instanceof Error ? err.message : "Unknown error"; setStatusError(message); } finally { setIsLoadingStatus(false); } }, [address]); useEffect(() => { void refreshStatus(); }, [refreshStatus]); const handleWithdraw = useCallback(async () => { setIsWithdrawing(true); setWithdrawError(null); setLastResult(null); try { const result = await withdrawOperatorStake(address, mode); setLastResult(result); if (result.success) { await refreshStatus(); onWithdrawn?.(result); } else { setWithdrawError("Withdraw reported no recovered stake."); } } catch (err) { const raw = err instanceof Error ? err.message : "Unknown error"; // Decode the structured detail surfaced by the API helper so the // user sees a humane line rather than a JSON blob. let friendly = raw; if (/stake_locked/i.test(raw)) { const blockMatch = raw.match(/"locked_until_block":\s*(\d+)/); friendly = blockMatch ? `Stake is locked until block ${blockMatch[1]}.` : "Stake is locked under the 72h slashable window."; } else if (/no_stake_to_withdraw/i.test(raw)) { friendly = "No active stake to withdraw."; } else if (/API 503/i.test(raw)) { friendly = "Arc RPC unavailable — try again shortly."; } setWithdrawError(friendly); } finally { setIsWithdrawing(false); } }, [address, mode, onWithdrawn, refreshStatus]); const stakeLabel = useMemo(() => { if (isLoadingStatus) return "Loading…"; if (!stakeStatus) return "—"; return stakeStatus.amount_usdc.toLocaleString("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 4, }); }, [isLoadingStatus, stakeStatus]); const isDisabled = isLoadingStatus || isWithdrawing || statusError !== null || !stakeStatus || !stakeStatus.can_withdraw; const tooltipText = useMemo(() => { if (statusError !== null) { return `Failed to load stake status: ${statusError}`; } if (isLoadingStatus) return "Loading stake status…"; if (!stakeStatus) return undefined; if (!stakeStatus.staked) return "No active stake"; if ( stakeStatus.staked && !stakeStatus.can_withdraw && stakeStatus.locked_until_block !== null ) { return `Stake locked until block ${stakeStatus.locked_until_block}`; } if (stakeStatus.staked && !stakeStatus.can_withdraw) { return "Stake not yet withdrawable"; } return undefined; }, [statusError, isLoadingStatus, stakeStatus]); const txUrl = lastResult?.tx_hash ? arcscanTxUrl(lastResult.tx_hash) : null; return (
{lastResult && lastResult.success ? (
Recovered{" "} ${lastResult.amount_recovered_usdc.toFixed(4)} {lastResult.is_simulated ? " (simulated)" : ""} {txUrl ? ( {lastResult.tx_hash?.slice(0, 10)}… ) : lastResult.tx_hash ? ( {lastResult.tx_hash} ) : null}
) : null} {withdrawError ? (
{withdrawError}
) : null}
); }