How to make your Smart Contract ABI more Human

How to make your Smart Contract ABI more Human

The Application Binary Interface(ABI) allow us to talk to smart contracts on the blockchain or call contract from our Decentralised Applications(dApp).

It was necessary because applications outside the blockchain, or even other smart contracts, needed a way to communicate to contracts deployed to the blockchain. The ABI defines the specific methods, events, and errors that a smart contract has specified in its code.

However, ABI generated by tools like Hardhat/Truffle or by Remix—an IDE that allows you to write smart contracts in your browser, often lives in a JSON RPC file. If you look at this file right now, you will see several lines of code that might take you a while to understand, even though the machine understands.

So, Is it possible to have ABIs in a form that both the EVM and humans—well, developer humans, can understand? Yes, that's where the Human-readable ABI comes into play.

In this guide, you will write a simple, smart contract, compile it using Hardhat, and then generate a Human-readable ABI from the Smart contracts Solidity JSON ABI. First, let's understand what a Human-readable ABI is.

What is a Human-readable ABI?

The Human-readable ABI is a format of the ABI that allows methods, events, and errors in smart contracts to be described using the properties they possess. Properties like name, type, inputs, outputs, etc. These properties are called Solidity Signatures.

It is important to note that a Solidity signature fully describes all the properties the ABI requires and is defined by the following:

  • name

  • type (constructor, event, function)

  • inputs (types, nested structures, and optionally names)

  • outputs (types nested structures and optionally names)

  • state mutability (for constructors and methods)

  • payability (for constructors and methods)

  • whether inputs are indexed (for events)

Human-readable ABI is a simple format. Machines can parse it. Humans can understand it. This makes it easy to write and use in your code, improving its readability.

If that isn't enticing enough, here's another cool benefit: when human-readable ABI is compared with the standard solidity ABI, it is considerably smaller, and this helps to trim your codebase extra fat.

And that's a good thing, whichever way you think about it.

Prerequisites

  1. Nodejs - You can download the latest version at the Node Js Website.

  2. Hardhat - This tool makes testing, writing, deploying, and debugging smart contracts for DApps easy.

  3. Visual Studio Code (VScode)- An Integrated Development Environment (IDE) that offers features like syntax highlighting and autocompletion for your code. You don't have to install VScode if you already have an IDE installed. This is just my preference.

  4. Basic Javascript Knowledge

Setting up project

To get started, open your terminal in any folder of your choice and run

mkdir human-readable-abi

This will create a new folder inside the folder you used to open the terminal and give it the name human-readable-abi .

Next, you need to initialize a package.json file inside your project. To do that, run:

cd human-readable-abi
npm init -y

That will navigate into the human-readable-abi folder and then initialize a node project. Also, selecting default options for the project.

Now you can install packages you need to write your smart contracts.

You will need Hardhat, so run this command to install Hardhat in your project:

npm install --save-dev hardhat

If you use yarn, run the code below instead

yarn add hardhat

When that is done, if you go to your package.json File, you should see Hardhat listed as a dependency. Great, let's create a hardhat project and write your smart contract.

Initializing a hardhat project

Initialize a new hardhat project:

npx hardhat

When prompted to choose a project type, Select Create an empty hardhat.config.js

$ npx hardhat
888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

👷 Welcome to Hardhat v2.9.9 👷‍

? What do you want to do? …
  Create a JavaScript project
  Create a TypeScript project
❯ Create an empty hardhat.config.js
  Quit

Writing smart contract

Start by creating a new directory called contracts and create a file inside the directory called. HelloWorld.sol.

Paste the code below into the file and take a minute to read the code.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract HelloWorld {

    //events
    event UpdatedMessages(string oldStr, string newStr);
    event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);

    ///You cannot do that
    error Unauthorized();

    // state variable, would be stored permanently on the blockchain
    string public message;
    address owner;

    mapping (uint => address) public ownersList;
    uint public ownersCount;

    enum dummyEnum {name,age}

    // a modifier to prevent function calls
    // from unauthorized actors
    modifier onlyOwner{
        if(msg.sender != owner) revert Unauthorized();
        _;
    }

    // allow the contract to be called with a setup value 
    // in this case pass initMessage to message(state var)
    constructor (string memory initMessage) {
        message = initMessage;
        owner = msg.sender;
        ownersCount = 1;
        ownersList[ownersCount] = owner;
    }

    function update(string memory newMessage) public {
        string memory oldMsg = message;
        message = newMessage;
        emit UpdatedMessages(oldMsg, newMessage);
    }

    function transferOwnership(address _newOwner) public onlyOwner returns(bool success){
        owner = _newOwner;
        ownersCount++;
        ownersList[ownersCount] = _newOwner;
        return true;
    }
}

This is a simple smart contract that has a state variable message, a constructor to initialize the message when the contract is deployed, and an update function to modify the message variable.

An event is also defined that would be emitted whenever the update function is called. Let's add a few features to the smart contract before we compile it.

The transferOwnership function would transfer contract ownership to a new address. There is a modifier onlyOwner that prevents unauthorized calls to transferOwnership.

ownersList Mapping keeps a list of the previous and current owners of the smart contract. ownersCount is an integer that tracks how many owners there are. The dummyEnum is simply presentational, so it does nothing. We included it to see what the Human-readable ABI format looks like with enums.

