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.