封面照片由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中运行:
我希望本文有用,并帮助您更好地了解GO中的并发编程和goroutines。