Last active
November 19, 2025 05:01
-
-
Save sebayaki/9ed0a19dc11b9795ae61ddd4430ec061 to your computer and use it in GitHub Desktop.
Mint Club Staking Leaderboard Query
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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