Introduction
Project Name: ChainLotto: Verifiable Decentralized Lottery
Concept: Traditional lotteries suffer from lack of transparency - players must trust that the operator is fair and that the winning numbers aren't manipulated. This project creates a completely transparent lottery system using blockchain technology, where every aspect of the lottery is publicly verifiable and the winning number is generated on-chain using cryptographic principles.
The Blockchain Solution: Instead of trusting a central authority to pick winning numbers, we use blockchain's immutability and transparency. Every ticket purchase is recorded on-chain, the winning number is generated using a verifiable random function (VRF) combining multiple on-chain data points, and all prize distributions are automatic and transparent. Anyone can verify the fairness of the entire process.
Features
- 🎲 Verifiable Randomness: Winning numbers are generated on-chain using cryptographic hashing
- 📝 Transparent Tickets: All ticket purchases are recorded and publicly viewable
- 💰 Automatic Prizes: Winners are automatically determined and can claim prizes
- 🔍 Verifiable History: Every lottery round can be audited by anyone
- ⏰ Scheduled Draws: Automatic draws at predetermined times
- 📊 Real-time Updates: Live display of ticket sales and jackpot amount
- ✅ Provably Fair: All randomness can be verified after the draw
- 🏆 Multiple Prize Tiers: Support for different winning combinations
Project File Structure
decentralized-lottery/ │ ├── index.php # Main application UI ├── style.css # Custom styling ├── script.js # Frontend JavaScript ├── api/ │ ├── buy_ticket.php # Purchase lottery tickets │ ├── draw_lottery.php # Trigger lottery draw (admin/auto) │ ├── get_lottery_status.php # Get current lottery info │ ├── get_history.php # Get past lottery results │ ├── verify_ticket.php # Verify if a ticket won │ └── claim_prize.php # Claim winnings ├── includes/ │ ├── config.php # Database configuration │ └── LotteryChain.php # Core blockchain logic for lottery ├── database/ │ └── schema.sql # Database schema ├── cron/ │ └── auto_draw.php # Automated draws via cron job └── README.md # Project documentation
Database Setup (MySQL)
1. Create a database named chainlotto_db.
2. Run the database/schema.sql script:
CREATE DATABASE IF NOT EXISTS chainlotto_db;
USE chainlotto_db;
-- Lottery rounds table
CREATE TABLE lottery_rounds (
id INT AUTO_INCREMENT PRIMARY KEY,
round_number INT NOT NULL UNIQUE,
ticket_price DECIMAL(10, 2) NOT NULL DEFAULT 1.00,
jackpot_amount DECIMAL(15, 2) NOT NULL DEFAULT 0,
start_time BIGINT NOT NULL,
end_time BIGINT NOT NULL,
draw_time BIGINT NULL,
winning_numbers VARCHAR(50) NULL,
winning_hash VARCHAR(64) NULL,
status ENUM('active', 'drawing', 'completed', 'cancelled') DEFAULT 'active',
total_tickets INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_status (status),
INDEX idx_round (round_number)
);
-- Lottery tickets table (blockchain)
CREATE TABLE lottery_tickets (
id INT AUTO_INCREMENT PRIMARY KEY,
block_index INT NOT NULL,
round_number INT NOT NULL,
ticket_number VARCHAR(20) NOT NULL,
buyer_name VARCHAR(100) NOT NULL,
buyer_email VARCHAR(255),
numbers VARCHAR(50) NOT NULL, -- Format: "n1,n2,n3,n4,n5,n6"
ticket_hash VARCHAR(64) NOT NULL UNIQUE,
previous_hash VARCHAR(64) NOT NULL,
timestamp BIGINT NOT NULL,
nonce INT NOT NULL,
is_winner BOOLEAN DEFAULT FALSE,
prize_amount DECIMAL(15, 2) DEFAULT 0,
claimed BOOLEAN DEFAULT FALSE,
claimed_at BIGINT NULL,
FOREIGN KEY (round_number) REFERENCES lottery_rounds(round_number),
INDEX idx_round (round_number),
INDEX idx_winner (is_winner),
INDEX idx_ticket (ticket_number)
);
-- Winners table
CREATE TABLE lottery_winners (
id INT AUTO_INCREMENT PRIMARY KEY,
round_number INT NOT NULL,
ticket_id INT NOT NULL,
ticket_number VARCHAR(20) NOT NULL,
buyer_name VARCHAR(100) NOT NULL,
numbers VARCHAR(50) NOT NULL,
matched_numbers INT NOT NULL,
prize_tier INT NOT NULL,
prize_amount DECIMAL(15, 2) NOT NULL,
claimed BOOLEAN DEFAULT FALSE,
claimed_at BIGINT NULL,
transaction_hash VARCHAR(64) NULL,
FOREIGN KEY (round_number) REFERENCES lottery_rounds(round_number),
FOREIGN KEY (ticket_id) REFERENCES lottery_tickets(id)
);
-- Randomness seeds table (for verifiable randomness)
CREATE TABLE randomness_seeds (
id INT AUTO_INCREMENT PRIMARY KEY,
round_number INT NOT NULL,
seed_type VARCHAR(20) NOT NULL, -- 'commit', 'reveal', 'final'
seed_value VARCHAR(256) NOT NULL,
timestamp BIGINT NOT NULL,
block_height INT,
FOREIGN KEY (round_number) REFERENCES lottery_rounds(round_number)
);
-- Insert genesis block for tickets
INSERT INTO lottery_tickets (block_index, round_number, ticket_number, buyer_name, numbers, ticket_hash, previous_hash, timestamp, nonce)
VALUES (0, 0, 'GENESIS', 'SYSTEM', '0,0,0,0,0,0',
SHA2(CONCAT('0', '0', 'GENESIS', 'SYSTEM', '0,0,0,0,0,0', UNIX_TIMESTAMP(), '0', '0'), 256),
'0', UNIX_TIMESTAMP(), 0);
-- Insert first lottery round
INSERT INTO lottery_rounds (round_number, ticket_price, jackpot_amount, start_time, end_time, status)
VALUES (1, 1.00, 100.00, UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + 86400, 'active');
Code Implementation
1. includes/config.php (Database Configuration)
<?php
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'chainlotto_db');
function getDBConnection() {
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($conn->connect_error) {
die(json_encode(['success' => false, 'message' => 'Database connection failed: ' . $conn->connect_error]));
}
return $conn;
}
// Lottery configuration
define('TICKET_PRICE', 1.00);
define('MAX_NUMBER', 49);
define('NUMBERS_PER_TICKET', 6);
define('JACKPOT_MULTIPLIER', 0.5); // 50% of ticket sales go to jackpot
define('DRAW_INTERVAL', 86400); // 24 hours in seconds
date_default_timezone_set('UTC');
?>
2. includes/LotteryChain.php (Core Blockchain Logic)
<?php
require_once 'config.php';
class LotteryChain {
private $conn;
private $difficulty = 2; // Proof of work difficulty for tickets
public function __construct() {
$this->conn = getDBConnection();
}
/**
* Calculate hash for a ticket block
*/
public static function calculateHash($index, $roundNumber, $ticketNumber, $buyerName, $numbers, $timestamp, $previousHash, $nonce) {
$data = $index . $roundNumber . $ticketNumber . $buyerName . $numbers . $timestamp . $previousHash . $nonce;
return hash('sha256', $data);
}
/**
* Get the last ticket block
*/
public function getLastBlock() {
$sql = "SELECT * FROM lottery_tickets ORDER BY block_index DESC LIMIT 1";
$result = $this->conn->query($sql);
if ($result && $result->num_rows > 0) {
return $result->fetch_assoc();
}
return null;
}
/**
* Get current active lottery round
*/
public function getCurrentRound() {
$sql = "SELECT * FROM lottery_rounds WHERE status = 'active' ORDER BY round_number DESC LIMIT 1";
$result = $this->conn->query($sql);
if ($result && $result->num_rows > 0) {
return $result->fetch_assoc();
}
return null;
}
/**
* Generate unique ticket number
*/
private function generateTicketNumber() {
$prefix = 'TKT';
$timestamp = time();
$random = rand(1000, 9999);
return $prefix . $timestamp . $random;
}
/**
* Validate lottery numbers
*/
public function validateNumbers($numbers) {
$numArray = explode(',', $numbers);
if (count($numArray) != NUMBERS_PER_TICKET) {
return false;
}
foreach ($numArray as $num) {
if (!is_numeric($num) || $num < 1 || $num > MAX_NUMBER) {
return false;
}
}
// Check for duplicates
if (count(array_unique($numArray)) != count($numArray)) {
return false;
}
return true;
}
/**
* Buy a lottery ticket
*/
public function buyTicket($buyerName, $buyerEmail, $numbers) {
try {
// Validate numbers
if (!$this->validateNumbers($numbers)) {
return ['success' => false, 'message' => 'Invalid numbers. Please select 6 unique numbers between 1 and ' . MAX_NUMBER];
}
// Get current round
$currentRound = $this->getCurrentRound();
if (!$currentRound) {
return ['success' => false, 'message' => 'No active lottery round'];
}
// Check if round is still active
if (time() > $currentRound['end_time']) {
return ['success' => false, 'message' => 'Lottery round has ended'];
}
// Get last block
$lastBlock = $this->getLastBlock();
if (!$lastBlock) {
return ['success' => false, 'message' => 'Ticket chain corrupted'];
}
$newIndex = $lastBlock['block_index'] + 1;
$timestamp = time();
$previousHash = $lastBlock['hash'];
$ticketNumber = $this->generateTicketNumber();
// Sort numbers for consistency
$numArray = explode(',', $numbers);
sort($numArray);
$sortedNumbers = implode(',', $numArray);
// Proof of Work
$nonce = 0;
$prefix = str_repeat('0', $this->difficulty);
do {
$hash = $this->calculateHash($newIndex, $currentRound['round_number'], $ticketNumber, $buyerName, $sortedNumbers, $timestamp, $previousHash, $nonce);
$nonce++;
if ($nonce > 1000000) {
throw new Exception("Proof of work timeout");
}
} while (substr($hash, 0, $this->difficulty) !== $prefix);
// Start transaction
$this->conn->begin_transaction();
// Insert ticket
$sql = "INSERT INTO lottery_tickets (block_index, round_number, ticket_number, buyer_name, buyer_email, numbers, ticket_hash, previous_hash, timestamp, nonce)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("iissssssii",
$newIndex,
$currentRound['round_number'],
$ticketNumber,
$buyerName,
$buyerEmail,
$sortedNumbers,
$hash,
$previousHash,
$timestamp,
$nonce - 1
);
if (!$stmt->execute()) {
throw new Exception("Failed to insert ticket: " . $stmt->error);
}
// Update round statistics
$newJackpot = $currentRound['jackpot_amount'] + (TICKET_PRICE * JACKPOT_MULTIPLIER);
$sql2 = "UPDATE lottery_rounds SET total_tickets = total_tickets + 1, jackpot_amount = ? WHERE round_number = ?";
$stmt2 = $this->conn->prepare($sql2);
$stmt2->bind_param("di", $newJackpot, $currentRound['round_number']);
if (!$stmt2->execute()) {
throw new Exception("Failed to update round");
}
$this->conn->commit();
// Get ticket ID
$ticketId = $this->conn->insert_id;
return [
'success' => true,
'message' => 'Ticket purchased successfully!',
'ticket' => [
'ticket_number' => $ticketNumber,
'numbers' => $sortedNumbers,
'round' => $currentRound['round_number'],
'block_index' => $newIndex,
'hash' => $hash,
'timestamp' => date('Y-m-d H:i:s', $timestamp)
]
];
} catch (Exception $e) {
$this->conn->rollback();
return ['success' => false, 'message' => 'Error purchasing ticket: ' . $e->getMessage()];
}
}
/**
* Generate winning numbers using verifiable randomness
*/
public function generateWinningNumbers($roundNumber) {
try {
// Get round info
$sql = "SELECT * FROM lottery_rounds WHERE round_number = ?";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("i", $roundNumber);
$stmt->execute();
$result = $stmt->get_result();
$round = $result->fetch_assoc();
if (!$round) {
throw new Exception("Round not found");
}
if ($round['status'] != 'active') {
throw new Exception("Round is not active");
}
// Get all tickets for this round
$sql = "SELECT * FROM lottery_tickets WHERE round_number = ? AND ticket_number != 'GENESIS'";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("i", $roundNumber);
$stmt->execute();
$tickets = $stmt->get_result();
// Create randomness seed
$seedData = '';
while ($ticket = $tickets->fetch_assoc()) {
$seedData .= $ticket['ticket_hash'] . $ticket['timestamp'];
}
// Add round data and current time
$seedData .= $round['round_number'] . $round['start_time'] . time();
// Generate random numbers using hash
$hash = hash('sha256', $seedData);
// Generate 6 unique winning numbers
$winningNumbers = [];
$hashParts = str_split($hash, 8);
foreach ($hashParts as $part) {
if (count($winningNumbers) >= NUMBERS_PER_TICKET) break;
// Convert hex to decimal and map to 1-MAX_NUMBER
$num = hexdec(substr($part, 0, 4)) % MAX_NUMBER + 1;
if (!in_array($num, $winningNumbers)) {
$winningNumbers[] = $num;
}
}
// If we don't have enough unique numbers, fill remaining
while (count($winningNumbers) < NUMBERS_PER_TICKET) {
$num = rand(1, MAX_NUMBER);
if (!in_array($num, $winningNumbers)) {
$winningNumbers[] = $num;
}
}
sort($winningNumbers);
$winningNumbersStr = implode(',', $winningNumbers);
// Store randomness seed
$sql = "INSERT INTO randomness_seeds (round_number, seed_type, seed_value, timestamp) VALUES (?, 'final', ?, ?)";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("isi", $roundNumber, $hash, time());
$stmt->execute();
return [
'success' => true,
'winning_numbers' => $winningNumbersStr,
'seed_hash' => $hash
];
} catch (Exception $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
/**
* Determine winners for a round
*/
public function determineWinners($roundNumber, $winningNumbers) {
try {
// Get all tickets for this round
$sql = "SELECT * FROM lottery_tickets WHERE round_number = ? AND ticket_number != 'GENESIS'";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("i", $roundNumber);
$stmt->execute();
$result = $stmt->get_result();
$winningArray = explode(',', $winningNumbers);
$winners = [];
$prizePool = 0;
// Calculate prize pool (80% of jackpot)
$sql = "SELECT jackpot_amount FROM lottery_rounds WHERE round_number = ?";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("i", $roundNumber);
$stmt->execute();
$round = $stmt->get_result()->fetch_assoc();
$prizePool = $round['jackpot_amount'] * 0.8;
// Prize tiers
$tiers = [
6 => 0.5, // 50% for jackpot (6 numbers)
5 => 0.2, // 20% for 5 numbers
4 => 0.15, // 15% for 4 numbers
3 => 0.1, // 10% for 3 numbers
2 => 0.05 // 5% for 2 numbers
];
// Count matches per tier
$matchCounts = [6 => 0, 5 => 0, 4 => 0, 3 => 0, 2 => 0];
$ticketWinners = [];
$this->conn->begin_transaction();
while ($ticket = $result->fetch_assoc()) {
$ticketNumbers = explode(',', $ticket['numbers']);
$matches = array_intersect($winningArray, $ticketNumbers);
$matchCount = count($matches);
if ($matchCount >= 2) {
$matchCounts[$matchCount]++;
$ticketWinners[] = [
'ticket' => $ticket,
'matches' => $matchCount
];
// Mark ticket as winner
$sql = "UPDATE lottery_tickets SET is_winner = TRUE WHERE id = ?";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("i", $ticket['id']);
$stmt->execute();
}
}
// Calculate and distribute prizes
foreach ($ticketWinners as $winner) {
$matches = $winner['matches'];
if (isset($tiers[$matches]) && $matchCounts[$matches] > 0) {
$prizeAmount = ($prizePool * $tiers[$matches]) / $matchCounts[$matches];
// Insert winner record
$sql = "INSERT INTO lottery_winners (round_number, ticket_id, ticket_number, buyer_name, numbers, matched_numbers, prize_tier, prize_amount)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("iisssiid",
$roundNumber,
$winner['ticket']['id'],
$winner['ticket']['ticket_number'],
$winner['ticket']['buyer_name'],
$winner['ticket']['numbers'],
$matches,
$matches,
$prizeAmount
);
$stmt->execute();
// Update ticket with prize amount
$sql = "UPDATE lottery_tickets SET prize_amount = ? WHERE id = ?";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("di", $prizeAmount, $winner['ticket']['id']);
$stmt->execute();
}
}
// Update round status
$sql = "UPDATE lottery_rounds SET status = 'completed', winning_numbers = ?, draw_time = ? WHERE round_number = ?";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("sii", $winningNumbers, time(), $roundNumber);
$stmt->execute();
// Create next round
$nextRound = $roundNumber + 1;
$startTime = time();
$endTime = $startTime + DRAW_INTERVAL;
$initialJackpot = 100.00; // Starting jackpot
$sql = "INSERT INTO lottery_rounds (round_number, ticket_price, jackpot_amount, start_time, end_time, status)
VALUES (?, ?, ?, ?, ?, 'active')";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("iddii", $nextRound, TICKET_PRICE, $initialJackpot, $startTime, $endTime);
$stmt->execute();
$this->conn->commit();
return [
'success' => true,
'message' => 'Winners determined successfully',
'winning_numbers' => $winningNumbers,
'total_winners' => count($ticketWinners),
'prize_pool' => $prizePool
];
} catch (Exception $e) {
$this->conn->rollback();
return ['success' => false, 'message' => $e->getMessage()];
}
}
/**
* Draw lottery (complete process)
*/
public function drawLottery($roundNumber = null) {
try {
if (!$roundNumber) {
$current = $this->getCurrentRound();
if ($current) {
$roundNumber = $current['round_number'];
} else {
throw new Exception("No active round found");
}
}
// Generate winning numbers
$numbersResult = $this->generateWinningNumbers($roundNumber);
if (!$numbersResult['success']) {
throw new Exception($numbersResult['message']);
}
// Determine winners
$winnersResult = $this->determineWinners($roundNumber, $numbersResult['winning_numbers']);
return $winnersResult;
} catch (Exception $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
/**
* Verify if a ticket won
*/
public function verifyTicket($ticketNumber) {
$sql = "SELECT t.*, r.winning_numbers, r.round_number
FROM lottery_tickets t
JOIN lottery_rounds r ON t.round_number = r.round_number
WHERE t.ticket_number = ?";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("s", $ticketNumber);
$stmt->execute();
$result = $stmt->get_result();
if ($result && $result->num_rows > 0) {
$ticket = $result->fetch_assoc();
if ($ticket['winning_numbers']) {
$winning = explode(',', $ticket['winning_numbers']);
$player = explode(',', $ticket['numbers']);
$matches = array_intersect($winning, $player);
return [
'success' => true,
'ticket' => $ticket,
'winning_numbers' => $ticket['winning_numbers'],
'matches' => count($matches),
'is_winner' => $ticket['is_winner'],
'prize_amount' => $ticket['prize_amount'],
'claimed' => $ticket['claimed']
];
} else {
return [
'success' => true,
'ticket' => $ticket,
'message' => 'Draw not completed yet',
'is_winner' => false
];
}
}
return ['success' => false, 'message' => 'Ticket not found'];
}
/**
* Claim prize
*/
public function claimPrize($ticketNumber, $claimantName) {
try {
$sql = "SELECT * FROM lottery_tickets WHERE ticket_number = ? AND is_winner = TRUE AND claimed = FALSE";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("s", $ticketNumber);
$stmt->execute();
$result = $stmt->get_result();
if ($result && $result->num_rows > 0) {
$ticket = $result->fetch_assoc();
$this->conn->begin_transaction();
// Update ticket
$sql = "UPDATE lottery_tickets SET claimed = TRUE, claimed_at = ? WHERE id = ?";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("ii", time(), $ticket['id']);
$stmt->execute();
// Update winners table
$sql = "UPDATE lottery_winners SET claimed = TRUE, claimed_at = ? WHERE ticket_id = ?";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("ii", time(), $ticket['id']);
$stmt->execute();
$this->conn->commit();
return [
'success' => true,
'message' => 'Prize claimed successfully!',
'amount' => $ticket['prize_amount']
];
}
return ['success' => false, 'message' => 'No unclaimed winning ticket found'];
} catch (Exception $e) {
$this->conn->rollback();
return ['success' => false, 'message' => $e->getMessage()];
}
}
/**
* Get lottery history
*/
public function getHistory($limit = 10) {
$sql = "SELECT * FROM lottery_rounds WHERE status = 'completed' ORDER BY round_number DESC LIMIT ?";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("i", $limit);
$stmt->execute();
$result = $stmt->get_result();
$history = [];
while ($row = $result->fetch_assoc()) {
// Get winners for this round
$sql2 = "SELECT COUNT(*) as winner_count, SUM(prize_amount) as total_prizes FROM lottery_winners WHERE round_number = ?";
$stmt2 = $this->conn->prepare($sql2);
$stmt2->bind_param("i", $row['round_number']);
$stmt2->execute();
$stats = $stmt2->get_result()->fetch_assoc();
$row['winner_count'] = $stats['winner_count'] ?? 0;
$row['total_prizes'] = $stats['total_prizes'] ?? 0;
$row['formatted_date'] = date('Y-m-d H:i:s', $row['draw_time']);
$history[] = $row;
}
return $history;
}
/**
* Get lottery status
*/
public function getStatus() {
$current = $this->getCurrentRound();
if ($current) {
$timeLeft = $current['end_time'] - time();
// Get recent tickets
$sql = "SELECT * FROM lottery_tickets WHERE round_number = ? AND ticket_number != 'GENESIS' ORDER BY block_index DESC LIMIT 10";
$stmt = $this->conn->prepare($sql);
$stmt->bind_param("i", $current['round_number']);
$stmt->execute();
$tickets = $stmt->get_result();
$recentTickets = [];
while ($ticket = $tickets->fetch_assoc()) {
$recentTickets[] = [
'ticket_number' => $ticket['ticket_number'],
'buyer_name' => substr($ticket['buyer_name'], 0, 3) . '***',
'time' => date('H:i:s', $ticket['timestamp'])
];
}
return [
'success' => true,
'active' => true,
'round' => $current['round_number'],
'jackpot' => $current['jackpot_amount'],
'tickets_sold' => $current['total_tickets'],
'time_remaining' => $timeLeft,
'end_time' => date('Y-m-d H:i:s', $current['end_time']),
'recent_tickets' => $recentTickets
];
} else {
return [
'success' => true,
'active' => false,
'message' => 'No active lottery round'
];
}
}
public function __destruct() {
$this->conn->close();
}
}
?>
3. api/buy_ticket.php (Purchase Ticket Endpoint)
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
require_once '../includes/LotteryChain.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
$buyerName = $_POST['buyer_name'] ?? '';
$buyerEmail = $_POST['buyer_email'] ?? '';
$numbers = $_POST['numbers'] ?? '';
} else {
$buyerName = $input['buyer_name'] ?? '';
$buyerEmail = $input['buyer_email'] ?? '';
$numbers = $input['numbers'] ?? '';
}
if (empty($buyerName) || empty($numbers)) {
echo json_encode(['success' => false, 'message' => 'Name and numbers are required']);
exit;
}
$lottery = new LotteryChain();
$result = $lottery->buyTicket($buyerName, $buyerEmail, $numbers);
echo json_encode($result);
?>
4. api/draw_lottery.php (Trigger Lottery Draw)
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
require_once '../includes/LotteryChain.php';
// Simple authentication for admin actions
$adminKey = $_POST['admin_key'] ?? '';
if ($adminKey !== 'CHANGEME_SECRET_KEY') { // Change this in production
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Unauthorized']);
exit;
}
$roundNumber = $_POST['round_number'] ?? null;
$lottery = new LotteryChain();
$result = $lottery->drawLottery($roundNumber);
echo json_encode($result);
?>
5. api/get_lottery_status.php (Get Current Status)
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
require_once '../includes/LotteryChain.php';
$lottery = new LotteryChain();
$status = $lottery->getStatus();
echo json_encode($status);
?>
6. api/get_history.php (Get Lottery History)
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
require_once '../includes/LotteryChain.php';
$limit = $_GET['limit'] ?? 10;
$lottery = new LotteryChain();
$history = $lottery->getHistory($limit);
echo json_encode([
'success' => true,
'history' => $history
]);
?>
7. api/verify_ticket.php (Verify Ticket)
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
require_once '../includes/LotteryChain.php';
$ticketNumber = $_GET['ticket'] ?? '';
if (empty($ticketNumber)) {
echo json_encode(['success' => false, 'message' => 'Ticket number required']);
exit;
}
$lottery = new LotteryChain();
$result = $lottery->verifyTicket($ticketNumber);
echo json_encode($result);
?>
8. api/claim_prize.php (Claim Winnings)
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
require_once '../includes/LotteryChain.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
$ticketNumber = $_POST['ticket_number'] ?? '';
$claimantName = $_POST['claimant_name'] ?? '';
} else {
$ticketNumber = $input['ticket_number'] ?? '';
$claimantName = $input['claimant_name'] ?? '';
}
if (empty($ticketNumber) || empty($claimantName)) {
echo json_encode(['success' => false, 'message' => 'Ticket number and claimant name required']);
exit;
}
$lottery = new LotteryChain();
$result = $lottery->claimPrize($ticketNumber, $claimantName);
echo json_encode($result);
?>
9. cron/auto_draw.php (Automated Draws)
<?php
// This file should be run by cron job every hour
// Example cron: 0 * * * * php /path/to/cron/auto_draw.php
require_once '../includes/LotteryChain.php';
$lottery = new LotteryChain();
$status = $lottery->getStatus();
if ($status['active'] && $status['time_remaining'] <= 0) {
// Time's up, draw the lottery
$result = $lottery->drawLottery($status['round']);
// Log the result
$logFile = '../logs/lottery_draw.log';
$logEntry = date('Y-m-d H:i:s') . " - Round {$status['round']}: " . json_encode($result) . PHP_EOL;
file_put_contents($logFile, $logEntry, FILE_APPEND);
echo "Lottery draw completed for round {$status['round']}\n";
} else {
$timeLeft = $status['time_remaining'] ?? 0;
echo "No draw needed. Next draw in {$timeLeft} seconds\n";
}
?>
10. index.php (Main Application UI)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ChainLotto | Decentralized Lottery</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;600;700;800;900&display=swap');
body {
font-family: 'Orbitron', sans-serif;
background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
min-height: 100vh;
}
.number-ball {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
.number-ball:hover {
transform: scale(1.1);
box-shadow: 0 0 20px rgba(102, 126, 234, 0.8);
}
.number-ball.selected {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
box-shadow: 0 0 30px rgba(245, 87, 108, 0.8);
}
.jackpot-glow {
animation: jackpotPulse 2s ease-in-out infinite;
}
@keyframes jackpotPulse {
0%, 100% {
text-shadow: 0 0 10px #ffd700, 0 0 20px #ffd700, 0 0 30px #ffd700;
}
50% {
text-shadow: 0 0 20px #ffd700, 0 0 40px #ffd700, 0 0 60px #ffd700;
}
}
.ticket-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
.ticket-card:hover {
transform: translateY(-5px);
border-color: #667eea;
}
.countdown {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.blockchain-badge {
background: rgba(0, 0, 0, 0.5);
border-left: 4px solid #667eea;
}
.winner-announcement {
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
</style>
</head>
<body class="text-white">
<div class="max-w-7xl mx-auto px-4 py-8">
<!-- Header -->
<div class="text-center mb-8">
<div class="flex items-center justify-center mb-4">
<i class="fas fa-dice-d6 text-6xl text-yellow-400 mr-4"></i>
<h1 class="text-6xl font-black bg-gradient-to-r from-yellow-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">
ChainLotto
</h1>
</div>
<p class="text-xl text-gray-300 mb-2">Decentralized • Transparent • Provably Fair</p>
<p class="text-gray-400 max-w-2xl mx-auto">
Every ticket is recorded on the blockchain. Winning numbers are generated on-chain using verifiable randomness.
No trust required.
</p>
</div>
<!-- Main Jackpot Display -->
<div class="text-center mb-12">
<div class="inline-block p-8 rounded-3xl bg-gradient-to-r from-purple-900/50 to-indigo-900/50 backdrop-blur-sm border border-purple-500/30">
<p class="text-sm text-gray-400 mb-2">Current Jackpot</p>
<div class="text-7xl font-black text-yellow-400 jackpot-glow mb-2">
$<span id="jackpotAmount">0</span>
</div>
<div class="flex justify-center items-center space-x-4 text-sm">
<span class="bg-purple-600/30 px-3 py-1 rounded-full">
<i class="fas fa-ticket-alt mr-1"></i>
<span id="ticketsSold">0</span> tickets sold
</span>
<span class="bg-indigo-600/30 px-3 py-1 rounded-full">
<i class="fas fa-clock mr-1"></i>
<span id="timeRemaining">00:00:00</span>
</span>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Left Column: Number Selection -->
<div class="lg:col-span-2">
<div class="bg-white/10 backdrop-blur-lg rounded-2xl p-6 border border-white/20">
<h2 class="text-2xl font-bold mb-6 flex items-center">
<i class="fas fa-dice-d20 text-purple-400 mr-3"></i>
Pick Your Numbers
</h2>
<div class="grid grid-cols-7 gap-3 mb-6">
<?php for ($i = 1; $i <= 49; $i++): ?>
<div class="number-ball cursor-pointer" onclick="toggleNumber(<?php echo $i; ?>)">
<?php echo $i; ?>
</div>
<?php endfor; ?>
</div>
<div class="flex space-x-4 mb-6">
<button onclick="randomNumbers()" class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transition flex items-center">
<i class="fas fa-random mr-2"></i>
Lucky Dip
</button>
<button onclick="clearNumbers()" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition flex items-center">
<i class="fas fa-undo mr-2"></i>
Clear
</button>
</div>
<div class="bg-black/30 rounded-lg p-4 mb-4">
<p class="text-sm text-gray-400 mb-2">Your selected numbers:</p>
<div id="selectedNumbers" class="flex space-x-2">
<span class="text-gray-500">None selected</span>
</div>
</div>
<!-- Purchase Form -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Your Name</label>
<input type="text" id="buyerName" placeholder="Enter your name"
class="w-full bg-black/30 border border-gray-600 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-purple-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Email (for notifications)</label>
<input type="email" id="buyerEmail" placeholder="optional"
class="w-full bg-black/30 border border-gray-600 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-purple-500">
</div>
</div>
<button onclick="buyTicket()" class="w-full mt-6 bg-gradient-to-r from-green-400 to-blue-500 text-white font-bold py-4 px-6 rounded-xl text-xl hover:opacity-90 transition transform hover:scale-105">
<i class="fas fa-ticket-alt mr-2"></i>
Buy Ticket - $1.00
</button>
</div>
<!-- Recent Winners -->
<div class="mt-8 bg-white/10 backdrop-blur-lg rounded-2xl p-6 border border-white/20">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fas fa-trophy text-yellow-400 mr-2"></i>
Recent Winners
</h2>
<div id="recentWinners" class="space-y-3">
<!-- Winners will be loaded here -->
<div class="text-center text-gray-500 py-4">
<i class="fas fa-spinner fa-spin mr-2"></i>
Loading winners...
</div>
</div>
</div>
</div>
<!-- Right Column: Lottery Info & Tickets -->
<div class="lg:col-span-1">
<!-- Current Round Info -->
<div class="bg-white/10 backdrop-blur-lg rounded-2xl p-6 border border-white/20 mb-6">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fas fa-info-circle text-blue-400 mr-2"></i>
Round <span id="roundNumber">1</span>
</h2>
<div class="space-y-4">
<div>
<p class="text-sm text-gray-400">Draw Time</p>
<p id="drawTime" class="text-lg font-semibold">Loading...</p>
</div>
<div>
<p class="text-sm text-gray-400">Time Remaining</p>
<div class="countdown" id="countdown">00:00:00</div>
</div>
<div class="blockchain-badge p-3 rounded-lg">
<p class="text-xs text-gray-400 mb-1">Blockchain Status</p>
<p class="text-sm">
<i class="fas fa-circle text-green-400 text-xs mr-2"></i>
Active & Valid
</p>
</div>
</div>
</div>
<!-- Recent Tickets -->
<div class="bg-white/10 backdrop-blur-lg rounded-2xl p-6 border border-white/20">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fas fa-history text-purple-400 mr-2"></i>
Recent Tickets
</h2>
<div id="recentTickets" class="space-y-3 max-h-96 overflow-y-auto pr-2">
<!-- Tickets will be loaded here -->
<div class="text-center text-gray-500 py-4">
<i class="fas fa-spinner fa-spin mr-2"></i>
Loading tickets...
</div>
</div>
</div>
<!-- Verify Ticket -->
<div class="mt-6 bg-white/10 backdrop-blur-lg rounded-2xl p-6 border border-white/20">
<h2 class="text-xl font-bold mb-4 flex items-center">
<i class="fas fa-search text-green-400 mr-2"></i>
Verify Ticket
</h2>
<div class="flex space-x-2">
<input type="text" id="verifyTicket" placeholder="Enter ticket number"
class="flex-1 bg-black/30 border border-gray-600 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-green-500">
<button onclick="verifyTicket()" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition">
<i class="fas fa-check"></i>
</button>
</div>
<div id="verificationResult" class="mt-3 text-sm hidden"></div>
</div>
</div>
</div>
<!-- Past Results -->
<div class="mt-12 bg-white/10 backdrop-blur-lg rounded-2xl p-6 border border-white/20">
<h2 class="text-2xl font-bold mb-6 flex items-center">
<i class="fas fa-history text-purple-400 mr-3"></i>
Past Lottery Results
</h2>
<div id="pastResults" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Results will be loaded here -->
<div class="text-center text-gray-500 py-8 col-span-3">
<i class="fas fa-spinner fa-spin mr-2"></i>
Loading history...
</div>
</div>
</div>
</div>
<!-- Winner Announcement Modal -->
<div id="winnerModal" class="fixed inset-0 bg-black bg-opacity-75 hidden items-center justify-center z-50">
<div class="bg-gradient-to-r from-purple-900 to-indigo-900 rounded-2xl p-8 max-w-md text-center transform scale-0 transition-transform duration-300">
<i class="fas fa-trophy text-6xl text-yellow-400 mb-4"></i>
<h2 class="text-3xl font-bold mb-2">Congratulations!</h2>
<p id="winnerMessage" class="text-xl mb-4"></p>
<button onclick="closeWinnerModal()" class="bg-white text-purple-900 px-6 py-2 rounded-lg font-bold hover:bg-gray-100 transition">
Claim Prize
</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
11. script.js (Frontend JavaScript)
// script.js
let selectedNumbers = [];
let countdownInterval;
let currentRound = null;
// Load data on page load
document.addEventListener('DOMContentLoaded', function() {
loadLotteryStatus();
loadPastResults();
// Start countdown
countdownInterval = setInterval(updateCountdown, 1000);
// Refresh status every 30 seconds
setInterval(loadLotteryStatus, 30000);
});
// Toggle number selection
function toggleNumber(num) {
const index = selectedNumbers.indexOf(num);
if (index === -1) {
if (selectedNumbers.length >= 6) {
showNotification('You can only select 6 numbers', 'error');
return;
}
selectedNumbers.push(num);
} else {
selectedNumbers.splice(index, 1);
}
// Update UI
updateNumberDisplay();
updateSelectedNumbersDisplay();
}
// Update number ball display
function updateNumberDisplay() {
document.querySelectorAll('.number-ball').forEach(ball => {
const num = parseInt(ball.textContent);
if (selectedNumbers.includes(num)) {
ball.classList.add('selected');
} else {
ball.classList.remove('selected');
}
});
}
// Update selected numbers display
function updateSelectedNumbersDisplay() {
const container = document.getElementById('selectedNumbers');
if (selectedNumbers.length === 0) {
container.innerHTML = '<span class="text-gray-500">None selected</span>';
return;
}
let html = '';
selectedNumbers.sort((a, b) => a - b).forEach(num => {
html += `<span class="number-ball !w-10 !h-10 !text-sm">${num}</span>`;
});
container.innerHTML = html;
}
// Random number selection
function randomNumbers() {
selectedNumbers = [];
while (selectedNumbers.length < 6) {
const num = Math.floor(Math.random() * 49) + 1;
if (!selectedNumbers.includes(num)) {
selectedNumbers.push(num);
}
}
updateNumberDisplay();
updateSelectedNumbersDisplay();
}
// Clear selection
function clearNumbers() {
selectedNumbers = [];
updateNumberDisplay();
updateSelectedNumbersDisplay();
}
// Buy ticket
async function buyTicket() {
if (selectedNumbers.length !== 6) {
showNotification('Please select 6 numbers', 'error');
return;
}
const buyerName = document.getElementById('buyerName').value;
if (!buyerName) {
showNotification('Please enter your name', 'error');
return;
}
const numbers = selectedNumbers.sort((a, b) => a - b).join(',');
const email = document.getElementById('buyerEmail').value;
const formData = {
buyer_name: buyerName,
buyer_email: email,
numbers: numbers
};
try {
const response = await fetch('api/buy_ticket.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const data = await response.json();
if (data.success) {
showNotification(data.message, 'success');
clearNumbers();
document.getElementById('buyerName').value = '';
document.getElementById('buyerEmail').value = '';
loadLotteryStatus();
// Show ticket details
alert(`Your ticket number: ${data.ticket.ticket_number}\nHash: ${data.ticket.hash.substring(0, 20)}...`);
} else {
showNotification('Error: ' + data.message, 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification('Failed to purchase ticket', 'error');
}
}
// Load lottery status
async function loadLotteryStatus() {
try {
const response = await fetch('api/get_lottery_status.php');
const data = await response.json();
if (data.success && data.active) {
currentRound = data;
document.getElementById('jackpotAmount').textContent = data.jackpot.toFixed(2);
document.getElementById('ticketsSold').textContent = data.tickets_sold;
document.getElementById('roundNumber').textContent = data.round;
document.getElementById('drawTime').textContent = data.end_time;
// Update recent tickets
displayRecentTickets(data.recent_tickets);
}
} catch (error) {
console.error('Error loading status:', error);
}
}
// Display recent tickets
function displayRecentTickets(tickets) {
const container = document.getElementById('recentTickets');
if (!tickets || tickets.length === 0) {
container.innerHTML = '<div class="text-center text-gray-500 py-4">No tickets yet</div>';
return;
}
let html = '';
tickets.forEach(ticket => {
html += `
<div class="ticket-card p-3 rounded-lg">
<div class="flex justify-between items-start">
<div>
<p class="font-mono text-xs text-purple-400">${ticket.ticket_number}</p>
<p class="text-sm">${ticket.buyer_name}</p>
</div>
<span class="text-xs text-gray-400">${ticket.time}</span>
</div>
</div>
`;
});
container.innerHTML = html;
}
// Update countdown
function updateCountdown() {
if (!currentRound) return;
const timeRemaining = currentRound.time_remaining;
if (timeRemaining <= 0) {
document.getElementById('countdown').textContent = 'DRAWING...';
document.getElementById('timeRemaining').textContent = 'Drawing soon';
return;
}
const hours = Math.floor(timeRemaining / 3600);
const minutes = Math.floor((timeRemaining % 3600) / 60);
const seconds = timeRemaining % 60;
document.getElementById('countdown').textContent =
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
document.getElementById('timeRemaining').textContent =
`${hours}h ${minutes}m ${seconds}s`;
}
// Load past results
async function loadPastResults() {
try {
const response = await fetch('api/get_history.php?limit=6');
const data = await response.json();
if (data.success) {
displayPastResults(data.history);
}
} catch (error) {
console.error('Error loading history:', error);
}
}
// Display past results
function displayPastResults(results) {
const container = document.getElementById('pastResults');
if (!results || results.length === 0) {
container.innerHTML = '<div class="text-center text-gray-500 py-8 col-span-3">No past results</div>';
return;
}
let html = '';
results.forEach(result => {
const winningNumbers = result.winning_numbers ? result.winning_numbers.split(',') : [];
html += `
<div class="ticket-card p-4 rounded-xl">
<div class="flex justify-between items-center mb-3">
<h3 class="font-bold">Round #${result.round_number}</h3>
<span class="text-xs text-gray-400">${result.formatted_date}</span>
</div>
<div class="mb-3">
<p class="text-sm text-gray-400 mb-2">Winning Numbers</p>
<div class="flex space-x-1">
${winningNumbers.map(num =>
`<span class="number-ball !w-8 !h-8 !text-xs">${num}</span>`
).join('')}
</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm">
<div>
<span class="text-gray-400">Jackpot:</span>
<span class="text-yellow-400">$${result.jackpot_amount.toFixed(2)}</span>
</div>
<div>
<span class="text-gray-400">Winners:</span>
<span class="text-green-400">${result.winner_count}</span>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
}
// Verify ticket
async function verifyTicket() {
const ticketNumber = document.getElementById('verifyTicket').value;
if (!ticketNumber) {
showNotification('Please enter a ticket number', 'error');
return;
}
try {
const response = await fetch(`api/verify_ticket.php?ticket=${encodeURIComponent(ticketNumber)}`);
const data = await response.json();
const resultDiv = document.getElementById('verificationResult');
resultDiv.classList.remove('hidden');
if (data.success) {
if (data.is_winner) {
resultDiv.className = 'mt-3 p-3 bg-green-600/20 border border-green-500 rounded-lg text-green-400';
resultDiv.innerHTML = `
<div class="flex items-center">
<i class="fas fa-trophy mr-2"></i>
<div>
<p class="font-bold">WINNER!</p>
<p>Matched ${data.matches} numbers</p>
<p class="text-lg font-bold">Prize: $${data.prize_amount.toFixed(2)}</p>
${!data.claimed ? '<button onclick="claimPrize(\'' + ticketNumber + '\')" class="mt-2 bg-green-600 text-white px-3 py-1 rounded text-sm">Claim Prize</button>' :
'<p class="text-sm text-gray-400">Already claimed</p>'}
</div>
</div>
`;
} else {
resultDiv.className = 'mt-3 p-3 bg-yellow-600/20 border border-yellow-500 rounded-lg text-yellow-400';
resultDiv.innerHTML = `
<div class="flex items-center">
<i class="fas fa-times-circle mr-2"></i>
<div>
<p class="font-bold">Not a winner</p>
<p class="text-sm">Better luck next time!</p>
</div>
</div>
`;
}
} else {
resultDiv.className = 'mt-3 p-3 bg-red-600/20 border border-red-500 rounded-lg text-red-400';
resultDiv.innerHTML = `
<div class="flex items-center">
<i class="fas fa-exclamation-circle mr-2"></i>
<div>
<p class="font-bold">Error</p>
<p class="text-sm">${data.message}</p>
</div>
</div>
`;
}
} catch (error) {
console.error('Error:', error);
showNotification('Verification failed', 'error');
}
}
// Claim prize
async function claimPrize(ticketNumber) {
const claimantName = prompt('Enter your name to claim prize:');
if (!claimantName) return;
try {
const response = await fetch('api/claim_prize.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
ticket_number: ticketNumber,
claimant_name: claimantName
})
});
const data = await response.json();
if (data.success) {
showNotification(`Prize claimed: $${data.amount.toFixed(2)}`, 'success');
verifyTicket(); // Refresh verification
} else {
showNotification('Error: ' + data.message, 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification('Failed to claim prize', 'error');
}
}
// Show winner modal
function showWinnerModal(ticketNumber, amount) {
const modal = document.getElementById('winnerModal');
document.getElementById('winnerMessage').textContent =
`Ticket ${ticketNumber} won $${amount.toFixed(2)}!`;
modal.classList.remove('hidden');
setTimeout(() => {
modal.querySelector('div').classList.add('scale-100');
}, 10);
}
// Close winner modal
function closeWinnerModal() {
const modal = document.getElementById('winnerModal');
modal.querySelector('div').classList.remove('scale-100');
setTimeout(() => {
modal.classList.add('hidden');
}, 300);
}
// Helper: Show notification
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 animate-bounce ${
type === 'success' ? 'bg-green-600' :
type === 'error' ? 'bg-red-600' :
'bg-blue-600'
} text-white`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// Helper: Escape HTML
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
12. style.css (Additional Styles)
/* style.css - Additional custom styles */
/* Custom scrollbar for recent tickets */
#recentTickets::-webkit-scrollbar {
width: 6px;
}
#recentTickets::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
#recentTickets::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 3px;
}
/* Number ball animations */
@keyframes selectPulse {
0%, 100% {
box-shadow: 0 0 10px #667eea;
}
50% {
box-shadow: 0 0 30px #667eea;
}
}
.number-ball.selected {
animation: selectPulse 2s infinite;
}
/* Jackpot counter animation */
@keyframes countUp {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
#jackpotAmount {
animation: countUp 0.5s ease-out;
}
/* Ticket card hover effects */
.ticket-card {
position: relative;
overflow: hidden;
}
.ticket-card::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
45deg,
transparent,
rgba(255, 255, 255, 0.1),
transparent
);
transform: rotate(45deg);
transition: all 0.5s ease;
opacity: 0;
}
.ticket-card:hover::before {
opacity: 1;
transform: rotate(45deg) translate(50%, 50%);
}
/* Countdown timer animation */
@keyframes countdownPulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.countdown {
animation: countdownPulse 2s infinite;
}
/* Winner announcement */
.winner-glow {
animation: winnerGlow 2s ease-in-out infinite;
}
@keyframes winnerGlow {
0%, 100% {
box-shadow: 0 0 20px gold;
}
50% {
box-shadow: 0 0 50px gold;
}
}
/* Blockchain verification badge */
.blockchain-verified {
background: linear-gradient(90deg, #10b981, #059669);
padding: 2px 8px;
border-radius: 9999px;
font-size: 0.7rem;
display: inline-flex;
align-items: center;
}
/* Loading spinner */
.lottery-spinner {
border: 4px solid rgba(102, 126, 234, 0.3);
border-radius: 50%;
border-top: 4px solid #667eea;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
How to Run
- Setup Environment:
- Ensure XAMPP/WAMP/Laragon is installed with PHP 7.4+ and MySQL
- Start Apache and MySQL services
- Database Setup:
- Open phpMyAdmin (http://localhost/phpmyadmin)
- Create database
chainlotto_db - Import
database/schema.sql
- Configure Database:
- Update
includes/config.phpwith your database credentials
- Set Admin Key:
- In
api/draw_lottery.php, change the admin key from 'CHANGEME_SECRET_KEY' to a secure value
- Setup Cron Job (Optional):
- For automated draws, add a cron job to run
cron/auto_draw.phpevery hour:0 * * * * php /path/to/your/project/cron/auto_draw.php
- Place Files:
- Create folder
decentralized-lotteryin web root - Copy all files maintaining the structure
- Access Application:
- Open browser and navigate to:
http://localhost/decentralized-lottery/
Testing the Lottery System
- Purchase Tickets:
- Select 6 numbers by clicking on number balls
- Enter your name
- Click "Buy Ticket"
- Save your ticket number for verification
- Monitor Lottery:
- Watch jackpot grow as tickets are sold
- See countdown to next draw
- View recent ticket purchases
- Test Verification:
- Enter your ticket number in the verify field
- Check if you won (after draw)
- Admin Functions:
- To trigger a manual draw (for testing):
curl -X POST -d "admin_key=CHANGEME_SECRET_KEY" http://localhost/decentralized-lottery/api/draw_lottery.php
- Verify Transparency:
- All tickets are recorded with their hashes
- Winning numbers are generated from ticket data
- Anyone can verify the randomness by checking the seed hash
Security Features
- Verifiable Randomness: Winning numbers are derived from all ticket hashes
- Immutable Tickets: Each ticket is part of a blockchain
- Transparent Process: All draws can be audited
- Proof of Work: Prevents spam ticket purchases
- Automatic Payouts: Winners can claim directly
Prize Structure
- 6 numbers (Jackpot): 50% of prize pool
- 5 numbers: 20% of prize pool
- 4 numbers: 15% of prize pool
- 3 numbers: 10% of prize pool
- 2 numbers: 5% of prize pool
This project demonstrates a fully transparent, verifiable lottery system where trust is replaced by mathematics and blockchain technology. No central authority can manipulate the results, and every aspect is publicly auditable.