Web3.js: How to Track NFT (ERC-721/1155) Transfers and Mints (+ Specific Address/NFT)

Events are essential to the Ethereum blockchain; they allow us to create interfaces that update dynamically as contracts complete executions. By watching the events in a block, we can track both NFTs transfers and mints in real-time.

Additionally, we can choose only to track specific NFT transactions or NFT transfers from/to a particular Ethereum address. Let’s learn how!

New ERC-712 transaction found in block 15089164 with hash 0xfe645cb621d0fd11ce7f88c6211525339a2c7d48c2f5b3572fcb6caaaf8d6828
From: 0x2A4b4eC07e380398700E47733e42DB8217f480bd
To: 0xD2e48bA114527126eF20a5b21282ed3AeD7c220a
Token contract: 0xdebb2D5f818B53e0732444b31d4EFe4AF887026A
Token ID: 2834

New ERC-1155 transaction found in block 15089239 with hash 0x43b63c7f0eb9df5a279f69338cc3bc0f935a039a821183a2fdd9cc5d736dfc5f
Operator: 0x1E0049783F008A0085193E00003D00cd54003c71
From: 0x5E5BA665bfaacc1E0eEaD7057CE8251298a60439
To: 0x57E283b1BfFf55471B6c78203a08AcC761DC6Ed4
id: 3
value: 1

Setting Up Our Project

Please create a new folder in which we can work on our project, then install web3js using npm.

npm install web3

Ensure you have your Infura account set up and have access to your endpoint URL. Feel free to read more about getting started with Infura.

At the top of our new Javascript file, we can add the following to import the web3js library that we just installed, and connect to the Infura websockets endpoint:

const Web3 = require('web3');
const web3 = new Web3('wss://mainnet.infura.io/ws/v3/<YOUR_PROJECT_ID>');

Make sure to replace YOUR_PROJECT_ID with your actual Infura project ID.

Reading ERC-721 and ERC-1155 Events

NFTs on the Ethereum blockchain typically use ERC-721 or ERC-1155, both of which are standards for deploying Non-Fungible Tokens to the network. Just like ERC-20 is used for fungible tokens, it allows developers to create a standardized smart contract with which other interfaces can easily interact.

ERC-721 is the most used standard to deploy NFTs on the Ethereum chain, with CryptoKitties kicking off its popularity. These days, while most NFTs get deployed with the ERC-721 spec, ERC-1155 is seeing booming usage, especially for game-related Dapps.

While ERC-721 only allows you to create non-fungible tokens, ERC-1155 supports both fungible- and non-fungible tokens. This ability to support both types of tokens would especially be helpful in games, as a currency could be fungible (like silver or gold), and rare items (like armor or accessories) could be collectibles and thus non-fungible.

Subscribing to Contract Events

The web3.eth.subscribe function in web3.js allows us to subscribe to events happening on the blockchain. Smart contracts can emit events during execution and are usually used to ‘announce’ that a contract executed a significant action. In our case, it would be the transfer of an NFT, but other examples of events could also include ERC-20 transfers or a swap happening on a decentralized exchange.

There’s a slight difference in the events that these standards emit. ERC-721 transactions emit the following event:

Transfer (address from, address to, uint256 tokenId)

ERC-1155 transactions emit the following event:

TransferSingle (address operator, address from, address to, uint256 id, uint256 value)

ERC-1155 smart contracts can also emit a TransferBatch event for batch transfers, but we won’t track this event in this tutorial.

In order to tell web3.eth.subscribe which events we should track, we can add the following filters:

let options721 = {
    topics: [
        web3.utils.sha3('Transfer(address,address,uint256)')
    ]
};

let options1155 = {
    topics: [
        web3.utils.sha3('TransferSingle(address,address,address,uint256,uint256)')
    ]
};

Then, initiate the subscriptions using web3.eth.subscribe with the filters we just set:

let subscription721 = web3.eth.subscribe('logs', options721);
let subscription1155 = web3.eth.subscribe('logs', options1155);

Additionally, we can add the following lines to see whether the subscription started successfully or if any errors occurred:

subscription721.on('error', err => { throw err });
subscription1155.on('error', err => { throw err });

subscription721.on('connected', nr => console.log('Subscription on ERC-721 started with ID %s', nr));
subscription1155.on('connected', nr => console.log('Subscription on ERC-1155 started with ID %s', nr));

A Quick Word on Topics and Data

Whenever a smart contract emits an event, its log record will consist of topics and data. Topics contain the event parameters (such as address from, address to, uint256 tokenId), while the data include the actual values (such as the recipient address or the ID of the token that was transferred).

If you’d like a more in-depth overview of how event logs work, take a look at Understanding event logs on the Ethereum blockchain.

Reading ERC-721 Transfers

Let’s create a listener for subscription721 :

subscription721.on('data', event => {
    if (event.topics.length == 4) {
				...
		}
});

We check to see whether the topics array consists of 4 values because ERC-20 transfers also emit a Transfer event but don’t include the last value as a topic but as data, and thus contain one less topic. Hence, we can differentiate between ERC-20 and ERC-721 by checking the number of topics.

By printing event, we get to see the transaction details, including the topics that contain the ERC-721 transfer details:

{
		removed: false,
    logIndex: 130,
    transactionIndex: 80,
    transactionHash: '0x14a22d9fadfad60884a7066d8d2ea45ee8cef8601b35b40585e8f60305c792dd',
    blockHash: '0x73d868c1e928f1707186e2e6ad1fb2ddb0db4d211d072c2f20c20de64b13b01e',
    blockNumber: 15101921,
    address: '0x19c158A6B5508468117F94FE5707eCCEb9afEC87',
    data: '0x',
    topics: [
		    '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
        '0x0000000000000000000000000000000000000000000000000000000000000000',
        '0x000000000000000000000000dab3c7e6a91d18b30415f21baf4c02279dfbf522',
        '0x0000000000000000000000000000000000000000000000000000000000000444'
    ],
    id: 'log_8d5117a5'
}

As we can’t read the event logs on its own, we have to decode it with the ERC-721 ABI.

let transaction = web3.eth.abi.decodeLog([{
            type: 'address',
            name: 'from',
            indexed: true
        }, {
            type: 'address',
            name: 'to',
            indexed: true
        }, {
            type: 'uint256',
            name: 'tokenId',
            indexed: true
        }],
            event.data,
            [event.topics[1], event.topics[2], event.topics[3]]);

We can now directly call from, to, and tokenId on transaction:

console.log(`\n` +
            `New ERC-712 transaction found in block ${event.blockNumber} with hash ${event.transactionHash}\n` +
            `From: ${(transaction.from === '0x0000000000000000000000000000000000000000') ? 'New mint!' : transaction.from}\n` +
            `To: ${transaction.to}\n` +
            `Token contract: ${event.address}\n` +
            `Token ID: ${transaction.tokenId}`
);

Note: If a transaction contains a new mint, its from parameter will be 0x0000000000000000000000000000000000000000.

This will result in the following getting logged to our terminal upon running the script, whenever a new block is found with ERC-721 transfers:

New ERC-712 transaction found in block 15102209 with hash 0x3b133c1ad2d138bee9a596d94da25892e12a2c95efd1f0916d6708a9b86745b0
From: 0xDd3c42eb2660c0C7745E48f25864ff743Fef9f33
To: 0x4c5Ca726584d9b171AE9D6ce67Ab8AFb706259CB
Token contract: 0x35f3b8f37e9341F289398b70Fa2c576Dd102DF75
Token ID: 950

Reading ERC-1155 Transfers

Tracking ERC-1155 transfers work more or less the same as ERC-721; let’s create a listener first:

subscription1155.on('data', event => {
		...
});

This time, we won’t have to check the number of topics as only ERC-1155 uses the TransferSingle event.

Again, as we can’t read the topics on its own, we need to decode it with the ERC-1155 ABI. ERC-1155 has some additional parameters, so we can define the following ABI:

let transaction = web3.eth.abi.decodeLog([{
        type: 'address',
        name: 'operator',
        indexed: true
    }, {
        type: 'address',
        name: 'from',
        indexed: true
    }, {
        type: 'address',
        name: 'to',
        indexed: true
    }, {
        type: 'uint256',
        name: 'id'
    }, {
        type: 'uint256',
        name: 'value'
    }],
        event.data,
        [event.topics[1], event.topics[2], event.topics[3]]);

We now have access to operator, from, to, id, and value. Let’s write those to our terminal as well:

console.log(`\n` +
        `New ERC-1155 transaction found in block ${event.blockNumber} with hash ${event.transactionHash}\n` +
        `Operator: ${transaction.operator}\n` +
        `From: ${(transaction.from === '0x0000000000000000000000000000000000000000') ? 'New mint!' : transaction.from}\n` +
        `To: ${transaction.to}\n` +
        `id: ${transaction.id}\n` +
        `value: ${transaction.value}`
    );

Resulting in the following in our terminal:

New ERC-1155 transaction found in block 15102209 with hash 0xa08afd7696ec7424f8b403ca3733ebbb916faf68442757e2e9349c2d1b90aa9a
Operator: 0x20251a0505Ead51fb2C6ce5c1f399924ea068322
From: New mint!
To: 0x20251a0505Ead51fb2C6ce5c1f399924ea068322
id: 2
value: 1

Tracking a Specific Address

We can directly read the sender and recipient of an NFT transaction by calling from and to on the transaction object. If you wish, you can add the following in either listeners we’ve just created to track a sender sending any NFT:

if (transaction.from == '0x495f947276749ce646f68ac8c248420045cb7b5e') { console.log('Specified address sent an NFT!') };

And to track a specific recipient receiving any NFT:

if (transaction.to == '0x495f947276749ce646f68ac8c248420045cb7b5e') { console.log('Specified address received an NFT!') };

Tracking a Specific NFT

It’s also possible to track a specific NFT being transferred, regardless to/from which address. To track an ERC-721 NFT, you can filter the ERC-721 contract address as event.address, and then filtering on a specific transaction.tokenId:

if (event.address == '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' && transaction.tokenId == 2500) { console.log('Specified ERC-721 NFT was transferred!') };

For ERC-1155 transactions, you can filter for a specific NFT by checking the transaction.id:

if (event.address == '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' && transaction.id == 2500) { console.log('Specified ERC-1155 NFT was transferred!') };

ERC-1155 transfers can additionally also contain a value parameter.

Complete Code Overview

const Web3 = require('web3');
const web3 = new Web3('wss://mainnet.infura.io/ws/v3/<YOUR_PROJECT_ID>');

let options721 = {
    topics: [
        web3.utils.sha3('Transfer(address,address,uint256)')
    ]
};

let options1155 = {
    topics: [
        web3.utils.sha3('TransferSingle(address,address,address,uint256,uint256)')
    ]
};

let subscription721 = web3.eth.subscribe('logs', options721);
let subscription1155 = web3.eth.subscribe('logs', options1155);

subscription721.on('data', event => {
    if (event.topics.length == 4) {
        let transaction = web3.eth.abi.decodeLog([{
            type: 'address',
            name: 'from',
            indexed: true
        }, {
            type: 'address',
            name: 'to',
            indexed: true
        }, {
            type: 'uint256',
            name: 'tokenId',
            indexed: true
        }],
            event.data,
            [event.topics[1], event.topics[2], event.topics[3]]);

        if (transaction.from == '0x495f947276749ce646f68ac8c248420045cb7b5e') { console.log('Specified address sent an NFT!') };
        if (transaction.to == '0x495f947276749ce646f68ac8c248420045cb7b5e') { console.log('Specified address received an NFT!') };
        if (event.address == '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D' && transaction.tokenId == 2500) { console.log('Specified NFT was transferred!') };

        console.log(`\n` +
            `New ERC-712 transaction found in block ${event.blockNumber} with hash ${event.transactionHash}\n` +
            `From: ${(transaction.from === '0x0000000000000000000000000000000000000000') ? 'New mint!' : transaction.from}\n` +
            `To: ${transaction.to}\n` +
            `Token contract: ${event.address}\n` +
            `Token ID: ${transaction.tokenId}`
        );
    }
})

subscription1155.on('data', event => {
    let transaction = web3.eth.abi.decodeLog([{
        type: 'address',
        name: 'operator',
        indexed: true
    }, {
        type: 'address',
        name: 'from',
        indexed: true
    }, {
        type: 'address',
        name: 'to',
        indexed: true
    }, {
        type: 'uint256',
        name: 'id'
    }, {
        type: 'uint256',
        name: 'value'
    }],
        event.data,
        [event.topics[1], event.topics[2], event.topics[3]]);

    console.log(`\n` +
        `New ERC-1155 transaction found in block ${event.blockNumber} with hash ${event.transactionHash}\n` +
        `Operator: ${transaction.operator}\n` +
        `From: ${(transaction.from === '0x0000000000000000000000000000000000000000') ? 'New mint!' : transaction.from}\n` +
        `To: ${transaction.to}\n` +
        `id: ${transaction.id}\n` +
        `value: ${transaction.value}`
    );
})

subscription721.on('error', err => { throw err });
subscription1155.on('error', err => { throw err });

subscription721.on('connected', nr => console.log('Subscription on ERC-721 started with ID %s', nr));
subscription1155.on('connected', nr => console.log('Subscription on ERC-1155 started with ID %s', nr));

Additional Resources

Understanding event logs on the Ethereum blockchain

Web3js docs: web3.eth.subscribe

Web3js docs: web3.eth.abi.decodeLog

7 Likes

web3.js just released a TypeScript rewrite of the library, version 4.x. Here is the slightly modified version of this code that works with web3.js’ latest release

1 Like