<- Articles
How to take a snapshot of all existing holders of your ERC-721 NFT collection
The simple 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", * } */
A more 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 try the snapshot tool (built with the code above) here: ERC-721 Snapshot