abi-decoder icon indicating copy to clipboard operation
abi-decoder copied to clipboard

decodeLogs not work for event logs with struct parameter

Open passionofvc opened this issue 4 years ago • 4 comments

decodeLogs can not decode events with struct tuple logs, it show empty value.

Environment

$ truffle version Truffle v5.1.37 (core: 5.1.37) Solidity - 0.7.1 (solc-js) Node v10.20.0 Web3.js v1.2.1

Steps to reproduce

  1. get test contract
pragma solidity ^0.7.1;
pragma experimental ABIEncoderV2;
 // SPDX-License-Identifier: MIT
 
 contract SimpleStructStorage {
     struct Data {
         uint a;
         string b;
     }
 
     Data storedData;
 
     event SET(Data d);
     function set(Data memory x) public {
         storedData = x;
         emit SET(x);
     }
 
     function get() public view returns (Data memory x) {
         return storedData;
     }
 }

  1. compile with truffle and run test
const SimpleStructStorage = artifacts.require('SimpleStructStorage')
const Web3 = require('web3');
 
 
contract('SimpleStructStorage', (accounts) => {
   let instance
   const web3 = new Web3('http://localhost:8545');
 
   before('setup', async () => {
     instance = await SimpleStructStorage.new()
     console.log("instance", instance.address);
 
   })
 
   describe('test struct input parameter', () => {
     it('should set struct data', async () => {
       const data = {a: 100, b: "test"}
       const {receipt: {transactionHash}} = await instance.set(data)
       console.log(transactionHash)
       const {a, b} = await instance.get()
       assert.equal(a, 100)
       assert.equal(b, 'test')
 
       web3.eth.getTransactionReceipt(transactionHash, function(e, receipt) {
         const decodedLogs = abiDecoder.decodeLogs(receipt.logs);
         console.log(decodeLogs)
       });
     })
   })
 })

  1. run decodeLogs test script node test_abi_decoder.js 0xxxx
//test_abi_decoder.js
const Web3 = require('web3');
const web3 = new Web3('http://localhost:8545');
const {addABI, decodeMethod, decodeLogs}= require('abi-decoder');
 
const abiJson = require('./build/contracts/SimpleStructStorage.json')
const ABI = abiJson.abi
addABI(ABI);
  
const transactionHash=process.argv[2] //you set tx hash
 
 //Input Data
web3.eth.getTransaction(transactionHash, function(e, data) {
   const inputData = data.input;
   const decodedData = decodeMethod(inputData);
   console.log(decodedData.params[0].value);
 });
 
 
 //Event Logs
web3.eth.getTransactionReceipt(transactionHash, function(e, receipt) {
   const decodedLogs = decodeLogs(receipt.logs);
   console.log(decodedLogs[0].events)
 });

Expected behaviour

decodeLogs Should show below output [ { name: 'd', type: 'tuple', value: [ '100', 'test' ] } ]

Actual behaviour

decodeLogs show empty value [ { name: 'd', type: 'tuple', value: [] } ]

passionofvc avatar Dec 19 '20 10:12 passionofvc

Hello. I had a same problem , too. So, I fixed some source. and It's works only one depth tuple.

replace _decodeLogs() function.

