Go Contract Bindings

This page introduces the concept of server-side native dapps. ETN-SC provides the tools required to generate Go language bindings to any Electroneum Smart Chain contract that is compile-time type safe, highly performant and can be generated completely automatically from a compiled contract.

Interacting with a contract on the Electroneum blockchain from Go is already possible via the RPC interfaces exposed by Electroneum clients. However, writing the boilerplate code that translates Go language constructs into RPC calls and back is time consuming and brittle - implementation bugs can only be detected during runtime and it's almost impossible to evolve a contract as even a tiny change in Solidity is awkward to port over to Go. Therefore, ETN-SC provides tools for easily converting contract code into Go code that can be used directly in Go applications.

This page provides an introduction to generating Go contract bindings and using them in a simple Go application.

Prerequisites

This page is fairly beginner-friendly and designed for people starting out with writing Go native dapps. The core concepts will be introduced gradually as a developer would encounter them. However, some basic familiarity with Ethereum, Solidity and Go is assumed.

What is an ABI?

Electroneum smart contracts have a schema that defines its functions and return types in the form of a JSON file. This JSON file is known as an Application Binary Interface, or ABI. The ABI acts as a specification for precisely how to encode data sent to a contract and how to decode the data the contract sends back. The ABI is the only essential piece of information required to generate Go bindings. Go developers can then use the bindings to interact with the contract from their Go application without having to deal directly with data encoding and decoding. An ABI is generated when a contract is compiled.

Abigen: Go binding generator

ETN-SC includes a source code generator called abigen that can convert Electroneum ABI definitions into easy to use, type-safe Go packages. With a valid Go development environment set up and the electroneum-sc repository checked out correctly, abigen can be built as follows:

$ cd $GOPATH/src/github.com/electroneum/electroneum-sc
$ go build ./cmd/abigen

Generating the bindings

To demonstrate the binding generator a contract is required. The contract Storage.sol implements two very simple functions: store updates a user-defined uint256 to the contract's storage, and retrieve displays the value stored in the contract to the user. The Solidity code is as follows:

// SPDX-License-Identifier: GPL-3.0

pragma solidity >0.7.0 < 0.9.0;
/**
* @title Storage
* @dev store or retrieve variable value
*/

contract Storage {

	uint256 value;

	function store(uint256 number) public{
		value = number;
	}

	function retrieve() public view returns (uint256){
		return value;
	}
}

This contract can be pasted into a text file and saved as Storage.sol.

The following code snippet shows how an ABI can be generated for Storage.sol using the Solidity compiler solc.

solc --abi Storage.sol -o build

The ABI can also be generated in other ways such as using the compile commands in development frameworks such as Truffle, Hardhat and Brownie or in the online IDE Remix.

The ABI for Storage.sol (Storage.abi) looks as follows:

