Jackpot Page
This is the entry point for the jackpot page.// /app/jackpot/page.tsx
import { JackpotComponent } from '@/components/jackpot-component';
export default function JackpotPage() {
return <JackpotComponent />;
}
Jackpot Component
- This component will display the jackpot’s size, ticket price, odds of winning and a countdown to the next drawing.
- This component will also display a form to purchase tickets.
// /app/components/jackpot-component.tsx
'use client';
import { Card, CardContent } from '@/components/ui/card';
import { getUsersInfo } from '@/lib/contract';
import { useEffect, useState } from 'react';
import { useAccount } from 'wagmi';
import { BuyTickets } from './jackpot-components/buy-tickets';
import { Countdown } from './jackpot-components/countdown';
import { CurrentJackpot } from './jackpot-components/current-jackpot';
import { LastJackpot } from './jackpot-components/last-jackpot';
import { TicketPrice } from './jackpot-components/ticket-price';
import { WinningOdds } from './jackpot-components/winning-odds';
import { WithdrawWinnings } from './jackpot-components/withdraw-winnings';
export function JackpotComponent() {
const { address, isConnected } = useAccount();
const [winningsAvailable, setWinningsAvailable] = useState<number>(0);
useEffect(() => {
if (!address) return;
const fetchWinningsAvailable = async () => {
const usersInfo = await getUsersInfo(address);
if (!usersInfo) return;
const winningsAvailable = usersInfo.winningsClaimable;
setWinningsAvailable(Number(winningsAvailable));
};
fetchWinningsAvailable();
}, [address]);
return (
<div className="space-y-6">
{winningsAvailable > 0 && (
<WithdrawWinnings winningsAvailable={winningsAvailable} />
)}
<CurrentJackpot />
<Countdown />
<Card className="bg-white rounded-xl shadow-sm">
<CardContent className="p-6">
<div className="text-center">
<TicketPrice />
<WinningOdds />
{isConnected && address && (
<BuyTickets walletAddress={address} />
)}
</div>
</CardContent>
</Card>
<LastJackpot />
</div>
);
}
Buy Tickets
This component will display the current jackpot’s size.// /app/components/jackpot-components/buy-tickets.tsx
import { BaseJackpotAbi } from '@/lib/abi';
import {
CONTRACT_ADDRESS,
ERC20_TOKEN_ADDRESS,
REFERRER_ADDRESS,
} from '@/lib/constants';
import { getTokenAllowance, getTokenBalance } from '@/lib/contract';
import { useEffect, useState } from 'react';
import { parseAbi } from 'viem';
import { useAccount, useWriteContract } from 'wagmi';
import { ConnectButton } from '../connect-button';
import { Button } from '../ui/button';
export function BuyTickets({
walletAddress,
}: {
walletAddress: `0x${string}`;
}) {
const { isConnected } = useAccount();
const { data, error, isError, isPending, writeContract } =
useWriteContract();
const [ticketCount, setTicketCount] = useState<number>(1);
const [walletFunded, setWalletFunded] = useState<boolean>(false);
const [allowance, setAllowance] = useState<number>(0);
const increment = () => setTicketCount((prev) => prev + 1);
const decrement = () => setTicketCount((prev) => (prev > 1 ? prev - 1 : 1));
useEffect(() => {
// Get the balance of the wallet so we can check
// if it's funded
const fetchWalletFunds = async () => {
try {
const balance = await getTokenBalance(walletAddress);
if (balance) {
setWalletFunded(balance >= ticketCount * 10 ** 6);
} else {
setWalletFunded(false);
}
} catch (error) {
console.error('Error fetching wallet funded status:', error);
}
};
// Fetch wallet funds every 5 seconds
const intervalFunds = setInterval(fetchWalletFunds, 5000);
fetchWalletFunds();
// Get the allowance of the wallet so we can check
// if it's approved to buy tickets
const fetchAllowance = async () => {
try {
const allowance = await getTokenAllowance(walletAddress);
if (allowance) {
setAllowance(allowance);
} else {
setAllowance(0);
}
} catch (error) {
console.error('Error fetching allowance:', error);
}
};
// Fetch allowance every 5 seconds
const intervalAllowance = setInterval(fetchAllowance, 5000);
fetchAllowance();
return () => {
clearInterval(intervalFunds);
clearInterval(intervalAllowance);
};
}, [walletAddress]);
const handleApproveToken = async () => {
try {
if (!walletAddress) {
throw new Error('Wallet not connected');
}
const approveAbi = [
'function approve(address spender, uint256 amount) returns (bool)',
];
// Approve the token to be spent by the contract
return writeContract?.({
abi: parseAbi(approveAbi),
address: ERC20_TOKEN_ADDRESS as `0x${string}`,
functionName: 'approve',
args: [CONTRACT_ADDRESS, walletAddress],
});
} catch (error) {
console.error('Error approving token:', error);
}
};
const handleBuyTicket = async () => {
try {
if (!walletAddress) {
throw new Error('Wallet not connected');
}
const ticketCost = BigInt(1) * BigInt(10 ** 6);
// This is YOUR wallet to collect referral fees
const referrerAddress = REFERRER_ADDRESS;
return writeContract?.({
abi: BaseJackpotAbi,
address: CONTRACT_ADDRESS as `0x${string}`,
functionName: 'purchaseTickets',
args: [referrerAddress, ticketCost, walletAddress],
});
} catch (error) {
console.error('Error buying ticket:', error);
}
};
return (
<div className="flex flex-col items-center">
<div className="flex items-center">
<button
onClick={decrement}
className="bg-emerald-500 text-white px-2 hover:bg-emerald-600"
>
-
</button>
<input
type="number"
value={ticketCount}
onChange={(e) =>
setTicketCount(Math.max(1, Number(e.target.value)))
}
className="mx-2 w-16 text-center border border-emerald-500 rounded"
min="1"
style={{ appearance: 'none', MozAppearance: 'textfield' }}
/>
<button
onClick={increment}
className="bg-emerald-500 text-white px-2 hover:bg-emerald-600"
>
+
</button>
</div>
{isConnected &&
walletFunded &&
allowance >= ticketCount * 10 ** 6 ? (
<Button
onClick={handleBuyTicket}
disabled={!isConnected || !walletFunded}
className="mt-2 w-full bg-emerald-500 hover:bg-emerald-600 text-white"
>
Buy Ticket
</Button>
) : isConnected &&
walletFunded &&
allowance < ticketCount * 10 ** 6 ? (
<Button
onClick={handleApproveToken}
disabled={!isConnected}
className="mt-2 w-full bg-emerald-500 hover:bg-emerald-600 text-white"
>
Approve USDC Token
</Button>
) : isConnected && !walletFunded ? (
<div className="mt-2 w-full bg-orange-500 hover:bg-orange-600 text-white">
Not enough USDC in wallet
</div>
) : (
<div className="mt-2 w-full bg-emerald-500 hover:bg-emerald-600 text-white">
<ConnectButton />
</div>
)}
</div>
);
}
Countdown
This component will display a countdown to the next drawing.// /app/components/jackpot-components/countdown.tsx
import { getTimeRemaining } from '@/lib/contract';
import { useEffect, useState } from 'react';
import { Card, CardContent } from '../ui/card';
export function Countdown() {
const [timeRemaining, setTimeRemaining] = useState<string | null>(null);
useEffect(() => {
// Fetch time remaining
const fetchTimeRemaining = async () => {
const timeRemaining = await getTimeRemaining();
if (timeRemaining) {
setTimeRemaining(timeRemaining.toString());
}
};
fetchTimeRemaining();
// Update time remaining every second
const timer = setInterval(() => {
setTimeRemaining((prev) => {
if (!prev) return null;
const seconds = parseInt(prev, 10);
if (seconds <= 0) return '0';
return (seconds - 1).toString();
});
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<Card className="bg-white rounded-xl shadow-sm">
<CardContent className="p-6">
<div className="text-center">
<h2 className="text-lg font-medium text-gray-500 mb-2">
Time Remaining
</h2>
<p className="text-3xl font-bold">
{timeRemaining
? formatTime(parseInt(timeRemaining, 10))
: '--:--:--'}
</p>
</div>
</CardContent>
</Card>
);
}
const formatTime = (totalSeconds: number) => {
if (totalSeconds <= 0) return '00:00:00';
const hours = Math.floor(totalSeconds / 3600) % 24;
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(
2,
'0'
)}:${String(seconds).padStart(2, '0')}`;
};
Current Jackpot
This component will display the current jackpot’s size.import { getJackpotAmount } from '@/lib/contract';
import { useEffect, useState } from 'react';
import { Card, CardContent } from '../ui/card';
export function CurrentJackpot() {
const [jackpotAmount, setJackpotAmount] = useState<number | undefined>(
undefined
);
useEffect(() => {
const fetchJackpotAmount = async () => {
const jackpotAmount = await getJackpotAmount();
setJackpotAmount(jackpotAmount);
};
fetchJackpotAmount();
}, []);
return (
<Card className="bg-white rounded-xl shadow-sm">
<CardContent className="p-6">
<div className="text-center">
<h2 className="text-lg font-medium text-gray-500 mb-2">
Current Jackpot
</h2>
<p className="text-4xl font-bold text-emerald-500">
{jackpotAmount
? `$${jackpotAmount.toFixed(2)}`
: 'Loading...'}
</p>
</div>
</CardContent>
</Card>
);
}
Last Jackpot
This component will display the last jackpot’s results.// /app/components/jackpot-components/last-jackpot.tsx
import { getLastJackpotResults } from '@/lib/contract';
import { useEffect, useState } from 'react';
import { zeroAddress } from 'viem';
import { Card, CardContent } from '../ui/card';
interface LastJackpotEvent {
time: number;
winner: string;
winningTicket: number;
winAmount: number;
ticketsPurchasedTotalBps: number;
}
export function LastJackpot() {
const [lastJackpot, setLastJackpot] = useState<
LastJackpotEvent | undefined
>(undefined);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchLastJackpot = async () => {
const lastJackpot = await getLastJackpotResults();
if (lastJackpot) {
const lastJackpotEvent = {
time: lastJackpot.time,
winner: lastJackpot.winner,
winningTicket: lastJackpot.winningTicket / 7000, // Divide by 7000 to get the winning ticket number
winAmount: lastJackpot.winAmount,
ticketsPurchasedTotalBps:
lastJackpot.ticketsPurchasedTotalBps,
};
setLastJackpot(lastJackpotEvent);
setIsLoading(false);
}
};
fetchLastJackpot();
}, []);
return (
<Card>
<CardContent>
<div className="flex flex-col items-center justify-center">
<h1 className="text-2xl font-bold">Last Jackpot</h1>
{lastJackpot && !isLoading ? (
<>
<div className="flex justify-between w-full">
<p className="text-lg text-emerald-500">
Winner:
</p>
<p className="text-lg text-emerald-500">
{lastJackpot.winner === zeroAddress
? 'LPs Won'
: lastJackpot.winner}
</p>
</div>
<div className="flex justify-between w-full">
<p className="text-lg text-emerald-500">
Win Amount:
</p>
<p className="text-lg text-emerald-500">
{(lastJackpot.winAmount / 10 ** 6).toFixed(
2
)}{' '}
USDC
</p>
</div>
</>
) : (
<p className="text-lg">Loading...</p>
)}
</div>
</CardContent>
</Card>
);
}
Ticket Price
This component will display the current ticket price.// /app/components/jackpot-components/ticket-price.tsx
import { getTicketPrice } from '@/lib/contract';
import { useEffect, useState } from 'react';
export function TicketPrice() {
const [ticketPrice, setTicketPrice] = useState<string | null>(null);
useEffect(() => {
const fetchTicketPrice = async () => {
const price = await getTicketPrice();
setTicketPrice(price?.toString() || null);
};
fetchTicketPrice();
}, []);
return (
<div>
<h2 className="text-lg font-medium text-gray-500 mb-2">
Ticket Price
</h2>
<p className="text-2xl font-bold mb-4">{ticketPrice} USDC</p>
</div>
);
}
Winning Odds
This component will display the odds of winning.// /app/components/jackpot-components/winning-odds.tsx
import { getJackpotOdds } from '@/lib/contract';
import { useEffect, useState } from 'react';
export function WinningOdds() {
const [jackpotOdds, setJackpotOdds] = useState<number | null>(null);
useEffect(() => {
const fetchJackpotOdds = async () => {
const odds = await getJackpotOdds();
setJackpotOdds(odds || null);
};
fetchJackpotOdds();
}, []);
return (
<p className="text-sm text-gray-500 mb-4">
Odds of winning: 1 in {Number(jackpotOdds).toFixed(2)}
</p>
);
}
Withdraw Winnings
This component will display the form to withdraw winnings.
import { BaseJackpotAbi } from '@/lib/abi';
import { CONTRACT_ADDRESS } from '@/lib/constants';
import { useWriteContract } from 'wagmi';
import { Button } from '../ui/button';
import { Card, CardContent } from '../ui/card';
export function WithdrawWinnings({
winningsAvailable,
}: {
winningsAvailable: number;
}) {
const { writeContract } = useWriteContract();
const handleWithdraw = async () => {
writeContract?.({
address: CONTRACT_ADDRESS as `0x${string}`,
abi: BaseJackpotAbi,
functionName: 'withdrawWinnings',
args: [],
});
};
return (
<Card className="bg-white rounded-xl shadow-sm">
<CardContent className="p-6">
<div className="flex flex-col items-center justify-center">
<h1 className="text-2xl font-bold">Withdraw Winnings</h1>
<p className="text-lg">
{(winningsAvailable / 10 ** 6).toFixed(2)} USDC
</p>
<Button
onClick={handleWithdraw}
className="mt-2 w-full bg-emerald-500 hover:bg-emerald-600 text-white"
>
Withdraw
</Button>
</div>
</CardContent>
</Card>
);
}
API Route
This API route will fetch the past jackpot results from the contract using BaseScan’s API.// /app/api/past-jackpot.ts
import { CONTRACT_ADDRESS, CONTRACT_START_BLOCK, JACKPOT_RUN_TOPIC } from '@/lib/constants';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const urlParams = new URLSearchParams({
module: 'logs',
action: 'getLogs',
address: CONTRACT_ADDRESS,
topic0: JACKPOT_RUN_TOPIC,
apikey: process.env.BASESCAN_API_KEY || '',
fromBlock: CONTRACT_START_BLOCK.toString(),
});
const response = await fetch(
`https://api.basescan.org/api?${urlParams.toString()}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}
);
const data = await response.json();
// reverse the data
const reversedData = data.result.reverse();
return NextResponse.json(reversedData);
} catch (error) {
console.error('Error fetching from BaseScan:', error);
return NextResponse.json(
{ message: 'Error fetching data' },
{ status: 500 }
);
}
}