function _decodeLogs(logs) {  
  return logs.filter(log => log.topics.length > 0).map((logItem) => {
    const methodID = logItem.topics[0].slice(2);
    const method = state.methodIDs[methodID];
    if (method) { 
      const logData = logItem.data;
      let decodedParams = [];
      let dataIndex = 0;
      let topicsIndex = 1;
      let dataTypes = [];
      method.inputs.map(function(input) {
        if (!input.indexed) {          
          if( input.type === "tuple" ) {
            // it works only one depth tuple
            // if multi depth => use recursive            
            const tupleType =  {};
            const tupleValue = {};
            for( let i =0;  i < input.components.length;  ++i)
              tupleValue[input.components[i].name] =  input.components[i].type;            
            tupleType[input.components.name] = tupleValue;            
            dataTypes.push(tupleType);    
          }
          else 
            dataTypes.push(input.type);
        }
      });
            
      const decodedData = abiCoder.decodeParameters(
        dataTypes,
        logData.slice(2)
      );

      // Loop topic and data to get the params
      method.inputs.map(function(param) {      
        let decodedP = {
          name: param.name,
          type: param.type,
        };

        if (param.indexed) {
          decodedP.value = logItem.topics[topicsIndex];
          topicsIndex++;
        } else {          
          decodedP.value = decodedData[dataIndex];
          dataIndex++;
        }
        

        if (param.type === "address") {
          decodedP.value = decodedP.value.toLowerCase();
          // 42 because len(0x) + 40
          if (decodedP.value.length > 42) {
            let toRemove = decodedP.value.length - 42;
            let temp = decodedP.value.split("");
            temp.splice(2, toRemove);
            decodedP.value = temp.join("");
          }
        }

        if (
          param.type === "uint256" ||
          param.type === "uint8" ||
          param.type === "int"
        ) {
          // ensure to remove leading 0x for hex numbers
          if (typeof decodedP.value === "string" && decodedP.value.startsWith("0x")) {
            decodedP.value = new BN(decodedP.value.slice(2), 16).toString(10);
          } else {
            decodedP.value = new BN(decodedP.value).toString(10);
          }
        }

        decodedParams.push(decodedP);
      });

      return {
        name: method.name,
        events: decodedParams,
        address: logItem.address,
      };
    }
  }).filter(decoded => state.keepNonDecodedLogs || decoded);
}

Output example :

[
  {
    name: 'minter',
    type: 'address',
    value: '0x...'
  },
  { name: 'tokenId', type: 'uint256', value: '0' },
  {
    name: 'uri',
    type: 'string',
    value: 'https://url.example.com'
  },
  { name: 'creatorId', type: 'uint256', value: '4' },
  {
    name: 'creatorInfo',
    type: 'tuple',
    value: [
      'bg',
      'https://someurl.png',
      'https://someurl/detail',
      name: 'bg',
      profileImageUrl: 'https://someurl.png',
      detailUrl: 'https://someurl/detail'
    ]
  }
]

Explanation :

This Library decode with "web3-eth-abi" Library. Link : https://web3js.readthedocs.io/en/v1.4.0/web3-eth-abi.html#decodeparameter

And the problem is dataTypes parameter for abiCoder.decodeParameters(); So, I add tuple data type parameter.

if( input.type === "tuple" ) {
            // it works only one depth tuple
            // if multi depth => use recursive            
            const tupleType =  {};
            const tupleValue = {};
            for( let i =0;  i < input.components.length;  ++i)
              tupleValue[input.components[i].name] =  input.components[i].type;            
            tupleType[input.components.name] = tupleValue;            
            dataTypes.push(tupleType);    
}

You know, It's works only single depth tuple. =)

BG-Kim avatar Aug 24 '21 11:08 BG-Kim

Hello. I had a same problem , too. So, I fixed some source. and It's works only one depth tuple.

replace _decodeLogs() function.

