leaderboard sup

This commit is contained in:
Sewmina 2025-08-02 00:31:47 +00:00
parent 0a34fac8c6
commit a12404fe36
7 changed files with 2972 additions and 12 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,11 @@
import express, { Request, Response } from 'express';
import cors from 'cors';
import fetch from 'node-fetch';
import { close, connection, fetchBets, refundBet } from './solana';
import { close, connection, fetchBets, refundBet } from './solana/bets';
import { log } from './logging_help';
import { duelfiApiUrl, GetReferreeWallet } from './shared';
import { createLeaderboard, getAllLeaderboardData, getLeaderboardById, getTicketSaleSolWallet, removeLeaderboard, updateScore } from './solana/leaderboard';
import { PublicKey } from '@solana/web3.js';
const app = express();
const port = process.env.USE_DEVNET ? 3033 : 3032;
@ -329,6 +331,41 @@ app.get('/getGames', async (_req: Request, res: Response) => {
return res.json({count: games_list.length, games: games_list});
});
app.get('/getLeaderboards', async (_req: Request, res: Response) => {
const leaderboards = await getAllLeaderboardData();
return res.json(leaderboards);
});
app.get("/getLeaderboard", async (_req: Request, res: Response) => {
const {leaderboard_id} = _req.query;
const leaderboard = await getLeaderboardById(leaderboard_id);
return res.json(leaderboard);
});
app.get("/updateLeaderboardScore", async (_req: Request, res: Response) => {
const {leaderboard_id, player_key, score} = _req.query;
const leaderboard = await updateScore(leaderboard_id, new PublicKey(player_key.toString()), score);
return res.json(leaderboard);
});
app.get("/admin/create_leaderboard", async (_req: Request, res: Response) => {
const {leaderboard_id, game_id, entryTicketCount, maxAttemptCount} = _req.query;
const leaderboard = await createLeaderboard(leaderboard_id, game_id, entryTicketCount, maxAttemptCount);
return res.json(leaderboard);
});
app.get("/admin/remove_leaderboard", async (_req: Request, res: Response) => {
const {leaderboard_id} = _req.query;
const leaderboard = await removeLeaderboard(leaderboard_id);
return res.json(leaderboard);
});
app.get("/admin/get_ticket_sale_sol_wallet", async (_req: Request, res: Response) => {
const {ticket_list} = _req.query;
const ticket_sale_sol_wallet = await getTicketSaleSolWallet(new PublicKey(ticket_list));
return res.json(ticket_sale_sol_wallet);
});
app.listen(port, () => {
log(`Server running at http://localhost:${port}`, 'server');
});

View File

@ -1,12 +1,14 @@
import { clusterApiUrl, PublicKey } from "@solana/web3.js";
import { log } from "./logging_help";
import { connection } from "./solana";
import { connection } from "./solana/bets";
// export const clusterUrl = "https://tiniest-cold-darkness.solana-mainnet.quiknode.pro/72332d636ff78d498b880bd8fdc3eb646c827da8/";
// export const clusterUrl = "https://go.getblock.io/908837801b534ae7a6f0869fc44cc567";
export const mainnetClusterUrl = "https://solana-mainnet.core.chainstack.com/c54e14eef17693283a0323efcc4ce731";
export const devnetClusterUrl = clusterApiUrl("devnet");
export const feeWallet = new PublicKey("9esrj2X33pr5og6fdkDMjaW6fdnnb9hT1cWshamxTdL4");
export const ticketsTokenMint = new PublicKey("Tktza8fjqeG89UerPf5z78xBPpiCGV5WMCwUMDpnu9m");
export const TOKEN_PROGRAM_ID = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
// Default to mainnet
export const clusterUrl = process.env.USE_DEVNET ? devnetClusterUrl : mainnetClusterUrl;

View File

