How to take a snapshot of an ERC721 collection

In this guide, you will learn how to take a snapshot of all the holders of a specific ERC-721 collection. This method is often used before airdropping NFT rewards.

The straightforward approach

Using the thirdweb SDK v5, we can call the getAllOwners method to return all the NFT token in circulation.

import { getAllOwners } from "thirdweb/extensions/erc721";
import { getContract } from "thirdweb";

const yourErc721Contract = getContract({
  address: "0x...,
  client: ... // thirdweb client,
  chain: ...
});

const data = await getAllOwners({
  contract: yourErc721Contract,
  start: 0, // note that you can custom this value if your contract starts at tokenId=1, for example
});

The result will look like this:

console.log(data[0]);

/* Result
 * {
 *   tokenId: 0n, // this is a bigint. you should convert this to string
 *   owner: "owner-address-of-this-token-id",
 * }
 */

An advanced approach

While using getAllOwners provides you the data that you need in a few lines of code, keep in mind that if your collection has 10,000 items, you are making 10,000 RPC requests at once. This might result in failed requests leading to incorrect data if the RPC service you are using has a limit of how many calls you can make in a second.

A solution is to use a multicall library to batch those requests. However using multicall is beyond the scope of this article. You can try the workaround below as a solution to "queue" all the requests into multiple smaller batches.

const getAllErc721TokenIds = async (
  contract: Readonly<ContractOptions<[]>>
): Promise<bigint[]> => {
  const options = {
    contract,
  };
  const [startTokenId_, maxSupply] = await Promise.allSettled([
    startTokenId(options),
    nextTokenIdToMint(options),
    totalSupply(options),
  ]).then(([_startTokenId, _next, _total]) => {
    // default to 0 if startTokenId is not available
    const startTokenId__ =
      _startTokenId.status === "fulfilled" ? _startTokenId.value : 0n;
    let maxSupply_: bigint;
    // prioritize nextTokenIdToMint
    if (_next.status === "fulfilled") {
      // because we always default the startTokenId to 0 we can safely just always subtract here
      maxSupply_ = _next.value - startTokenId__;
    }
    // otherwise use totalSupply
    else if (_total.status === "fulfilled") {
      maxSupply_ = _total.value;
    } else {
      throw new Error(
        "Contract requires either `nextTokenIdToMint` or `totalSupply` function available to determine the next token ID to mint"
      );
    }
    return [startTokenId__, maxSupply_] as const;
  });

  const maxId = maxSupply + startTokenId_;
  const allTokenIds: bigint[] = [];
  for (let i = startTokenId_; i < maxId; i++) {
    allTokenIds.push(i);
  }

  return allTokenIds;
};

...

const allTokenIds = await getAllErc721TokenIds(contract);
const chunkSize = 100; // RPC limit
const chunkedArrays: bigint[][] = [];
let _allOwners: string[] = [];
for (let i = 0; i < allTokenIds.length; i += chunkSize) {
  const chunk = allTokenIds.slice(i, i + chunkSize);
  chunkedArrays.push(chunk);
}
for (let i = 0; i < chunkedArrays.length; i++) {
  const data = await Promise.all(
    chunkedArrays[i].map((tokenId) =>
      ownerOf({ contract, tokenId: tokenId }).catch(() => ADDRESS_ZERO)
    )
  );
  _allOwners = _allOwners.concat(data);
}

type TokenOwner = {
  tokenId: bigint;
  owner: string;
}

const _data: TokenOwner[] = _allOwners.map((owner, index) => ({
  owner,
  tokenId: allTokenIds[index],
}));

 

You can test the snapshot tool, created using the code above, at this link: ERC-721 Snapshot

 

Can’t get this working? If you’ve followed the above and still have issues, contact our support team for help.

 
Did this answer your question?
😞
😐
🤩