Back to blog

Find And Backup Top NFT Collections On IPFS

Find And Backup Top NFT Collections On IPFS

Justin Hunter

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 CID
  • getTokenURI(): Fetches token URI from the smart contract
  • fetchMetadata(): Retrieves the metadata JSON from a token URI
  • extractCIDsFromMetadata(): Analyzes metadata to find all IPFS content
  • pinCIDToPinata(): 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!

Subscribe to paid plan image

Share this post:

Stay up to date

Join our newsletter for the latest stories & product updates from the Pinata community.