[
  {
    "inputs": [],
    "name": "retrieve",
    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [{ "internalType": "uint256", "name": "number", "type": "uint256" }],
    "name": "store",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

The contract binding can then be generated by passing the ABI to abigen as follows:

$ abigen --abi Storage.abi --pkg main --type Storage --out Storage.go

Where the flags are:

  • --abi: Mandatory path to the contract ABI to bind to

  • --pkg: Mandatory Go package name to place the Go code into

  • --type: Optional Go type name to assign to the binding struct

  • --out: Optional output path for the generated Go source file (not set = stdout)

This will generate a type-safe Go binding for the Storage contract. The generated code will look something like the snippet below, the full version of which can be viewed here.

// Code generated - DO NOT EDIT.
// This file is a generated binding and any manual changes will be lost.

package main

import (
	"errors"
	"math/big"
	"strings"

	electroneum "github.com/electroneum/electroneum-sc"
	"github.com/electroneum/electroneum-sc/accounts/abi"
	"github.com/electroneum/electroneum-sc/accounts/abi/bind"
	"github.com/electroneum/electroneum-sc/common"
	"github.com/electroneum/electroneum-sc/core/types"
	"github.com/electroneum/electroneum-sc/event"
)

// Reference imports to suppress errors if they are not otherwise used.
var (
	_ = errors.New
	_ = big.NewInt
	_ = strings.NewReader
	_ = ethereum.NotFound
	_ = bind.Bind
	_ = common.Big1
	_ = types.BloomLookup
	_ = event.NewSubscription
)

// StorageMetaData contains all metadata concerning the Storage contract.
var StorageMetaData = &bind.MetaData{
	ABI: "[{\"inputs\":[],\"name\":\"retrieve\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"number\",\"type\":\"uint256\"}],\"name\":\"store\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]",
}

// StorageABI is the input ABI used to generate the binding from.
// Deprecated: Use StorageMetaData.ABI instead.
var StorageABI = StorageMetaData.ABI

// Storage is an auto generated Go binding around an Ethereum contract.
type Storage struct {
	StorageCaller     // Read-only binding to the contract
	StorageTransactor // Write-only binding to the contract
	StorageFilterer   // Log filterer for contract events
}
...

Storage.go contains all the bindings required to interact with Storage.sol from a Go application. However, this isn't very useful unless the contract is actually deployed on Electroneum mainnet or one of Electroneum's testnets. The following sections will demonstrate how to deploy the contract to the Electroneum testnet and interact with it using the Go bindings.

Deploying contracts to Ethereum

In the previous section, the contract ABI was sufficient for generating the contract bindings from its ABI. However, deploying the contract requires some additional information in the form of the compiled bytecode.

The bytecode is obtained by running the compiler again but this passing the --bin flag, e.g.

solc --bin Storage.sol -o Storage.bin

Then abigen can be run again, this time passing Storage.bin:

$ abigen --abi Storage.abi --pkg main --type Storage --out Storage.go --bin Storage.bin

This will generate something similar to the bindings generated in the previous section. However, an additional DeployStorage function has been injected:

// DeployStorage deploys a new Ethereum contract, binding an instance of Storage to it.
func DeployStorage(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *Storage, error) {
	parsed, err := StorageMetaData.GetAbi()
	if err != nil {
		return common.Address{}, nil, nil, err
	}
	if parsed == nil {
		return common.Address{}, nil, nil, errors.New("GetABI returned nil")
	}

	address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(StorageBin), backend)
	if err != nil {
		return common.Address{}, nil, nil, err
	}
	return address, tx, &Storage{StorageCaller: StorageCaller{contract: contract}, StorageTransactor: StorageTransactor{contract: contract}, StorageFilterer: StorageFilterer{contract: contract}}, nil
}

View the full file here.

The new DeployStorage() function can be used to deploy the contract to the Electroneum testnet from a Go application. To do this requires incorporating the bindings into a Go application that also handles account management, authorization and Electroneum backend to deploy the contract through. Specifically, this requires:

  1. A running ETN-SC node connected to the Electroneum testnet

  2. An account in the keystore prefunded with enough ETN to cover gas costs for deploying and interacting with the contract

Assuming these prerequisites exist, a new ethclient can be instantiated with the local ETN-SC node's ipc file, providing access to the testnet from the Go application. The key can be instantiated as a variable in the application by copying the JSON object from the keyfile in the keystore.

Putting it all together would result in:

package main

import (
	"fmt"
	"log"
	"math/big"
	"strings"
	"time"

	"github.com/electroneum/electroneum-sc/accounts/abi/bind"
	"github.com/electroneum/electroneum-sc/ethclient"

)

const key = `<<json object from keystore>>`

func main() {
	// Create an IPC based RPC connection to a remote node and an authorized transactor
	conn, err := ethclient.Dial("/home/electroneum-sc/testnet/etn-sc.ipc")
	if err != nil {
		log.Fatalf("Failed to connect to the Electroneum client: %v", err)
	}
	auth, err := bind.NewTransactor(strings.NewReader(key), "<<strong_password>>")
	if err != nil {
		log.Fatalf("Failed to create authorized transactor: %v", err)
	}
	// Deploy the contract passing the newly created `auth` and `conn` vars
	address, tx, instance, err := DeployStorage(auth, conn), new(big.Int), "Storage contract in Go!", 0, "Go!")
	if err != nil {
		log.Fatalf("Failed to deploy new storage contract: %v", err)
	}
	fmt.Printf("Contract pending deploy: 0x%x\n", address)
	fmt.Printf("Transaction waiting to be mined: 0x%x\n\n", tx.Hash())

	time.Sleep(250 * time.Millisecond) // Allow it to be processed by the local node :P

	// function call on `instance`. Retrieves pending name
	name, err := instance.Name(&bind.CallOpts{Pending: true})
	if err != nil {
		log.Fatalf("Failed to retrieve pending name: %v", err)
	}
	fmt.Println("Pending name:", name)
}

