Skip to content

Instantly share code, notes, and snippets.

@sebayaki
Last active November 19, 2025 05:01
Show Gist options
  • Select an option

  • Save sebayaki/9ed0a19dc11b9795ae61ddd4430ec061 to your computer and use it in GitHub Desktop.

Select an option

Save sebayaki/9ed0a19dc11b9795ae61ddd4430ec061 to your computer and use it in GitHub Desktop.
Mint Club Staking Leaderboard Query
import { createPublicClient, http, fallback, parseAbiItem, formatUnits } from 'viem';
import { base } from 'viem/chains';
// Configuration
const POOL_ID = 52; // Change this to the desired Pool ID
const CONTRACT_ADDRESS = '0x9ab05eca10d087f23a1b22a44a714cdbba76e802';
// RPC List (Fallbacks)
const baseRpcUrls = [
'https://base-rpc.publicnode.com',
'https://base.llamarpc.com',
'https://mainnet.base.org',
'https://developer-access-mainnet.base.org',
'https://base-mainnet.public.blastapi.io',
'https://base-public.nodies.app',
'https://rpc.poolz.finance/base',
'https://base.meowrpc.com',
'https://api.zan.top/base-mainnet',
'https://1rpc.io/base',
'https://endpoints.omniatech.io/v1/base/mainnet/public',
'https://rpc.owlracle.info/base/70d38ce1826c4a60bb2a8e05a6c8b20f',
'https://base.public.blockpi.network/v1/rpc/public',
'https://base.drpc.org',
];
// Initialize Client
const client = createPublicClient({
chain: base,
transport: fallback(baseRpcUrls.map(url => http(url)))
});
// ABIs
const stakedEvent = parseAbiItem('event Staked(uint256 indexed poolId, address indexed staker, uint104 indexed amount)');
const unstakedEvent = parseAbiItem('event Unstaked(uint256 indexed poolId, address indexed staker, uint104 indexed amount, bool rewardClaimed)');
// Simplified ABI just to get token info and start time
const poolAbi = [{
"inputs": [{"internalType": "uint256", "name": "poolId", "type": "uint256"}],
"name": "getPool",
"outputs": [{
"components": [
{"internalType": "uint256", "name": "poolId", "type": "uint256"},
{"internalType": "address", "name": "stakingToken", "type": "address"}, // [0][1]
{"internalType": "bool", "name": "isStakingTokenERC20", "type": "bool"},
{"internalType": "address", "name": "rewardToken", "type": "address"},
{"internalType": "address", "name": "creator", "type": "address"},
{"internalType": "uint104", "name": "rewardAmount", "type": "uint104"},
{"internalType": "uint32", "name": "rewardDuration", "type": "uint32"},
{"internalType": "uint40", "name": "rewardStartsAt", "type": "uint40"},
{"internalType": "uint40", "name": "rewardStartedAt", "type": "uint40"}, // [0][7]
{"internalType": "uint40", "name": "cancelledAt", "type": "uint40"},
// ... other fields ignored for brevity
], "internalType": "struct Stake.Pool", "name": "pool", "type": "tuple"},
{"components": [
{"internalType": "string", "name": "symbol", "type": "string"},
{"internalType": "string", "name": "name", "type": "string"},
{"internalType": "uint8", "name": "decimals", "type": "uint8"}
], "internalType": "struct Stake.TokenInfo", "name": "stakingToken", "type": "tuple"}
],
"stateMutability": "view",
"type": "function"
}];
// Helper to handle RPC limits by splitting requests
async function getLogsInChunks(event: any, args: any, fromBlock: bigint, toBlock: bigint) {
const chunkSize = 3000n; // Safe chunk size for public RPCs
const logs = [];
for (let i = fromBlock; i <= toBlock; i += chunkSize) {
const end = (i + chunkSize - 1n) > toBlock ? toBlock : (i + chunkSize - 1n);
const chunk = await client.getLogs({
address: CONTRACT_ADDRESS,
event,
args,
fromBlock: i,
toBlock: end,
strict: true
});
logs.push(...chunk);
}
return logs;
}
async function getRankings(poolId: number) {
console.log(`Fetching data for Pool ${poolId}...`);
// 1. Get Pool Info to find decimals and approximate start time
// Note: In a raw script, we map the array response manually or use detailed ABI types
const poolData = await client.readContract({
address: CONTRACT_ADDRESS,
abi: poolAbi,
functionName: 'getPool',
args: [BigInt(poolId)]
}) as any;
const pool = poolData[0];
const token = poolData[1];
const decimals = token.decimals;
// 2. Estimate Start Block (Timestamp -> Block)
// Base average block time is ~2s.
// This is a rough estimation. For precision, use an external service like DefiLlama or Etherscan API.
const currentBlock = await client.getBlockNumber();
const currentTimestamp = Math.floor(Date.now() / 1000);
const secondsAgo = currentTimestamp - Number(pool.rewardStartedAt);
const blocksAgo = BigInt(Math.floor(secondsAgo / 2));
// Safety check: don't go before 0
let startBlock = currentBlock - blocksAgo;
if (startBlock < 0n) startBlock = 0n;
// Note: If the pool hasn't started, rewardStartedAt is 0, causing a full chain scan.
// You might want to set a hardcoded "Contract Deployment Block" (e.g. 12000000n) as a minimum floor.
console.log(`Scanning from block ${startBlock} to ${currentBlock}...`);
// 3. Fetch all Staked and Unstaked events
const [stakedLogs, unstakedLogs] = await Promise.all([
getLogsInChunks(stakedEvent, { poolId: BigInt(poolId) }, startBlock, currentBlock),
getLogsInChunks(unstakedEvent, { poolId: BigInt(poolId) }, startBlock, currentBlock)
]);
// 4. Aggregate Balances
const balances = new Map<string, bigint>();
stakedLogs.forEach(log => {
const { staker, amount } = log.args;
const current = balances.get(staker!) || 0n;
balances.set(staker!, current + amount!);
});
unstakedLogs.forEach(log => {
const { staker, amount } = log.args;
const current = balances.get(staker!) || 0n;
balances.set(staker!, current - amount!);
});
// 5. Format and Sort
const ranking = Array.from(balances.entries())
.filter(([_, balance]) => balance > 0n) // Remove 0 balances
.map(([address, balance]) => ({
address,
rawBalance: balance,
formattedBalance: formatUnits(balance, decimals),
symbol: token.symbol
}))
.sort((a, b) => {
if (b.rawBalance > a.rawBalance) return 1;
if (b.rawBalance < a.rawBalance) return -1;
return 0;
});
return ranking;
}
// Execute
getRankings(POOL_ID)
.then(data => {
console.log(`Found ${data.length} active stakers.`);
console.table(data.slice(0, 20)); // Show top 20
})
.catch(console.error);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment