paint-brush
Create an API to interact with Ethereum Blockchain using Golang PART 1by@luisacerv
9,105 reads
9,105 reads

Create an API to interact with Ethereum Blockchain using Golang PART 1

by LuisAcervOctober 17th, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this tutorial, we are going to learn how to create a simple REST API to interact with the Ethereum blockchain using Golang. We will use Web3.js, geth and go-ethereum libraries for interacting with the blockchain. Go is the de-facto library to interact in JavaScript and Node.js. For this tutorial I chose to use Go as our weapon, Go is written in go 1.13.8 for this tutorial. We need a provider, a provider is a node we have access to make RPC calls over the internet. We are using Go to set up a local virtual node using ganache.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Create an API to interact with Ethereum Blockchain using Golang PART 1
LuisAcerv HackerNoon profile picture

Hi folks! In this tutorial, we are going to learn how to create a simple REST API to interact with the Ethereum blockchain using Golang

Web3.js is the de-facto library to interact for interacting with Ethereum in JavaScript and Node.js. It takes care of encoding payloads and generating the RPC calls. Web3.js is very popular and heavily documented.

On the other hand, (geth), the most popular Ethereum implementation, is written in Go. It’s a complete Ethereum node. If you build a dApp in Go, then you’ll be using the go-ethereum libraries directly which means you can do everything the node can do.

So, for this tutorial, I chose to use Go as our weapon.

In simple terms, interacting with the blockchain means that you will be making RPC calls over HTTP. Any language will be able to do that for you but, using a compiled language such as Go will give you a better performance… If performance is important to you or your team.

Enough of boring introduction!

For our tutorial, we are going to set up four endpoints:

Get the latest blockGet transaction by hashGet address balanceTransfer ether to an address

This is not a big deal, but I think is cool as a starting point to more complex implementations.

If you want to get the whole code you can download it here.

SET UP

I used go 1.13.8 for this tutorial, to get your Go version just run:

$ go version
# outputs
go version go1.13.8 darwin/amd64

First we are going to create our project by running the following command:

$ go mod init

That will create at the root of your working directory the file

go.mod
with the following content:

module github.com/LuisAcerv/goeth-api

go 1.13

Now let's create our project structure. I have to say here that you can use the structure that better fits your needs.

At the root of your project create a new

main.go
file.

$ echo "package main" >> main.go

Now we need to create three directories:

$ mkdir handler
$ mkdir models
$ mkdir modules

And inside each of those folders we are going to create a

main.go
file, and we should have the following structure:

.
├── handler
│   └── main.go
├── models
│   └── main.go
├── modules
│   └── main.go
├── go.mod
├── go.sum
├── main.go

Ganache-CLI

In order to interact with the Ethereum blockchain, we need a provider, a provider is a node we have access to make RPC calls over HTTP. You can use a testnet such as ropsten or kovan through a provider such as Infura, but for this tutorial, we are going to set up a local virtual node using ganache.

At the official truffle website you can download ganache, is pretty easy to set up and will give you all you need for testing proposes. It comes with a nice UI which will show you the transactions, accounts and logs of your "node".

Once you have ganache installed and running we are good to start writing some code.

We have a

./models/main.go
file. This file contains the structures we are going to use in our API.

We add the following content:

package models

// Block data structure
type Block struct {
	BlockNumber       int64         `json:"blockNumber"`
	Timestamp         uint64        `json:"timestamp"`
	Difficulty        uint64        `json:"difficulty"`
	Hash              string        `json:"hash"`
	TransactionsCount int           `json:"transactionsCount"`
	Transactions      []Transaction `json:"transactions"`
}

// Transaction data structure
type Transaction struct {
	Hash     string `json:"hash"`
	Value    string `json:"value"`
	Gas      uint64 `json:"gas"`
	GasPrice uint64 `json:"gasPrice"`
	Nonce    uint64 `json:"nonce"`
	To       string `json:"to"`
	Pending  bool   `json:"pending"`
}

// TransferEthRequest data structure
type TransferEthRequest struct {
	PrivKey string `json:"privKey"`
	To      string `json:"to"`
	Amount  int64  `json:"amount"`
}

// HashResponse data structure
type HashResponse struct {
	Hash string `json:"hash"`
}

// BalanceResponse data structure
type BalanceResponse struct {
	Address string `json:"address"`
	Balance string `json:"balance"`
	Symbol  string `json:"symbol"`
	Units   string `json:"units"`
}

// Error data structure
type Error struct {
	Code    uint64 `json:"code"`
	Message string `json:"message"`
}