function _decodeLogs(logs) {  
  return logs.filter(log => log.topics.length > 0).map((logItem) => {
    const methodID = logItem.topics[0].slice(2);
    const method = state.methodIDs[methodID];
    if (method) { 
      const logData = logItem.data;
      let decodedParams = [];
      let dataIndex = 0;
      let topicsIndex = 1;
      let dataTypes = [];
      method.inputs.map(function(input) {
        if (!input.indexed) {          
          if( input.type === "tuple" ) {
            // it works only one depth tuple
            // if multi depth => use recursive            
            const tupleType =  {};
            const tupleValue = {};
            for( let i =0;  i < input.components.length;  ++i)
              tupleValue[input.components[i].name] =  input.components[i].type;            
            tupleType[input.components.name] = tupleValue;            
            dataTypes.push(tupleType);    
          }
          else 
            dataTypes.push(input.type);
        }
      });
            
      const decodedData = abiCoder.decodeParameters(
        dataTypes,
        logData.slice(2)
      );

      // Loop topic and data to get the params
      method.inputs.map(function(param) {      
        let decodedP = {
          name: param.name,
          type: param.type,
        };

        if (param.indexed) {
          decodedP.value = logItem.topics[topicsIndex];
          topicsIndex++;
        } else {          
          decodedP.value = decodedData[dataIndex];
          dataIndex++;
        }
        

        if (param.type === "address") {
          decodedP.value = decodedP.value.toLowerCase();
          // 42 because len(0x) + 40
          if (decodedP.value.length > 42) {
            let toRemove = decodedP.value.length - 42;
            let temp = decodedP.value.split("");
            temp.splice(2, toRemove);
            decodedP.value = temp.join("");
          }
        }

        if (
          param.type === "uint256" ||
          param.type === "uint8" ||
          param.type === "int"
        ) {
          // ensure to remove leading 0x for hex numbers
          if (typeof decodedP.value === "string" && decodedP.value.startsWith("0x")) {
            decodedP.value = new BN(decodedP.value.slice(2), 16).toString(10);
          } else {
            decodedP.value = new BN(decodedP.value).toString(10);
          }
        }

        decodedParams.push(decodedP);
      });

      return {
        name: method.name,
        events: decodedParams,
        address: logItem.address,
      };
    }
  }).filter(decoded => state.keepNonDecodedLogs || decoded);
}

Output example :

[
  {
    name: 'minter',
    type: 'address',
    value: '0x...'
  },
  { name: 'tokenId', type: 'uint256', value: '0' },
  {
    name: 'uri',
    type: 'string',
    value: 'https://url.example.com'
  },
  { name: 'creatorId', type: 'uint256', value: '4' },
  {
    name: 'creatorInfo',
    type: 'tuple',
    value: [
      'bg',
      'https://someurl.png',
      'https://someurl/detail',
      name: 'bg',
      profileImageUrl: 'https://someurl.png',
      detailUrl: 'https://someurl/detail'
    ]
  }
]

Explanation :

This Library decode with "web3-eth-abi" Library. Link : https://web3js.readthedocs.io/en/v1.4.0/web3-eth-abi.html#decodeparameter

And the problem is dataTypes parameter for abiCoder.decodeParameters(); So, I add tuple data type parameter.

if( input.type === "tuple" ) {
            // it works only one depth tuple
            // if multi depth => use recursive            
            const tupleType =  {};
            const tupleValue = {};
            for( let i =0;  i < input.components.length;  ++i)
              tupleValue[input.components[i].name] =  input.components[i].type;            
            tupleType[input.components.name] = tupleValue;            
            dataTypes.push(tupleType);    
}

You know, It's works only single depth tuple. =)

how to use it for more depth than 1 level tupples? :D

GrinOleksandr avatar May 02 '22 17:05 GrinOleksandr

Hello. I had a same problem , too. So, I fixed some source. and It's works only one depth tuple. replace _decodeLogs() function.

