rubysmartcontractsabievm

How to declare the 'types' argument to `Eth::Abi.decode` to parse the response of a Solidity function that returns a single struct


I am using eth gem to interact with a Smart Contract in EVM.

The Smart Contract has a function whose ABI in JSON format is this:

{
    "inputs": [
      {"internalType": "address", "name": "_owner", "type": "address"}
    ],
    "name": "getUserSuperChainAccount",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "smartAccount",
            "type": "address"
          },
          {"internalType": "string", "name": "superChainID", "type": "string"},
          {"internalType": "uint256", "name": "points", "type": "uint256"},
          {"internalType": "uint16", "name": "level", "type": "uint16"},
          {
            "components": [
              {
                "internalType": "uint48",
                "name": "background",
                "type": "uint48"
              },
              {"internalType": "uint48", "name": "body", "type": "uint48"},
              {"internalType": "uint48", "name": "accessory", "type": "uint48"},
              {"internalType": "uint48", "name": "head", "type": "uint48"},
              {"internalType": "uint48", "name": "glasses", "type": "uint48"}
            ],
            "internalType": "struct NounMetadata",
            "name": "noun",
            "type": "tuple"
          }
        ],
        "internalType": "struct ISuperChainModule.Account",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }

I am calling this function and I can confirm that it returns the correct response:

"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a746d1f8880503fee173ba4ab255c8223ba8f3ad000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000aa00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000001d000000000000000000000000000000000000000000000000000000000000006e00000000000000000000000000000000000000000000000000000000000000be000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000147275646e6576736b792e70726f73706572697479000000000000000000000000"

In order to parse the result, I am writing Ruby code like this:

types = ["tuple(address,string,uint256,uint16,tuple(uint48,uint48,uint48,uint48,uint48))"]
data = Eth::Abi.decode(types, response["result"])

The last line is failing with the error:

undefined method `none?' for nil:NilClass (NoMethodError)

          elsif base_type == "tuple" && components.none?(&:dynamic?)
                                                  ^^^^^^

So, it seems that the types argument is not set as it should.

Can anyone help?


Solution

  • While I agree that the string parsing issue should definitely be considered a bug.

    In reviewing the spec tests I noticed there does not appear to be any testing for nested tuples.

    What I did identify is that decode uses Eth::Abi::Type::parse and those spec tests use the same structure as your referenced JSON document.

    So rather than relying on the flawed String parsing for "type" (also in Eth::Abi::Type::parse) we can instead use the JSON you have to pass the base type and its components directly to Eth::Abi::Type::parse which outputs the expected results:

    Your JSON

    json_doc = <<JSON
    {
        "inputs": [
          {"internalType": "address", "name": "_owner", "type": "address"}
        ],
        "name": "getUserSuperChainAccount",
        "outputs": [
          {
            "components": [
              {
                "internalType": "address",
                "name": "smartAccount",
                "type": "address"
              },
              {"internalType": "string", "name": "superChainID", "type": "string"},
              {"internalType": "uint256", "name": "points", "type": "uint256"},
              {"internalType": "uint16", "name": "level", "type": "uint16"},
              {
                "components": [
                  {
                    "internalType": "uint48",
                    "name": "background",
                    "type": "uint48"
                  },
                  {"internalType": "uint48", "name": "body", "type": "uint48"},
                  {"internalType": "uint48", "name": "accessory", "type": "uint48"},
                  {"internalType": "uint48", "name": "head", "type": "uint48"},
                  {"internalType": "uint48", "name": "glasses", "type": "uint48"}
                ],
                "internalType": "struct NounMetadata",
                "name": "noun",
                "type": "tuple"
              }
            ],
            "internalType": "struct ISuperChainModule.Account",
            "name": "",
            "type": "tuple"
          }
        ],
        "stateMutability": "view",
        "type": "function"
      }
    JSON
    

    Your Response Object

    response = {"result" => "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a746d1f8880503fee173ba4ab255c8223ba8f3ad000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000aa00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000001d000000000000000000000000000000000000000000000000000000000000006e00000000000000000000000000000000000000000000000000000000000000be000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000147275646e6576736b792e70726f73706572697479000000000000000000000000"}
    

    Working Code

    require 'eth'
    require 'json'
    
    types = JSON.parse(json_doc)["outputs"].map do |type| 
               Eth::Abi::Type.parse(type["type"],type["components"])
            end
    
    Eth::Abi.decode(types, response["result"])
    #=> [
    # {"smartAccount"=>"0xa746d1f8880503fee173ba4ab255c8223ba8f3ad", 
    #  "superChainID"=>"rudnevsky.prosperity", 
    #  "points"=>170, 
    #  "level"=>2, 
    #  "noun"=>{
    #     "background"=>1, 
    #     "body"=>29, 
    #     "accessory"=>110, 
    #     "head"=>190, 
    #     "glasses"=>14}
    # }]