Running this code requests the creation of a brand new Storage contract on the Testnet blockchain. The contract functions can be called while the contract is waiting to be included in a block.

Contract pending deploy: 0x46506d900559ad005feb4645dcbb2dbbf65e19cc
Transaction waiting to be mined: 0x6a81231874edd2461879b7280ddde1a857162a744e3658ca7ec276984802183b

Pending name: Storage contract in Go!

Once the contract deployment has been included in a validated block, the contract exists permanently at its deployment address and can now be interacted with from other applications without ever needing to be redeployed.

Note that DeployStorage returns four variables:

  • address: the deployment address of the contract

  • tx: the transaction hash that can be queried using ETN-SC

  • instance: an instance of the deployed contract whose functions can be called in the Go application

  • err: a variable that handles errors in case of a deployment failure

Accessing an Electroneum contract

To interact with a contract already deployed on the blockchain, the deployment address is required and a backend through which to access Electroneum must be defined. The binding generator provides an RPC backend out-of-the-box that can be used to attach to an existing Electroneum node via IPC, HTTP or WebSockets.

As in the previous section, a ETN-SC node running on the Electroneum testnet and an account with some test ETN to cover gas is required. The Storage.sol deployment address is also needed.

Again, an instance of ethclient can be created, passing the path to ETN-SC's ipc file. In the example below this backend is assigned to the variable conn.

// Create an IPC based RPC connection to a remote node
// NOTE update the path to the ipc file!
conn, err := ethclient.Dial("/home/electroneum-sc/testnet/etn-sc.ipc")
if err != nil {
	log.Fatalf("Failed to connect to the Electroneum client: %v", err)
}

The functions available for interacting with the Storage contract are defined in Storage.go. To create a new instance of the contract in a Go application, the NewStorage() function can be used. The function is defined in Storage.go as follows:

// NewStorage creates a new instance of Storage, bound to a specific deployed contract.
func NewStorage(address common.Address, backend bind.ContractBackend) (*Storage, error) {
	contract, err := bindStorage(address, backend, backend, backend)
	if err != nil {
		return nil, err
	}
	return &Storage{StorageCaller: StorageCaller{contract: contract}, StorageTransactor: StorageTransactor{contract: contract}, StorageFilterer: StorageFilterer{contract: contract}}, nil
}

NewStorage() takes two arguments: the deployment address and a backend (conn) and returns an instance of the deployed contract. In the example below, the instance is assigned to store.

package main

import (
	"fmt"
	"log"

	"github.com/electroneum/electroneum-sc/common"
	"github.com/electroneum/electroneum-sc/ethclient"
)

