├── .gitignore ├── .env.sample ├── README.md ├── package.json └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | SOLANA_RPC_ENDPOINT=https://api.mainnet-beta.solana.com -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This script provides simple utility to analyze the top largest holders of a given Solana Token Contract Address and see their Solana, USDC, and USDT balance 2 | 3 | 1) Run `npm install` 4 | 2) Create a .env file using .env.sample as an example 5 | 3) Replace env variable with a custom RPC url if you have it 6 | 4) Run the script using `node index.js TOKEN_CONTRACT_ADDRESS_HERE` 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-token-holders", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "axios": "^1.7.5", 14 | "nodenv": "^0.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const axios = require('axios'); 3 | 4 | // Set the Solana/Helius RPC endpoint from the environment variables 5 | const SOLANA_RPC_ENDPOINT = process.env.SOLANA_RPC_ENDPOINT; 6 | const usdcMintAddress = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; 7 | const usdtMintAddress = 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'; 8 | 9 | // Function to get the Solana balance of a wallet 10 | async function getBalanceSOL(walletAddress) { 11 | const payload = { 12 | jsonrpc: '2.0', 13 | id: 1, 14 | method: 'getBalance', 15 | params: [walletAddress] 16 | }; 17 | 18 | const response = await makeRequestWithBackoff(payload); 19 | return response?.data?.result.value / 1e9; // Convert lamports to SOL 20 | } 21 | // Function to get the token balance of a wallet 22 | async function getTokenBalance(walletAddress, tokenMintAddress) { 23 | const payload = { 24 | jsonrpc: '2.0', 25 | id: 1, 26 | method: 'getTokenAccountsByOwner', 27 | params: [ 28 | walletAddress, 29 | { 30 | mint: tokenMintAddress 31 | }, 32 | { 33 | encoding: 'jsonParsed' 34 | } 35 | ] 36 | }; 37 | const response = await makeRequestWithBackoff(payload); 38 | const accounts = response?.data?.result.value; 39 | if (accounts.length === 0) { 40 | return 0; 41 | } 42 | return accounts[0].account.data.parsed.info.tokenAmount.uiAmount; 43 | } 44 | // Function to get the largest token accounts of a token 45 | async function getTokenLargestAccounts(tokenMintAddress) { 46 | const payload = { 47 | jsonrpc: "2.0", 48 | id: 1, 49 | method: "getTokenLargestAccounts", 50 | params: [ 51 | tokenMintAddress, 52 | { commitment: "finalized" } 53 | ] 54 | }; 55 | const response = await makeRequestWithBackoff(payload); 56 | const accounts = response?.data?.result.value; 57 | return accounts; // Returns the top 20 holders 58 | } 59 | 60 | async function getAccountOwner(tokenAccount) { 61 | const payload = { 62 | jsonrpc: "2.0", 63 | id: 1, 64 | method: "getAccountInfo", 65 | params: [ 66 | tokenAccount, 67 | { encoding: "jsonParsed" } 68 | ] 69 | }; 70 | const response = await makeRequestWithBackoff(payload) 71 | const owner = response?.data?.result.value.data.parsed.info.owner; 72 | return owner; 73 | } 74 | 75 | async function makeRequestWithBackoff(payload) { 76 | const maxRetries = 5; 77 | let attempt = 0; 78 | let delay = 1000; // Initial delay of 1 second 79 | 80 | while (attempt < maxRetries) { 81 | try { 82 | const response = await axios.post(SOLANA_RPC_ENDPOINT, payload, { 83 | headers: { 84 | "Content-Type": "application/json" 85 | } 86 | }); 87 | return response; 88 | 89 | } catch (error) { 90 | if (error.response && error.response.status === 429) { 91 | // Rate limit hit, wait and retry 92 | // console.warn(`Rate limited, retrying in ${delay / 1000} seconds...`); 93 | await new Promise(resolve => setTimeout(resolve, delay)); 94 | delay *= 2; // Exponential backoff 95 | attempt++; 96 | } else { 97 | // Log error and return null for other errors 98 | console.error(`Error fetching owner for token account ${tokenAccount}:`, error); 99 | return null; 100 | } 101 | } 102 | } 103 | } 104 | // Function to get owners of token accounts and gather their portfolios 105 | async function processTopHolders(tokenAccounts) { 106 | console.log("Processing top holders:"); 107 | 108 | for (const tokenAccount of tokenAccounts) { 109 | console.log("-------------------"); 110 | const accountOwner = await getAccountOwner(tokenAccount.address) 111 | try { 112 | console.log(accountOwner) 113 | console.log("Token Balance: ", tokenAccount.amount) 114 | const solBalance = await getBalanceSOL(accountOwner); 115 | console.log(`SOL Balance: ${solBalance} SOL`); 116 | 117 | const usdcBalance = await getTokenBalance(accountOwner, usdcMintAddress); 118 | console.log(`USDC Balance: ${usdcBalance} USDC`); 119 | 120 | const usdtBalance = await getTokenBalance(accountOwner, usdtMintAddress); 121 | console.log(`USDT Balance: ${usdtBalance} USDT`); 122 | } catch (error) { 123 | console.error('Error fetching balances:', error); 124 | } 125 | } 126 | } 127 | async function main() { 128 | const args = process.argv.slice(2); 129 | 130 | if (args.length === 0) { 131 | console.error("Error: Please provide a token mint address as a command-line argument."); 132 | process.exit(1); 133 | } 134 | 135 | const TOKEN_MINT_ADDRESS = args[0]; 136 | const topTokenAccounts = await getTokenLargestAccounts(TOKEN_MINT_ADDRESS); 137 | await processTopHolders(topTokenAccounts); 138 | } 139 | 140 | main(); 141 | --------------------------------------------------------------------------------