Back to blog

Find And Backup Top NFT Collections On IPFS
NFTs are an interesting form factor in the sense that unlock traditional art, anyone can preserve the underlying assets. Usually. As long as the NFT metadata and assets are stored on IPFS, people can find this information and pin the files to IPFS themselves, increasing resiliency.
Today, we’re going to write a script that queries the top 1000 NFT collections all time based on volume. The script will find all the Ethereum NFTs, then it will find the contract addresses, the token URIs, and finally (if the tokenURI is on IPFS) it will pin the file to Pinata.
Prerequisites
- Node.js and npm installed
- Ethereum provider API url (I’ll use Alchemy)
- An OpenSea API key
- A free Pinata account
- API Key JWT
- Gateway URL
- Gateway Access Token
To get your Pinata API key, sign up for your account, then follow this guide to create your API key. You’ll need the JWT portion of the key output. You’ll need your account’s Dedicated IPFS Gateway URL as well. You can find that by going to the Gateways page. copy the URL and put it into your .env
file.
You will need to fetch files from the IPFS network that are not stored on your Pinata account. To do that, you must create a gateway access token for your gateway. Follow this guide to do so. Once created, copy that key and add it to your .env
.
To get your OpenSea API key, follow these docs.
Project Setup
Now, let's set up our project. From your command line, run the following commands:
mkdir nft-backup
cd nft-backup
npm init -y
npm install pinata ethers dotenv @pinata/ipfs-gateway-tools
Create a .env
file to store your API keys:
PINATA_JWT=your_pinata_jwt_here
PINATA_GATEWAY_URL=your_pinata_gateway_url
PINATA_GATEWAY_TOKEN=your_pinata_gateway_token
ETH_PROVIDER_URL=your_ethereum_provider_url_here
OPENSEA_API_KEY=your_opensea_api_key
Project Structure
Our streamlined project structure:
nft-backup/
│
├── .env # Environment variables
├── package.json # Project dependencies
└── nft-backup.js # Main script for NFT backup
The NFT Backup Script
Let's create a single, focused script that handles everything:
// nft-backup.js
require("dotenv").config();
const { ethers } = require("ethers");
const { PinataSDK } = require("pinata");
const fs = require("fs");
// Initialize Ethereum provider
const provider = new ethers.providers.JsonRpcProvider(
process.env.ETH_PROVIDER_URL
);
// Initialize Pinata SDK
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT,
});
const gatewayTools = new IPFSGatewayTools();
// ERC-721 and ERC-1155 interface fragments we need
const ERC721_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function tokenURI(uint256 tokenId) view returns (string)",
"function totalSupply() view returns (uint256)",
];
const ERC1155_ABI = ["function uri(uint256 id) view returns (string)"];
function extractIPFSCID(uri) {
if (!uri) return null;
const details = gatewayTools.containsCID(uri);
if (details.containsCid) {
return details.cid;
}
return null;
}
async function getTokenURI(contract, tokenId) {
try {
// Different contracts use different functions to get the token URI
if (contract.standard === "ERC721") {
return await contract.tokenURI(tokenId);
} else if (contract.standard === "ERC1155") {
return await contract.uri(tokenId);
}
return null;
} catch (error) {
console.error(
`Error getting token URI for token ${tokenId}: ${error.message}`
);
return null;
}
}
async function fetchMetadata(cid) {
try {
const result = await pinata.gateways.public.get(`${cid}?pinataGatewayToken=${process.env.GATEWAY_TOKEN}`);
const json = result.data;
if(json) {
return json;
}
return null;
} catch (error) {
console.error(`Error fetching metadata from ${tokenURI}: ${error.message}`);
return null;
}
}
function extractCIDsFromMetadata(metadata) {
if (!metadata) return [];
const cids = new Set();
const keys = Object.from(metadata);
for(const key of keys) {
const cid = extractIPFSCID(metadata[key]);
if(cid) {
cids.add(cid);
}
}
return Array.from(cids);
}
async function pinCIDToPinata(cid, metadata) {
try {
console.log(`Pinning CID: ${cid}`);
const result = await pinata.upload.public
.cid(cid)
.name(metadata.name || `NFT Backup: ${cid}`)
.keyvalues({
source: "nft-backup-script",
collection: metadata.collection || "Unknown",
backupDate: new Date().toISOString(),
...metadata.custom,
});
console.log(`Successfully pinned ${cid}`);
return { cid, success: true, result };
} catch (error) {
console.error(`Error pinning CID ${cid}: ${error.message}`);
return { cid, success: false, error: error.message };
}
}
async function fetchTopNFTCollections() {
const apiKey = process.env.OPENSEA_API_KEY;
const url =
"<https://api.opensea.io/api/v2/collections?chain_identifier=ethereum&order_by=volume&order_direction=desc&limit=20>";
try {
const response = await fetch(url, {
method: "GET",
headers: {
"X-API-KEY": apiKey,
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
// Transform the data into the required format
const formattedCollections = data.collections.map((collection) => {
return {
name: collection.name,
address: collection.primary_asset_contracts[0]?.address || "N/A",
chainId: 1, // Ethereum
standard:
collection.primary_asset_contracts[0]?.schema_name || "ERC721",
};
});
// Output the formatted collection data
console.log(JSON.stringify(formattedCollections, null, 2));
return formattedCollections;
} catch (error) {
console.error("Error fetching NFT collections:", error);
return null;
}
}
async function backupNFTsToPinata() {
console.log("Starting NFT backup process...");
const results = {
collections: [],
totalCIDsFound: 0,
totalCIDsPinned: 0,
errors: [],
};
const topNftContracts = await fetchTopNFTCollections();
for (const nftProject of topNftContracts) {
console.log(`\\nProcessing collection: ${nftProject.name}`);
const collectionResult = {
name: nftProject.name,
address: nftProject.address,
cids: [],
pinResults: [],
};
try {
// Initialize contract based on standard
const contractABI =
nftProject.standard === "ERC721" ? ERC721_ABI : ERC1155_ABI;
const contract = new ethers.Contract(
nftProject.address,
contractABI,
provider
);
// We will only check tokenId 1
const tokenId = 1;
// Get token URI and extract IPFS CID
console.log(`Checking token ID: ${tokenId}`);
// Get token URI
const tokenURI = await getTokenURI(contract, tokenId);
if (!tokenURI) {
console.log(`No token URI found for token ${tokenId}`);
continue;
}
console.log(`Token URI: ${tokenURI}`);
// Check if the token URI itself is on IPFS
const tokenURICID = extractIPFSCID(tokenURI);
if (tokenURICID) {
console.log(`Found IPFS CID in token URI: ${tokenURICID}`);
collectionResult.cids.push(tokenURICID);
// Fetch metadata to check for more IPFS content
const metadata = await fetchMetadata(tokenURICID);
if (metadata) {
const metadataCIDs = extractCIDsFromMetadata(metadata);
console.log(`Found ${metadataCIDs.length} IPFS CIDs in metadata`);
collectionResult.cids.push(...metadataCIDs);
}
// Remove duplicates
collectionResult.cids = [...new Set(collectionResult.cids)];
console.log(
`Found ${collectionResult.cids.length} unique IPFS CIDs for ${nftProject.name}`
);
// Pin each CID to Pinata
for (const cid of collectionResult.cids) {
const pinResult = await pinCIDToPinata(cid, {
collection: nftProject.name,
name: `${nftProject.name} - ${cid}`,
custom: {
contractAddress: nftProject.address,
chainId: nftProject.chainId,
},
});
collectionResult.pinResults.push(pinResult);
if (pinResult.success) {
results.totalCIDsPinned++;
}
// Add delay between pins to avoid rate limiting
await new Promise((resolve) => setTimeout(resolve, 1000));
}
results.totalCIDsFound += collectionResult.cids.length;
} else {
console.log("Metadata not on IPFS")
}
} catch (error) {
console.error(
`Error processing collection ${nftProject.name}: ${error.message}`
);
results.errors.push({
collection: nftProject.name,
error: error.message,
});
}
results.collections.push(collectionResult);
}
// Save results to file
fs.writeFileSync("./backup-results.json", JSON.stringify(results, null, 2));
console.log("\\nNFT Backup Summary:");
console.log(`- Collections processed: ${results.collections.length}`);
console.log(`- Total CIDs found: ${results.totalCIDsFound}`);
console.log(`- Successfully pinned: ${results.totalCIDsPinned}`);
console.log(`- Errors: ${results.errors.length}`);
console.log("Full results saved to backup-results.json");
}
// Run the backup process
backupNFTsToPinata()
.then(() => console.log("Backup process completed"))
.catch((error) => {
console.error(`Backup process failed: ${error.message}`);
process.exit(1);
});
This is a lot, so let’s walk through how this script works in detail.
We start with a list of top NFT projects, including their names, contract addresses, and standards (ERC721 or ERC1155). For each contract, we fetch the token URIs for token id number. These URIs point to the metadata for each NFT.
Next, we check if the token URIs themselves are on IPFS. We also fetch the metadata and look for IPFS content within it (like images and animations).
Finally, all discovered IPFS content is pinned to Pinata with helpful metadata.
Here are some of the key functions in this script:
extractIPFSCID()
: Detects if a URI is an IPFS link and extracts the CIDgetTokenURI()
: Fetches token URI from the smart contractfetchMetadata()
: Retrieves the metadata JSON from a token URIextractCIDsFromMetadata()
: Analyzes metadata to find all IPFS contentpinCIDToPinata()
: Pins content to Pinata with proper metadata
This script is pretty basic, but there’s a lot you can do to improve and extend it. For example, you could add more chains to query. We focused on Ethereum mainnet only, but maybe you want to check on other chains OpenSea supports.
Conclusion
NFTs are artifacts that those who own them tend to want to preserve. Others may want to preserve these artifacts as well. This script and tutorial walks you through how to find NFTs on IPFS from the top collections and pin them on IPFS using your own Pinata account which helps ensure long term reliability and resiliency of these NFTs.
Happy Pinning!