Files
mhunt_validator/index.js

480 lines
15 KiB
JavaScript

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; i<tournamentBlocks.length; i++){
if(tournamentBlocks[i].tournamentId == tournamentId){
if(lastValidBlockIndex>0){
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;
}
}