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:
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:
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!