Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

decodeLogs not work for event logs with struct parameter #61

Open
passionofvc opened this issue Dec 19, 2020 · 4 comments
Open

decodeLogs not work for event logs with struct parameter #61

passionofvc opened this issue Dec 19, 2020 · 4 comments

Comments

@passionofvc
Copy link

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: [] } ]

@BG-Kim
Copy link

BG-Kim commented Aug 24, 2021

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. =)

@GrinOleksandr
Copy link

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

@aymantaybi
Copy link

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
Copy link

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;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants