diff --git a/frontend/src/app/app/page.tsx b/frontend/src/app/app/page.tsx index 5a236b2..1369413 100644 --- a/frontend/src/app/app/page.tsx +++ b/frontend/src/app/app/page.tsx @@ -2,12 +2,28 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Address, formatUnits, parseUnits } from "viem"; -import { contractAddresses, ERC20_ABI, V2_LOTTERY_ABI, WETH_LIKE_ABI } from "@/lib/contracts"; -import { formatToken, publicClient, getWalletClientFromEIP1193 } from "@/lib/wallet"; +import { + contractAddresses, + ERC20_ABI, + V2_LOTTERY_ABI, + WETH_LIKE_ABI, +} from "@/lib/contracts"; +import { + formatToken, + publicClient, + getWalletClientFromEIP1193, +} from "@/lib/wallet"; import Tooltip from "@/components/Tooltip"; import { fetchTokenPriceUsd, cn } from "@/lib/utils"; import { NETWORKS } from "@/lib/chains"; -import { ChevronDown, Loader2, Ticket, Coins, Gift, Sparkles } from "lucide-react"; +import { + ChevronDown, + Loader2, + Ticket, + Coins, + Gift, + Sparkles, +} from "lucide-react"; import { usePrivy, useWallets } from "@privy-io/react-auth"; export default function AppPage() { @@ -19,10 +35,12 @@ export default function AppPage() { const [userTickets, setUserTickets] = useState(BigInt(0)); const [totalTickets, setTotalTickets] = useState(BigInt(0)); const [prizePool, setPrizePool] = useState(BigInt(0)); - const [ticketUnit, setTicketUnit] = useState(BigInt("100000000000000000")); + const [ticketUnit, setTicketUnit] = useState( + BigInt("100000000000000000") + ); const [amountInput, setAmountInput] = useState(""); // token amount const [usdInput, setUsdInput] = useState(""); - const [amountMode, setAmountMode] = useState<'usd' | 'token'>("usd"); + const [amountMode, setAmountMode] = useState<"usd" | "token">("usd"); const [withdrawInput, setWithdrawInput] = useState(""); const [isTyping, setIsTyping] = useState(false); const typingTimerRef = useRef(null); @@ -45,7 +63,9 @@ export default function AppPage() { const [drawPrize, setDrawPrize] = useState(BigInt(0)); const [lastWinner, setLastWinner] = useState(null); const [lastPrize, setLastPrize] = useState(BigInt(0)); - const [lastFinalizedRound, setLastFinalizedRound] = useState(BigInt(0)); + const [lastFinalizedRound, setLastFinalizedRound] = useState( + BigInt(0) + ); const [acknowledgedWin, setAcknowledgedWin] = useState(false); const [incentiveBps, setIncentiveBps] = useState(0); const [demoMode, setDemoMode] = useState(false); @@ -56,18 +76,29 @@ export default function AppPage() { const [demoTotalTickets, setDemoTotalTickets] = useState(BigInt(0)); const [demoUserDeposit, setDemoUserDeposit] = useState(BigInt(0)); const [demoUserTickets, setDemoUserTickets] = useState(BigInt(0)); - const [demoParticipants, setDemoParticipants] = useState>([]); + const [demoParticipants, setDemoParticipants] = useState< + Array<{ addr: string; tickets: bigint }> + >([]); const [demoCanClose, setDemoCanClose] = useState(false); const [demoCanFinalize, setDemoCanFinalize] = useState(false); const [canCloseRound, setCanCloseRound] = useState(false); const [canFinalizeRound, setCanFinalizeRound] = useState(false); const [roundState, setRoundState] = useState(0); // 0=Active,1=Closed,2=Finalized const [drawBlock, setDrawBlock] = useState(BigInt(0)); - const [actionBusy, setActionBusy] = useState(null); + const [actionBusy, setActionBusy] = useState< + null | "close" | "finalize" | "harvest" + >(null); const [devSimEnd, setDevSimEnd] = useState(false); + const [currentBlock, setCurrentBlock] = useState(BigInt(0)); + const [connectedChainId, setConnectedChainId] = useState(null); const { login, logout, ready, authenticated } = usePrivy() as any; - const { wallets, ready: walletsReady, connectWallet: privyConnectWallet, disconnectWallet } = useWallets() as any; + const { + wallets, + ready: walletsReady, + connectWallet: privyConnectWallet, + disconnectWallet, + } = useWallets() as any; const onConnect = useCallback(async () => { if (!ready) return; @@ -108,6 +139,16 @@ export default function AppPage() { if (!client) return; const [addr] = await client.requestAddresses(); setAddress(addr as Address); + try { + const hexId = await provider.request({ method: 'eth_chainId' } as any); + const id = typeof hexId === 'string' ? parseInt(hexId, 16) : Number(hexId); + if (!Number.isNaN(id)) setConnectedChainId(id); + if (typeof (provider as any).on === 'function') { + (provider as any).on('chainChanged', (cid: string) => { + try { setConnectedChainId(parseInt(cid, 16)); } catch { /* noop */ } + }); + } + } catch {} } catch {} })(); }, [authenticated, walletsReady, wallets]); @@ -159,7 +200,7 @@ export default function AppPage() { let active = true; async function load() { try { - const [tUnit, tTickets, pPool, roundInfo] = await Promise.all([ + const [tUnit, tTickets, pPool, roundInfo, blk] = await Promise.all([ publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, @@ -180,6 +221,7 @@ export default function AppPage() { abi: V2_LOTTERY_ABI, functionName: "getCurrentRoundInfo", }) as Promise<[bigint, bigint, boolean, boolean]>, + publicClient.getBlockNumber(), ]); if (!active) return; const roundId = roundInfo[0]; @@ -196,6 +238,7 @@ export default function AppPage() { setTimeLeft(Number(roundInfo[1])); setCanCloseRound(Boolean(roundInfo[2])); setCanFinalizeRound(Boolean(roundInfo[3])); + setCurrentBlock(blk); try { setDrawBlock((roundData?.[1] as bigint) ?? BigInt(0)); setRoundState(Number(roundData?.[5] ?? 0)); @@ -235,7 +278,7 @@ export default function AppPage() { functionName: "getCurrentRoundInfo", })) as [bigint, bigint, boolean, boolean]; const roundId = roundInfo[0]; - const [tTickets, pPool, roundData] = await Promise.all([ + const [tTickets, pPool, roundData, blk] = await Promise.all([ publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, @@ -252,6 +295,7 @@ export default function AppPage() { functionName: "rounds", args: [roundId], }) as Promise, + publicClient.getBlockNumber(), ]); setCurrentRound(roundId); setTimeLeft(Number(roundInfo[1])); @@ -259,6 +303,7 @@ export default function AppPage() { setCanFinalizeRound(Boolean(roundInfo[3])); setTotalTickets(tTickets); setPrizePool(pPool); + setCurrentBlock(blk); try { setDrawBlock((roundData?.[1] as bigint) ?? BigInt(0)); setRoundState(Number(roundData?.[5] ?? 0)); @@ -305,7 +350,9 @@ export default function AppPage() { functionName: "balanceOf", args: [address as Address], }) as Promise, - ]).then(([depTix, userTix, balVal]) => [depTix[0], userTix, balVal] as const); + ]).then( + ([depTix, userTix, balVal]) => [depTix[0], userTix, balVal] as const + ); setUserDeposit(dep); setUserTickets(tix); setTokenBalance(bal); @@ -345,7 +392,7 @@ export default function AppPage() { const parsedAmount = useMemo(() => { try { - if (amountMode === 'token') { + if (amountMode === "token") { if (!amountInput) return BigInt(0); return parseUnits(amountInput, decimals); } @@ -361,31 +408,34 @@ export default function AppPage() { } }, [amountInput, usdInput, amountMode, usdPrice, decimals]); - const switchAmountMode = useCallback((next: 'usd' | 'token') => { - if (next === amountMode) return; - if (next === 'usd') { - // convert current token amount to usd - if (usdPrice && Number(usdPrice) > 0) { - const tokens = Number(amountInput || '0'); - if (isFinite(tokens) && tokens > 0) { - const usd = tokens * Number(usdPrice); - setUsdInput(usd.toFixed(2)); + const switchAmountMode = useCallback( + (next: "usd" | "token") => { + if (next === amountMode) return; + if (next === "usd") { + // convert current token amount to usd + if (usdPrice && Number(usdPrice) > 0) { + const tokens = Number(amountInput || "0"); + if (isFinite(tokens) && tokens > 0) { + const usd = tokens * Number(usdPrice); + setUsdInput(usd.toFixed(2)); + } } - } - setAmountMode('usd'); - } else { - // convert current usd to token - if (usdPrice && Number(usdPrice) > 0) { - const usdVal = Number(usdInput || '0'); - if (isFinite(usdVal) && usdVal > 0) { - const tokens = usdVal / Number(usdPrice); - const precision = Math.min(6, decimals); - setAmountInput(tokens.toFixed(precision)); + setAmountMode("usd"); + } else { + // convert current usd to token + if (usdPrice && Number(usdPrice) > 0) { + const usdVal = Number(usdInput || "0"); + if (isFinite(usdVal) && usdVal > 0) { + const tokens = usdVal / Number(usdPrice); + const precision = Math.min(6, decimals); + setAmountInput(tokens.toFixed(precision)); + } } + setAmountMode("token"); } - setAmountMode('token'); - } - }, [amountMode, amountInput, usdInput, usdPrice, decimals]); + }, + [amountMode, amountInput, usdInput, usdPrice, decimals] + ); const parsedWithdraw = useMemo(() => { if (!withdrawInput) return BigInt(0); @@ -416,10 +466,35 @@ export default function AppPage() { return (Number(user) / Number(total)) * 100; }, [totalTickets, userTickets, potentialTickets]); + const canShowDepositOdds = useMemo(() => { + const roundAllowsDeposits = + demoMode || (roundState === 0 && !(devSimEnd || timeLeft <= 0)); + return parsedAmount > BigInt(0) && roundAllowsDeposits; + }, [parsedAmount, demoMode, roundState, devSimEnd, timeLeft]); + const onDeposit = useCallback(async () => { setTxError(null); - if (!address || parsedAmount === BigInt(0)) return; - if (ticketUnit !== BigInt(0) && parsedAmount % ticketUnit !== BigInt(0)) return; + if (!address) { + setTxError("Please connect your wallet to deposit."); + try { if (typeof window !== 'undefined') window.alert('Please connect your wallet to deposit.'); } catch {} + return; + } + if (parsedAmount === BigInt(0)) { + setTxError("Enter a valid amount to deposit."); + try { if (typeof window !== 'undefined') window.alert('Enter a valid amount to deposit.'); } catch {} + return; + } + if (ticketUnit !== BigInt(0) && parsedAmount % ticketUnit !== BigInt(0)) { + const unitStr = formatToken(ticketUnit, decimals); + setTxError(`Amount must be a multiple of the ticket unit (${unitStr}).`); + try { if (typeof window !== 'undefined') window.alert(`Amount must be a multiple of the ticket unit (${unitStr}).`); } catch {} + return; + } + if (!demoMode && !(roundState === 0 && !(devSimEnd || timeLeft <= 0))) { + setTxError("Deposits are disabled for the current round."); + try { if (typeof window !== 'undefined') window.alert('Deposits are disabled for the current round.'); } catch {} + return; + } setIsSubmitting(true); try { const primary = wallets && wallets.length > 0 ? wallets[0] : null; @@ -452,7 +527,10 @@ export default function AppPage() { address: contractAddresses.depositToken as Address, abi: ERC20_ABI, functionName: "allowance", - args: [address as Address, contractAddresses.lotteryContract as Address], + args: [ + address as Address, + contractAddresses.lotteryContract as Address, + ], })) as bigint; if (allowance < parsedAmount) { @@ -481,16 +559,17 @@ export default function AppPage() { await Promise.all([refreshUser(), refreshRound()]); setInfoOpen(true); } catch (err: any) { - setTxError(err?.shortMessage || err?.message || 'Transaction failed'); + setTxError(err?.shortMessage || err?.message || "Transaction failed"); } finally { setIsSubmitting(false); } - }, [address, parsedAmount, ticketUnit, wallets, refreshUser, refreshRound]); + }, [address, parsedAmount, ticketUnit, decimals, demoMode, roundState, devSimEnd, timeLeft, wallets, refreshUser, refreshRound]); const onWithdraw = useCallback(async () => { setTxError(null); if (!address || parsedWithdraw === BigInt(0)) return; - if (ticketUnit !== BigInt(0) && parsedWithdraw % ticketUnit !== BigInt(0)) return; + if (ticketUnit !== BigInt(0) && parsedWithdraw % ticketUnit !== BigInt(0)) + return; setIsSubmitting(true); try { const primary = wallets && wallets.length > 0 ? wallets[0] : null; @@ -509,7 +588,7 @@ export default function AppPage() { setWithdrawInput(""); await Promise.all([refreshUser(), refreshRound()]); } catch (err: any) { - setTxError(err?.shortMessage || err?.message || 'Transaction failed'); + setTxError(err?.shortMessage || err?.message || "Transaction failed"); } finally { setIsSubmitting(false); } @@ -518,7 +597,7 @@ export default function AppPage() { const onCloseRound = useCallback(async () => { if (!address) return; setTxError(null); - setActionBusy('close'); + setActionBusy("close"); try { const primary = wallets && wallets.length > 0 ? wallets[0] : null; const provider = primary ? await primary.getEthereumProvider() : null; @@ -535,7 +614,7 @@ export default function AppPage() { await publicClient.waitForTransactionReceipt({ hash }); await refreshRound(); } catch (err: any) { - setTxError(err?.shortMessage || err?.message || 'Transaction failed'); + setTxError(err?.shortMessage || err?.message || "Transaction failed"); } finally { setActionBusy(null); } @@ -544,7 +623,7 @@ export default function AppPage() { const onFinalizeRound = useCallback(async () => { if (!address) return; setTxError(null); - setActionBusy('finalize'); + setActionBusy("finalize"); try { const primary = wallets && wallets.length > 0 ? wallets[0] : null; const provider = primary ? await primary.getEthereumProvider() : null; @@ -563,29 +642,28 @@ export default function AppPage() { setDrawOpen(true); const hash = await walletClient.writeContract(request); await publicClient.waitForTransactionReceipt({ hash }); - await refreshRound(); - // After finalize, winner/prize belong to previous round (currentRound-1) + // After finalize, contract increments currentRound. Fetch new current, then read previous round data. + const [newCurrentRound] = (await publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "getCurrentRoundInfo", + })) as [bigint, bigint, boolean, boolean]; + const finalizedRound = newCurrentRound - BigInt(1); try { - const info = (await publicClient.readContract({ - address: contractAddresses.lotteryContract as Address, - abi: V2_LOTTERY_ABI, - functionName: "getRoundInfo", - args: [currentRound], - })) as [bigint, bigint, bigint, bigint, string, number]; - // If finalize just happened, currentRound may have been incremented – fetch previous const prevInfo = (await publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "getRoundInfo", - args: [info ? currentRound : currentRound - BigInt(1)], + args: [finalizedRound], })) as [bigint, bigint, bigint, bigint, string, number]; const winnerAddr = prevInfo[4]; const prizeAmt = prevInfo[3]; setDrawWinner(winnerAddr); setDrawPrize(prizeAmt); } catch {} + await refreshRound(); } catch (err: any) { - setTxError(err?.shortMessage || err?.message || 'Transaction failed'); + setTxError(err?.shortMessage || err?.message || "Transaction failed"); } finally { setActionBusy(null); setDrawing(false); @@ -595,7 +673,7 @@ export default function AppPage() { const onHarvestYield = useCallback(async () => { if (!address) return; setTxError(null); - setActionBusy('close'); // reuse busy flag styling + setActionBusy("harvest"); try { const primary = wallets && wallets.length > 0 ? wallets[0] : null; const provider = primary ? await primary.getEthereumProvider() : null; @@ -612,7 +690,7 @@ export default function AppPage() { await publicClient.waitForTransactionReceipt({ hash }); await refreshRound(); } catch (err: any) { - setTxError(err?.shortMessage || err?.message || 'Transaction failed'); + setTxError(err?.shortMessage || err?.message || "Transaction failed"); } finally { setActionBusy(null); } @@ -624,43 +702,185 @@ export default function AppPage() { return `${(pct * 100).toFixed(2)}%`; }, [totalTickets, userTickets]); + const isOnHyperEVM = useMemo(() => { + return connectedChainId === NETWORKS.hyperEVM.chainId; + }, [connectedChainId]); + + const onSwitchHyperEVM = useCallback(async () => { + try { + const primary = wallets && wallets.length > 0 ? wallets[0] : null; + const provider = primary ? await primary.getEthereumProvider() : null; + if (!provider) return; + const chainIdHex = '0x' + NETWORKS.hyperEVM.chainId.toString(16); + await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: chainIdHex }] } as any); + } catch (e) { + // ignore; user may reject + } + }, [wallets]); + + const onAddHyperEVM = useCallback(async () => { + try { + const primary = wallets && wallets.length > 0 ? wallets[0] : null; + const provider = primary ? await primary.getEthereumProvider() : null; + if (!provider) return; + const chainIdHex = '0x' + NETWORKS.hyperEVM.chainId.toString(16); + await provider.request({ + method: 'wallet_addEthereumChain', + params: [{ + chainId: chainIdHex, + chainName: NETWORKS.hyperEVM.name, + nativeCurrency: NETWORKS.hyperEVM.nativeCurrency, + rpcUrls: [NETWORKS.hyperEVM.rpcUrl], + blockExplorerUrls: NETWORKS.hyperEVM.blockExplorer ? [NETWORKS.hyperEVM.blockExplorer] : [], + }], + } as any); + } catch (e) { + // ignore; user may reject + } + }, [wallets]); + + const blocksUntilDraw = useMemo(() => { + if ( + roundState !== 1 || + drawBlock === BigInt(0) || + currentBlock === BigInt(0) + ) + return 0; + const diff = Number(drawBlock - currentBlock); + return diff > 0 ? diff : 0; + }, [roundState, drawBlock, currentBlock]); + const loadDebug = useCallback(async () => { setDebugLoading(true); try { - const [blockNumber, roundInfo, ticketUnit, lotInt, harvInt, drawDelay, incBps, totDeps, pPool, lastHarv, curRound, totTix] = await Promise.all([ + const [ + blockNumber, + roundInfo, + ticketUnit, + lotInt, + harvInt, + drawDelay, + incBps, + totDeps, + pPool, + lastHarv, + curRound, + totTix, + ] = await Promise.all([ publicClient.getBlockNumber(), - publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "getCurrentRoundInfo" }) as Promise<[bigint, bigint, boolean, boolean]>, - publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "TICKET_UNIT" }) as Promise, - publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "LOTTERY_INTERVAL" }) as Promise, - publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "HARVEST_INTERVAL" }) as Promise, - publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "DRAW_BLOCKS_DELAY" }) as Promise, - publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "INCENTIVE_BPS" }) as Promise, - publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "totalDeposits" }) as Promise, - publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "prizePool" }) as Promise, - publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "lastHarvestTime" }) as Promise, - publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "currentRound" }) as Promise, - publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "totalTickets" }) as Promise, + publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "getCurrentRoundInfo", + }) as Promise<[bigint, bigint, boolean, boolean]>, + publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "TICKET_UNIT", + }) as Promise, + publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "LOTTERY_INTERVAL", + }) as Promise, + publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "HARVEST_INTERVAL", + }) as Promise, + publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "DRAW_BLOCKS_DELAY", + }) as Promise, + publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "INCENTIVE_BPS", + }) as Promise, + publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "totalDeposits", + }) as Promise, + publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "prizePool", + }) as Promise, + publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "lastHarvestTime", + }) as Promise, + publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "currentRound", + }) as Promise, + publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "totalTickets", + }) as Promise, ]); const roundId = roundInfo[0]; const [roundData, curRoundInfo] = await Promise.all([ - publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "getRoundInfo", args: [roundId] }) as Promise<[bigint, bigint, bigint, bigint, string, number]>, - publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "rounds", args: [roundId] }) as Promise, + publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "getRoundInfo", + args: [roundId], + }) as Promise<[bigint, bigint, bigint, bigint, string, number]>, + publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "rounds", + args: [roundId], + }) as Promise, ]); const participantsArr: Array<{ addr: string; tickets: string }> = []; for (let i = 0; i < 200; i++) { try { - const addr = (await publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "participants", args: [BigInt(i)] })) as Address; - if (!addr || addr === "0x0000000000000000000000000000000000000000") break; - const tix = (await publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "tickets", args: [addr] })) as bigint; - if (tix > BigInt(0)) participantsArr.push({ addr, tickets: tix.toString() }); - } catch { break; } + const addr = (await publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "participants", + args: [BigInt(i)], + })) as Address; + if (!addr || addr === "0x0000000000000000000000000000000000000000") + break; + const tix = (await publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "tickets", + args: [addr], + })) as bigint; + if (tix > BigInt(0)) + participantsArr.push({ addr, tickets: tix.toString() }); + } catch { + break; + } } - let userDep: bigint | null = null; let userTix: bigint | null = null; let userBal: bigint | null = null; + let userDep: bigint | null = null; + let userTix: bigint | null = null; + let userBal: bigint | null = null; if (address) { try { - const userInfo = (await publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, functionName: "getUserInfo", args: [address as Address] })) as [bigint, bigint]; - userDep = userInfo[0]; userTix = userInfo[1]; - userBal = (await publicClient.readContract({ address: contractAddresses.depositToken as Address, abi: ERC20_ABI, functionName: "balanceOf", args: [address as Address] })) as bigint; + const userInfo = (await publicClient.readContract({ + address: contractAddresses.lotteryContract as Address, + abi: V2_LOTTERY_ABI, + functionName: "getUserInfo", + args: [address as Address], + })) as [bigint, bigint]; + userDep = userInfo[0]; + userTix = userInfo[1]; + userBal = (await publicClient.readContract({ + address: contractAddresses.depositToken as Address, + abi: ERC20_ABI, + functionName: "balanceOf", + args: [address as Address], + })) as bigint; } catch {} } const dbg = { @@ -701,19 +921,28 @@ export default function AppPage() { state: Number(curRoundInfo?.[5] ?? 0), }, }, - lastFinalized: lastWinner ? { round: lastFinalizedRound.toString(), winner: lastWinner, prize: lastPrize.toString() } : null, + lastFinalized: lastWinner + ? { + round: lastFinalizedRound.toString(), + winner: lastWinner, + prize: lastPrize.toString(), + } + : null, participants: { count: participantsArr.length, list: participantsArr, }, - user: address ? { - address, - deposit: userDep ? userDep.toString() : null, - tickets: userTix ? userTix.toString() : null, - tokenBalance: userBal ? userBal.toString() : null, - } : null, + user: address + ? { + address, + deposit: userDep ? userDep.toString() : null, + tickets: userTix ? userTix.toString() : null, + tokenBalance: userBal ? userBal.toString() : null, + } + : null, }; - const replacer = (_k: string, v: any) => (typeof v === 'bigint' ? v.toString() : v); + const replacer = (_k: string, v: any) => + typeof v === "bigint" ? v.toString() : v; setDebugJson(JSON.stringify(dbg, replacer, 2)); } catch (e) { setDebugJson(`{"error":"Failed to load debug state"}`); @@ -732,24 +961,25 @@ export default function AppPage() {
{address.slice(0, 6)}...{address.slice(-4)}
- - - - - ) : ( - )} @@ -757,61 +987,142 @@ export default function AppPage() {
+ {/* Network warning */} + {!isOnHyperEVM && address && ( +
+
+
+ You are connected to chain ID {connectedChainId ?? 'unknown'}. Please switch to {NETWORKS.hyperEVM.name} (chain ID {NETWORKS.hyperEVM.chainId}). +
+
+ + +
+
+
+ )}
-

Enter the Lottery{demoMode ? ' (Demo)' : ''}

- ? +

+ Enter the Lottery{demoMode ? " (Demo)" : ""} +

+ + + ? + +
-

Boost your odds by depositing more. Withdraw anytime.

+

+ Boost your odds by depositing more. Withdraw anytime. +

{ - if (amountMode==='usd') { + if (amountMode === "usd") { setUsdInput(e.target.value); } else { setAmountInput(e.target.value); } setIsTyping(true); - if (typingTimerRef.current) clearTimeout(typingTimerRef.current); - typingTimerRef.current = setTimeout(() => setIsTyping(false), 700); + if (typingTimerRef.current) + clearTimeout(typingTimerRef.current); + typingTimerRef.current = setTimeout( + () => setIsTyping(false), + 700 + ); }} inputMode="decimal" />
- - + +
- {usdPrice !== null && (amountMode==='usd' ? ( -
≈ {formatToken(parsedAmount, decimals)} HYPE
- ) : ( -
≈ ${((Number(formatToken(parsedAmount, decimals))||0) * (usdPrice||0)).toFixed(2)} USD
- ))} - {roundState !== 0 || devSimEnd || timeLeft <= 0 ? ( @@ -820,94 +1131,309 @@ export default function AppPage() {
) : null}
- {parsedAmount > BigInt(0) && ( + {canShowDepositOdds && (
)}
- Your Deposit: {formatToken(demoMode ? demoUserDeposit : userDeposit, decimals)} {symbol}{usdPrice!==null?` ($${(Number(formatToken(demoMode ? demoUserDeposit : userDeposit, decimals)) * usdPrice).toFixed(2)})`:''} - Your tickets: {String(demoMode ? demoUserTickets : userTickets)} (0.1 HYPE per ticket) - Updated: {new Date(lastUpdatedMs).toLocaleTimeString()} + + Your Deposit:{" "} + {formatToken( + demoMode ? demoUserDeposit : userDeposit, + decimals + )}{" "} + {symbol} + {usdPrice !== null + ? ` ($${( + Number( + formatToken( + demoMode ? demoUserDeposit : userDeposit, + decimals + ) + ) * usdPrice + ).toFixed(2)})` + : ""} + + + Your tickets: {String(demoMode ? demoUserTickets : userTickets)}{" "} + (0.1 HYPE per ticket) + + + Updated: {new Date(lastUpdatedMs).toLocaleTimeString()} +
+ + {/* Status panel at top */} +
+
+ {demoMode + ? demoRoundState === 0 + ? demoTimeLeft > 0 + ? `Demo: Round is live — ${Math.floor( + demoTimeLeft / 3600 + )}h ${Math.floor((demoTimeLeft % 3600) / 60)}m left.` + : "Demo: End the round to schedule draw." + : demoRoundState === 1 + ? "Demo: Ready to draw." + : "Demo: Finalized" + : roundState === 0 + ? timeLeft > 0 + ? `Round is live — ${Math.floor( + timeLeft / 3600 + )}h ${Math.floor((timeLeft % 3600) / 60)}m left.` + : "Round has ended. You can close the round to schedule the draw." + : roundState === 1 + ? blocksUntilDraw > 0 + ? `Draw scheduled — ~${blocksUntilDraw} blocks to go` + : "Draw ready — you can draw now." + : "Finalized — next round running."} +
+
+ + {/* Actions panel below status */} +
+
+ {!demoMode && ( + <> + + {timeLeft <= 0 && ( + + )} + {roundState === 1 && ( + + )} + + )} + {demoMode && ( + <> + + + + + )} +
+ {txError && ( +
{txError}
+ )} +

Stats

- ? + + + ? + +
- - - - {!!incentiveBps && } - - - - 0 ? `${Math.floor((demoMode ? demoTimeLeft : timeLeft)/3600)}h ${Math.floor(((demoMode ? demoTimeLeft : timeLeft)%3600)/60)}m` : 'Ended')} /> + + + + {!!incentiveBps && ( + + )} + + + + 0 + ? `${Math.floor( + (demoMode ? demoTimeLeft : timeLeft) / 3600 + )}h ${Math.floor( + ((demoMode ? demoTimeLeft : timeLeft) % 3600) / 60 + )}m` + : "Ended" + } + /> {lastWinner && ( <> - - + + )}
- {(!demoMode && ((roundState === 0 && (devSimEnd || timeLeft <= 0) && canCloseRound) || (roundState === 1))) || (demoMode && (demoRoundState !== 2)) ? ( -
-
-
- {demoMode ? ( - demoRoundState === 0 ? 'Demo: End the round to schedule draw.' : (demoRoundState === 1 ? 'Demo: Ready to draw.' : 'Demo: Finalized') - ) : ( - roundState === 0 ? 'Round has ended. You can close the round to schedule the draw.' : (canFinalizeRound ? 'Draw block reached. You can finalize to select a winner.' : `Waiting for draw block #${drawBlock.toString()} to be reachable...`) - )} -
-
- {!demoMode && ( - <> - - {roundState === 0 && (devSimEnd || timeLeft <= 0) && canCloseRound && ( - - )} - {roundState === 1 && ( - - )} - - )} - {demoMode && ( - <> - - {demoRoundState === 0 && ( - - )} - {demoRoundState === 1 && ( - - )} - - )} + {/* duplicate bottom status/actions panel removed */} + {lastWinner && + address && + lastWinner.toLowerCase() === address.toLowerCase() && + !acknowledgedWin && ( +
+
+ 🎉 You won round {String(lastFinalizedRound)}! Prize:{" "} + {formatToken(lastPrize, decimals)} {symbol}. The prize has been sent to your wallet.
+
- {txError &&
{txError}
} -
- ) : null} - {lastWinner && address && lastWinner.toLowerCase() === address.toLowerCase() && !acknowledgedWin && ( -
-
🎉 You won round {String(lastFinalizedRound)}! Prize: {formatToken(lastPrize, decimals)} {symbol}
- -
- )} + )}

Live Participants

- ? + + + ? + +
{demoMode ? (
@@ -921,13 +1447,29 @@ export default function AppPage() { {demoParticipants.length === 0 ? ( - No participants yet. + + + No participants yet. + + ) : ( demoParticipants.map((p) => { - const pct = demoTotalTickets === BigInt(0) ? 0 : (Number(p.tickets) / Number(demoTotalTickets)) * 100; + const pct = + demoTotalTickets === BigInt(0) + ? 0 + : (Number(p.tickets) / Number(demoTotalTickets)) * + 100; return ( - - {p.addr.slice(0,6)}...{p.addr.slice(-4)} + + + {p.addr.slice(0, 6)}...{p.addr.slice(-4)} + {String(p.tickets)} {pct.toFixed(2)}% @@ -938,15 +1480,26 @@ export default function AppPage() {
) : ( - + )}

Recent Winners

- ? + + + ? + +
- +
@@ -960,9 +1513,19 @@ export default function AppPage() { >

Withdraw

- ? + + + ? + +
- + {withdrawOpen && (
@@ -973,7 +1536,13 @@ export default function AppPage() { onChange={(e) => setWithdrawInput(e.target.value)} inputMode="decimal" /> -
@@ -983,10 +1552,17 @@ export default function AppPage() {

Contract Details

- ? + + + ? + +
- +
@@ -996,25 +1572,55 @@ export default function AppPage() { {debugOpen && (
{debugLoading ? ( -
Loading…
+
+ Loading… +
) : ( -
{debugJson}
+
+                      {debugJson}
+                    
)}
- - + +
)} @@ -1025,10 +1631,21 @@ export default function AppPage() { {drawOpen && ( setDrawOpen(false)} - onClaim={async () => { setClaiming(true); await new Promise(r=>setTimeout(r,1200)); setClaiming(false); setClaimed(true); }} + onClaim={async () => { + setClaiming(true); + await new Promise((r) => setTimeout(r, 1200)); + setClaiming(false); + setClaimed(true); + }} claiming={claiming} claimed={claimed} - prize={drawPrize && drawPrize > BigInt(0) ? drawPrize : (prizePool > BigInt(0) ? prizePool : BigInt(100000000000000000))} + prize={ + drawPrize && drawPrize > BigInt(0) + ? drawPrize + : prizePool > BigInt(0) + ? prizePool + : BigInt(100000000000000000) + } symbol={symbol} decimals={decimals} usdPrice={usdPrice} @@ -1036,9 +1653,7 @@ export default function AppPage() { winnerAddress={drawWinner} /> )} - {infoOpen && ( - setInfoOpen(false)} /> - )} + {infoOpen && setInfoOpen(false)} />}
); } @@ -1046,19 +1661,26 @@ export default function AppPage() { function Stat({ label, value }: { label: string; value: string }) { return (
-
{label}
+
+ {label} +
{value}
); } function ContractLink({ label, address }: { label: string; address: string }) { - const explorer = 'https://hyperevmscan.io'; + const explorer = "https://hyperevmscan.io"; return ( ); @@ -1073,37 +1695,86 @@ function InfoModal({ onClose }: { onClose: () => void }) {
-

You’re in! Here’s what happens next

+

+ You're in! Here's what happens next +

-

Sit back and watch your odds rise as yield grows the prize.

+

+ Sit back and watch your odds rise as yield grows the prize. +

-
Yield
-
Your deposit earns yield continuously. We don’t risk principal.
+
+ + Yield +
+
+ Your deposit earns yield continuously. We don't risk principal. +
-
Prize Pool
-
Yield is harvested into the prize pool at intervals.
+
+ + + Prize Pool + +
+
+ Yield is harvested into the prize pool at intervals. +
-
Draw
-
At round end, a winner is selected proportionally to tickets.
+
+ + Draw +
+
+ At round end, a winner is selected proportionally to tickets. +
- +
- +
); } -function DrawModal({ onClose, onClaim, claiming, claimed, prize, symbol, decimals, usdPrice, yourAddress, winnerAddress }: - { onClose: () => void; onClaim: () => void | Promise; claiming: boolean; claimed: boolean; prize: bigint; symbol: string; decimals: number; usdPrice: number | null; yourAddress: string; winnerAddress?: string | null }) { - const [phase, setPhase] = useState<'spinning' | 'reveal' | 'won'>("spinning"); - const [displayAddr, setDisplayAddr] = useState("0x????????????????????????????????????????"); +function DrawModal({ + onClose, + onClaim, + claiming, + claimed, + prize, + symbol, + decimals, + usdPrice, + yourAddress, + winnerAddress, +}: { + onClose: () => void; + onClaim: () => void | Promise; + claiming: boolean; + claimed: boolean; + prize: bigint; + symbol: string; + decimals: number; + usdPrice: number | null; + yourAddress: string; + winnerAddress?: string | null; +}) { + const [phase, setPhase] = useState<"spinning" | "reveal" | "won">("spinning"); + const [displayAddr, setDisplayAddr] = useState( + "0x????????????????????????????????????????" + ); useEffect(() => { // Spin: rapidly shuffle addresses, then reveal user's address let stop = false; @@ -1112,7 +1783,7 @@ function DrawModal({ onClose, onClaim, claiming, claimed, prize, symbol, decimal "0x8B4C...9A77", "0xDEAD...BEEF", "0xCAFE...BABE", - `${yourAddress?.slice(0,6)}...${yourAddress?.slice(-4)}`, + `${yourAddress?.slice(0, 6)}...${yourAddress?.slice(-4)}`, ]; let i = 0; const spin = () => { @@ -1137,35 +1808,70 @@ function DrawModal({ onClose, onClaim, claiming, claimed, prize, symbol, decimal return (
- {phase === 'won' && } + {phase === "won" && }

Lottery draw in progress…

-

Selecting a winner fairly from all tickets.

+

+ Selecting a winner fairly from all tickets. +

- -
{displayAddr}
+ +
+ {displayAddr} +
- {phase !== 'won' ? ( + {phase !== "won" ? (
- +
) : (
-

🎉 You won!

+

+ {winnerAddress && + yourAddress && + winnerAddress.toLowerCase() === yourAddress.toLowerCase() + ? "🎉 You won!" + : "Result"} +

+ Winner + + {winnerAddress + ? `${winnerAddress.slice(0, 6)}...${winnerAddress.slice( + -4 + )}` + : "—"} + +
+
Prize - {formatToken(prize, decimals)} {symbol}{usdPrice!==null?` ($${(Number(formatToken(prize, decimals)) * usdPrice).toFixed(2)})`:''} + + {formatToken(prize, decimals)} {symbol} + {usdPrice !== null + ? ` ($${( + Number(formatToken(prize, decimals)) * usdPrice + ).toFixed(2)})` + : ""} +
-
- {!claimed ? ( - - ) : ( -
Claimed! 🎊
- )} - +
+
+ {winnerAddress && yourAddress && winnerAddress.toLowerCase() === yourAddress.toLowerCase() + ? 'Prize has been sent to your wallet.' + : 'Prize has been sent to the winner.'} +
+
)} @@ -1183,7 +1889,11 @@ function SpinnerStrip({ active }: { active: boolean }) { ))}
@@ -1211,11 +1921,13 @@ function ConfettiBurst() { className="absolute rounded-sm" style={{ left: `${left}%`, - top: '-10px', + top: "-10px", width: `${size}px`, height: `${size}px`, backgroundColor: `hsl(${hue} 90% 60%)`, - transform: `translateY(${go ? '140%' : '-20%'}) rotate(${go ? 360 : 0}deg)`, + transform: `translateY(${go ? "140%" : "-20%"}) rotate(${ + go ? 360 : 0 + }deg)`, transition: `transform 1200ms cubic-bezier(.2,.8,.2,1) ${delay}ms`, }} /> @@ -1234,7 +1946,13 @@ function Badge({ label, value }: { label: string; value: string }) { ); } -function OddsTicker({ targetPct, active }: { targetPct: number; active: boolean }) { +function OddsTicker({ + targetPct, + active, +}: { + targetPct: number; + active: boolean; +}) { const [displayPct, setDisplayPct] = useState(0); const rafRef = useRef(null); useEffect(() => { @@ -1256,11 +1974,13 @@ function OddsTicker({ targetPct, active }: { targetPct: number; active: boolean }; }, [targetPct]); - // Wiggle effect when typing to feel “alive” + // Wiggle effect when typing to feel "alive" useEffect(() => { if (!active) return; const id = setInterval(() => { - setDisplayPct((prev) => Math.max(0, Math.min(99.99, prev + (Math.random() - 0.5) * 0.6))); + setDisplayPct((prev) => + Math.max(0, Math.min(99.99, prev + (Math.random() - 0.5) * 0.6)) + ); }, 120); return () => clearInterval(id); }, [active]); @@ -1282,13 +2002,33 @@ function OddsTicker({ targetPct, active }: { targetPct: number; active: boolean ); } -function RecentWinners({ currentRound, symbol, decimals }: { currentRound: bigint; symbol: string; decimals: number }) { +function RecentWinners({ + currentRound, + symbol, + decimals, +}: { + currentRound: bigint; + symbol: string; + decimals: number; +}) { // Dummy data for now - const [rows] = useState>([ - { round: currentRound - BigInt(1), winner: "0x8ba1f109551bD432803012645Ac136ddd64DBA72", prize: BigInt("250000000000000000") }, - { round: currentRound - BigInt(2), winner: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", prize: BigInt("180000000000000000") }, - { round: currentRound - BigInt(3), winner: "0x66f820a414680B5bcda5eECA5dea238543F42054", prize: BigInt("120000000000000000") }, - ].filter(r => r.round > BigInt(0))); + const rows = [ + { + round: BigInt(1), + winner: "0x8ba1f109551bD432803012645Ac136ddd64DBA72", + prize: BigInt("250000000000000000"), + }, + { + round: BigInt(2), + winner: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + prize: BigInt("180000000000000000"), + }, + { + round: BigInt(3), + winner: "0x66f820a414680B5bcda5eECA5dea238543F42054", + prize: BigInt("120000000000000000"), + }, + ]; return (
@@ -1301,13 +2041,21 @@ function RecentWinners({ currentRound, symbol, decimals }: { currentRound: bigin {rows.length === 0 ? ( - + + + ) : ( rows.map((r) => ( - - + + )) )} @@ -1317,8 +2065,16 @@ function RecentWinners({ currentRound, symbol, decimals }: { currentRound: bigin ); } -function CurrentParticipantsLive({ totalTickets, symbol }: { totalTickets: bigint; symbol: string }) { - const [rows, setRows] = useState>([]); +function CurrentParticipantsLive({ + totalTickets, + symbol, +}: { + totalTickets: bigint; + symbol: string; +}) { + const [rows, setRows] = useState>( + [] + ); useEffect(() => { let active = true; async function load() { @@ -1333,7 +2089,8 @@ function CurrentParticipantsLive({ totalTickets, symbol }: { totalTickets: bigin functionName: "participants", args: [BigInt(i)], })) as Address; - if (!addr || addr === "0x0000000000000000000000000000000000000000") break; + if (!addr || addr === "0x0000000000000000000000000000000000000000") + break; const tix = (await publicClient.readContract({ address: contractAddresses.lotteryContract as Address, abi: V2_LOTTERY_ABI, @@ -1368,13 +2125,22 @@ function CurrentParticipantsLive({ totalTickets, symbol }: { totalTickets: bigin {rows.length === 0 ? ( - + + + ) : ( rows.map((p) => { - const pct = denom === BigInt(0) ? 0 : (Number(p.tickets) / Number(denom)) * 100; + const pct = + denom === BigInt(0) + ? 0 + : (Number(p.tickets) / Number(denom)) * 100; return ( - + @@ -1385,4 +2151,4 @@ function CurrentParticipantsLive({ totalTickets, symbol }: { totalTickets: bigin
No winners yet.
+ No winners yet. +
{String(r.round)}{r.winner.slice(0,6)}...{r.winner.slice(-4)}{formatToken(r.prize, decimals)} {symbol} + {r.winner.slice(0, 6)}...{r.winner.slice(-4)} + + {formatToken(r.prize, decimals)} {symbol} +
No participants yet.
+ No participants yet. +
{p.addr.slice(0,6)}...{p.addr.slice(-4)} + {p.addr.slice(0, 6)}...{p.addr.slice(-4)} + {String(p.tickets)} {pct.toFixed(2)}%
); -} \ No newline at end of file +} diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx index 7adb55c..221662a 100644 --- a/frontend/src/app/providers.tsx +++ b/frontend/src/app/providers.tsx @@ -1,5 +1,6 @@ "use client"; +import { hyperEVMChain } from "@/lib/wallet"; import { PrivyProvider } from "@privy-io/react-auth"; export default function Providers({ children }: { children: React.ReactNode }) { @@ -14,6 +15,7 @@ export default function Providers({ children }: { children: React.ReactNode }) { embeddedWallets: { createOnLogin: "users-without-wallets", }, + supportedChains: [hyperEVMChain], }} > {children} @@ -22,5 +24,3 @@ export default function Providers({ children }: { children: React.ReactNode }) { <>{children} ); } - -