ساختن تماشاگر کیف پول اتریوم با استفاده از برنامه نویسی همزمان در GoLang

عکس روی جلد توسط Mika Baumeister / Unsplash
در آخرین مقاله خود در مورد اینکه چقدر ساده است ساخت برنامه هایی که از برنامه نویسی همزمان با استفاده از زبان Go استفاده می کنند صحبت کردم، دیدیم که پیاده سازی آن چقدر ساده است و چقدر می تواند کارآمد باشد، به لطف ساخت آن که ابزارهای قدرتمندی را در اختیار توسعه دهندگان قرار می دهد. برای استفاده کامل از پتانسیل برنامه نویسی همزمان. در این مقاله نگاهی گام به گام عملی به نحوه ساخت یک واچ کیف پول اتریوم با استفاده از این قدرت GoLang خواهیم داشت.
متن نوشته
وب 3 از زمانی که به مطالعات برنامه نویسی خود بازگشتم همیشه موضوعی بوده است که بسیار به آن علاقه مند هستم، من از علاقه مندان به فناوری بلاک چین در پشت اکثر محصولات توسعه یافته هستم و به اصطلاح اخیراً در راه حل های سنتی تر مورد استفاده قرار گرفته است. برخی از کیف پولها کلیدهای خصوصی خود را به صورت عمومی در معرض دید عموم قرار میدهند، اکثر آنها توسط کیتهای توسعه در دسترس هستند که میتوانید یک گره آزمایشی را در یک محیط محلی اجرا کنید. برخی از کاربران با بی دقتی و عدم توجه، در نهایت مقادیری را به آدرس این کیف پول در شبکه اصلی (به اصطلاح در حال تولید) ارسال می کنند.
هدف
ما یک سرویس در GoLang ایجاد خواهیم کرد تا کیف پولهای اتریوم را بررسی کنیم و اگر تراکنشهای موجودی ورودی وجود داشته باشد، سعی میکنیم یک تراکنش خروجی برای دریافت آن مقادیر انجام دهیم.
برای مثال بیشتر استفاده از برنامه نویسی همزمان، یک API برای جستجوی اطلاعات از یک آدرس اتریوم نیز ارائه خواهیم کرد.
به طور خلاصه، ما سرویس Wallet Watcher خود را در کنار یک استراحت API اجرا خواهیم کرد. بیا بریم؟
قبل از اینکه شروع کنیم
سورس کد این پروژه در GitHub من است، شما می توانید آن را با استفاده از git clone و اجرای آن بر روی دستگاه خود کلون کنید، فراموش نکنید که یک فایل .env در ریشه پروژه به دنبال .env.example ایجاد کنید.
git clone https://github.com/ronilsonalves/go-wallet-watcher.git
اگر همچنین می خواهید Rest API را برای پرس و جوها بسازید، باید یک کلید API از Etherscan.io داشته باشید، برای این کار باید یک حساب کاربری داشته باشید و یک کلید رایگان در 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
حال باید یک دایرکتوری به نام داخلی ایجاد کنیم که در آن فایل های داخلی پروژه خود را سازماندهی کنیم، در داخل آن پوشه دایرکتوری دیگری ایجاد کنیم که بسته دامنه ما خواهد بود و در نهایت فایل wallet.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"`
}
ایجاد تماشاگر کیف پول ما با استفاده از گوروتین ها
هنوز در دایرکتوری داخلی، بسته دیگری به نام “watcher” ایجاد می کنیم، در داخل آن فایل service.go خود را ایجاد می کنیم که در آن تماشاگر خود را پیاده سازی می کنیم، ابتدا تابعی را ایجاد می کنیم که مسئول راه اندازی سرویس خود است:
// 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
}
در قطعه کد بالا، متغیرهای محیطی خود را بارگذاری میکنیم، جایی که دادههایی را که نباید در معرض نمایش قرار گیرند، ذخیره میکنیم، در این مثال، 20 کیف پول و کلیدهای خصوصی مربوط به آنها را با استفاده از محدوده for بارگذاری میکنیم.
هنوز در service.go ما، یک گروه همگامسازی برای گوروتینهای خود ایجاد خواهیم کرد:
// 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
}
در ادامه، ما گوروتین های خود را ایجاد خواهیم کرد:
// 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())
}
}
سرویس نظارت بر کیف پول ما آماده است، watcher/service.go ما باید به صورت زیر باشد:
در این مرحله، اگر نمیخواهیم API Rest ایجاد کنیم، فقط باید StartWatcherService() خود را به 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 {}
}
قرار دادن یک Rest API در معرض نمایش داده شد
ما از Gin Web Framework برای ساخت یک Rest API استفاده خواهیم کرد که در آن نقطه پایانی را در معرض پرس و جوی موجودی کیف پول و تراکنش های اخیر از یک آدرس قرار می دهیم. برای انجام این کار باید ماژول o gin-gonic را به پروژه خود اضافه کنیم:
go get github.com/gin-gonic/gin
ایجاد یک سرویس برای بسته “کیف پول” ما
اکنون، در داخل داخلی، یک بسته کیف پول ایجاد می کنیم، در این بسته ما یک فایل service.go ایجاد می کنیم، اینجاست که با API Etherscan.io تماس می گیریم تا پرس و جوهای تراکنش و تعادل را برقرار کنیم:
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{}
}
ابتدا روشی برای دریافت اطلاعات از آدرسی که به عنوان پارامتر دریافت خواهیم کرد ایجاد خواهیم کرد (به زودی تابع gin handler را خواهیم دید):
// 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 GET برای API Etherscan ایجاد می کنیم و محتوای پاسخ را می خوانیم:
// 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
}
ما قبلاً روش خود را برای دریافت اطلاعات موجودی از یک آدرس کیف پول داریم، اکنون بیایید روش دیگری را در فایل wallet/service.go خود ایجاد کنیم تا تراکنش ها را نشان دهیم، منطق مانند روش قبلی خواهد بود، تفاوت در نحوه انجام ما خواهد بود. پاسخ Etherscan.io API و نحوه ایجاد نقطه پایانی URL برای درخواست GET را ترسیم کنید، زیرا صفحه و تعداد موارد در هر صفحه فراتر از آدرس را به عنوان پارامتر خواهیم داشت:
// 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
}
کیف پول/service.go خود را تمام کردیم و کل فایل باید مانند اصل زیر باشد:
ایجاد توابع gin handler برای افشای API ما
در ادامه ما gin handlerFunc خود را برای تعامل با کیف پول/service.go خود ایجاد می کنیم و نقاط پایانی لازم را برای پرس و جو از موجودی کیف پول و تراکنش های کیف پول اتریوم در معرض نمایش می گذاریم.
ما یک دایرکتوری در ریشه پروژه خود ایجاد می کنیم و آن را به عنوان cmd می کنیم (در اینجا main.go و بسته handler را از 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
در نهایت بسته کنترل کننده خود را ایجاد می کنیم، در داخل آن یک فایل 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}
}
در داخل این فایل ما دو تابع handler ایجاد خواهیم کرد: 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 خود را نمونه سازی می کنیم، یک سرور جین ایجاد می کنیم و آن را در داخل یک گوروتین نمونه سازی می کنیم:
func main() {
wService := wallet.NewService()
wHandler := handler.NewWalletHandler(wService)
r := gin.New()
r.Use(gin.Recovery(), gin.Logger())
r.GET("https://dev.to/", 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 خود راه اندازی کنیم و سرویس ناظر کیف پول ما به طور همزمان اجرا شود، بنابراین در نهایت، سرویس ناظر خود را در داخل گوروتین دیگری شروع می کنیم:
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/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 از منابع موجود مراقبت کرده و به نحو احسن استفاده میکند. در مثال ما، 20 گوروتینی که به صورت همزمان در هر 300 میلی ثانیه اجرا میشوند، 63 مگابایت از حافظه موجود مصرف میکنند و استفاده از CPU 6٪ بود، این سرویس در داخل یک نمونه مشترک از 256 مگابایت از سطح رایگان Fly.io اجرا میشد:
امیدوارم این مقاله مفید بوده باشد و به شما در درک بهتر برنامه نویسی همزمان و گوروتین ها در Go کمک کرده باشد.