@ -1,12 +1,12 @@
import { AnchorProvider, Wallet, Program } from "@coral-xyz/anchor";
import { Keypair, Connection, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
import { Bets } from "./bets";
import { testerSk, cocSk, clusterUrl, duelfiApiUrl, feeWallet, GetReferreeWallet, add_new_activity } from "./shared";
import { log } from "./logging_help";
import { getCurrencyByMint } from "./data/currencies";
import { Bets } from "../bets";
import { testerSk, cocSk, clusterUrl, duelfiApiUrl, feeWallet, GetReferreeWallet, add_new_activity } from "../shared";
import { log } from "../logging_help";
import { getCurrencyByMint } from "../data/currencies";
import { createAssociatedTokenAccount, getAssociatedTokenAddressSync } from "@solana/spl-token";
const IDL = require("./bets.json");
const IDL = require("../bets.json");
const keypair = Keypair.fromSecretKey(Uint8Array.from(cocSk));
export const connection = new Connection(clusterUrl);

235
src/solana/leaderboard.ts Normal file
View File

@ -0,0 +1,235 @@
import { AnchorProvider, Wallet, Program, BN } from "@coral-xyz/anchor";
import { Keypair, Connection, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
import { Bets } from "../bets";
import { testerSk, cocSk, clusterUrl, duelfiApiUrl, feeWallet, GetReferreeWallet, add_new_activity, ticketsTokenMint, TOKEN_PROGRAM_ID } from "../shared";
import { log } from "../logging_help";
import { getCurrencyByMint } from "../data/currencies";
import { createAssociatedTokenAccount, getAssociatedTokenAddressSync } from "@solana/spl-token";
import { Leaderboard, LeaderboardProcessed } from "../types";
const IDL = require("../bets.json");
const keypair = Keypair.fromSecretKey(Uint8Array.from(cocSk));
export const connection = new Connection(clusterUrl);
const provider = new AnchorProvider(connection, new Wallet(keypair));
const program:Program<Bets> = new Program<Bets>(IDL,provider);
let leaderboard_list_pda: PublicKey;
let leaderboard_key_list: PublicKey[];
let leaderboard_list: LeaderboardProcessed[];
// Simple cache with timestamps
let lastLeaderboardListFetch: number = 0;
let lastLeaderboardDataFetch: number = 0;
const CACHE_DURATION = 5000; // 5 seconds in milliseconds
async function initialize() {
[leaderboard_list_pda] = await PublicKey.findProgramAddress([Buffer.from("ticket_leaderboards_list")], program.programId);
log(`init on ${clusterUrl}\nLeaderboard list PDA : ${leaderboard_list_pda}`, "solana");
try {
await program.account.ticketLeaderboardList.fetch(leaderboard_list_pda);
} catch (err) {
log("Leaderboard list PDA not found, initializing", "solana");
const tx = await program.methods.initLeaderboardList()
.accounts({
})
.rpc();
await connection.confirmTransaction(tx, "confirmed");
}
log(`getting leaderboard list at init, on ${clusterUrl}`, "solana");
await getAllLeaderboards();
await getAllLeaderboardData();
}
initialize();
export async function getAllLeaderboardData(){
const now = Date.now();
// Check if we have cached data and it's less than 5 seconds old
if (leaderboard_list && (now - lastLeaderboardDataFetch) < CACHE_DURATION) {
log("Serving cached leaderboard data", "cache");
return leaderboard_list;
}
// Fetch fresh data from blockchain
await getAllLeaderboards();
log("Fetching fresh leaderboard data from blockchain", "cache");
leaderboard_list = [];
for (const leaderboard_key of leaderboard_key_list){
console.log(`fetching data for leaderboard: ${leaderboard_key}`)
const leaderboard = await getLeaderboardByPubkey(leaderboard_key);
const leaderboard_processed: LeaderboardProcessed = {
id: leaderboard.id.toNumber(),
gameId: leaderboard.gameId,
players: [],
isOver: leaderboard.isOver,
entryTicketCount: leaderboard.entryTicketCount.toNumber(),
maxAttemptCount: leaderboard.maxAttempts.toNumber()
}
for (const element of leaderboard.players) {
const [player_vault_pda] = await PublicKey.findProgramAddress(
[Buffer.from("leaderboard_entry"), leaderboard_key.toBuffer(), element.toBuffer()],
program.programId
);
const leaderboard_entry = await program.account.leaderboardEntry.fetch(player_vault_pda);
leaderboard_processed.players.push({
username: element.toString(),
score: leaderboard_entry.highscore.toNumber(),
uid:leaderboard_entry.uid.toString(),
attempts: leaderboard_entry.totalTickets.toNumber()
})
}
leaderboard_list.push(leaderboard_processed);
}
lastLeaderboardDataFetch = now;
return leaderboard_list;
}
export async function getAllLeaderboards(){
const now = Date.now();
// Check if we have cached data and it's less than 5 seconds old
if (leaderboard_key_list && (now - lastLeaderboardListFetch) < CACHE_DURATION) {
log("Serving cached leaderboard list", "cache");
return leaderboard_key_list;
}
// Fetch fresh data from blockchain
log("Fetching fresh leaderboard list from blockchain", "cache");
const leaderboard_list_acc = await program.account.ticketLeaderboardList.fetch(leaderboard_list_pda);
leaderboard_key_list = leaderboard_list_acc.leaderboards;
lastLeaderboardListFetch = now;
return leaderboard_key_list;
}
export async function getLeaderboardByPubkey(leaderboard_key: PublicKey){
const leaderboard = await program.account.ticketLeaderboard.fetch(leaderboard_key);
return leaderboard;
}
export async function getLeaderboardById(leaderboard_id: number){
const [leaderboard_pda] = await PublicKey.findProgramAddress(
[Buffer.from("ticket_leaderboard"), new BN(leaderboard_id).toArrayLike(Buffer, "le", 8)],
program.programId
);
const leaderboard = await program.account.ticketLeaderboard.fetch(leaderboard_pda);
return leaderboard;
}
export async function updateScore(leaderboard_id: number, player_key: PublicKey, score: number){
const leaderboardId = new BN(leaderboard_id);
const [leaderboard_pda] = await PublicKey.findProgramAddress(
[Buffer.from("ticket_leaderboard"), leaderboardId.toArrayLike(Buffer, "le", 8)],
program.programId
);
const [leaderboard_entry_pda] = await PublicKey.findProgramAddress(
[Buffer.from("leaderboard_entry"), leaderboard_pda.toBuffer(), player_key.toBuffer()],
program.programId
);
const [receipt_pda] = await PublicKey.findProgramAddress(
[Buffer.from("receipt"), player_key.toBuffer()],
program.programId
);
console.log(`leaderboard: ${leaderboard_pda}, id: ${leaderboardId}`)
console.log(`leaderboard_entry: ${leaderboard_entry_pda}`)
console.log(`receipt: ${receipt_pda}`)
try{
const tx = await program.methods.updateLeaderboard(leaderboardId, player_key, new BN(score)).accounts(
{
payer: provider.wallet.publicKey
}
).rpc();
await connection.confirmTransaction(tx, "finalized");
const leaderboardNew = await getLeaderboardByPubkey(leaderboard_pda);
CheckAndReward(leaderboard_id);
return leaderboardNew;
}
catch(err){
console.log(err)
return {error: err};
}
}
async function CheckAndReward(leaderboard_id:number){
const [leaderboard_pda] = await PublicKey.findProgramAddress(
[Buffer.from("ticket_leaderboard"), new BN(leaderboard_id).toArrayLike(Buffer, "le", 8)],
program.programId
);
const leaderboard = await getLeaderboardByPubkey(leaderboard_pda);
let total_attempts = 0;
let winner:{key:PublicKey, score:number} = {key:null, score:0};
for (const player of leaderboard.players){
const [leaderboard_entry_pda] = await PublicKey.findProgramAddress(
[Buffer.from("leaderboard_entry"), leaderboard_pda.toBuffer(), player.toBuffer()],
program.programId
);
const leaderboard_entry = await program.account.leaderboardEntry.fetch(leaderboard_entry_pda);
total_attempts += leaderboard_entry.totalTickets.toNumber();
if (leaderboard_entry.highscore.toNumber() > winner.score){
winner.key = player;
winner.score = leaderboard_entry.highscore.toNumber();
}
}
if (total_attempts >= leaderboard.maxAttempts.toNumber()){
console.log("rewarding leaderboard id ", leaderboard_id);
const tx = await program.methods.rewardLeaderboard(new BN(leaderboard_id)).accounts(
{
payer: provider.wallet.publicKey,
winner: winner.key,
tokenMint: ticketsTokenMint,
tokenProgram: TOKEN_PROGRAM_ID,
}
).rpc();
await connection.confirmTransaction(tx, "finalized");
await createLeaderboard(leaderboard_id + 1, leaderboard.gameId, leaderboard.entryTicketCount.toNumber(), leaderboard.maxAttempts.toNumber());
}
}
export async function createLeaderboard(leaderboard_id: number, game_id: string, entryTicketCount: number, maxAttemptCount: number){
const tx = await program.methods.createLeaderboard(new BN(leaderboard_id), game_id, new BN(entryTicketCount), new BN(maxAttemptCount)).accounts(
{
payer: provider.wallet.publicKey,
tokenMint: ticketsTokenMint,
tokenProgram: TOKEN_PROGRAM_ID
}
).rpc();
await connection.confirmTransaction(tx, "confirmed");
}
export async function removeLeaderboard(leaderboard_id: number){
const tx = await program.methods.removeLeaderboard(new BN(leaderboard_id)).accounts(
{
payer: provider.wallet.publicKey,
}
).rpc();
await connection.confirmTransaction(tx, "confirmed");
}
export async function getTicketSaleSolWallet(ticket_list:PublicKey){
const [ticket_sale_sol_wallet_pda] = await PublicKey.findProgramAddress(
[Buffer.from("sol_vault"), ticket_list.toBuffer()],
program.programId
);
return ticket_sale_sol_wallet_pda;
}

View File

@ -1,3 +1,6 @@
import { BN } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
export type PlayerSubmission = {
username: string;
winner: string;
@ -9,4 +12,27 @@ export type GameData = {
master?: PlayerSubmission;
client?: PlayerSubmission;
validated?: boolean;
};
};
export type Leaderboard = {
id: BN;
gameId: string;
players: PublicKey[];
isOver: boolean;
entryTicketCount: BN;
maxAttemptCount: BN;
}
export type LeaderboardProcessed = {
id: number;
gameId: string;
players: {
username: string;
score: number;
uid:string;
attempts:number;
}[];
isOver: boolean;
entryTicketCount: number;
maxAttemptCount: number;
}