Overview
NFTs are crucial part of any blockchain ecosystem be it for ownership, art or collection. On the Solana blockchain, Non-Fungible Tokens (NFTs) represent a vibrant and dynamic digital asset landscape that goes far beyond simple digital art.
In this Function example, we will fetch and analyze NFTs from the Solana Mainnet blockchain for a given wallet address.
Sample Function
The following is the code for the Function to get NFT data in JavaScript Node.js v20 runtime:
// Initialize the main function
const { Connection, PublicKey } = require('@solana/web3.js');
const axios = require('axios');
const BN = require('bn.js');
const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');
function cleanString(str) {
if (typeof str !== 'string') return '';
return str.replace(/\0/g, '').trim();
}
async function fetchNFTs(params) {
const wallet = params.user_data.wallet;
const network = params.user_data['x-qn-network'] || 'mainnet-beta';
const chain = params.user_data['x-qn-chain'] || 'SOL';
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
});
// Count total NFTs and sort token accounts
let totalItems = 0;
const sortedTokenAccounts = tokenAccounts.value
.filter(({ account }) => {
const parsedInfo = account.data.parsed.info;
const amount = new BN(parsedInfo.tokenAmount.amount);
const decimals = parsedInfo.tokenAmount.decimals;
if ((amount.eq(new BN(1)) || amount.eq(new BN(0))) && decimals === 0) {
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 paginatedAccounts = sortedTokenAccounts.slice(startIndex, endIndex);
const nftMetadata = await Promise.all(paginatedAccounts.map(async ({ account }) => {
const parsedInfo = account.data.parsed.info;
const mintAddress = new PublicKey(parsedInfo.mint);
return await fetchNFTMetadata(connection, mintAddress);
}));
const assets = await Promise.all(nftMetadata.filter(Boolean).map(async (metadata) => {
const provenance = await fetchProvenance(connection, new PublicKey(metadata.mint));
const uri = cleanString(metadata.data.uri);
const externalMetadata = await fetchExternalMetadata(uri);
const asset = {
name: cleanString(metadata.data.name),
collectionName: metadata.collection?.name ? cleanString(metadata.collection.name) : "Unknown",
tokenAddress: metadata.mint,
collectionAddress: metadata.collection?.key || '',
imageUrl: cleanString(metadata.data.uri),
traits: externalMetadata.attributes || [],
chain: chain,
creators: metadata.data.creators ? metadata.data.creators.map(creator => ({
address: creator.address,
verified: creator.verified,
share: creator.share
})) : [],
network: network,
description: cleanString(externalMetadata.description),
symbol: cleanString(metadata.data.symbol),
external_url: cleanString(externalMetadata.external_url),
properties: externalMetadata.properties || [],
provenance: provenance,
tokenType: metadata.tokenStandard || '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 fetchNFTs:', error);
throw error;
}
}
async function fetchNFTMetadata(connection, mintAddress) {
try {
const [metadataPDA] = await PublicKey.findProgramAddress(
[Buffer.from('metadata'), METADATA_PROGRAM_ID.toBuffer(), mintAddress.toBuffer()],
METADATA_PROGRAM_ID
);
const accountInfo = await connection.getAccountInfo(metadataPDA);
if (accountInfo) {
const metadata = decodeMetadata(accountInfo.data);
return {
...metadata,
mint: mintAddress.toBase58()
};
}
} catch (error) {
console.error(`Error fetching metadata for ${mintAddress.toBase58()}:`, error);
}
return 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;
};
const key = buffer.readUInt8(0);
offset = 1;
const updateAuthority = new PublicKey(buffer.slice(offset, offset + 32)).toBase58();
offset += 32;
const mint = new PublicKey(buffer.slice(offset, offset + 32)).toBase58();
offset += 32;
const name = readString();
const symbol = readString();
const uri = readString();
const sellerFeeBasisPoints = buffer.readUInt16LE(offset);
offset += 2;
let creators = null;
if (buffer[offset] === 1) {
offset += 1;
const creatorCount = buffer.readUInt32LE(offset);
offset += 4;
creators = [];
for (let i = 0; i < creatorCount; i++) {
const creator = new PublicKey(buffer.slice(offset, offset + 32));
offset += 32;
const verified = buffer[offset] === 1;
offset += 1;
const share = buffer[offset];
offset += 1;
creators.push({ address: creator.toBase58(), verified, share });
}
} else {
offset += 1;
}
let collection = null;
if (offset < buffer.length && buffer[offset] === 1) {
offset += 1;
collection = {
verified: buffer[offset] === 1,
key: new PublicKey(buffer.slice(offset + 1, offset + 33)).toBase58()
};
offset += 33;
}
let uses = null;
if (offset < buffer.length && buffer[offset] === 1) {
offset += 1;
uses = {
useMethod: buffer.readUInt8(offset),
remaining: buffer.readBigUInt64LE(offset + 1),
total: buffer.readBigUInt64LE(offset + 9)
};
offset += 17;
}
return {
key,
updateAuthority,
mint,
data: {
name,
symbol,
uri,
sellerFeeBasisPoints,
creators,
collection,
uses
},
primarySaleHappened: buffer[offset] === 1,
isMutable: buffer[offset + 1] === 1
};
}
async function fetchProvenance(connection, mintAddress) {
const signatures = await connection.getSignaturesForAddress(mintAddress, { limit: 100 });
const provenance = [];
for (const sig of signatures) {
try {
const tx = await connection.getTransaction(sig.signature, {
maxSupportedTransactionVersion: 1
});
if (tx) {
provenance.push({
txHash: sig.signature,
blockNumber: tx.slot,
date: new Date(tx.blockTime * 1000).toISOString()
});
}
} catch (error) {
console.error(`Error fetching transaction ${sig.signature}:`, error);
}
}
return provenance;
}
async function fetchExternalMetadata(uri) {
try {
const response = await axios.get(uri);
return response.data;
} catch (error) {
console.error('Error fetching external metadata:', error);
return {};
}
}
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 fetchNFTs(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 NFT data', details: error.message }
}
}));
}
module.exports.main = main;
Replace https://my-sol-endpoint.quiknode.pro/SECRET/ one line 22 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:
{
"statusCode": 200,
"body": {
"jsonrpc": "2.0",
"id": 1,
"result": {
"owner": "9erdVynZfBRsxdoQXPKbxZ8oKCP98sYKQf4Bbi8ADYpL",
"assets": [
{
"name": "Key2 Silver Las Vegas",
"collectionName": "Unknown",
"tokenAddress": "96P5B6zUMWiyCrDNFQxfiJ1hTCKEeNCBTTrpoUNKZCiX",
"collectionAddress": "",
"imageUrl": "https://arweave.net/_jUIzw9j-pBFjioFjcsnM0hC6CNBfqs7R1PKAJYfCb8",
"traits": [
{
"trait_type": "Level",
"value": "Silver"
},
{
"trait_type": "Branch",
"value": "Las Vegas"
},
{
"trait_type": "Serial number",
"value": "16652"
},
{
"trait_type": "Created",
"value": "1713014510068",
"display_type": "date"
}
],
"chain": "SOL",
"creators": [
{
"address": "key2ioV2TtEGS6so624PyH4pgULrd7swXDxnP4nTBSF",
"verified": true,
"share": 100
}
],
"network": "mainnet-beta",
"description": "Unlock Vegas with the Silver Key! Navigate Sin City like a true local, gaining access to the best experiences Las Vegas has to offer. What Happens in Vegas Stays in Vegas.",
"symbol": "K2-S-LAS",
"external_url": "https://key2.io/r/K2-S-LAS?m=96P5B6zUMWiyCrDNFQxfiJ1hTCKEeNCBTTrpoUNKZCiX",
"properties": {
"files": [
{
"type": "image/png",
"uri": "https://arweave.net/48c2abpaaZcqpA3aaiGGOyWpCtkYzaggrsTyrLOwwOU"
}
]
},
"provenance": [
{
"txHash": "37MuQcpr21ZyHNneMWMhASLWj5FrWnqXAGWHZ2irfSkJ9Tn2xjRZAUS4Dvi18bceSLYppw8nW7XeRDjwfKV6P8ro",
"blockNumber": 259901676,
"date": "2024-04-13T13:22:26.000Z"
}
],
"tokenType": "Unknown"
}
],
"totalPages": 1,
"pageNumber": 1,
"totalItems": 1
}
}
}
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.