func main() {
	// Create an IPC based RPC connection to a remote node
	// NOTE update the path to the ipc file!
	conn, err := ethclient.Dial("/home/electroneum-sc/testnet/etn-sc.ipc")
	if err != nil {
		log.Fatalf("Failed to connect to the Electroneum client: %v", err)
	}
	// Instantiate the contract and display its name
	// NOTE update the deployment address!
	store, err := NewStorage(common.HexToAddress("0x21e6fc92f93c8a1bb41e2be64b4e1f88a54d3576"), conn)
	if err != nil {
		log.Fatalf("Failed to instantiate Storage contract: %v", err)
	}

The contract instance is then available to interact with in the Go application. To read a value from the blockchain, for example the value stored in the contract, the contract's Retrieve() function can be called. Again, the function is defined in Storage.go as follows:

// Retrieve is a free data retrieval call binding the contract method 0x2e64cec1.
//
// Solidity: function retrieve() view returns(uint256)
func (_Storage *StorageCaller) Retrieve(opts *bind.CallOpts) (*big.Int, error) {
	var out []interface{}
	err := _Storage.contract.Call(opts, &out, "retrieve")

	if err != nil {
		return *new(*big.Int), err
	}

	out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int)

	return out0, err

}

Note that the Retrieve() function requires a parameter to be passed, even though the original Solidity contract didn't require any at all none. The parameter required is a *bind.CallOpts type, which can be used to fine tune the call. If no adjustments to the call are required, pass nil. Adjustments to the call include:

  • Pending: Whether to access pending contract state or the current stable one

  • GasLimit: Place a limit on the computing resources the call might consume

So to call the Retrieve() function in the Go application:

value, err := store.Retrieve(nil)
if err != nil {
	log.Fatalf("Failed to retrieve value: %v", err)
}
fmt.Println("Value: ", value)
}

The output will be something like:

Value: 56

Transacting with an Electroneum contract

Invoking a method that changes contract state (i.e. transacting) is a bit more involved, as a live transaction needs to be authorized and broadcast into the network. Go bindings require local signing of transactions and do not delegate this to a remote node. This is to keep accounts private within dapps, and not shared (by default) between them.

Thus to allow transacting with a contract, your code needs to implement a method that given an input transaction, signs it and returns an authorized output transaction. Since most users have their keys in the Web3 Secret Storage format, the bind package contains a small utility method (bind.NewTransactor(keyjson, passphrase)) that can create an authorized transactor from a key file and associated password, without the user needing to implement key signing themselves.

Changing the previous code snippet to update the value stored in the contract:

package main

import (
	"fmt"
	"log"
	"math/big"
	"strings"

	"github.com/electroneum/electroneum-sc/accounts/abi/bind"
	"github.com/electroneum/electroneum-sc/common"
	"github.com/electroneum/electroneum-sc/ethclient"
)

const key = `json object from keystore`

func main() {
	// Create an IPC based RPC connection to a remote node and instantiate a contract binding
	conn, err := ethclient.Dial("/home/electroneum-sc/testnet/etn-sc.ipc")
	if err != nil {
		log.Fatalf("Failed to connect to the Electroneum client: %v", err)
	}
	store, err := NewStorage(common.HexToAddress("0x21e6fc92f93c8a1bb41e2be64b4e1f88a54d3576"), conn)
	if err != nil {
		log.Fatalf("Failed to instantiate a Storage contract: %v", err)
	}
	// Create an authorized transactor and call the store function
	auth, err := bind.NewStorageTransactor(strings.NewReader(key), "strong_password")
	if err != nil {
		log.Fatalf("Failed to create authorized transactor: %v", err)
	}
	// Call the store() function
	tx, err := store.Store(auth, big.NewInt(420))
	if err != nil {
		log.Fatalf("Failed to update value: %v", err)
	}
	fmt.Printf("Update pending: 0x%x\n", tx.Hash())
}

And the output:

Update pending: 0x4f4aaeb29ed48e88dd653a81f0b05d4df64a86c99d4e83b5bfeb0f0006b0e55b

Similar to the method invocations in the previous section which only read contract state, transacting methods also require a mandatory first parameter, a *bind.TransactOpts type, which authorizes the transaction and potentially fine tunes it:

  • From: Address of the account to invoke the method with (mandatory)

  • Signer: Method to sign a transaction locally before broadcasting it (mandatory)

  • Nonce: Account nonce to use for the transaction ordering (optional)

  • GasLimit: Place a limit on the computing resources the call might consume (optional)

  • GasPrice: Explicitly set the gas price to run the transaction with (optional)

  • Value: Any funds to transfer along with the method call (optional)

