Consensys / abi-decoder

Nodejs and Javascript library for decoding data params and events from ethereum transactions
GNU General Public License v3.0
633 stars 218 forks source link

decodeLogs not work for event logs with struct parameter #61

Open passionofvc opened 3 years ago

passionofvc commented 3 years ago

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;
     }
    }
2. 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)
   });
 })

}) })

3. 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 commented 3 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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