Web3JS – Fix Decoding Logs of Approval Event Error

erc-721eventslogsweb3js

I am trying to decode the Approval event of an ERC721 contract.
I fetch the logs for the contract with the alchemy API and then try to decode them
with web3js.
The code looks like this:

const logs = await alchemy.core.getLogs({
  address: contractAddress,
  topics: ["0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"],
  fromBlock: firstNumber,
  toBlock: secondNumber
})


for(let key in logs){
  const data = logs[key].data
  const topics = logs[key].topics

let result = web3.eth.abi.decodeLog(
  [
    {type: 'address', name: 'owner', indexed: true}, 
    {type: 'address', name:'approved', indexed: true},
    {type: 'uint256', name:'tokenId', indexed: true},
  ],
  [data],
  topics
)}

This results in the following error:

Uncaught (in promise) null: value out of range (argument="value", value=20, code=INVALID_ARGUMENT, version=bytes/5.7.0)

I tried to use toString() on the data and the topic before, as it worked with decoding other events, but this resulted in this error:

Uncaught (in promise) Error: invalid arrayify value (argument="value", value="{topic values}", code=INVALID_ARGUMENT, version=bytes/5.7.0

Looking at the data and topics I get from the logs, I noticed that data always is just 0x. The topics are the actual value.
Looking up the transactions on etherscan I also noticed that the data value is also 0x so that seems correct?

What am I doing wrong?

Best Answer

dev advocate at Chainstack here.

When you retrieve the logs, you receive an array of different elements. Let's take the Approval event from an ERC-721 contract as an example since it's what you need.

In this case, this is how it looks in the smart contract:

event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

As you can see, this event has 3 indexed parameters, which means that those values will be returned into the topics array in the logs object. You can only mark up to 3 elements as indexed; the non-indexed parameters will be returned in a separate array named data.

You ask: Looking at the data and topics I get from the logs, I noticed that data always is just 0x. The topics are the actual value. Looking up the transactions on etherscan I also noticed that the data value is also 0x, so that seems correct?

This is the case because all parameters are indexed in the Approval event, so the data array is returned empty as 0x.

A typical topics array for the approval event looks like this:

topics: [
      '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', // Event signature
      '0x000000000000000000000000f9cc4b93728328ccfce3cfe446d4c52d1571aa20', // Owner address
      '0x0000000000000000000000000000000000000000000000000000000000000000', // Approved address
      '0x000000000000000000000000000000000000000000000000000000000000043c' // Token ID
    ],

As you can see, there are 4 elements because the first element is the event signature (like you have in your code). The last 3 elements correspond to what's returned by the event:

  • Address owner
  • Address approved
  • Token ID

These elements are encoded as 32-byte size, and to take the addresses, you have to take the last 20 bytes; in this case, the last 20 bytes equal to the last 40 characters as they are in hex format 2 hex digits make up one byte.

Note that you have to add 0x in front of those 20 bytes to parse a valid address

I'm not very familiar with the Alchemy SDK, but here is a script in web3.js to retrieve and parse the Approval event from an ERC-721 contract using the getPastLogs method.

const Web3 = require("web3");
require('dotenv').config();

// Initialize connection to the node. 
const node_url = process.env.ETHEREUM_CHAINSTACK;
const web3 = new Web3(new Web3.providers.HttpProvider(node_url))

// Retrieve the logs based on the Transfer event
async function getLogs() {
    
    // Initialize blocks to query
    const startBlock = 16599340
    const latestBlock = await web3.eth.getBlockNumber()    

    // Set the parameters for the getPastLogs method
    const params = {
        fromBlock: startBlock,
        toBlock: 'latest',
        address: "0x9401518f4EBBA857BAA879D9f76E1Cc8b31ed197", // smart contract address
        topics: ["0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"], // Approval event signature
    };

    // Print some info on the screen.
    console.log(`Getting logs starting from block ${startBlock} to ${latestBlock} \n`)

    // Retrieve logs
    const logs = await web3.eth.getPastLogs(params)
    //console.log(logs)     
    
    return logs
}

// Parse the logs and extract only the tx hash and topics.
async function parseLogs(logs) {

    for (let object of logs) {

        const txhash = object.transactionHash
        const from = object.topics[1]
        const fromAddress = from.slice(-40); // This is how we take the last 20 bytes

        const to = object.topics[2]
        const toAddress = to.slice(-40);

        const tokenId = object.topics[3]

        console.log(`Approval tx hash: ${txhash}`)
        console.log(`Owner address: 0x${fromAddress}`)
        console.log(`Approved address: 0x${toAddress}`)

        console.log(`Token ID: ${web3.utils.hexToNumber(tokenId)} \n`) // web3.utils.hexToNumber converts to the decimal token id
    }
}

// Main program.
async function main() {
    const approvalLogs = await getLogs()
    parseLogs(approvalLogs)
}

main();

Everything in the getLogs function is similar to what you already do. The parseLogs function extracts the topics, decodes them, and prints them on the screen.

Running it will give you something like this:

Getting logs starting from block 16600272 to 16600472

Approval tx hash: 0x7dee982e52154f0750f58ce77bcc6047c08d38339ade1a060e317eccd17f3b6c
Owner address: 0xf9cc4b93728328ccfce3cfe446d4c52d1571aa20
Approved address: 0x0000000000000000000000000000000000000000
Token ID: 1084

Approval tx hash: 0xfe319b5527fd4f9a240abc0d0676599251e1ce58fa87a45eb674a9e39862ddf3
Owner address: 0x2ed0f4e9769591d8eff038abd884187c8101c7f3
Approved address: 0x0000000000000000000000000000000000000000
Token ID: 828