abi-decoder
abi-decoder copied to clipboard
decodeLogs not work for event logs with struct parameter
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
- 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;
}
}
- 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)
});
})
})
})
- 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: [] } ]
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. =)
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
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;
}
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;
}