const express = require('express') const app = express() const port = 9700 const web3operatorAddress = "http://vps.playpoolstudios.com:2015/" const apiAddress = "https://vps.playpoolstudios.com/metahunt/api/" app.use(express.json()); app.get('/', (req, res) => { res.send('Validator is validating') }) app.listen(port, () => { console.log(`Mhunt Validator is listening on port ${port}`) }) app.get('/validateSession', async (req, res) => { const { tournamentId, address } = req.query; if (!tournamentId || !address) { res.send("invalid params"); return; } let tournament = GetTournamentById(tournamentId); if (tournament == null) { await CheckForStartedTourneys(); tournament = GetTournamentById(tournamentId); } if (tournament == null) { console.log(`tourney id:${tournamentId} is not available`); //res.send("This tournament is not either started or valid"); // return; } tournament.displayDetails(); const found = tournament.participents.some(participant => participant == address); if (found) { return res.send("0"); } else { return res.send("-1"); } }) const tournamentTimers = new Map(); app.post('/updateBlock', async (req, res) => { const jsonData = req.body; if (!jsonData) { return res.status(400).send("No JSON data received"); } const tournamentBlock = new TournamentBlockData(jsonData); tournamentBlock.minute = timeIndexToMinute(new Date()); if (Object.keys(tournamentBlock.leaderboard).length > 0) { tournamentBlocks.push(tournamentBlock); }else{ console.log("Empty leaderboard, probably the initial one. Not pushing"); res.send.status(200); return; } const tournamentData = GetTournamentById(tournamentBlock.tournamentId); const tournamentEndMinute = timeIndexToMinute(new Date(tournamentData.start_date)) + 10; if(tournamentBlock.minute >= tournamentEndMinute){ console.log(`this must be the last block ${tournamentBlock.minute}>${tournamentEndMinute}`); // console.log(`${minuteToTimeIndex(tournamentBlock.minute)}:${minuteToTimeIndex(tournamentEndMinute)}. Time:${(new Date()).toString()}`) //Final block from this user for this tournament if (!tournamentTimers.has(tournamentBlock.tournamentId)) { console.log(`Final block received for tournament ${tournamentBlock.tournamentId}. Starting 5-second timer...`); // Set a 5-second timer for this tournament const timer = setTimeout(async () => { ScanBlocks(); console.log(`Scanned blocks after finishing t:${tournamentBlock.tournamentId}`); RewardTournament(tournamentBlock.tournamentId); }, 5000); // Store the timer in the Map for this tournament tournamentTimers.set(tournamentBlock.tournamentId, timer); }else{ } } res.sendStatus(200); }); function timeIndexToMinute(time) { // Get the total number of milliseconds since the Unix epoch (January 1st, 1970) const milliseconds = time.getTime(); // Convert milliseconds to minutes (1 minute = 60,000 milliseconds) const minutes = Math.floor(milliseconds / 60000); return minutes; } function minuteToTimeIndex(minutes) { // Convert minutes back to milliseconds (1 minute = 60,000 milliseconds) const milliseconds = minutes * 60000; // Create a new Date object using the milliseconds since the Unix epoch const date = new Date(milliseconds); return date; } /* ------------------------- DEV GETS --------------------------------------- */ app.get("/getBlocks", (req,res)=>{ res.setHeader('Content-Type', 'application/json'); res.send(JSON.stringify(tournamentBlocks)); }) app.get("/getTourneys",(req,res)=>{ res.setHeader('Content-Type', 'application/json'); res.send(JSON.stringify(startedTournaments)); }) app.get("/getLeaderboard", (req,res)=>{ res.setHeader('Content-Type', 'application/json'); const {id} = req.query; if(!id){ res.send(JSON.stringify(confirmedLeaderboards)); }else{ res.send(JSON.stringify(confirmedLeaderboards[id])); } }) app.get("/getTournamentEndResults", (req,res)=>{ res.setHeader('Content-Type', 'application/json'); const {id} = req.query; if(id){ try{ for(const endResult in tournamentEndResults){ if(endResult.id === id){ res.send(JSON.stringify(endResult)); return; } } res.send("0"); }catch{ res.send("-1"); } }else{ res.send(JSON.stringify(tournamentEndResults)); } }) app.get("/getTournamentTimers",(req,res)=>{ res.setHeader('Content-Type', 'application/json'); res.send(JSON.stringify(tournamentTimers)); }) /* ------------------------------------------------------------------------------- */ let tournamentsList ; let startedTournaments; let tournamentBlocks = []; let tournamentEndResults=[]; /* ------------------------------------------------------------------------------- */ async function RewardTournament(tournamentId){ const leaderboard = confirmedLeaderboards[tournamentId]; let mostKills =-1; let topPlayer = ""; for(const playerId in leaderboard){ const playerStat = leaderboard[playerId]; if(playerStat.kills > mostKills){ mostKills =playerStat.kills; topPlayer = playerId; } } let winnerWallet = ""; for(const block in tournamentBlocks){ if(block.tournamentId == tournamentId && topPlayer == block.owner){ winnerWallet = block.owner; break; } } try{ const tx = await fetch(web3operatorAddress+`rewardTournament?password=SekretWordHere&tournamentId=${tournamentId}&winnerWallet=${winnerWallet}`); const endResult = new TournamentEndResult(tournamentId, winnerWallet, tx); tournamentEndResults.push(endResult); console.log(`*** Rewarded ${winnerWallet} for tourney : ${tournamentId}`); }catch{ console.log("*** ERROR REWARDING ***"); } } CheckForStartedTourneys(); setInterval(async () => { CheckForStartedTourneys(); }, 60000) async function CheckForStartedTourneys() { try{ const tournamentsResponse = await fetch(apiAddress + "get_tournaments_raw.php"); const tournaments = await tournamentsResponse.json(); tournamentsList = []; startedTournaments = []; const now = new Date(); const tenMinutesAgo = new Date(now.getTime() - 10 * 60000); // 10 minutes ago from now tournaments.forEach(async tournament => { const tournamentDate = new Date(tournament.start_date); // Converts the string date to a Date object // Create a new TournamentData instance using the tournament JSON data const newTournament = new TournamentData( tournament.id, tournament.name, tournament.start_date, tournament.game_mode, tournament.reward, tournament.php_reward, tournament.is_test, tournament.ticket_count, tournament.owner_id ); if (tournamentDate >= tenMinutesAgo && tournamentDate <= now || true) { console.log(`Tournament "${tournament.name}" started within the last 10 minutes.`); try { const participentsUrl = web3operatorAddress + "getTournamentParticipants?id=" + newTournament.id; const participentsResponse = await fetch(participentsUrl); const participentsJson = await participentsResponse.json(); const participents = participentsJson["wallets"].split(','); participents.forEach(participent => { newTournament.participents.push(participent); }) //newTournament.displayDetails(); } catch { console.log(`tourneyId:${newTournament.id} has no participents. ${JSON.stringify(participents)}`) } startedTournaments.push(newTournament); } else if (tournamentDate > now) { console.log(`Tournament "${tournament.name}" is yet to come`) } else { // console.log(`Tournament "${tournament.name}" is expired`) } // newTournament.displayDetails(); // Push the new tournament instance into tournamentsList tournamentsList.push(newTournament); }); }catch{ console.log("*** API SERVER IS NOT RESPONDING? ***"); return; } } /* ------------------------------------------------------------------------------- */ startScanBlocksAt30Seconds(); function startScanBlocksAt30Seconds() { const now = new Date(); const seconds = now.getSeconds(); // Calculate time (in ms) to the next 30th second of the minute let delay; if (seconds < 30) { delay = (30 - seconds) * 1000; // wait till 30th second } else { delay = (60 - seconds + 30) * 1000; // wait till next minute's 30th second } setTimeout(() => { // First run at the exact 30th second ScanBlocks(); // Continue running every 60 seconds after this setInterval(() => { ScanBlocks(); }, 60000); // Run every 60 seconds }, delay); } // Define a dictionary to store tournament leaderboards after validation const confirmedLeaderboards = {}; // Function to scan and analyze blocks function ScanBlocks() { const blockValidationCounts = {}; // First, validate each block and count valid ones for each tournament for (let i = 0; i < tournamentBlocks.length; i++) { let isValid = true; // Loop through the started tournaments to validate the block for (let j = 0; j < startedTournaments.length; j++) { if (startedTournaments[j].id === tournamentBlocks[i].tournamentId) { // Found the matching tournament for this block if (!startedTournaments[j].participants.includes(tournamentBlocks[i].owner)) { console.log("***Block was sent by non-participant. Allowing this for now."); isValid = false; break; } // Compare blocks with the same tournamentId and minute number for (let k = 0; k < tournamentBlocks.length; k++) { if ( k !== i && tournamentBlocks[k].tournamentId === tournamentBlocks[i].tournamentId && tournamentBlocks[k].minute === tournamentBlocks[i].minute ) { // Found another block with the same tournamentId and minute if (JSON.stringify(tournamentBlocks[k].leaderboard) !== JSON.stringify(tournamentBlocks[i].leaderboard)) { console.log(`***Mismatch detected in leaderboard for tournament ${tournamentBlocks[i].tournamentId} at minute ${tournamentBlocks[i].minute}`); isValid = false; break; } } } } } if (isValid) { console.log(`Block ${i + 1} is valid.`); // Track the valid blocks per tournament const tournamentId = tournamentBlocks[i].tournamentId; if (!blockValidationCounts[tournamentId]) { blockValidationCounts[tournamentId] = { count: 0, total: 0 }; } blockValidationCounts[tournamentId].count++; tournamentBlocks[i].validation=1; } else { console.log(`Block ${i + 1} is invalid.`); tournamentBlocks[i].validation=-1; } // Track total blocks per tournament for comparison later const tournamentId = tournamentBlocks[i].tournamentId; if (!blockValidationCounts[tournamentId]) { blockValidationCounts[tournamentId] = { count: 0, total: 0 }; } blockValidationCounts[tournamentId].total++; } // After validation, confirm tournaments with more than 50% valid blocks for (const tournamentId in blockValidationCounts) { const { count, total } = blockValidationCounts[tournamentId]; if (count > total / 2) { // More than 50% of blocks are valid, store the leaderboard let lastValidBlockIndex=-1; for(let i=0; i0){ if(tournamentBlocks[lastValidBlockIndex].minute < tournamentBlocks[i].minute){ lastValidBlockIndex=i; } }else{ lastValidBlockIndex=i; } } } const validBlock = tournamentBlocks[lastValidBlockIndex]; if (validBlock) { if (Object.keys(validBlock.leaderboard).length > 0) { confirmedLeaderboards[tournamentId] = validBlock.leaderboard; console.log(`*** Tournament ${tournamentId} confirmed with a valid leaderboard.`); }else{ console.log(`Block ${validBlock.tournamentId} from ${validBlock.owner} has an empty leaderboard. Ignoring for now`); } }else{ console.log(`Could not find a block for t:${tournamentId}`); } } else { console.log(`*** Tournament ${tournamentId} did not meet the 50% valid block requirement.`); } } } /* ----------------------------------METHODS----------------------------------- */ function GetTournamentById(tournamentId) { const tournament = startedTournaments.find(tournament => tournament.id == tournamentId); return tournament; } /* ------------------------------------------------------------------------------- */ /* ------------------------------CUSTOM CLASSES------------------------------------ */ class TournamentData { constructor(id, name, start_date, game_mode, reward, php_reward, is_test, ticket_count, owner_id) { this.id = id; this.name = name; this.start_date = start_date; this.game_mode = game_mode; this.reward = reward; this.php_reward = php_reward; this.is_test = is_test; this.ticket_count = ticket_count; this.owner_id = owner_id; this.participents = []; } // Method to display tournament details displayDetails() { console.log(`Tournament ID: ${this.id}`); console.log(`Name: ${this.name}`); console.log(`Date: ${this.start_date}`); console.log(`Game Mode: ${this.game_mode}`); console.log(`Reward: ${this.reward}`); console.log(`PHP Reward: ${this.php_reward}`); console.log(`Test Tournament: ${this.is_test ? "Yes" : "No"}`); console.log(`Ticket Count: ${this.ticket_count}`); console.log(`Owner ID: ${this.owner_id}`); console.log(`Participents: ${this.participents}`); } } // Define the PlayerStat class class PlayerStat { constructor(data) { this.username = data.username || ''; this.xp = data.xp || 0; this.kills = data.kills || 0; this.deaths = data.deaths || 0; this.assists = data.assists || 0; } } // Define the TournamentBlockData class class TournamentBlockData { constructor(data) { console.log(data); this.tournamentId = data.tournamentId || 0; this.owner = data.owner || ''; this.owner_username=data.owner_username || ''; this.minute=data.minute; this.validation =0; this.leaderboard = {}; // Populate the leaderboard if (data.leaderboard) { for (const playerId in data.leaderboard) { if (data.leaderboard.hasOwnProperty(playerId)) { this.leaderboard[playerId] = new PlayerStat(data.leaderboard[playerId]); } } } } } class TournamentEndResult{ constructor(id, winner, tx){ this.id=id; this.winner = winner; this.tx=tx; } }