使用Golang使用并发编程来构建以太坊钱包观察者
#编程 #go #learning #ethereum

封面照片由Mika Baumeister / Unsplash < / p>

在我的上一篇文章中,我谈到了构建使用GO语言并发编程的应用程序的简单简单,我们看到了它的实现程度以及它的效率,这要归功于其构造,从而提供了开发人员的构建具有强大的工具,可以充分利用并发编程的潜力。在本文中,我们将逐步研究如何利用Golang的这种力量来建立以太坊钱包。

语境

Web3自从我回到编程研究以来一直是我非常感兴趣的话题,所以我是大多数开发的产品背后的区块链技术的爱好者,并且可以在更传统的解决方案中采用。一些钱包公开曝光了私钥,其中大多数由开发套件提供,您可以在本地环境中运行测试节点。有些用户由于粗心和缺乏关注,最终将价值发送到主要网络上此钱包的地址(可以说是在生产中)。

目标

我们将在Golang构建一项服务以查看以太坊钱包,如果有任何传入的余额交易,我们将尝试执行一项传出交易以获取这些价值。

为了进一步说明并发编程的使用,我们还将提供一个用于从以太坊地址查询信息的API。

总而言之,我们将与REST API并排运行Wallet Watcher服务。让我们走吗?

在我们开始之前

该项目的源代码位于我的github上,您可以使用git克隆克隆并在计算机上运行它,不要忘记在.env.env.env.example之后在项目的根部创建.env文件

git clone https://github.com/ronilsonalves/go-wallet-watcher.git

如果您还想为查询构建RET REST API,则需要从Etherscan.io中拥有一个API键,为此,您需要拥有一个帐户,并在https://etherscan.io/myapikey

请求免费密钥

入门

我们将在哪里保存我们的项目,为此,在终端中我们需要键入:

go mod init 'project-name'

在继续之前,让我们安装将有助于我们建立服务的Go-Ethereum和Godotenv模块:

go get github.com/ethereum/go-ethereum
go get github.com/joho/godotenv

现在,我们必须创建一个名为“内部”的目录,我们将在其中整理项目的内部文件,其中我们必须创建另一个目录文件夹,该文件夹将是我们的域包,最后,我们将创建我们的文件allet.go将包含一个代表以太坊钱包的结构:

package domain

type Wallet struct {
    Address      string        `json:"address"`
    SecretKey    string        `json:"secret-key,omitempty"`
    Balance      float64       `json:"balance"`
    Transactions []Transaction `json:"transactions,omitempty"`
}

另外,我们必须创建一个结构来表示交易,我们将进一步使用:

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"`
}

使用Goroutines创建钱包观察者

仍然在内部目录内,我们将创建另一个名为“观察者”的软件包,其中我们将创建我们的服务。Go文件,我们将在其中实现Watcher,Fist,我们将创建一个负责启动我们服务的函数:

// 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
}

在上面的代码段中,我们加载环境变量,我们将存储不应暴露的数据,在此示例中,我们将使用FOR范围加载20个钱包及其各自的私钥。

仍在我们的服务中。

// StartWatcherService load from environment the data and start running goroutines to perform wallet watcher service.
func StartWatcherService() {

    // contains filtered fields or functions

    // Create a wait group to synchronize goroutines
    var wg sync.WaitGroup
    wg.Add(len(wallets))

    // contains filtered fields or functions
}

以下,我们将创建我们的goroutines:

// StartWatcherService load from environment the data and start runing goroutines to perform wallet watcher service.
func StartWatcherService() {
    // contains filtered fields or functions
    // 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)

    // 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) // Definimos o limite para a taxa de Gas da transação baseada no seu tipo
    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())
    }
}

我们的钱包观察者服务已经准备就绪,我们的观察者/服务应该是:

在这一点

func main() {
    // filtered fields or functions

    // Start our watcher
    go watcher.StartWatcherService()

    // Wait for the server and the watcher service to finish
    select {}
}

