No meu último artigo falei sobre como é simples construir aplicações que se utilizam da programação concorrente usando a linguagem Go, vimos que é simples a sua implementação e quão eficiente pode ser, graças à sua construção que fornece aos desenvolvedores ferramentas poderosas para se aproveitar ao máximo o potencial da programação concorrente. Neste artigo iremos ver na prática um passo-a-passo de como construir um ethereum wallet watcher aproveitando-se desse ponto forte de Go Lang.
Contexto
A web3 desde que retornei aos meus estudos de programação sempre foi um tópico pelo qual muito me interesso, sou um entusiasta da tecnologia blockchain por trás da maioria dos produtos desenvolvidos e quem vem sendo adotada em soluções mais tradicionais por assim dizer recentemente. Algumas carteiras têm suas chaves privadas expostas publicamente, sendo a maioria disponibilizada por kits de desenvolvimento onde você pode rodar um nó de testes em ambiente local. Alguns usuários, por descuido e falta de atenção acabam mandando valores para o endereço dessa carteira na rede principal (em produção por assim dizer).
Objetivo
Iremos construir um serviço em GoLang para observar carteiras Ethereum e havendo quaisquer transações de entrada de saldo, tentaremos realizar uma transação de saída para obter esses valores.
Para exemplificar ainda mais o uso da programação concorrente também disponibilizaremos uma API para consulta de informações de um endereço Ethereum.
Em resumo, rodaremos nosso serviço de wallet watcher lado a lado a uma API rest. Vamos lá?
Antes de começarmos
O código fonte deste projeto está em meu GitHub, você pode cloná-lo usando o git clone e rodando na sua máquina, não esqueça de criar um arquivo .env na raiz do projeto seguindo o .env.example
git clone https://github.com/ronilsonalves/go-wallet-watcher.gitCaso deseje construir também a API Rest para consultas, você precisará ter uma chave de API da Etherscan.io, para isso você precisará ter uma conta e solicitar uma chave gratuita em https://etherscan.io/myapikey
Inicializando o projeto Go
Onde salvaremos nosso projeto o iniciaremos, para isso, no terminal precisamos digitar:
go mod init 'project-name'Antes de prosseguir, vamos instalar os módulos do go-ethereum e godotenv que nos ajudarão a construir nosso serviço:
go get github.com/ethereum/go-ethereum
go get github.com/joho/godotenvAgora vamos criar uma pasta chamada internal onde organizaremos os arquivos internos do nosso projeto, dentro dela criaremos outra pasta que será nosso pacote domain e por fim, criaremos nosso arquivo wallet.go que conterá a nossa struct que representará uma carteira Ethereum:
package domain
type Wallet struct {
Address string `json:"address"`
SecretKey string `json:"secret-key,omitempty"`
Balance float64 `json:"balance"`
Transactions []Transaction `json:"transactions,omitempty"`
}Também vamos criar uma struct para representar uma transação, iremos usá-la no futuro:
package domain
type Transaction struct {
BlockNumber string `json:"blockNumber,omitempty"`
TimeStamp string `json:"timeStamp,omitempty"`
Hash string `json:"hash,omitempty"`
Nonce string `json:"nonce,omitempty"`
BlockHash string `json:"blockHash,omitempty"`
TransactionIndex string `json:"transactionIndex,omitempty"`
From string `json:"from,omitempty"`
To string `json:"to,omitempty"`
Value string `json:"value,omitempty"`
Gas string `json:"gas,omitempty"`
GasPrice string `json:"gasPrice,omitempty"`
IsError string `json:"isError,omitempty"`
TxreceiptStatus string `json:"txreceipt_status,omitempty"`
Input string `json:"input,omitempty"`
ContractAddress string `json:"contractAddress,omitempty"`
CumulativeGasUsed string `json:"cumulativeGasUsed,omitempty"`
GasUsed string `json:"gasUsed,omitempty"`
Confirmations string `json:"confirmations,omitempty"`
MethodId string `json:"methodId,omitempty"`
FunctionName string `json:"functionName,omitempty"`
}Criando nosso wallet watcher usando as go routines
Ainda dentro de internal criaremos outro pacote com nome de watcher, dentro dele iremos criar nosso arquivo service.go onde implementaremos nosso watcher, primeiro criaremos uma função responsável por iniciar nosso serviço:
// StartWatcherService load from environment the data and start running goroutines to perform wallet watcher service.
func StartWatcherService() {
err := godotenv.Load()
if err != nil {
log.Fatalln("Error loading .env file", err.Error())
}
var wfe [20]domain.Wallet
var wallets []domain.Wallet
for index := range wfe {
wallet := domain.Wallet{
Address: os.Getenv("WATCHER_WALLET" + strconv.Itoa(index+1)),
SecretKey: os.Getenv("WATCHER_SECRET" + strconv.Itoa(index+1)),
}
wallets = append(wallets, wallet)
}
// contains filtered fields or functions
}No trecho de código acima carregamos nossas variáveis de ambiente onde armazenaremos dados que não devem ser expostos, nesse exemplo, iremos carregar 20 carteiras e suas respectivas chaves privadas usando o for range.
Ainda em nosso service.go, vamos agora criar um grupo de sincronização para as nossas go routines que serão criadas:
// StartWatcherService load from environment the data and start running go routines to perform wallet watcher service.
func StartWatcherService() {
// contains filtered fields or functions
// Create a wait group to synchronize go routines
var wg sync.WaitGroup
wg.Add(len(wallets))
// contains filtered fields or functions
}Seguindo, vamos criar as nossas go routines:
// StartWatcherService load from environment the data and start runing go routines to perform wallet watcher service.
func StartWatcherService() {
// contains filtered fields or functions
// Start a go routine for each wallet
for _, wallet := range wallets {
go func(wallet domain.Wallet) {
// Connect to the Ethereum client
client, err := rpc.Dial(os.Getenv("WATCHER_RPC_ADDRESS"))
if err != nil {
log.Printf("Failed to connect to the RPC client for address %s: %v \n Trying fallback rpc server...", wallet.Address.Hex(), err)
}
client, err = rpc.Dial(os.Getenv("WATCHER_RPC_FALLBACK_ADDRESS"))
if err != nil {
log.Printf("Failed to connect to the Ethereum client for address %s: %v", wallet.Address.Hex(), err)
wg.Done()
return
}
// Create an instance of the Ethereum client
ethClient := ethclient.NewClient(client)
for {
// We query balance addresss
balance, err := ethClient.BalanceAt(context.Background(), common.HexToAddress(wallet.Address), nil)
if err != nil {
log.Printf("Failed to get balance for address %s: %v", wallet.Address.Hex(), err)
continue
}
balanceInEther := new(big.Float).Quo(new(big.Float).SetInt(balance), big.NewFloat(1e18))
log.Printf("Balance for address %s: %.16f ETH", wallet.Address.Hex(), balanceInEther)
// if the wallet has a balance superior to 0.0005 ETH, we are sending the balance to another wallet.
if balanceInEther.Cmp(big.NewFloat(0.0005)) > 0 {
sendBalanceToAnotherWallet(common.HexToAddress(wallet.Address), balance, wallet.SecretKey)
}
time.Sleep(300 * time.Millisecond) // Wait for a while before checking for the next block
}
}(wallet)
}
// Wait for all goroutines to finish
wg.Wait()
}Por fim, agora vamos criar nossa função responsável por gerar e assinar uma transação que enviará o saldo das carteiras para outra carteira:
// sendBalanceToAnotherWallet when find some values in any wallet perform a SendTransaction(ctx context.Context,
// tx *types.Transaction) function
func sendBalanceToAnotherWallet(fromAddress common.Address, balance *big.Int, privateKeyHex string) {
toAddress := common.HexToAddress(os.Getenv("WATCHER_DEST_ADDRESS"))
chainID := big.NewInt(1) // Replace with the appropriate chain ID
// Connect to the Ethereum client
client, err := rpc.Dial(os.Getenv("WATCHER_RPC_ADDRESS"))
if err != nil {
log.Printf("Failed to connect to the Ethereum client: %v...", err)
}
ethClient := ethclient.NewClient(client)
// Load the private key
privateKey, err := crypto.HexToECDSA(privateKeyHex[2:])
if err != nil {
log.Fatalf("Failed to load private key: %v", err)
}
// Get the current nonce for the fromAddress
nonce, err := ethClient.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Printf("Failed to retrieve nonce: %v", err)
}
// Create a new transaction
gasLimit := uint64(21000) // Set the gas limit based on the transaction type
gasPrice, err := ethClient.SuggestGasPrice(context.Background())
if err != nil {
log.Printf("Failed to retrieve gas price: %v", err)
}
tx := types.NewTx(&types.LegacyTx{
Nonce: nonce,
GasPrice: gasPrice,
Gas: gasLimit,
To: &toAddress,
Value: new(big.Int).Sub(balance, new(big.Int).Mul(gasPrice, big.NewInt(int64(gasLimit)))),
Data: nil,
})
valueInEther := new(big.Float).Quo(new(big.Float).SetInt(tx.Value()), big.NewFloat(1e18))
if valueInEther.Cmp(big.NewFloat(0)) < 0 {
log.Println("ERROR: Insufficient funds to make transfer")
}
// Sign the transaction
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
log.Printf("Failed to sign transaction: %v", err)
}
// Send the signed transaction
err = ethClient.SendTransaction(context.Background(), signedTx)
if err != nil {
log.Printf("Failed to send transaction: %v", err)
} else {
log.Printf("Transaction sent: %s", signedTx.Hash().Hex())
}
}O nosso serviço para observar as carteiras já está pronto, o nosso arquivo service.go está assim:
package watcher
import (
"bytes"
"context"
"encoding/json"
"fmt"
"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"
"github.com/ethereum/go-ethereum/rpc"
"github.com/joho/godotenv"
"github.com/ronilsonalves/go-wallet-watcher/internal/domain"
"log"
"math/big"
"net/http"
"os"
"strconv"
"sync"
"time"
)
// StartWatcherService load from environment the data and start running goroutines to perform wallet watcher service.
func StartWatcherService() {
err := godotenv.Load()
if err != nil {
log.Fatalln("Error loading .env file", err.Error())
}
var wfe [20]domain.Wallet
var wallets []domain.Wallet
for index := range wfe {
wallet := domain.Wallet{
Address: os.Getenv("WATCHER_WALLET" + strconv.Itoa(index+1)),
SecretKey: os.Getenv("WATCHER_SECRET" + strconv.Itoa(index+1)),
}
wallets = append(wallets, wallet)
}
// Create a wait group to synchronize goroutines
var wg sync.WaitGroup
wg.Add(len(wallets))
// Start a goroutine for each wallet
for _, wallet := range wallets {
go func(wallet domain.Wallet) {
// Connect to the Ethereum client
client, err := rpc.Dial(os.Getenv("WATCHER_RPC_ADDRESS"))
if err != nil {
log.Printf("Failed to connect to the RPC client for address %s: %v \n Trying fallback rpc server", wallet.Address.Hex(), err)
}
client, err = rpc.Dial(os.Getenv("WATCHER_RPC_FALLBACK_ADDRESS"))
if err != nil {
log.Printf("Failed to connect to the Ethereum client for address %s: %v", wallet.Address.Hex(), err)
wg.Done()
return
}
// Create an instance of the Ethereum client
ethClient := ethclient.NewClient(client)
for {
// Get the balance of the address
balance, err := ethClient.BalanceAt(context.Background(), common.HexToAddress(wallet.Address), nil)
if err != nil {
log.Printf("Failed to get balance for address %s: %v", wallet.Address.Hex(), err)
continue
}
balanceInEther := new(big.Float).Quo(new(big.Float).SetInt(balance), big.NewFloat(1e18))
log.Printf("Balance for address %s: %.16f ETH", wallet.Address.Hex(), balanceInEther)
// if the wallet has a balance superior to 0.0005 ETH, we are sending the balance to another wallet
if balanceInEther.Cmp(big.NewFloat(0.0005)) > 0 {
sendBalanceToAnotherWallet(common.HexToAddress(wallet.Address), balance, wallet.SecretKey)
}
time.Sleep(300 * time.Millisecond) // Wait for a while before checking for the next block
}
}(wallet)
}
// Wait for all goroutines to finish
wg.Wait()
}
// sendBalanceToAnotherWallet when find some values in any wallet perform a SendTransaction(ctx context.Context,
// tx *types.Transaction) function
func sendBalanceToAnotherWallet(fromAddress common.Address, balance *big.Int, privateKeyHex string) {
toAddress := common.HexToAddress(os.Getenv("WATCHER_DEST_ADDRESS"))
chainID := big.NewInt(1) // Replace with the appropriate chain ID
// Connect to the Ethereum client
client, err := rpc.Dial(os.Getenv("WATCHER_RPC_ADDRESS"))
if err != nil {
log.Printf("Failed to connect to the Ethereum client: %v...", err)
}
ethClient := ethclient.NewClient(client)
// Load the private key
privateKey, err := crypto.HexToECDSA(privateKeyHex[2:])
if err != nil {
log.Fatalf("Failed to load private key: %v", err)
}
// Get the current nonce for the fromAddress
nonce, err := ethClient.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
log.Printf("Failed to retrieve nonce: %v", err)
}
// Create a new transaction
gasLimit := uint64(21000) // Set the gas limit based on the transaction type
gasPrice, err := ethClient.SuggestGasPrice(context.Background())
if err != nil {
log.Printf("Failed to retrieve gas price: %v", err)
}
tx := types.NewTx(&types.LegacyTx{
Nonce: nonce,
GasPrice: gasPrice,
Gas: gasLimit,
To: &toAddress,
Value: new(big.Int).Sub(balance, new(big.Int).Mul(gasPrice, big.NewInt(int64(gasLimit)))),
Data: nil,
})
valueInEther := new(big.Float).Quo(new(big.Float).SetInt(tx.Value()), big.NewFloat(1e18))
if valueInEther.Cmp(big.NewFloat(0)) < 0 {
log.Println("ERROR: Insufficient funds to make transfer")
}
// Sign the transaction
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
log.Printf("Failed to sign transaction: %v", err)
}
// Send the signed transaction
err = ethClient.SendTransaction(context.Background(), signedTx)
if err != nil {
log.Printf("Failed to send transaction: %v", err)
} else {
log.Printf("Transaction sent: %s", signedTx.Hash().Hex())
}
}A esse ponto, se não desejarmos criar uma API rest, bastamos chamarmos nossa função StartWatcherService() em nosso main.go
func main() {
// filtered fields or functions
// Start our watcher
go watcher.StartWatcherService()
// Wait for the server and the watcher service to finish
select {}
}Expondo nossa API REST para consultas
Vamos usar o Gin Web Framework para construir uma API Rest onde iremos expor um endpoint para consulta de saldo e transações recentes de um endereço. Para isso precisamos adicionar o gin-gonic ao nosso projeto:
go get github.com/gin-gonic/ginCriando um serviço para nosso pacote wallet
Agora, dentro de internal vamos criar um pacote 'wallet', nesse pacote teremos um arquivo chamado service.go, é nele que faremos chamadas a API da etherscan.io para consulta de saldo e transações:
type Service interface {
GetWalletBalanceByAddress(address string) (domain.Wallet, error)
GetTransactionsByAddress(address, page, size string) (domain.Wallet, error)
}
type service struct{}
// NewService creates a new instance of the Wallet Service.
func NewService() Service {
return &service{}
}Primeiro vamos criar um método para obter informações do endereço que receberemos como parâmetro do endpoint de nossa API (veremos esse handler logo mais):
// GetWalletBalanceByAddress retrieves the wallet balance for the given address
func (s service) GetWalletBalanceByAddress(address string) (domain.Wallet, error) {
}Dentro desse método vamos obter a chave da Etherscan API do nosso ambiente:
// GetWalletBalanceByAddress retrieves the wallet balance for the given address
func (s service) GetWalletBalanceByAddress(address string) (domain.Wallet, error) {
// Retrieves Etherscan.io API Key from environment
apiKey := os.Getenv("WATCHER_ETHERSCAN_API")
url := fmt.Sprintf(fmt.Sprintf("https://api.etherscan.io/api?module=account&action=balance&address=%s&tag=latest&apikey=%s", address, apiKey))
// Contains filtered fields or functions
}Em seguida, faremos uma chamada HTTP GET para a API da Etherscan e ler o conteúdo da resposta:
// GetWalletBalanceByAddress retrieves the wallet balance for the given address
func (s service) GetWalletBalanceByAddress(address string) (domain.Wallet, error) {
// Contains filtered fields or functions
// Send GET request to the Etherscan API
response, err := http.Get(url)
if err != nil {
log.Printf("Failed to make Etherscan API request: %v", err)
return domain.Wallet{}, err
}
defer response.Body.Close()
// Read the response body
body, err := io.ReadAll(response.Body)
if err != nil {
log.Printf("Failed to read response body: %v", err)
return domain.Wallet{}, err
}
// Creates a struct to represent etherscan API response
var result struct {
Status string `json:"status"`
Message string `json:"message"`
Result string `json:"result"`
}
// Contains filtered fields or functions
}Por fim, iremos analisar a resposta da chamada a API da Etherscan e estruturá-la de acordo com a nossa struct 'result', realizar algumas validações e definir um retorno para esse método:
// GetWalletBalanceByAddress retrieves the wallet balance for the given address
func (s service) GetWalletBalanceByAddress(address string) (domain.Wallet, error) {
// Contains filtered fields or functions
// Parse the JSON response
err = json.Unmarshal(body, &result)
if err != nil {
log.Printf("Failed to parse JSON response: %v", err)
return domain.Wallet{}, err
}
if result.Status != "1" {
log.Printf("API returned error: %s", result.Message)
return domain.Wallet{}, fmt.Errorf("API error: %s", result.Message)
}
wbBigInt := new(big.Int)
wbBigInt, ok := wbBigInt.SetString(result.Result, 10)
if !ok {
log.Println("Failed to parse string to BigInt")
return domain.Wallet{}, fmt.Errorf("failed to parse string into BigInt. result.Result value: %s", result.Result)
}
wb := new(big.Float).Quo(new(big.Float).SetInt(wbBigInt), big.NewFloat(1e18))
v, _ := strconv.ParseFloat(wb.String(), 64)
return domain.Wallet{
Address: address,
Balance: v,
}, nil
}Já temos nossa função para obter as informações de saldo de uma carteira, vamos agora criar outro método em nosso service.go para exibir as transações, a lógica será a mesma do método anterior, a diferença será em como mapearemos a resposta e na construção da URL que iremos chamar em nossa requisição GET, pois teremos como parâmetros a página e a quantidade de items por página além do endereço:
// GetTransactionsByAddress retrieves the wallet balance and last transactions for the given address paggeable
func (s service) GetTransactionsByAddress(address, page, size string) (domain.Wallet, error) {
// Call to GetWalletBalanceByAddres to mount a wallet and their balance
wallet, _ := s.GetWalletBalanceByAddress(address)
apiKey := os.Getenv("WATCHER_ETHERSCAN_API")
url := fmt.Sprintf("https://api.etherscan.io/api?module=account&action=txlist&address=%s&startblock=0&endblock=99999999&page=%s&offset=%s&sort=desc&apikey=%s", address, page, size, apiKey)
// Contains filtered fields or functions
// Parse the JSON response
var transactions struct {
Status string `json:"status"`
Message string `json:"message"`
Result []domain.Transaction `json:"result"`
}
// Add transactions to our wallet struct
wallet.Transactions = append(wallet.Transactions, transactions.Result...)
return wallet, nil
}Finalizamos o serviço do nosso pacote wallet e o arquivo inteiro se parecerá com o gist abaixo:
package wallet
import (
"encoding/json"
"fmt"
"github.com/ronilsonalves/go-wallet-watcher/internal/domain"
"io"
"log"
"math/big"
"net/http"
"os"
"strconv"
)
type Service interface {
GetWalletBalanceByAddress(address string) (domain.Wallet, error)
GetTransactionsByAddress(address, page, size string) (domain.Wallet, error)
}
type service struct{}
// NewService creates a new instance of the Wallet Service.
func NewService() Service {
return &service{}
}
// GetWalletBalanceByAddress retrieves the wallet balance for the given address
func (s service) GetWalletBalanceByAddress(address string) (domain.Wallet, error) {
// Retrieves Etherscan.io API Key from environment
apiKey := os.Getenv("WATCHER_ETHERSCAN_API")
url := fmt.Sprintf(fmt.Sprintf("https://api.etherscan.io/api?module=account&action=balance&address=%s&tag=latest&apikey=%s", address, apiKey))
// Send GET request to the Etherscan API
response, err := http.Get(url)
if err != nil {
log.Printf("Failed to make Etherscan API request: %v", err)
return domain.Wallet{}, err
}
defer response.Body.Close()
// Read the response body
body, err := io.ReadAll(response.Body)
if err != nil {
log.Printf("Failed to read response body: %v", err)
return domain.Wallet{}, err
}
// Creates a struct to represent etherscan API response
var result struct {
Status string `json:"status"`
Message string `json:"message"`
Result string `json:"result"`
}
// Parse the JSON response
err = json.Unmarshal(body, &result)
if err != nil {
log.Printf("Failed to parse JSON response: %v", err)
return domain.Wallet{}, err
}
if result.Status != "1" {
log.Printf("API returned error: %s", result.Message)
return domain.Wallet{}, fmt.Errorf("API error: %s", result.Message)
}
wbBigInt := new(big.Int)
wbBigInt, ok := wbBigInt.SetString(result.Result, 10)
if !ok {
log.Println("Failed to parse string to BigInt")
return domain.Wallet{}, fmt.Errorf("failed to parse string into BigInt. result.Result value: %s", result.Result)
}
wb := new(big.Float).Quo(new(big.Float).SetInt(wbBigInt), big.NewFloat(1e18))
v, _ := strconv.ParseFloat(wb.String(), 64)
return domain.Wallet{
Address: address,
Balance: v,
}, nil
}
// GetTransactionsByAddress retrieves the wallet balance and last transactions for the given address paggeable
func (s service) GetTransactionsByAddress(address, page, size string) (domain.Wallet, error) {
wallet, _ := s.GetWalletBalanceByAddress(address)
apiKey := os.Getenv("WATCHER_ETHERSCAN_API")
url := fmt.Sprintf("https://api.etherscan.io/api?module=account&action=txlist&address=%s&startblock=0&endblock=99999999&page=%s&offset=%s&sort=desc&apikey=%s", address, page, size, apiKey)
// Send GET request to the Etherscan API
response, err := http.Get(url)
if err != nil {
log.Printf("Failed to make API request: %v", err)
return domain.Wallet{}, err
}
defer response.Body.Close()
// Read the response body
body, err := io.ReadAll(response.Body)
if err != nil {
log.Printf("Failed to read response body: %v", err)
return domain.Wallet{}, err
}
// Parse the JSON response
var transactions struct {
Status string `json:"status"`
Message string `json:"message"`
Result []domain.Transaction `json:"result"`
}
err = json.Unmarshal(body, &transactions)
if err != nil {
log.Printf("Failed to parse JSON response: %v", err)
return domain.Wallet{}, err
}
if transactions.Status != "1" {
log.Printf("API returned error: %s", transactions.Message)
return domain.Wallet{}, fmt.Errorf("API error: %s", transactions.Message)
}
wallet.Transactions = append(wallet.Transactions, transactions.Result...)
return wallet, nil
}Criando as gin handler functions para expor a nossa API
Em seguida vamos construir nossas gin handlerFunc para interagir com nosso wallet/service.go e expor os endpoints necessários para consulta do saldo e das transações de uma carteira Ethereum.
Vamos criar um diretório na raiz do nosso projeto nomeado de cmd (aqui iremos colocar o nosso main.go/api.go e o nosso wallet handler da nossa API, a estrutura ficará assim:
.env.example
cmd
|-- server
| |-- handler
| | |-- wallet.go
| |-- main.go
internal
|-- domain
| |-- transaction.go
| |-- wallet.go
|-- wallet
| |-- dto.go
| |-- service.go
|-- watcher
| |-- service.go
pkg
|-- web
| |-- response.go
Criamos por fim uma pasta/pacote chamada handler e dentro dela criamos um arquivo nomeado de wallet.go:
package handler
type walletHandler struct {
s wallet.Service
}
// NewWalletHandler creates a new instance of the Wallet Handler.
func NewWalletHandler(s wallet.Service) *walletHandler {
return &walletHandler{s: s}
}Dentro desse arquivo criaremos duas handler functions: GetWalletByAddress() e GetTransactionsByAddress() para exibir o saldo e as transações de uma carteira:
// GetWalletByAddress get wallet info balance from a given address
func (h *walletHandler) GetWalletByAddress() gin.HandlerFunc {
return func(ctx *gin.Context) {
ap := ctx.Param("address")
w, err := h.s.GetWalletBalanceByAddress(ap)
if err != nil {
web.BadResponse(ctx, http.StatusBadRequest, "error", err.Error())
return
}
web.OKResponse(ctx, http.StatusOK, w)
}
}
// GetTransactionsByAddress retrieves up to 10000 transactions by given adrress in a paggeable response
func (h *walletHandler) GetTransactionsByAddress() gin.HandlerFunc {
return func(ctx *gin.Context) {
address := ctx.Param("address")
page := ctx.Query("page")
size := ctx.Query("pageSize")
if len(page) == 0 {
page = "1"
}
if len(size) == 0 {
size = "10"
}
if _, err := strconv.Atoi(page); err != nil {
web.BadResponse(ctx, http.StatusBadRequest, "error", fmt.Sprintf("Invalid page param. Verify page value: %s", page))
return
}
if _, err := strconv.Atoi(size); err != nil {
web.BadResponse(ctx, http.StatusBadRequest, "error", fmt.Sprintf("Invalid pageSize param. Verify pageSize value: %s", size))
return
}
response, err := h.s.GetTransactionsByAddress(address, page, size)
if err != nil {
web.BadResponse(ctx, http.StatusBadRequest, "error", err.Error())
return
}
var pageableResponse struct {
Page string `json:"page"`
Items string `json:"items"`
Data interface{} `json:"data"`
}
pageableResponse.Page = page
pageableResponse.Items = size
pageableResponse.Data = response
web.OKResponse(ctx, http.StatusOK, pageableResponse)
}
}Voltando a programação concorrente, em nossa main.go iremos instanciar nosso walletService e walletHandler, criar um gin server e iniciá-lo em uma go routine:
func main() {
wService := wallet.NewService()
wHandler := handler.NewWalletHandler(wService)
r := gin.New()
r.Use(gin.Recovery(), gin.Logger())
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Everything is okay here",
})
})
api := r.Group("/api/v1")
{
ethNet := api.Group("/eth/wallets")
{
ethNet.GET(":address", wHandler.GetWalletByAddress())
ethNet.GET(":address/transactions", wHandler.GetTransactionsByAddress())
}
}
// Start the Gin server in a goroutine
go func() {
if err := r.Run(":8080"); err != nil {
log.Println("ERROR IN GONIC: ", err.Error())
}
}()
// Contains filtered fields or functions
}Precisamos iniciar o gin server dentro de uma go routine para que nossa API e o nosso serviço de wallet watcher rodem de maneira simultânea, então por fim, vamos iniciar dentro de uma outra go routine o nosso watcher:
func main() {
// Filtered fields or functions
// Start our watcher
go watcher.StartWatcherService()
// Wait for the server and the watcher service to finish
select {}
}Finalizamos a implementação da nossa API e do nosso watcher, como queremos que as go routines relacionadas ao watcher se mantenham rodando enquanto nossa aplicação estiver rodando o select{} vai aguardar a finalização delas, ao criarmos nossa go routine para cada uma das carteiras, incluímos um 'for' sem cláusula de saída, ele será o responsável por impedir que nossas go routines sejam concluídas após sua primeira execução:
func StartWatcherService() {
// Contains filtered fiels or functions
go func(wallet domain.Wallet) {
for {
// Get the balance of the address
balance, err := ethClient.BalanceAt(context.Background(), common.HexToAddress(wallet.Address), nil)
// Contains filtered fields or functions
time.Sleep(300 * time.Millisecond) // Wait for a while before checking for the next block
// Contains filtered fields or functions
}
}
// Waiting for all routines to finish - will not cause for is running in an infinity loop.
wg.Wait()
// Contains filtered fields or functions
}Conclusão
Programação concorrente/simultânea é uma poderosa ferramenta no desenvolvimento de software, neste artigo/tutorial passo-a-passo vimos como implementá-la e extrair seu melhor em um Ethereum wallet watcher, troque as 20 carteiras por milhares, ou as carteiras por milhares de filas de mensageria e/ou stream de dados. Em nosso exemplo, as 20 go routines rodando simultaneamente a cada 300ms consumiram 63MB da memória disponível e utilização da CPU ficou em incríveis 6% em uma instância compartilhada de 256MB do tier gratuito do Fly.io:


