It’s time to Blockchain with Golang. If you haven’t seen my previous post Part 1 Introduction and Part 2 Hardhat , please read them first.
Again, does Dapp need a Backend?
reddit
You can pretty much make a dapp without backend, but there are some things that can’t be done with a smart contract.
You need a backend among other reasons for off-chain or metadata that won’t be stored in the smart contracts.
Have you ever thought about how Moralis works?
Off-chain : Backend infrastructure that collects data from the blockchain, offers an API to clients like web apps and mobile apps, indexes the blockchain, provides real-time alerts, coordinates events that are happening on different chains, handles the user life-cycle and so much more. Moralis Dapp is used in order to speed up the implementation of the off-chain infrastructure. Moralis Dapp is a bundled solution of all the features most Dapps need in order to get going as soon as possible.
Although I’m more familiar with Node.js and Ruby than Golang, I choose Golang Gin web framework to implement a backend, and use go-ethereum to communicate with blockchain. But I totally have no idea about organizing Golang/Gin file structure, Ruby on rails and Adonis / Node.js handles it very well. After some survey, I find a Go clean architechture repo. It file structure is similar to RoR and Adonis, and it integrates with Go Fx to make dependency injection easy. That’s why I use it in my backend project.
Files and Folder structure
Let me focus on DApp part, and just skip the folder structure introduction.
Docker The Docker compose includes 3 services such as api
, database
, and adminer
. There are many files so I hope you can go through the repo yourself.
Migration database Check Makefile
and migration
first. It’s important to run migration in the beginning.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 include .envMIGRATE=docker-compose exec api sql-migrate ifeq ($(p) ,host) MIGRATE=sql-migrate endif migrate-status: $(MIGRATE) status migrate-up: $(MIGRATE) up migrate-down: $(MIGRATE) down redo: @read -p "Are you sure to reapply the last migration? [y/n]" -n 1 -r; \ if [[ $$REPLY =~ ^[Yy] ]]; \ then \ $(MIGRATE) redo; \ fi create: @read -p "What is the name of migration?" NAME; \ ${MIGRATE} new $$NAME .PHONY : migrate-status migrate-up migrate-down redo create
migration/xxxxxxx-create_users_table.sql
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 CREATE TABLE IF NOT EXISTS `users` ( `id` BINARY (16 ) NOT NULL , `email` VARCHAR (100 ) NOT NULL , `name` VARCHAR (20 ) NOT NULL , `birthday` DATETIME, `wallet_address` VARCHAR (42 ) NOT NULL , `member_number` VARCHAR (100 ), `created_at` DATETIME NOT NULL , `updated_at` DATETIME NOT NULL , PRIMARY KEY (`id`), CONSTRAINT email_unique UNIQUE (email), CONSTRAINT wallet_address_unique UNIQUE INDEX (wallet_address) )ENGINE = InnoDB DEFAULT CHARSET= utf8mb4; DROP TABLE IF EXISTS `users`;
The migration shows to create a users
tables when migrate up, and I use the following command to run migration:
make migrate-up
Step 1: Authencation Users need to sign up or sign in when they clicked the connect wallet button, so I add auth route and controller logics first.
api/routes/auth_route.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package routesimport ( "hardhat-backend/api/controllers" "hardhat-backend/infrastructure" "hardhat-backend/lib/loggers" ) type AuthRoutes struct { logger loggers.Logger handler infrastructure.Router authController controllers.JWTAuthController } func (s *AuthRoutes) Setup() { s.logger.Info("Setting up routes" ) auth := s.handler.Group("/api" ).Group("/auth" ) { auth.POST("/login" , s.authController.SignIn) } } func NewAuthRoutes ( handler infrastructure.Router, authController controllers.JWTAuthController, logger loggers.Logger, ) *AuthRoutes { return &AuthRoutes{ handler: handler, logger: logger, authController: authController, } }
api/controllers/jwt_auth_controller.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package controllersimport ( "hardhat-backend/api_errors" "hardhat-backend/lib/loggers" "hardhat-backend/services" "hardhat-backend/utils" "github.com/gin-gonic/gin" ) type JWTAuthController struct { logger loggers.Logger service services.JWTAuthService userService *services.UserService } type SignInRequest struct { WalletAddress string `json:"walletAddress"` } func NewJWTAuthController ( logger loggers.Logger, service services.JWTAuthService, userService *services.UserService, ) JWTAuthController { return JWTAuthController{ logger: logger, service: service, userService: userService, } } func (jwt JWTAuthController) SignIn(c *gin.Context) { jwt.logger.Info("SignIn route called" ) var request SignInRequest err := c.BindJSON(&request) if err != nil { utils.HandleValidationError(jwt.logger, c, api_errors.ErrInvalidRequest) return } walletAddress := request.WalletAddress user, newUser, err := jwt.userService.GetOneUserByWalletAddress(walletAddress) if err != nil { utils.HandleError(jwt.logger, c, err) return } c.JSON(200 , gin.H{ "message" : "logged in successfully" , "data" : gin.H{ "user" : user, "newUser" : newUser, }, }) }
services/user_service.go
1 2 3 4 5 6 7 8 ... func (s UserService) GetOneUserByWalletAddress(walletAddress string ) (user models.User, newUser int64 , err error ) { result := s.repository.FirstOrCreate(&user, models.User{WalletAddress: walletAddress}) return user, result.RowsAffected, result.Error } ...
The UserService called Gorm FirstOrCreate
method to get the user by WalletAddress
string. Hence the /api/auth/login
endpoint can return the user to frontend.
Step 2: Sign-in with Ethereum (SIWE) Sometimes, I need to verify the message from client, so I add SIWE controller to handle it.
I’m supposed to add an endpoint to get nonce
, but this version nonce
is generated from frontend.
api/routes/siwe_route.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package routesimport ( "hardhat-backend/api/controllers" "hardhat-backend/infrastructure" "hardhat-backend/lib/loggers" ) type SiweRoutes struct { logger loggers.Logger handler infrastructure.Router siweController controllers.SiweController } func (s *SiweRoutes) Setup() { s.logger.Info("Setting up routes" ) api := s.handler.Group("/api" ) { api.POST("/siwe/verify" , s.siweController.PostVerify) } } func NewSiweRoutes ( handler infrastructure.Router, logger loggers.Logger, siweController controllers.SiweController, ) *SiweRoutes { return &SiweRoutes{ handler: handler, logger: logger, siweController: siweController, } }
api/controllers/siwe_controller.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package controllersimport ( "hardhat-backend/api_errors" "hardhat-backend/lib/loggers" "hardhat-backend/services" "hardhat-backend/utils" "github.com/gin-gonic/gin" ) type SiweController struct { service *services.SiweService logger loggers.Logger } type VerifyRequest struct { Message string `json:"message"` Signature string `json:"signature"` } func NewSiweController (siweService *services.SiweService, logger loggers.Logger) SiweController { return SiweController{ service: siweService, logger: logger, } } func (s SiweController) PostVerify(c *gin.Context) { var request VerifyRequest err := c.BindJSON(&request) if err != nil { utils.HandleValidationError(s.logger, c, api_errors.ErrInvalidRequest) return } _, err = s.service.Verify(request.Message, request.Signature) if err != nil { utils.HandleError(s.logger, c, err) return } c.JSON(200 , gin.H{ "data" : "Verified" , }) }
In the SiweService
, I install the Sign-In with Ethereum
package siwe-go , which is the same package I used in Next.js frontend.
SIWE-go usage: https://docs.login.xyz/libraries/go
services/siwe_service.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package servicesimport ( "hardhat-backend/lib/loggers" siwe "github.com/spruceid/siwe-go" ) type SiweService struct { logger loggers.Logger } func NewSiweService ( logger loggers.Logger, ) *SiweService { return &SiweService{ logger: logger, } } func (s SiweService) Verify(messageStr string , signature string ) (message *siwe.Message, err error ) { message, err = siwe.ParseMessage(messageStr) if err != nil { return nil , err } _, err = message.Verify(signature, nil , nil , nil ) if err != nil { return nil , err } return message, err }
Final Step: Interact with contract:
Final step should be a focal point of this post. Go-etherneum !
In order to follow the project structure, I have to create Ether.go
lib to manipulate go-ethereum
. In Ether.go
, I have 2 functions with different ways to get greeting , a function to post greeting , a function to get account balance , and a internal function to get auth .
lib/ether.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 package libimport ( "bytes" "context" "crypto/ecdsa" "encoding/hex" "hardhat-backend/config" "hardhat-backend/lib/loggers" "math" "math/big" "strings" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" greeter "hardhat-backend/contracts" ) type EtherClient struct { *ethclient.Client loggers.Logger Env *config.Env } func NewEther (env *config.Env, logger loggers.Logger) EtherClient { client, err := ethclient.Dial(env.EthereumURL) if err != nil { logger.Error(err.Error()) } defer client.Close() logger.Info("we have a connection" ) _ = client return EtherClient{ Client: client, Logger: logger, Env: env, } } func (c EtherClient) GetBalance(address string ) *big.Float { account := common.HexToAddress(address) balance, err := c.Client.BalanceAt(context.Background(), account, nil ) if err != nil { c.Logger.Error(err.Error()) } fbalance := new (big.Float) fbalance.SetString(balance.String()) ethValue := new (big.Float).Quo(fbalance, big.NewFloat(math.Pow10(18 ))) return ethValue } func (c EtherClient) GetGreeting() string { blockHeader, _ := c.Client.HeaderByNumber(context.Background(), nil ) contractAddr := common.HexToAddress(c.Env.ContractAddr) data, _ := hexutil.Decode("0xcfae3217" ) callMsg := ethereum.CallMsg{ To: &contractAddr, Data: data, } res, err := c.Client.CallContract(context.Background(), callMsg, blockHeader.Number) if err != nil { c.Logger.Fatalf("Error calling contract: %v" , err) } s, _ := hex.DecodeString(common.Bytes2Hex(res[:])) c.Logger.Info(res[:]) c.Logger.Info(string (s)) res = bytes.Trim(res[:], "\x00 \x0c" ) return strings.TrimSpace(string (res[:])) } func (c EtherClient) GetGreetingFromInstance() string { contractAddr := common.HexToAddress(c.Env.ContractAddr) instance, err := greeter.NewGreeter(contractAddr, c.Client) tx, err := instance.Greet(new (bind.CallOpts)) if err != nil { c.Logger.Fatal(err) } return tx } func (c EtherClient) PostGreetingToInstance(greeting string ) string { auth, err := GetAuth(c, c.Env.AccountPrivateKey) contractAddr := common.HexToAddress(c.Env.ContractAddr) instance, err := greeter.NewGreeter(contractAddr, c.Client) tx, err := instance.SetGreeting(auth, greeting) if err != nil { c.Logger.Fatal(err) } c.Logger.Infof("tx sent: %s" , tx.Hash().Hex()) return "ok" } func GetAuth (c EtherClient, accountAddress string ) (*bind.TransactOpts, error ) { privateKey, err := crypto.HexToECDSA(accountAddress) if err != nil { c.Logger.Fatal(err) return nil , err } publicKey := privateKey.Public() publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) if !ok { c.Logger.Fatal("cannot assert type: publicKey is not of type *ecdsa.PublicKey" ) return nil , err } fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) nonce, err := c.Client.PendingNonceAt(context.Background(), fromAddress) if err != nil { c.Logger.Fatal(err) return nil , err } gasPrice, err := c.Client.SuggestGasPrice(context.Background()) if err != nil { c.Logger.Fatal(err) return nil , err } auth := bind.NewKeyedTransactor(privateKey) auth.Nonce = big.NewInt(int64 (nonce)) auth.Value = big.NewInt(0 ) auth.GasLimit = uint64 (300000 ) auth.GasPrice = gasPrice return auth, nil }
GetBalance
: gets account balance from ethclient
.
GetGreeting
: uses contract deployed address c.Env.ContractAddr
and function address 0xcfae3217
in CallMsg
to call solidity greeting(). and
Line 79` to remove some char from the string result.
GetGreetingFromInstance
: imports greeter
module generated from AbiGen
to get solidity greeting()
result.
PostGreetingToInstance
: gets auth with c.Env.AccountPrivateKey
private key that might belong to a contract owner, aka a backend’s administractor. and calls solidity setGreeting(string)
by greeter
instance.
Now, just add route
, controller
, and service
. There are three endpoints:
api/routes/contract_route.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package routesimport ( "hardhat-backend/api/controllers" "hardhat-backend/infrastructure" "hardhat-backend/lib/loggers" ) type ContractRoutes struct { logger loggers.Logger handler infrastructure.Router contractController controllers.ContractController } func (s *ContractRoutes) Setup() { s.logger.Info("Setting up routes" ) api := s.handler.Group("/api" ) { api.GET("/contract/balance/:address" , s.contractController.GetBalance) api.GET("/contract/greeting" , s.contractController.GetGreeting) api.POST("/contract/greeting" , s.contractController.PostGreeting) } } func NewContractRoutes ( logger loggers.Logger, handler infrastructure.Router, contractController controllers.ContractController, ) *ContractRoutes { return &ContractRoutes{ handler: handler, logger: logger, contractController: contractController, } }
api/controllers/contract_controller.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package controllersimport ( "hardhat-backend/api_errors" "hardhat-backend/lib/loggers" "hardhat-backend/services" "hardhat-backend/utils" "github.com/gin-gonic/gin" ) type ContractController struct { service services.ContractService logger loggers.Logger } type GreetingRequest struct { GreetingStr string `json:"greeting"` } func NewContractController (contractService services.ContractService, logger loggers.Logger) ContractController { return ContractController{ service: contractService, logger: logger, } } func (contract ContractController) GetBalance(c *gin.Context) { address := c.Param("address" ) c.JSON(200 , gin.H{"data" : contract.service.GetBalance(address)}) } func (contract ContractController) GetGreeting(c *gin.Context) { c.JSON(200 , gin.H{"data" : contract.service.GetGreeting()}) } func (contract ContractController) PostGreeting(c *gin.Context) { var request GreetingRequest err := c.BindJSON(&request) if err != nil { utils.HandleValidationError(contract.logger, c, api_errors.ErrInvalidRequest) return } c.JSON(200 , gin.H{"data" : contract.service.PostGreeting(request.GreetingStr)}) }
services/contract_service.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package servicesimport ( "hardhat-backend/lib" "hardhat-backend/lib/loggers" "math/big" ) type ContractService struct { logger loggers.Logger client lib.EtherClient } func NewContractService (logger loggers.Logger, client lib.EtherClient) ContractService { return ContractService{ logger: logger, client: client, } } func (c ContractService) GetBalance(address string ) *big.Float { return c.client.GetBalance(address) } func (c ContractService) GetGreeting() string { return c.client.GetGreetingFromInstance() } func (c ContractService) PostGreeting(greeting string ) string { return c.client.PostGreetingToInstance(greeting) }
You can change Line 28 and 29 to try to get greeting by different ways.
Everything’s done. To test it, I use Postman app.
Please also check docker logs for each request.
Get Balance
Get Greeting (initial greeting message)
Set Greeting
To be honestly, I’m a junior in Golang, and I’m still learning how to use Golang. After coding this backend, I’m getting familiar with it.
Okay, backend part is finished! Next article will introduce how to interact with contract with frontend.