将REST API暴露于查询

我们将使用GIN Web框架来构建一个REST API,我们将公开一个端点来查询钱包的平衡和从地址的最新交易。为此,我们需要在我们的项目中添加O Gin-Gonic模块:

go get github.com/gin-gonic/gin

为我们的“钱包”包创建服务

现在,内部内部我们将创建一个“钱包”软件包,在此软件包中,我们将创建一个service.go文件,在这里我们将拨打eTherscan.io api以平衡和交易查询:

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) {

}

在此内,我们将从我们的环境中获得etherscan.io apikey:

// 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
}

以下内容,我们将使HTTP获得cal到etherscan API并阅读响应内容:

// 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
}

最后,我们将解析etherscan.io API响应并根据我们的结构进行构造,我们还需要进行一些验证并返回数据:

// 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
}

我们已经有了从钱包地址获取平衡信息的方法,现在让我们在钱包/服务中创建另一种方法。文件以显示交易,逻辑将与以前的方法相同,差异将为在如何映射etherscan.io api响应以及如何构建URL端点以获取请求的方式中,因为我们将作为参数作为页面和地址之外的每个页面的数量:

// GetTransactionsByAddress retrieves the wallet balance and last transactions for the given address paggeable
func (s service) GetTransactionsByAddress(address, page, size string) (domain.Wallet, error) {
    // Fazemos a chamada para o método GetWalletBalanceByAddress para montarmos uma carteira e seu saldo
    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"`
    }

    // Adicionamos as transações à nossa struct carteira
    wallet.Transactions = append(wallet.Transactions, transactions.Result...)

    return wallet, nil
}

我们完成了钱包/服务。

创建杜松子酒处理程序功能以暴露我们的API

之后,我们将创建杜松子酒处理程序与钱包/服务互动。

我们将在我们的项目root中创建一个目录,并将其作为CMD(在这里我们将Main.Go和API中的处理程序包放置,目录结构必须像这样:

.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

最后,我们将创建我们的处理程序软件包,其中我们将创建一个钱包。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}
}

在此文件中,我们将创建两个处理程序函数:getWalletByAddress()和getTransactionsbyaddress()以显示钱包的平衡和交易:

// 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)
    }
}

返回并发编程,在我们的main中。go文件,我们将实例化Walletservice和Wallethandler,创建杜松子酒服务器并在Goroutine内实例化:

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
}

我们需要在API的Goroutine内部启动杜松子酒服务器,并同时运行Wallet Watcher服务,因此,最后,我们将在另一个Goroutine内部开始我们的观察器服务:

func main() {
    // Filtered fields or functions

    // Start our watcher
    go watcher.StartWatcherService()

    // Wait for the server and the watcher service to finish
    select {}
}

我们最终确定了API和Watcher服务的实现,因为我们希望与观察者相关的Goroutines继续运行,而我们的应用程序INS运行select {}将等待其完成,而当我们为每个钱包创建我们的goroutine时我们包括一个没有退出条款的“ for”,它将负责防止我们的goroutines在第一次执行后完成:

//watcher/service.go

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
                }
        }
        // Esperando por todas as goroutines para finalizar - não irá finalizar por conta do for rodando pelo infinito
        wg.Wait()

        // Contains filtered fields or functions
}

结论

并发编程是软件开发中的强大工具,在本文/教程中,我们看到了如何实施并在以太坊钱包观察者中提取最好的工具,将20个钱包交换成千上万的钱包,或者成千上万的钱包对于成千上万的消息队列或数据流,GO调度程序将注意并有效地使用可用资源。在我们的示例中,每300ms每300ms从可用的内存中消耗63MB,CPU利用率为6%,此服务在Fly.io Free Tier的256MB中运行:

The memory consume
The CPU utilization

我希望本文有用,并帮助您更好地了解GO中的并发编程和goroutines。