Skip to main content

Solana SPL Token Fetcher

Updated on
Jan 02, 2025

Overview

SPL tokens are Solana's native token standard, similar to ERC-20 on Ethereum. They enable the creation of fungible tokens that can represent anything from cryptocurrencies and stablecoins to governance tokens and wrapped assets. Tracking and analyzing token holdings is essential for any blockchain application, whether for portfolio management or user analytics.

In this Function example, we'll demonstrate how to efficiently fetch and process SPL token balances for any Solana wallet address, providing detailed information about these fungible token holdings.

Sample Function

The following is the code for the Function to get SPL token data in JavaScript Node.js v20 runtime:

// Initialize the main function
const { Connection, PublicKey } = require('@solana/web3.js');
const BN = require('bn.js');

const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');

async function fetchTokens(params) {
const wallet = params.user_data.wallet;
const page = Math.max(1, parseInt(params.user_data.page) || 1);
const perPage = Math.min(40, Math.max(1, parseInt(params.user_data.perPage) || 20));
const omitFields = params.user_data.omitFields ? params.user_data.omitFields.split(',') : [];

const rpcUrl = 'https://my-sol-endpoint.quiknode.pro/SECRET/'; // Replace with your actual Solana endpoint URL

const connection = new Connection(rpcUrl);

try {
const walletPublicKey = new PublicKey(wallet);

// Fetch all token accounts
const tokenAccounts = await connection.getParsedTokenAccountsByOwner(walletPublicKey, {
programId: TOKEN_PROGRAM_ID,
});

// Filter valid SPL tokens (non-NFT tokens with decimals > 0 and balance > 0)
let totalItems = 0;
const tokenBalances = tokenAccounts.value
.filter(({ account }) => {
const parsedInfo = account.data.parsed.info;
const tokenAmount = new BN(parsedInfo.tokenAmount.amount);
const decimals = parsedInfo.tokenAmount.decimals;
const balanceInUI = tokenAmount.div(new BN(10).pow(new BN(decimals))).toString();
if (decimals > 0 && balanceInUI > 0) { // Exclude NFTs and tokens with exactly 0 balance
totalItems++;
return true;
}
return false;
})
.sort((a, b) => a.pubkey.toBase58().localeCompare(b.pubkey.toBase58()));

const totalPages = Math.ceil(totalItems / perPage);
const startIndex = (page - 1) * perPage;
const endIndex = Math.min(startIndex + perPage, totalItems);
const paginatedBalances = tokenBalances.slice(startIndex, endIndex);

// Map token balances to a simplified structure with metadata
const assets = await Promise.all(paginatedBalances.map(async ({ pubkey, account }) => {
const parsedInfo = account.data.parsed.info;
const tokenAmount = new BN(parsedInfo.tokenAmount.amount);
const decimals = parsedInfo.tokenAmount.decimals;
const uiAmount = tokenAmount.div(new BN(10).pow(new BN(decimals))).toString();

// Fetch token metadata (name and symbol)
const metadata = await fetchTokenMetadata(connection, parsedInfo.mint);

// Create the asset object without owner, chain, and network
const asset = {
tokenAddress: parsedInfo.mint,
balance: uiAmount,
decimals,
name: metadata.name || 'Unknown',
symbol: metadata.symbol || 'Unknown',
};

omitFields.forEach(field => {
delete asset[field];
});

return asset;
}));

return {
owner: wallet,
assets: assets,
totalPages: totalPages,
pageNumber: page,
totalItems: totalItems,
};
} catch (error) {
console.error('Error in fetchTokens:', error);
throw error;
}
}

async function fetchTokenMetadata(connection, mintAddress) {
try {
const mintPublicKey = new PublicKey(mintAddress);
const [metadataPDA] = await PublicKey.findProgramAddress(
[Buffer.from('metadata'), METADATA_PROGRAM_ID.toBuffer(), mintPublicKey.toBuffer()],
METADATA_PROGRAM_ID
);

const accountInfo = await connection.getAccountInfo(metadataPDA);
if (accountInfo) {
return decodeMetadata(accountInfo.data);
}
} catch (error) {
console.error(`Error fetching metadata for token ${mintAddress}:`, error);
}
return { name: null, symbol: null };
}