error Unauthorized(); It is a custom error that we can use to inform whoever or whatever consumes our contract that they are not permitted to call the transferOwnership function if they do not own the agreement.

At this stage, we can go ahead and compile your smart contracts.

Compiling Smart Contract

We need to compile our smart contract into a form the EVM can understand. To compile the contract, run npx hardhat compile it in your terminal. The compile task is one of Hardhat's built-in tasks.

This will compile your code and generate an ABI file for your smart contract. The default location for the ABI JSON is inside the artifacts\contracts\HelloWorld.sol folder.

Navigate to that folder and open the HelloWorld.json. It should look like this:

{
  "_format": "hh-sol-artifact-1",
  "contractName": "HelloWorld",
  "sourceName": "contracts/HelloWorld.sol",
  "abi": [
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "initMessage",
          "type": "string"
        }
      ],
      "stateMutability": "nonpayable",
      "type": "constructor"
    },
    {
      "inputs": [],
      "name": "Unauthorized",
      "type": "error"
    },
...
// even more lines of JSON code

Awesome! You have just generated an ABI for your smart contract. With this, you can write DAPPs to talk to your smart contract once you deploy it on the blockchain. Let's go ahead and convert that to a human-readable ABI.

Generating Human-readable ABIs

First, I will show you how to generate the Human-readable ABI using a custom generator script then, we will use a simple tool I built, Humanizer, to accomplish the same thing.

Writing a Generator script

Create a new file inside your scripts folder named. ABIconverter.js.

We would need ethers.js, a library that helps us interact with the blockchain. Thankfully, Hardhat comes with a copy of ethers.js out of the box, so we don't need to install it explicitly.

Let's import Hardhat inside ABIconverter.js

const hre = require('hardhat');

Inside the file, you need to import your HelloWorld JSON ABI. Here's how you will do that:

const helloWorld = require("./artifacts/contracts/HelloWorld.sol/HelloWorld.json");

After the import, here comes the actual conversion part; add the following code to the rest of the code:

const helloWorld = new hre.ethers.utils.Interface(helloWorldJson.abi);
const { full, minimal } = hre.ethers.utils.FormatTypes;

// `full` 
const HelloWorld = helloWorld.format(full);

// Replace `full` with `minimal` to reduce the ABI further.
// const HelloWorld = helloWorld.format(minimal);


console.log(HelloWorld);

The new hre.ethers.utils.Interface will create an interface using the ABI inside our helloworld import. helloWorld.format applies a format to the interface. You can decide between the full or minimal format types.

There is a subtle difference between the two formats; you can read it up at the ethers docs

Finally, we log the Human-readable ABI to the console. To test our script, go to your terminal and run

node scripts/ABIconverter.js

You should see your Human-readable ABI printed on your console. It should look like this:

[
  'constructor(string initMessage)',
  'error Unauthorized()',
  'event OwnershipTransferred(address indexed oldOwner, address indexed newOwner)',
  'event UpdatedMessages(string oldStr, string newStr)',
  'function message() view returns (string)',
  'function ownersCount() view returns (uint256)',
  'function ownersList(uint256) view returns (address)',
  'function transferOwnership(address _newOwner) returns (bool success)',
  'function update(string newMessage)'
]

Great Job! You just took an almost unreadable form of ABI and turned it into something you can understand. Even crazier, the EVM can understand too.

With this form, you can see all the functions, events, and other signatures provided by your HelloWorld smart contract, and to top it all, you can also know the type and name of the argument they need. This is way better than that chunk of hideous ABI you used before, right?

There is another way to generate Human-readable ABI. I dare say it's way easier and could make your workflow faster.

Using the Humanizer tool

Humanizer is a tool that converts JSON ABI to Human-readable ABIs — all in three clicks. Humanizer can compile your ABI whether you compiled it with Hardhat or wrote the code in Remix.

To use Humanizer, follow the steps described below.

Step 1: Open the HelloWorld.json ABI file. You should find it in ./artifacts/contracts/HelloWorld.sol/HelloWorld.json

Step 2: Copy the contents of the file, i.e., the entire HelloWorld.json content

Step 3: Open your browser and go to https://kohasummons.github.io/humanizer

Step 4: Paste the JSON you copied inside Humanizer's code editor

Step 5: Select a format type, Full or Minimal

Step 6: Click Generate and then copy the transformed ABI.

Generating Human-readable ABI with humanizer.gif

That's how simple it is to convert your JSON ABI to Human-readable ABI. You can support Humanizer by giving the project a star on GitHub

How to use the Human-readable ABI

To use the ABI we generated, create a new file called contracts.js in the root of your app project.

touch contracts.js

Paste the Human-readable ABI code inside contracts.js. Now all you need to do is import the ABI wherever you need to create a call to the smart contract.

import { HelloWorld } from "./contracts.js"
import { ethers } from "ethers.js"

const { ethereum } = window;
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();

// HelloWorld_Contract_Address - Address where contract was deployed to
const HelloWorld = new ethers.Contract(
    HelloWorld_Contract_Address,
    HelloWorld,
    signer
);

console.log(await HelloWorld.ownersCount());

Conclusion

Human-readable ABIs improve your code in two significant ways:

  1. Your code instantly becomes more readable,

  2. You cut out code fat. Two of the most beautiful things you can wish for as a developer.

Now you can go out there and improve your developer experience. Thank you for reading this article. If you found value from it please do like, share and leave a comment.