The two mandatory fields are automatically set by the bind package if the auth options are constructed using bind.NewTransactor. The nonce and gas related fields are automatically derived by the binding if they are not set. Unset values are assumed to be zero.

Pre-configured contract sessions

Reading and state modifying contract-calls require a mandatory first parameter which can authorize and fine tune some of the internal parameters. However, most of the time the same accounts and parameters will be used to issue many transactions, so constructing the call/transact options individually quickly becomes unwieldy.

To avoid this, the generator also creates specialized wrappers that can be pre-configured with tuning and authorization parameters, allowing all the Solidity defined methods to be invoked without needing an extra parameter.

These are named similarly to the original contract type name but suffixed with Sessions:

// Wrap the Storage contract instance into a session
session := &StorageSession{
	Contract: store,
	CallOpts: bind.CallOpts{
		Pending: true,
	},
	TransactOpts: bind.TransactOpts{
		From:     auth.From,
		Signer:   auth.Signer,
		GasLimit: big.NewInt(3141592),
	},
}
// Call the previous methods without the option parameters
session.Store(big.NewInt(69))

Bind Solidity directly

The compilation and binding steps can be joined together into a pipeline, for example:

solc Storage.sol --combined-json abi,bin | abigen --pkg main --type storage --out Storage.go --combined-json -

Project integration (go generate)

The abigen command was made in such a way as to integrate easily into existing Go toolchains: instead of having to remember the exact command needed to bind an Electroneum contract into a Go project, go generate can handle all the fine details.

Place the binding generation command into a Go source file before the package definition:

//go:generate abigen --sol Storage.sol --pkg main --out Storage.go

After which whenever the Solidity contract is modified, instead of needing to remember and run the above command, we can simply call go generate on the package (or even the entire source tree via go generate ./...), and it will correctly generate the new bindings for us.

Blockchain simulator

Being able to deploy and access deployed Electroneum contracts from native Go code is a powerful feature. However, using public testnets as a backend does not lend itself well to automated unit testing. Therefore, ETN-SC also implements a simulated blockchain that can be set as a backend to native contracts the same way as a live RPC backend, using the command backends.NewSimulatedBackend(genesisAccounts). The code snippet below shows how this can be used as a backend in a Go application.

package main

import (
	"fmt"
	"log"
	"math/big"

	"github.com/electroneum/electroneum-sc/accounts/abi/bind"
	"github.com/electroneum/electroneum-sc/accounts/abi/bind/backends"
	"github.com/electroneum/electroneum-sc/core"
	"github.com/electroneum/electroneum-sc/crypto"
)

func main() {
	// Generate a new random account and a funded simulator
	key, _ := crypto.GenerateKey()
	auth := bind.NewKeyedTransactor(key)

	sim := backends.NewSimulatedBackend(core.GenesisAccount{Address: auth.From, Balance: big.NewInt(10000000000)})

	// instantiate contract
	store, err := NewStorage(common.HexToAddress("0x21e6fc92f93c8a1bb41e2be64b4e1f88a54d3576"), sim)
	if err != nil {
		log.Fatalf("Failed to instantiate a Storage contract: %v", err)
	}
	// Create an authorized transactor and call the store function
	auth, err := bind.NewStorageTransactor(strings.NewReader(key), "strong_password")
	if err != nil {
		log.Fatalf("Failed to create authorized transactor: %v", err)
	}
	// Call the store() function
	tx, err := store.Store(auth, big.NewInt(420))
	if err != nil {
		log.Fatalf("Failed to update value: %v", err)
	}
	fmt.Printf("Update pending: 0x%x\n", tx.Hash())
}

Summary

To make interacting with Electroneum contracts easier for Go developers, ETN-SC provides tools that generate contract bindings automatically. This makes contract functions available in Go native applications.

Last updated