function _decodeLogs(logs) {  
  return logs.filter(log => log.topics.length > 0).map((logItem) => {
    const methodID = logItem.topics[0].slice(2);
    const method = state.methodIDs[methodID];
    if (method) { 
      const logData = logItem.data;
      let decodedParams = [];
      let dataIndex = 0;
      let topicsIndex = 1;
      let dataTypes = [];
      method.inputs.map(function(input) {
        if (!input.indexed) {          
          if( input.type === "tuple" ) {
            // it works only one depth tuple
            // if multi depth => use recursive            
            const tupleType =  {};
            const tupleValue = {};
            for( let i =0;  i < input.components.length;  ++i)
              tupleValue[input.components[i].name] =  input.components[i].type;            
            tupleType[input.components.name] = tupleValue;            
            dataTypes.push(tupleType);    
          }
          else 
            dataTypes.push(input.type);
        }
      });
            
      const decodedData = abiCoder.decodeParameters(
        dataTypes,
        logData.slice(2)
      );

      // Loop topic and data to get the params
      method.inputs.map(function(param) {      
        let decodedP = {
          name: param.name,
          type: param.type,
        };

        if (param.indexed) {
          decodedP.value = logItem.topics[topicsIndex];
          topicsIndex++;
        } else {          
          decodedP.value = decodedData[dataIndex];
          dataIndex++;
        }
        

        if (param.type === "address") {
          decodedP.value = decodedP.value.toLowerCase();
          // 42 because len(0x) + 40
          if (decodedP.value.length > 42) {
            let toRemove = decodedP.value.length - 42;
            let temp = decodedP.value.split("");
            temp.splice(2, toRemove);
            decodedP.value = temp.join("");
          }
        }

        if (
          param.type === "uint256" ||
          param.type === "uint8" ||
          param.type === "int"
        ) {
          // ensure to remove leading 0x for hex numbers
          if (typeof decodedP.value === "string" && decodedP.value.startsWith("0x")) {
            decodedP.value = new BN(decodedP.value.slice(2), 16).toString(10);
          } else {
            decodedP.value = new BN(decodedP.value).toString(10);
          }
        }

        decodedParams.push(decodedP);
      });

      return {
        name: method.name,
        events: decodedParams,
        address: logItem.address,
      };
    }
  }).filter(decoded => state.keepNonDecodedLogs || decoded);
}

Output example :

[
  {
    name: 'minter',
    type: 'address',
    value: '0x...'
  },
  { name: 'tokenId', type: 'uint256', value: '0' },
  {
    name: 'uri',
    type: 'string',
    value: 'https://url.example.com'
  },
  { name: 'creatorId', type: 'uint256', value: '4' },
  {
    name: 'creatorInfo',
    type: 'tuple',
    value: [
      'bg',
      'https://someurl.png',
      'https://someurl/detail',
      name: 'bg',
      profileImageUrl: 'https://someurl.png',
      detailUrl: 'https://someurl/detail'
    ]
  }
]

Explanation : This Library decode with "web3-eth-abi" Library. Link : https://web3js.readthedocs.io/en/v1.4.0/web3-eth-abi.html#decodeparameter And the problem is dataTypes parameter for abiCoder.decodeParameters(); So, I add tuple data type parameter.

if( input.type === "tuple" ) {
            // it works only one depth tuple
            // if multi depth => use recursive            
            const tupleType =  {};
            const tupleValue = {};
            for( let i =0;  i < input.components.length;  ++i)
              tupleValue[input.components[i].name] =  input.components[i].type;            
            tupleType[input.components.name] = tupleValue;            
            dataTypes.push(tupleType);    
}

You know, It's works only single depth tuple. =)

how to use it for more depth than 1 level tupples? :D

Decode the input using ABICoder.decodeParameters(inputs, hexString);

Then format each item of the output array using this function :

Typescript :

function formatStruct(element: Array<any>): any[] | { [key: string]: string } {
  if (!Array.isArray(element)) return element;
  let objectKeys = Object.keys(element);
  if (objectKeys.every((key: any) => !isNaN(key)))
    return element.map((value: any) => formatStruct(value));
  let formatted: { [key: string]: any } = {};
  objectKeys
    .filter((key: any) => isNaN(key))
    .forEach((key: any) => {
      formatted[key] = formatStruct(element[key]);
    });
  return formatted;
}

aymantaybi avatar Aug 26 '22 22:08 aymantaybi

EDIT :

Just decode the paramters using ABICoder.decodeParameters(inputs, hexString) and format the whole output using this function :

Typescript

function formatDecoded(element: {
  [key: string]: any;
}): any[] | { [key: string]: string } {
  if (typeof element != "object") return element;
  let objectKeys = Object.keys(element);
  if (objectKeys.every((key: any) => !isNaN(key)))
    return element.map((value: any) => formatDecoded(value));
  let formatted: { [key: string]: any } = {};
  objectKeys
    .filter((key: any) => isNaN(key) && key != "__length__")
    .forEach((key: any) => {
      formatted[key] = formatDecoded(element[key]);
    });
  return formatted;
}

aymantaybi avatar Aug 26 '22 22:08 aymantaybi