Now that we have our models defined and ready to be used, we are going to create the methods in charge of interacting with the blockchain.

First of all we want to install the

go-ethereum
module, and we can do that by running the following command:

$ go get -u github.com/ethereum/go-ethereum

In our

./modules/main.go
we are going to create a function that retrieves the latest block from the blockchain.

This function is going to give us the information about the block and the transactions embedded in it.

func GetLatestBlock(client ethclient.Client) *Models.Block {
	// We add a recover function from panics to prevent our API from crashing due to an unexpected error
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()

	// Query the latest block
	header, _ := client.HeaderByNumber(context.Background(), nil)
	blockNumber := big.NewInt(header.Number.Int64())
	block, err := client.BlockByNumber(context.Background(), blockNumber)

	if err != nil {
		log.Fatal(err)
	}

	// Build the response to our model
	_block := &Models.Block{
		BlockNumber:       block.Number().Int64(),
		Timestamp:         block.Time(),
		Difficulty:        block.Difficulty().Uint64(),
		Hash:              block.Hash().String(),
		TransactionsCount: len(block.Transactions()),
		Transactions:      []Models.Transaction{},
	}

	for _, tx := range block.Transactions() {
		_block.Transactions = append(_block.Transactions, Models.Transaction{
			Hash:     tx.Hash().String(),
			Value:    tx.Value().String(),
			Gas:      tx.Gas(),
			GasPrice: tx.GasPrice().Uint64(),
			Nonce:    tx.Nonce(),
			To:       tx.To().String(),
		})
	}

	return _block
}

Now, we want to have a function to retrieve information about a given transaction, for example, if we transfer ether to from one account to another, the API will respond with de transaction hash, and we can use it to get the transaction information.

To do that we add a new function to our

./modules/main.go

// GetTxByHash by a given hash
func GetTxByHash(client ethclient.Client, hash common.Hash) *Models.Transaction {

	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()

	tx, pending, err := client.TransactionByHash(context.Background(), hash)
	if err != nil {
		fmt.Println(err)
	}

	return &Models.Transaction{
		Hash:     tx.Hash().String(),
		Value:    tx.Value().String(),
		Gas:      tx.Gas(),
		GasPrice: tx.GasPrice().Uint64(),
		To:       tx.To().String(),
		Pending:  pending,
		Nonce:    tx.Nonce(),
	}
}

Another thing we want to know is our balance, for that we need to add another function.

// GetAddressBalance returns the given address balance =P
func GetAddressBalance(client ethclient.Client, address string) (string, error) {
	account := common.HexToAddress(address)
	balance, err := client.BalanceAt(context.Background(), account, nil)
	if err != nil {
		return "0", err
	}

	return balance.String(), nil
}

The last function we are going to add into our modules package is the one that will allow us to send ether from one account to another.

And I want to make a parenthesis here. This function requires that the client sends the sender private key to sign the transaction, that means that your client will broadcast the user's private key through HTTP and if you are going to do something like this using with this code or another else, then at least make sure you are using HTTPS.

Said that let's add the function to our modules package.

func TransferEth(client ethclient.Client, privKey string, to string, amount int64) (string, error) {

	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()

	// Assuming you've already connected a client, the next step is to load your private key.
	privateKey, err := crypto.HexToECDSA(privKey)
	if err != nil {
		return "", err
	}

	// Function requires the public address of the account we're sending from -- which we can derive from the private key.
	publicKey := privateKey.Public()
	publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
	if !ok {
		return "", err
	}

	fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)

	// Now we can read the nonce that we should use for the account's transaction.
	nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
	if err != nil {
		return "", err
	}

	value := big.NewInt(amount) // in wei (1 eth)
	gasLimit := uint64(21000)   // in units
	gasPrice, err := client.SuggestGasPrice(context.Background())
	if err != nil {
		return "", err
	}

	// We figure out who we're sending the ETH to.
	toAddress := common.HexToAddress(to)
	var data []byte

	// We create the transaction payload
	tx := types.NewTransaction(nonce, toAddress, value, gasLimit, gasPrice, data)

	chainID, err := client.NetworkID(context.Background())
	if err != nil {
		return "", err
	}

	// We sign the transaction using the sender's private key
	signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
	if err != nil {
		return "", err
	}

	// Now we are finally ready to broadcast the transaction to the entire network
	err = client.SendTransaction(context.Background(), signedTx)
	if err != nil {
		return "", err
	}

	// We return the transaction hash
	return signedTx.Hash().String(), nil
}

Nice! we have a function to transfer ether, now all together:

