ABIv2 comes to Web3j

We’ve just released a major milestone for Web3j - support for the Solidity ABIv2 specification! In this post I’m going to dive into what ABIv2 means for you as a smart contract developer and how Web3j supports it.

One of the most powerful features in Web3j is the ability to generate smart contract wrappers, which gives the ability to work in a type safe manner with Solidity smart contracts in Java. This is possible because the output of the solidity compiler provides an ABI (Application Binary Interface) which is essentially a JSON file describing the functions in a smart contract. The ABI encoder in the Solidity compiler does allow the use of structs to be used as input/output/event parameters. However with the “pragma experimental ABIEncoderV2;” directive it is possible to enable this feature.

An example

When ABIv2 is enabled in your smart contract you can start using structs as parameters to functions or events. A simple example contract using these features could look something like this:

pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;

contract ExampleContract {

struct Foo {
    string id;
    string name;
}

struct Bar {
    uint id;
    string data;
}

Foo foo;
Bar bar;

constructor(Foo memory _foo, Bar memory_bar) public {
foo = _foo;
bar = _bar;
emit Event(msg.sender, _foo, _bar);
}


function setFoo(Foo memory _toSet) public {
     foo = _toSet;
}


function setBar(Bar memory _toSet) public {
     bar = _toSet;
}


function getFooBar() public view returns (Foo memory, Bar memory) {
     return (foo, bar);
}


event Event(address indexed _address, Foo _foo, Bar _bar);
}

This contract has two structs called Foo and Bar. Foo contains two fields that are both strings while Bar contains an unsigned integer and a string. These are just simple example structs, but it is possible to use other types like byte arrays, addresses and it is even possible to have arbitrarily nested structs within each other. In other words it is possible to define a struct A that has a field represented by struct B.

constructor(Foo memory _foo, Bar memory _bar) public {
    foo = _foo;
    bar = _bar;
    emit Event(msg.sender, _foo, _bar);
}

The above example has a constructor which takes a Foo and a Bar struct as input parameters. The constructor sets the input parameters to the global fields in the contract and then emits an event which stores the Foo and the Bar structs that were passed in.

function setFoo(Foo memory _toSet) public {
    foo = _toSet;
}


function setBar(Bar memory _toSet) public {
    bar = _toSet;
}


function getFooBar() public view returns (Foo memory, Bar memory) {
    return (foo, bar);
}

There are also three functions, two of which are setters for the Foo and Bar structs respectively. They take in as an input parameter a struct. The final function is a getter which returns a tuple representing both the Foo and Bar structs that are global fields in the contract.

The ABI

Once the above contract is compiled with the Solidity compiler it will generate an ABI file which is essentially a list of JSON objects that represents each function and event in the example contract. It defines each input and output parameter for each function and event. The JSON object that represents the setFoo function in the example contract above will look like this:

{
  "inputs": [
    {
      "components": [
        {
          "internalType": "string",
          "name": "id",
          "type": "string"
        },
        {
          "internalType": "string",
          "name": "name",
          "type": "string"
      }
    ],
    "internalType": "struct ExampleContract.Foo",
    "name": "_toSet",
    "type": "tuple"
   }
  ],
  "name": "setFoo",
  "outputs": [],
  "stateMutability": "nonpayable",
  "type": "function"
}

This JSON object has an inputs property with a single object. You can see that the type property of the input is “tuple”, but this is not very helpful because we can only distinguish different tuples by determining that they have different components. Since Solidity version 0.6.0 there is also an internalType property which gives us the type of the parameter that can be found within the context of Solidity.

Web3j

To generate type-safe Java code for this function we would have to generate a class to correspond to the struct Foo. Web3j’s smart contract  generator will generate a class like this:

public static class Foo extends DynamicStruct {
  public String id;


public String name;


public Foo(String id, String name) {
    super(new org.web3j.abi.datatypes.Utf8String(id), new org.web3j.abi.datatypes.Utf8String(name));
    this.id = id;
    this.name = name;
}


public Foo(Utf8String id, Utf8String name) {
    super(id, name);
    this.id = id.getValue();
    this.name = name.getValue();
  }
}

Struct Foo contains two strings, which are of dynamic type. Therefore the java class Foo extends DynamicType. This lets the type encoder/decoder handle all dynamic structs in the same way.

There are two constructors, one with java types and one with Web3j Solidity wrapper types (i.e. Utf8String implements Type). The Java types are there for developer convenience, so one can easily construct an instance of Foo to use for making contract calls. The other constructor is used by the type decoder to construct an instance of Foo from the response of a contract call.

Nesting challenges

Building out a type safe generator to support structs came with many challenges. One of the biggest challenges is parsing the ABI to find unique nested structs. This is relatively easy for contracts compiled with Solidity v 0.6.x because the internalType field exposes information about the struct that makes it unique. However, without this field in the ABI the problem becomes much more complex.

Get #Buidling 

ABIv2 support is available in Web3j from version 4.6.0 onwards. We encourage you to start hacking on it and make the most of this great new functionality.

If you want to get up and running really quickly, I encourage you to check out our Epirus SDK which makes the project really straight forward!