function decodeMetadata(buffer) {
let offset = 0;

const readString = () => {
const length = buffer.readUInt32LE(offset);
offset += 4;
const str = buffer.slice(offset, offset + length).toString('utf8');
offset += length;
return str;
};

offset += 1; // Skip metadata key
offset += 32; // Skip update authority
offset += 32; // Skip mint
const name = readString();
const symbol = readString();
readString(); // Skip URI

return { name: name.replace(/\0/g, '').trim(), symbol: symbol.replace(/\0/g, '').trim() };
}

function main(params) {
if (!params.user_data || !params.user_data.wallet) {
return {
statusCode: 400,
body: {
jsonrpc: "2.0",
id: 1,
error: { message: 'Wallet address is required' },
},
};
}

return fetchTokens(params)
.then(result => ({
statusCode: 200,
body: {
jsonrpc: "2.0",
id: 1,
result: result,
},
}))
.catch(error => ({
statusCode: 500,
body: {
jsonrpc: "2.0",
id: 1,
error: { message: 'An error occurred while fetching token data', details: error.message },
},
}));
}

module.exports.main = main;

Replace https://my-sol-endpoint.quiknode.pro/SECRET/ one line 14 with your actual Solana mainnet RPC URL.

Request

We will invoke the function with the following cURL command. A few notes:

  • Replace the YOUR_API_KEY with your own QuickNode API key - follow this guide for creating an API key.
  • Replace the FUNCTION_ID with the ID of your Function - you can find this in the URL when viewing your Function in the QuickNode Dashboard.
  • Replace the WALLET_TO_TRACK with the Solana wallet address to fetch NFTs of that wallet.
  • Use the perPage parameter to the number of NFTs per page, default is 20.
  • Use the page parameter to specify page number for pagination, default is 1.
  • Use the block_number parameter to specify the block number you want to analyze. You can also omit this property for the function to run against the latest block.
curl -X POST "https://api.quicknode.com/functions/rest/v1/functions/FUNCTION_ID/call?result_only=true" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"user_data": {
"wallet": "WALLET_TO_TRACK"
}
}'

Response

Resulting in the following response (below is a paginated version of the output):