package modules

import (
	"context"
	"crypto/ecdsa"
	"fmt"
	"log"
	"math/big"

	Models "github.com/LuisAcerv/goeth-api/models"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
)

// GetLatestBlock from blockchain
func GetLatestBlock(client ethclient.Client) *Models.Block {
	// We add a recover function from panics to prevent our API from crashing due to an unexpected error
	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()

	// Query the latest block
	header, _ := client.HeaderByNumber(context.Background(), nil)
	blockNumber := big.NewInt(header.Number.Int64())
	block, err := client.BlockByNumber(context.Background(), blockNumber)

	if err != nil {
		log.Fatal(err)
	}

	// Build the response to our model
	_block := &Models.Block{
		BlockNumber:       block.Number().Int64(),
		Timestamp:         block.Time(),
		Difficulty:        block.Difficulty().Uint64(),
		Hash:              block.Hash().String(),
		TransactionsCount: len(block.Transactions()),
		Transactions:      []Models.Transaction{},
	}

	for _, tx := range block.Transactions() {
		_block.Transactions = append(_block.Transactions, Models.Transaction{
			Hash:     tx.Hash().String(),
			Value:    tx.Value().String(),
			Gas:      tx.Gas(),
			GasPrice: tx.GasPrice().Uint64(),
			Nonce:    tx.Nonce(),
			To:       tx.To().String(),
		})
	}

	return _block
}

// GetTxByHash by a given hash
func GetTxByHash(client ethclient.Client, hash common.Hash) *Models.Transaction {

	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()

	tx, pending, err := client.TransactionByHash(context.Background(), hash)
	if err != nil {
		fmt.Println(err)
	}

	return &Models.Transaction{
		Hash:     tx.Hash().String(),
		Value:    tx.Value().String(),
		Gas:      tx.Gas(),
		GasPrice: tx.GasPrice().Uint64(),
		To:       tx.To().String(),
		Pending:  pending,
		Nonce:    tx.Nonce(),
	}
}

// TransferEth from one account to another
func TransferEth(client ethclient.Client, privKey string, to string, amount int64) (string, error) {

	defer func() {
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()

	// Assuming you've already connected a client, the next step is to load your private key.
	privateKey, err := crypto.HexToECDSA(privKey)
	if err != nil {
		return "", err
	}

	// Function requires the public address of the account we're sending from -- which we can derive from the private key.
	publicKey := privateKey.Public()
	publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
	if !ok {
		return "", err
	}

	fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)

	// Now we can read the nonce that we should use for the account's transaction.
	nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
	if err != nil {
		return "", err
	}

	value := big.NewInt(amount) // in wei (1 eth)
	gasLimit := uint64(21000)   // in units
	gasPrice, err := client.SuggestGasPrice(context.Background())
	if err != nil {
		return "", err
	}

	// We figure out who we're sending the ETH to.
	toAddress := common.HexToAddress(to)
	var data []byte

	// We create the transaction payload
	tx := types.NewTransaction(nonce, toAddress, value, gasLimit, gasPrice, data)

	chainID, err := client.NetworkID(context.Background())
	if err != nil {
		return "", err
	}

	// We sign the transaction using the sender's private key
	signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
	if err != nil {
		return "", err
	}

	// Now we are finally ready to broadcast the transaction to the entire network
	err = client.SendTransaction(context.Background(), signedTx)
	if err != nil {
		return "", err
	}

	// We return the transaction hash
	return signedTx.Hash().String(), nil
}

// GetAddressBalance returns the given address balance =P
func GetAddressBalance(client ethclient.Client, address string) (string, error) {
	account := common.HexToAddress(address)
	balance, err := client.BalanceAt(context.Background(), account, nil)
	if err != nil {
		return "0", err
	}

	return balance.String(), nil
}

Now we need to set up our API to interact with the functions we wrote through HTTP endpoints. To do this we are going to use gorilla/mux.

In our main file we are going the add the following content:

package main

import (
	"fmt"
	"log"
	"net/http"

	Handlers "github.com/LuisAcerv/goeth-api/handler"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/gorilla/mux"
)

func main() {
	// Create a client instance to connect to our providr
	client, err := ethclient.Dial("http://localhost:7545")

	if err != nil {
		fmt.Println(err)
	}

	// Create a mux router
	r := mux.NewRouter()

	// We will define a single endpoint
	r.Handle("/api/v1/eth/{module}", Handlers.ClientHandler{client})
	log.Fatal(http.ListenAndServe(":8080", r))
}

