0

Is there any other method to retrieve SPL token holders?

I'm currently using the Helius API, but it's giving me an "exceeded limit" error.

2 Answers 2

1

To preface, I am an employee at Helius.

The DAS API only returns up to 1000 records at a time. If you need to retrieve more than 1000 items, then you'll need to paginate your records. This is achieved by making multiple API calls and crawling through "pages" of data. We support both page-based and keyset pagination. You can read more about that here. For most tokens, you can expect to have more than 1000 holders.

The Get Token Accounts method is useful for retrieving information about all token accounts for a specific mint or owner. You can use pagination with this method to fetch all the holders of a given SPL token. We have the following example from our docs, which uses page-based pagination to get all the holders of $BONK and writes their pubkey to a new file:

const url = `https://mainnet.helius-rpc.com/?api-key=<API-KEY>`;
const fs = require("fs");

const getTokenAccounts = async () => {
  const fetch = (await import("node-fetch")).default;
  let page = 1;
  let allOwners = new Set();

  while (true) {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        method: "getTokenAccounts",
        id: "helius-test",
        params: {
          page: page,
          limit: 1000,
          displayOptions: {},
          mint: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
        },
      }),
    });
    const data = await response.json();

    if (!data.result || data.result.token_accounts.length === 0) {
      console.log(`No more results. Total pages: ${page - 1}`);
      break;
    }
    console.log(`Processing results from page ${page}`);
    data.result.token_accounts.forEach((account) =>
      allOwners.add(account.owner)
    );
    page++;
  }

  fs.writeFileSync(
    "output.json",
    JSON.stringify(Array.from(allOwners), null, 2)
  );
};

getTokenAccounts();

If you have any additional questions, feel free to ask away here or in the Helius Discord!

0

ok for those of you don't want to use Helius, this is how I got it.

const { Connection, PublicKey } = require('@solana/web3.js');
const { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, getMint } = require('@solana/spl-token');
const fs = require('fs');
const path = require('path');

const apiKey = // NODE API KEY HERE e.g. https://<YADAYADA>.solana-mainnet.quicknode.pro/<YADAYADA> 
const tokenContracts = [{ address: "ATLASXmbPQxBUYbxPsV97usA3fPQYEqzQBUHgiFCUsXx", ticker: "ATLAS", tokenName: "AtlasToken", decimals: 8 }];
    

// Initialize Solana connection
const solanaConnection = new Connection(apiKey);

async function getAllTokenHoldersForContract({ address, ticker, tokenName, decimals }) {
  try {
    const mintPublicKey = new PublicKey(address);

    // Fetch the total supply of the token
    const mintAccountInfo = await solanaConnection.getParsedAccountInfo(mintPublicKey);
    const totalSupply = mintAccountInfo.value.data.parsed.info.supply;

    const minBalanceThreshold = 0;

    // Fetch the current slot (block number)
    const currentSlot = await solanaConnection.getSlot();

    const programId = TOKEN_PROGRAM_ID; // change this to TOKEN_2022_PROGRAM_ID if needed

    // Fetch all token accounts for the given mint
    const tokenAccounts = await solanaConnection.getParsedProgramAccounts(
      programId,
      {
        filters: [
          {
            dataSize: 165, // Size of the token account data
          },
          {
            memcmp: {
              offset: 0, // Start of the mint address in the account data
              bytes: mintPublicKey.toBase58(), // The mint address to filter by
            },
          },
        ],
      }
    );

    if (tokenAccounts.length === 0) {
      console.log(`No token holders found for contract ${tokenName} (${ticker}).`);
      return;
    }


    // Filter out accounts with a non-zero balance greater than the threshold
    const holders = tokenAccounts
      .map((account) => {
        const accountInfo = account.account.data.parsed.info;
        return {
          holderAddress: accountInfo.owner,
          amountHeld: accountInfo.tokenAmount.uiAmount,
        };
      })
      .filter((holder) => holder.amountHeld > minBalanceThreshold); // Filter out accounts below the threshold

    // Prepare CSV data
    const csvData = [
      ["Holder Address", "Amount Held", "Total Supply", "Contract Address", "Ticker", "Token Name", "Slot"],
      ...holders.map(holder => [
        holder.holderAddress,
        holder.amountHeld,
        totalSupply / Math.pow(10, decimals),
        address,
        ticker,
        tokenName,
        currentSlot,
      ]),
    ];

    // Convert array to CSV format
    const csvContent = csvData.map(row => row.join(",")).join("\n");

    // Save the CSV file with the token name and slot number
    const filePath = path.join(__dirname, `csv/${tokenName}_slot${currentSlot}.csv`);
    fs.writeFileSync(filePath, csvContent);

    console.log(`CSV file saved as ${filePath}`);
  } catch (error) {
    console.error(`Error fetching token holders and balances for ${tokenName} (${ticker}):`, error);
  }
}

async function processTokenContracts() {
  for (const contract of tokenContracts) {
    await getAllTokenHoldersForContract(contract);
  }
}

// Run the function to process all token contracts
processTokenContracts();

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.