Decoding Ethereum ABIs with Go: A Developer’s Guide
Understanding ABIs
An ABI is a JSON-formatted file that describes the structure of a smart contract, including:
- The functions it exposes.
- The parameters each function accepts.
- The return values of functions.
- Events emitted by the contract.
It acts as a blueprint, allowing users or applications to understand how to call a smart contract’s methods and handle its responses.
Since smart contracts operate in bytecode on the Ethereum Virtual Machine (EVM), human-readable function names and structures must be translated into this bytecode. ABIs provide the map for encoding data into the required binary format.
ABI parsing in GoLang
In GoLang, the ABI, which is usually written as JSON, needs to be converted into Go-encoded ABI before being used for encoding or decoding.
We will be using the go-ethereum
package here. It can be added to the project repo using
go get github.com/ethereum/go-ethereum
ABI Example
{
"type": "function",
"name": "encode",
"inputs": [
{
"name": "data",
"type": "bytes"
}
],
"outputs": [
{
"components": [
{
"name": "transferId",
"type": "string"
},
{
"name": "collectionName",
"type": "string"
},
{
"name": "tokens",
"type": "tuple",
"components": [
{
"name": "amount",
"type": "uint256"
},
{
"name": "tokenId",
"type": "string"
}
]
}
],
"type": "tuple",
"name": "transfers"
}
],
"stateMutability": "nonpayable"
}
This can be converted into Go-encoded ABI format as,
transferAbi, err := abi.NewType("tuple", "data", []abi.ArgumentMarshalling{
{Name: "transferId", Type: "string"},
{Name: "collectionName", Type: "string"},
{Name: "tokens", Type: "tuple", Components: []abi.ArgumentMarshalling{
{Name: "tokenId", Type: "string"},
{Name: "amount", Type: "uint256"},
}},
})
Once we have the ABIs in place, we can use them to encode and decode the data.
Let’s consider the data is a hex-encoded byte array and we need to decode using the above ABI. We initially convert it into a byte array using the hexutil
library in the Ethereum library. Then use the byte array for parsing using the ABI.
The complete workflow has been illustrated in the code below.
package main
import (
"fmt"
"math/big"
"fmt"
"math/big"
"github.com/ethreum/go-ethereum/accounts/abi"
"github.com/ethreum/go-ethereum/common/hexutil"
)
func main() {
transferAbi, err := abi.NewType("tuple", "data", []abi.ArgumentMarshalling{
{Name: "transferId", Type: "string"},
{Name: "collectionName", Type: "string"},
{Name: "tokens", Type: "tuple", Components: []abi.ArgumentMarshalling{
{Name: "tokenId", Type: "string"},
{Name: "amount", Type: "uint256"},
}},
})
hexString := "hex encoded string..."
decoded, err := decode(transferAbi, hexString)
if err != nil {
fmt.Println(err)
}
result := decoded[0].(struct {
TransferId string `json:"transferId"`
CollectionName string `json:"collectionName"`
Tokens []struct {
TokenId string `json:"tokenId"`
Amount *big.Int `json:"amount"`
} `json:"tokens"`
})
fmt.Println(result)
}
func decode(args abi.Arguments, data string) ([]interface{}, error) {
byteArr, err := hexutil.Decode(data)
if err != nil {
return nil, err
}
unpacked, err := args.Unpack(byteArr)
if err != nil {
return nil, err
}
return unpacked, nil
}
}
The same can be done in the case of encoding. Just use the abi.Pack()
function. The encoding function can be as follows
func encode(args abi.Arguments, value interface{}) (string, error) {
packed, err := args.Pack(value)
if err != nil {
return nil, err
}
return hexutil.Encode(packed), nil
}