Now we need to create handler for our endpoint, since we have added a parameter

module
to our endpoint we will be able to handle our functions with a single handler. As I said before feel free to use the architecture you wish for your own project.

Now in our

./handler/main.go
we are going to add the following content:

package handlers

import (
	"encoding/json"
	"fmt"
	"net/http"

	Models "github.com/LuisAcerv/goeth-api/models"
	Modules "github.com/LuisAcerv/goeth-api/modules"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/gorilla/mux"
)

// ClientHandler ethereum client instance
type ClientHandler struct {
	*ethclient.Client
}

func (client ClientHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Get parameter from url request
	vars := mux.Vars(r)
	module := vars["module"]

	// Get the query parameters from url request
	address := r.URL.Query().Get("address")
	hash := r.URL.Query().Get("hash")

	// Set our response header
	w.Header().Set("Content-Type", "application/json")

	// Handle each request using the module parameter:
	switch module {
	case "latest-block":
		_block := Modules.GetLatestBlock(*client.Client)
		json.NewEncoder(w).Encode(_block)

	case "get-tx":
		if hash == "" {
			json.NewEncoder(w).Encode(&Models.Error{
				Code:    400,
				Message: "Malformed request",
			})
			return
		}
		txHash := common.HexToHash(hash)
		_tx := Modules.GetTxByHash(*client.Client, txHash)

		if _tx != nil {
			json.NewEncoder(w).Encode(_tx)
			return
		}

		json.NewEncoder(w).Encode(&Models.Error{
			Code:    404,
			Message: "Tx Not Found!",
		})

	case "send-eth":
		decoder := json.NewDecoder(r.Body)
		var t Models.TransferEthRequest

		err := decoder.Decode(&t)

		if err != nil {
			fmt.Println(err)
			json.NewEncoder(w).Encode(&Models.Error{
				Code:    400,
				Message: "Malformed request",
			})
			return
		}
		_hash, err := Modules.TransferEth(*client.Client, t.PrivKey, t.To, t.Amount)

		if err != nil {
			fmt.Println(err)
			json.NewEncoder(w).Encode(&Models.Error{
				Code:    500,
				Message: "Internal server error",
			})
			return
		}

		json.NewEncoder(w).Encode(&Models.HashResponse{
			Hash: _hash,
		})

	case "get-balance":
		if address == "" {
			json.NewEncoder(w).Encode(&Models.Error{
				Code:    400,
				Message: "Malformed request",
			})
			return
		}

		balance, err := Modules.GetAddressBalance(*client.Client, address)

		if err != nil {
			fmt.Println(err)
			json.NewEncoder(w).Encode(&Models.Error{
				Code:    500,
				Message: "Internal server error",
			})
			return
		}

		json.NewEncoder(w).Encode(&Models.BalanceResponse{
			Address: address,
			Balance: balance,
			Symbol:  "Ether",
			Units:   "Wei",
		})

	}

}

That's it, if we go to our terminal and run:

$ go run main.go

We can start testing our API. Make sure you are running your ganache instance and in your browser go to: http://localhost:8080/api/v1/eth/latest-block and if everything is ok then you should see something like this:

Now let's try to transfer some ether from one account to another, first copy the private key of the sender from ganache:

And also copy an address to send the ether:

Now using

curl
, let's make a transfer!:

$ curl -d '{"privKey":"12a770fe34a793800abaab0a48b7a394ae440b9117d000178af81b61cda8ff15", "to":"0xa8Ce5Fb2DAB781B8f743a8096601eB01Ff0a246d", "amount":1000000000000000000}' -H "Content-Type: application/json" -X POST http://localhost:8080/api/v1/eth/send-eth

You should receive the transaction hash as response:

{"hash":"0xa5417ae03a817e41ddf36303f3ea985e6bd64a504c662d988bcb47913be8d472"}

Now let's get the transaction information using our API, go to: http://localhost:8080/api/v1/eth/get-tx?hash=<tx-hash-from-response>

And you should see something like this:

And finally, let's check the balance of the address by going to http://localhost:8080/api/v1/eth/get-balance?address=<the-recipient-address>

That's it, we have created a simple API to start interacting with the Ethereum blockchain and perform some basic actions.

In the following part we are going to improve our existing code and we are going to add functionality to interact with smart contracts and ERC20 tokens.

Repository: https://github.com/LuisAcerv/goeth-api

Check out this tutorial on how create bitcoin HD Wallet using Go.

And that's it, if you want to talk then follow me on twitter.

See you soon with the next part or in another coding adventure.

Happy hacking!