{
"statusCode": 200,
"body": {
"jsonrpc": "2.0",
"id": 1,
"result": {
"owner": "5BVKvgJB9WQVn7UnS8BHTSaMXF4AhgC4fS4REPkiUTin",
"assets": [
{
"tokenAddress": "4ttNFKRxC3uJVygoRGJWnXTXAphhTwfZJAevouE9QNre",
"balance": "2627218207",
"decimals": 9,
"name": "SunJoker",
"symbol": "SunJoker"
},
{
"tokenAddress": "G2c9VTgtksLKM8nmZjyquyAtT8v5mfatbG3KVoWJ9Rcc",
"balance": "344709",
"decimals": 9,
"name": "Dragon Sun",
"symbol": "Dragon Sun"
},
{
"tokenAddress": "8xkXdGQeeEu45PQEg2xyjft9ygyM46CXaK619dYdU5vg",
"balance": "28661936",
"decimals": 9,
"name": "Cyber Dog",
"symbol": "Cyber Dog"
},
{
"tokenAddress": "5Th8pLSwMYK2gC3DVgnCCjW6SgWm7ufbWUqVDCTFmT2u",
"balance": "27",
"decimals": 6,
"name": "Sponge Party",
"symbol": "RAVE"
},
{
"tokenAddress": "F4iD3WbMm6UitcYFjQwr9nmh7Qrh1R6ShRf5mB9H4ssR",
"balance": "198293",
"decimals": 9,
"name": "Twins",
"symbol": "TWN"
},
{
"tokenAddress": "2PgHDND9o9pP4ebEXZzn7A2R8C5mww6Cz6hSb5D1682A",
"balance": "99128",
"decimals": 9,
"name": "Jirry nanoto",
"symbol": "JIRRY"
},
{
"tokenAddress": "DiGfWHo8w6GfpBxMobHWTPwrLFet4qr1bZ31UgeLetVE",
"balance": "5711",
"decimals": 6,
"name": "SantaPnut",
"symbol": "SPnut"
},
{
"tokenAddress": "26wsG4Zavit8iuHWDwuHsyCrTnZmFCU1khiwhnwxjB93",
"balance": "53509",
"decimals": 6,
"name": "TIMECOIN",
"symbol": "$TIME"
},
{
"tokenAddress": "5QPJmipcsmSbYbBDThpvvfvaqz9vS3C9nLLxX8trx7ei",
"balance": "42",
"decimals": 6,
"name": "jerry",
"symbol": "jerry"
},
{
"tokenAddress": "FyzeX6JLr6QXQ2Y7UqzJnKLixKQrBevx4taH6U8o41yu",
"balance": "594864",
"decimals": 9,
"name": "Shiba Inu on Solana",
"symbol": "SHBS"
},
{
"tokenAddress": "Fhry1c2Wq2NmdNWgJoopjV5Z9Lm1X6hVD1bHNrx7LBRL",
"balance": "16835759",
"decimals": 9,
"name": "B1COIN",
"symbol": "B1COIN"
},
{
"tokenAddress": "2KLqBn63LmG9gU3kdcvQptfsqKYCCsmWijDHeiesun4t",
"balance": "5728334",
"decimals": 9,
"name": "FIRST CITY ON MARS",
"symbol": "TERMINUS"
},
{
"tokenAddress": "DBL4rDYS1aGRzjBxwz9ovJYhY6k5TjUr1Rj2KKNZDUm3",
"balance": "416306",
"decimals": 6,
"name": "GOODDOG",
"symbol": "GoodDog"
},
{
"tokenAddress": "8GMZJuLDq9YoPbhqjdgEhYFrwrUExcMDxeRptH13KaHq",
"balance": "12065081",
"decimals": 9,
"name": "Joe",
"symbol": "Joe"
},
{
"tokenAddress": "9EZqRymdqpG66GQParaEdGcaoG9CjfN7ra2zAX6i1Epn",
"balance": "113421",
"decimals": 9,
"name": "Orto umai",
"symbol": "ORTO"
},
{
"tokenAddress": "M62KYnfTzCyj3mTHyTsx6fYmkAxc3FzFbQeGaQGYdUD",
"balance": "3817150",
"decimals": 9,
"name": "Baby Blue Whale",
"symbol": "BBW"
},
{
"tokenAddress": "ELKSEN9F5zp9ukrgUAQZwRmT87WxZiqLkxzzJkKd2m4j",
"balance": "115622257",
"decimals": 9,
"name": "Feels Good Man",
"symbol": "FGM"
},
{
"tokenAddress": "3kx5XnEtPFVHE99Dn1m3pJHAZ1AjrwK6UNkiyswmviVg",
"balance": "70",
"decimals": 8,
"name": "Tether USD",
"symbol": "USDT"
},
{
"tokenAddress": "BFYpfi6jK6BvC1XhDQ9f8WELpycYxAxT3K6nim9eCSkL",
"balance": "98090",
"decimals": 9,
"name": "Twins",
"symbol": "TWN"
},
{
"tokenAddress": "Fa3EQ9VfWE6gu2sLuo6z4VNScmEwUjZ6mESK3xRVAZX7",
"balance": "3564715",
"decimals": 9,
"name": "Labibu",
"symbol": "LABIBU"
}
],
"totalPages": 13,
"pageNumber": 1,
"totalItems": 253
}
}
}

Learn more about QuickNode Functions.

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this doc