Given a contract, I want to map the holders to their token ids.

For example, the contract 0xFBeef911Dc5821886e1dda71586d90eD28174B7d is an ERC-721 contract for KnownOriginDigitalAsset. Etherscan offers a nice interface that maps the holders with the amount of tokens they own. I'd like to create something similar, but instead of the total tokens owned, I want to show the list of tokens.

Each time an ERC-721 token is transferred, it triggers a Transfer event. A first approach is to listen to all past and future Transfer events and use them to create and update my map of holders to token ids.

I synchronized my node running parity with the default options (note that the default pruning strategy is fast). Then, I run the following script

const Web3 = require('web3')
const web3 = new Web3(new Web3.providers.WebsocketProvider('ws://localhost:8546/'))
const erc721abi = require('./ERC721.json')
const contractAddress = '0xf26A23019b4699068bb54457f32dAFCF22A9D371'
const contract = new web3.eth.Contract(erc721abi, contractAddress)
const acc = []
  {fromBlock: 0, toBlock: 'latest'},
  (err, data) => !err && acc.push(data)

At the time of writing, the script emits 31 Transfer events, while Etherscan states there are 81 holders. (Note: minting a token emits a Transfer event as well.)

Is this the correct approach? Should I synchronize the blockchain using parity --purge=archive to see all events?

  • This seems related: github.com/paritytech/parity-ethereum/issues/9188 Nov 2, 2018 at 9:59
  • Thanks @SimondelaRouviere. Yes, it should be related. At the beginning I thought that warp would sync all data, but his is not true. Plus, there are issues with different versions of Parity (I went from beta, to stable, to another beta, and it's still not able to fully sync)
    – vrde
    Nov 2, 2018 at 12:46

1 Answer 1



We are considering how to query the Ethereum blockchain using the Web3 JS API to collect events, summarizing this data to find the most recent recipient of each ERC-721 token.

Running your own infrastructure (Geth / Parity) is outside the scope of this question.


This answer uses Node.js. First, create this package.json:

  "dependencies": {
    "@0xcert/ethereum-erc721": "^2.0.0-rc1",
    "web3": "^1.0.0-beta.37"

This includes web3js and the ABI for ERC-721 from 0xcert, the reference implementation.

Run npm install.

Next, create query.js:

const CONTRACT_ACCOUNT = "0xE9e3F9cfc1A64DFca53614a0182CFAD56c10624F";
const CONTRACT_START = 6645906;
const INFURA_KEY = "55397e793412497fb349e0ff77f154f2";

const Web3 = require('web3'); // Use [email protected]+ https://github.com/ethereum/web3.js/issues/1916
const web3 = new Web3(new Web3.providers.HttpProvider('https://mainnet.infura.io/v3/' + INFURA_KEY));
const erc721 = require("@0xcert/ethereum-erc721/build/erc721.json").ERC721;
const contract = new web3.eth.Contract(erc721.abi, CONTRACT_ACCOUNT);
var idToOwner = {};

contract.getPastEvents('Transfer', {fromBlock: CONTRACT_START, toBlock: CONTRACT_START+60000}).then(events => {
  events.forEach(event => {
    idToOwner[event.returnValues._tokenId] = event.returnValues._to

This is a testing key for Infura, you're welcome. Or to Infura, I'm sorry.


Here's a few notes I learned working on this answer. All of these are bugs/intricacies that apply to the Infura service, but they may also apply to other services:

  • Using CONTRACT_START rather than starting at block zero halves the runtime.
  • If at all possible, consider using adding a filter: {from: 0x...} parameter. The is a speedup of 10x for the same data.
  • The request must be broken into 60,000 blocks at a time and then repeated to avoid timing out.

The contract address below is for Su Squares. Luckily, we are able to use a from: filter for our use case and this allows us to query Infura all in one pass.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.