LapTrinhBlockchain

Chia sẻ kiến thức về Lập Trình Blockchain

Kiến thức Blockchain, Kiến thức lập trình, Kiến thức phần mềm, Lập trình Blockchain, Lập Trình DApp, Lập trình GoLang, Nâng cao Kiến thức

Hướng dẫn lập trình Go (Go Language) tương tác với Blockchain

Hướng dẫn lập trình GoLang tương tác với Blockchain

Hướng dẫn lập trình GoLang tương tác với Blockchain

Chia sẻ bài viết
5
(89)

Những ai làm về Blockchain chắc không xa lạ gì với ngôn ngữ Go, đây là ngôn ngữ sử dụng để tạo ra blochain Ethereum rất nổi tiếng, sau này nhiều nền tảng blockchain khác đã tham khảo source code của Ethereum. Bài viết này sẽ tập trung tìm hiểu và sử dụng ngôn ngữ Go để tương tác với Blockchain.

Mục lục

Cài đặt và bắt đầu làm quen Go

Cài đặt Go

Nếu không muốn cài Go, bạn có thể sử dụng trang The Go Playground để bắt đầu lập trình. Thường dành cho nhưng bạn bắt đầu tìm hiểu và học, chứ nếu bạn làm project về Go thì bạn nên cài đặt.

Nếu bạn muốn lập trình Go trên máy, bạn cần phải cài đặt Go. Để cài đặt Go bảng 1.19.13 bạn sử dụng lệnh sau:

# Cài đặt Go 1.19.13
# Nếu muốn cài Go 1.21.0 thì bạn chỉ cần thay 1.19.13 thành 1.21.0
sudo -i
rm -rf /usr/local/go
wget https://go.dev/dl/go1.19.13.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.19.13.linux-amd64.tar.gz
rm go1.19.13.linux-amd64.tar.gz
exit

# Cài đặt công cụ hỗ trợ build các thư viện sau này
sudo apt-get install build-essential

# Thêm đường dẫn vào biến môi trường PATH
export PATH=$PATH:/usr/local/go/bin

# Tốt nhất hãy thêm dòng dưới vào cuối tệp .profile hoặc /etc/profile
# Để các lần sau ta không phải đánh lệnh trên nữa
PATH=$PATH:/usr/local/go/bin

# Kiểm tra lại
go version

# Xem thông tin cấu hình môi trường của Go
go env

Bạn nên lựa chọn cài đặt phiên bản cho phù hợp, chi tiết cài đặt xem tại: https://go.dev/doc/install.

IDE lập trình Go bạn có thể sử dụng Go, hoặc có thể sử dụng các công cụ khác như Visual Studio Code… Nếu bạn chỉ sử dụng với mục đích học tập và tìm hiểu thì bạn có thể dùng các trình soạn thảo Online như:

  • Go Playgound: Trình soạn thảo dễ nhìn, chọn được nhiều phiên bản Go và có một số ví dụ sẵn để tìm hiểu cho người mới.
  • Programiz – Go Online Compiler: Hỗ trợ nhiều ngôn ngữ trong đó có Go.

Viết ứng dụng đầu tiên

Go mặc định tìm hàm main() trong gói main để chạy đầu tiên. Nên khi viết ứng dụng bằng Go thì bạn bắt buộc phải cài đặt hàm này. Ví dụ tệp hello-world.go:

package main

import "fmt"

func main() {
    fmt.Println("Hello World!!!")
}

Lệnh để chạy như sau:

go run hello-world.go

Viết ứng dụng sử dụng thư viện trên Github

Ví dụ ta viết ứng dụng use-ethclient.go sử dụng ethclient trong thư viện go-ethereum:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/ethereum/go-ethereum/ethclient"
)

func main() {
    client, err := ethclient.Dial("https://rpc.ankr.com/eth")
    if err != nil {
        log.Fatalf("Failed to connect to the Ethereum network: %v", err)
    } else {
        fmt.Println("Success! you are connected to the Ethereum Network")
        fmt.Printf("EthClient: %+v\n", client)
    }

    header, err := client.HeaderByNumber(context.Background(), nil)
    if err != nil {
        log.Fatal(err)
    } else {
        fmt.Println("BlockNumber:", header.Number.String())
    }
}

Trong đoạn mã trên ta có sử dụng thêm thư viện ethclient trên github. Để chạy được tệp này ta đánh các lệnh sau:

# Tạo tệp go.mod và go.sum để quản lý thư viện
# Điều này sẽ đảm bảo ethclient được tải từ Github và phiên bản mới nhất được sử dụng
go mod init use-ethclient

# Trong trường hợp gặp vấn đề cài đặt ethclient
# Hãy sử dụng lệnh dưới để tải trực tiếp
go get github.com/ethereum/go-ethereum/ethclient

# Chạy ứng dụng
go run use-ethclient.go

# Nếu tiếp đó bạn sửa code và thêm các thư viện mới bạn gọi lệnh dưới để cập nhật lại thư viện
go mod tidy

Khi chạy bạn nhận được kết quả như sau:

Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
BlockNumber: 18218977

Chi tiết xem How to connect to Ethereum network using Go

Cách chạy tệp .go thế nào phần này đã nói rất rõ. Các phần sau trở đi tôi không tập trung vào phần code, tôi cố gắng viết code ngắn gọn nhất có thể để các bạn có thể dễ dàng đọc hiểu.

Một số lệnh cơ bản trên Go

Về cơ bản trên các ngôn ngữ đều có các lệnh cơ bản tương tự nhau nhưng khác nhau ở cách khai báo. Phần này là phần cơ bản, bạn nên tham khảo ở link dưới để nắm được:

  • Go cheatsheet => Sẽ dùng rất nhiều. Vì lập trình viên thường phải biết nhiều ngôn ngữ, nếu lâu ko dùng một ngôn ngữ nào đó sẽ dần quên hoặc lẫn cú pháp khai báo. Vì thế link này sẽ rất tiện để tra cứu lại.
  • Go Cheat Sheet => Một trong khác tương tự
  • Lập trình Go => Nắm kiến thức cơ bản, một số khai báo đặc biệt

Ở đây tôi chỉ ra một số phần đặc biệt bạn cần chú ý:

  • Các thư viện hay dùng
    • fmt: Dùng để hiển thị log ra màn hình nhưng không có thời gian. Hai hàm ba dùng nhất là fmt.Printf(), fmt.Println()fmt.Sprintf().
    • log: Các hàm tương tự như trong thư viện fmt nhưng có thời gian. Các hàm trong thư viện này mặc định hiển thị thời gian đến mức giây, nếu bạn muốn hiển thị cả mức nhỏ hơn giây thì thiết lập cấu hình bằng lệnh sau:
      log.SetFlags(log.LstdFlags | log.Lmicroseconds)
    • math: Các hàm toán học cơ bản
  • Cú pháp gán “:=” là cú pháp ngắn gọn để chúng ta khai báo 1 biến và gán giá trị cho nó. Với cu pháp này chúng ta không cần phải dùng từ khóa var và không cần khai báo kiểu dữ liệu
  • Cách khai báo nhiều biến cùng kiểu, nhiều biến khác kiểu, khởi tạo biến và hằng
  • Các kiểu dữ liệu trong Go:
    • bool: Chỉ nhận giá trị true false
    • string: Chuỗi kí tự nằm trong dấu nháy kép
    • Kiểu số nguyên: int, int8, int16, int32, int64. Bạn muốn biết range các kiểu này có thể sử dụng thư viện math: math.MinInt8, math.MaxInt8,… Kiểu int sẽ là int32 hay int64 tùy thuộc vào hệ điều hành 32 bit hay 64 bit
    • Kiểu số nguyên dương: uint, uint8, uint16, uint32, uint64
    • byte: Kiểu kí tự, có thể dùng dạng kí tự với dấu nháy đơn hoặc số.
    • Kiểu số thực: float32, float64
    • Kiểu số phức: complex64, complex128
      var z1 complex64 = 10 + 2i
      var z2 = complex64 20 + 3i
      var z = z1 + z2
    • rune: Một kiểu số nguyên 32-bit và được sử dụng để đại diện cho các ký tự Unicode. Rune cung cấp khả năng xử lý và lưu trữ các ký tự Unicode trong mã nguồn của chương trình.
    • uintptr: Lưu địa chỉ của con trỏ
    • Kiểu dữ liệu mảng: Thường chúng ta hay khai báo mảng tĩnh, tức là mảng với số phần tử cố định. Nếu muốn tạo mảng động, bạn phải sử dụng hàm make():
      b := make([]byte, 8)
      pairInfos := make([]PairInfo, n)
    • Kiểu dữ liệu cấu trúc struct
  • Hàm trả về nhiều giá trị
  • Giá trị mặc định: Trong Go, nếu bạn khai báo một biến mà không khởi tạo thì nó sẽ có giá trị mặc định. Giá trị mặc định phụ thuộc vào kiểu dữ liệu:
    • 0 cho các kiểu số
    • false cho kiểu bool
    • “” cho kiểu string
  • Chuyển đổi kiểu dữ liệu
    • Go không hỗ trợ tự động chuyển kiểu dữ liệu, mà trong code bạn phải thực hiện chuyển kiểu tường minh, chẳng hạn:
      var a int = 1
      var b float64 = 2.1
      fmt.Println(float64(a) + b)
    • Cách chuyển đổi từ dạng interface{} sang các sạng khác như Map hoặc Array => Tham khảo: How to convert interface{} to map
    • Hãy tìm hiểu kỹ hơn qua bài Type Assertions
  • Cách truyền giá trị và tham chiếu trong Go: Tham khảo bài viết Truyền giá trị và truyền tham chiếu trong Go. Chú ý map là đối tượng đặc biệt luôn luôn là tham chiếu, nếu muốn dùng tham trị ta phải truyền vào bản copy của nó.
  • Tìm hiểu về Defer: Defer Keyword in Go

Truyền tham số kiểu tham trị và tham chiếu trong Go

Nếu bạn đã từng làm việc với C/C++ thì bạn phải rất để ý đến cách truyền tham số kiểu Tham trị (Pass by value) hay kiểu Tham chiếu (Pass by reference), việc này rất quan trọng giúp đảm bảo dữ liệu đúng như bạn mong muốn khi ứng dụng chạy. Trên Go, chúng ta cũng phải nắm rõ được vấn đề này để giúp đảm bảo tính toàn vẹn dữ liệu và giúp giảm phức tạp trong code.

Mô phỏng cơ chế Tham trị và Tham chiếu

Cơ chế này phụ thuộc vào từng kiểu dữ liệu khác nhau:

Kiểu dữ liệu cơ bản (Basic Data Type)

Với các kiểu dữ liệu cơ bản như int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, string, bool, byte, rune, Array, Structs thì chúng ta:

  • Truyền tham trị: Truyền trực tiếp biến vào
  • Truyền tham chiếu: Truyền địa chỉ của biến. Sử dụng toán tử & để lấy địa chỉ 1 biến, toán tử * để truy cập vào giá trị 1 con trỏ.
package main

import "fmt"

// Truyền dạng Tham trị
func Add(x int) {
	x++
}

// Truyền dạng Tham chiếu
func AddPtr(x *int) {
	(*x)++
}

func main() {
	a := 0
	fmt.Println("[1]: a =", a)
	Add(a)
	fmt.Println("[2]: a =", a)
	fmt.Println("")

	b := 0
	fmt.Println("[1]: b =", b)
	AddPtr(&b)
	fmt.Println("[2]: b =", b)
}

Kết quả trả về như sau:

[1]: a = 0
[2]: a = 0

[1]: b = 0
[2]: b = 1

Kiểu dữ liệu được tham chiếu (Referenced Data Type)

Một số kiểu dữ liệu được tham chiếu như slices ([] – tham chiếu tới Array), map, chan (Channel) thì:

  • Truyền tham trị: Chúng ta phải tạo một bản copy của nó rồi dùng bản copy đó để truyền vào
  • Truyền tham chiếu: Truyền trực tiếp vào

Xây dựng cấu trúc thư mục cho dự án Go

Đối với các project nhỏ bạn có thể đặt các tệp cùng một package và cùng một thư mục, nhưng với một dự án lớn thì bạn cần phải phân chia theo package, theo thư mục theo các nghiệp vụ của dự án. Khi tôi thử đặt các file khác nhau, các package khác nhau trong các thư mục khác nhau theo hướng dẫn trong bài trên StackOverFlow “package XXX is not in GOROOT” when building a Go project, tôi nhận được lỗi phát sinh như:

"package XXX is not in GOROOT" when building a Go project

Sau khi tham khảo bài viết Package Is Not in Goroot: Why It Happens and How To Fix It in Go, tôi đã biết cách sửa cho dự án của mình.

Chúng ta có thể sử dụng cấu trúc thư mục theo phiên bản sau:

calculatorv3

├── go.mod
└── src
    ├─── main.go
    ├─── basic/
    │    ├─── add.go
    │    ├─── add_test.go
    │    ├─── multiply.go
    │    └─── multiply_test.go
    └─── advanced/
         ├─── square.go
         ├─── square_test.go
         └─── scientific/
              ├─── declog.go
              └─── declog_test.go

Các lệnh để chúng ta có thể chạy project này như sau:

# Tắt chế độ sử dụng GO111MODULE
go env -w GO111MODULE=off

# Thiết lập lại biến GOPATH
export GOPATH=$(pwd)

# Khởi tạo tệp go.mod
go mod init src/main.go

# Run project
go run ./src/...

# Build project
go build -o bin/calculatorv3 ./src

Lập trình Go thực hiện một số thao tác tới Blockchain

Đọc dữ liệu từ Blockchain

Các thao tác với blockchain thường chúng ta phải sử dụng thư viện ethclient, chi tiết các hàm bạn hãy tìm hiểu trong tài liệu của thư viện.

Đọc dữ liệu cơ bản từ blockchain

Trong ví dụ phần trước, tôi đã sử dụng hàm HeaderByNumber(), các hàm khác bạn sử dụng tương tự. Bây giờ chúng ta nâng cấp tệp use-ethclient.go để lấy thêm dữ liệu:

  • Lấy balance ETH của 1 địa chỉ
  • Lấy thông tin block từ block number

Với yêu cầu này chúng ta chỉ cần dùng các hàm của thư viện ethclient là đủ:

package main

import (
    "context"
    "fmt"
    "log"
    "math/big"

    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/ethereum/go-ethereum/common"
)

func main() {
    // Connect to RPC
    client, err := ethclient.Dial("https://rpc.ankr.com/eth")
    if err != nil {
        log.Fatalf("Failed to connect to the Ethereum network: %v", err)
    } else {
        fmt.Println("Success! you are connected to the Ethereum Network")
        fmt.Printf("EthClient: %+v\n", client)
    }

    // Get header
    header, err := client.HeaderByNumber(context.Background(), nil)
    if err != nil {
        log.Fatal(err)
    } else {
        fmt.Println("Lastest Block Number:", header.Number.String())
    }

    // Get ETH balance
    addr := "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5"
    balance, err := client.BalanceAt(context.Background(), common.HexToAddress(addr), nil)
    if err != nil {
        log.Fatal(err)
    } else {
        fmt.Println("Balance of ETH", addr, balance.String())
    }

    // Get current gas price
    gasPrice, err := client.SuggestGasPrice(context.Background())
    if err != nil {
        log.Fatal(err)
    } else {
        fmt.Println("Current Gas Price", gasPrice.String())
    }

    // Get Block Info
    blockNumber := big.NewInt(18224273)
    blockInfo, err := client.BlockByNumber(context.Background(), blockNumber)
    if err != nil {
        log.Fatal(err)
    } else {
        fmt.Println("BlockInfo for block number:", blockNumber.String())
        fmt.Println("\tHash:", blockInfo.Hash())
        fmt.Println("\tGasUsed:", blockInfo.GasUsed())
        fmt.Println("\tDiffculty:", blockInfo.Difficulty())
        fmt.Println("\tNonce:", blockInfo.Nonce())
        fmt.Println("\tBaseFee:", blockInfo.BaseFee())
    }
}

Do chúng ta sử dụng thêm thư viện nên chúng ta cần đánh lệnh sau để chạy:

# Cập nhật lại thư viện
go mod tidy

# Chạy ứng dụng
go run use-ethclient.go

Kết quả chúng ta sẽ có thông tin như dưới:

Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
Lastest Block Number: 18224491
Balance of ETH 0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5 6315453006381396322
Current Gas Price 7042837432
BlockInfo for block number: 18224273
        Hash: 0x423bcc58da9a26e9fc9a6ddcbb6c123be0fd54a62f5ec52cbe291936fcd1f47e
        GasUsed: 14630445
        Diffculty: 0
        Nonce: 0
        BaseFee: 7949807025

Đọc dữ liệu blockchain thông qua gọi hàm của Smart Contract

Hiện tại qua tìm hiểu thì tôi thấy có ba cách để thực hiện gọi hàm của Smart Contract:

  • C1: Sử dụng qua ABI thu gọn
    • B1: Đầu tiên chúng ta có string chứa ABI thu gọn (ABI chỉ chứa các hàm cần)
    • B2: Gọi hàm abi.JSON() trong thư viện abi => Được đối tượng
    • B3: Gọi hàm Pack() của đối tượng để đóng gói tên hàm và dữ liệu đầu vào thành dữ liệu
    • B4: Tạo đối tượng CallMsg
    • B5: Gọi hàm CallContract của đối tượng ethclient
    • B6: Gọi hàm Unpack() của đối tượng abi để được dữ liệu đầu ra
  • C2: Sử dụng qua hàm NewMethod
    Quá trình này phức tạp hơn nhiều vì chúng ta phải tự làm phần chuyển đổi từ ABI sang đối tượng Method
    • B1: Tạo đối tượng abi.Arguments cho phần inputs
    • B2: Tạo đối tượng abi.Arguments cho phần outputs
    • B3: Tạo đối tượng Method qua hàm NewMethod().
    • B4: Dùng hàm crypto.Keccak256() để tạo signature (Chú ý chỉ lấy 4 byte đầu tiên)
    • B5: Mã hóa inputs sử dụng hàm Pack()
    • B6: Nối signature với inputs đã mã hóa ta được dữ liệu data
    • B7: Tạo đối tượng CallMsg
    • B8: Gọi hàm CallContract của đối tượng ethclient
    • B9: Gọi hàm Unpack() của đối tượng abi để được dữ liệu đầu ra
  • C3: Sinh tệp .go từ tệp ABI thông qua công cụ abigen
    => Nói chi tiết ở phần sau

Chúng ta tạo tệp call-smartcontract.go gọi hàm smart contract theo C1 và C2:

  • Gọi hàm balanceOf() để lấy lượng token USDT theo C1
  • Gọi hàm getReserves() để lấy thông tin dự trữ của Pair V2 theo C2
package main

import (
    "context"
    "fmt"
    "log"
    "strings"
    "encoding/hex"

    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/accounts/abi"
    "github.com/ethereum/go-ethereum/crypto"
)

func main() {
    // Connect to RPC
    client, err := ethclient.Dial("https://rpc.ankr.com/eth")
    if err != nil {
        log.Fatalf("Failed to connect to the Ethereum network: %v", err)
        return
    }
    fmt.Println("Success! you are connected to the Ethereum Network")
    fmt.Printf("EthClient: %+v\n", client)
    fmt.Println("------------------------------------------------------")

    // Call balanceOf() of USDT contract over ABI
    ABI := "[{\"constant\":true,\"inputs\":[{\"name\":\"who\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]"
    tokenAbi, err := abi.JSON(strings.NewReader(ABI))
    data, err := tokenAbi.Pack("balanceOf", common.HexToAddress("0xF977814e90dA44bFA03b6295A0616a897441aceC"))
    tokenAddr := common.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7")
    msg := ethereum.CallMsg{
       To: &tokenAddr,
       Data: data,
    }
    outputData, err := client.CallContract(context.Background(), msg, nil)
    if err != nil {
        log.Fatalf("Call balanceOf() ERROR: %v", err)
        return
    }
    balance, err := tokenAbi.Unpack("balanceOf", outputData)
    if err != nil {
        log.Fatalf("Decode output of balanceOf() ERROR: %v", err)
        return
    }
    fmt.Println("USDT:", balance[0])
    fmt.Println("------------------------------------------------------")

    // Call getReserves() using NewMethod
    funcName := "getReserves"
    var inputs abi.Arguments = nil
    uint112Type, err := abi.NewType("uint112", "uint112", nil)
    outputs := abi.Arguments{
        abi.Argument{
            Name:    "reserve0",
            Type:    uint112Type,
            Indexed: false,
        },
        abi.Argument{
            Name:    "reserve1",
            Type:    uint112Type,
            Indexed: false,
        },
    }
    method := abi.NewMethod(funcName, funcName, abi.FunctionType(3), "view", false, false, inputs, outputs)
    sig := crypto.Keccak256([]byte(method.Sig))
    sig = []byte{sig[0], sig[1], sig[2], sig[3]}
    fmt.Println("Method:", method.String(), " => Sig:", "0x" + hex.EncodeToString(sig))
    data = sig
    pairV2Addr := common.HexToAddress("0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc")
    msg = ethereum.CallMsg{
       To: &pairV2Addr,
       Data: data,
    }
    outputData, err = client.CallContract(context.Background(), msg, nil)
    if err != nil {
        log.Fatalf("Call getReserves() ERROR: %v", err)
        return
    }
    reserves, err := outputs.Unpack(outputData)
    if err != nil {
        log.Fatalf("Decode output of getReserves() ERROR: %v", err)
        return
    }
    fmt.Println("Reserves:", reserves)
    fmt.Println("------------------------------------------------------")
}

Khi chạy chúng ta nhận kết quả như sau:

Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00016a090}
------------------------------------------------------
USDT: 3165153978018000
------------------------------------------------------
Method: function getReserves() view returns(uint112 reserve0, uint112 reserve1)  => Sig: 0x0902f1ac
Reserves: [27717096146069 17239779481733920536936]
------------------------------------------------------

Tương tác với Smart Contract thông qua công cụ abigen

Abigen là một công cụ giúp chuyển đổi ABI sang Go, qua đó giúp bạn dễ dàng tương tác với Smart Contract. Khi bạn sử dụng thư viện go-ethereum thì sẽ có source code của công cụ này, bạn tìm bằng lệnh sau:

# Tìm vị trí mã nguồn abigen
# Tôi tìm thấy trong: /home/ubuntu/go/pkg/mod/github.com/ethereum/go-ethereum@v1.13.1/cmd/abigen
locate abigen

# Build công cụ abigen
cd /home/ubuntu/go/pkg/mod/github.com/ethereum/go-ethereum@v1.13.1
go build ./cmd/abigen

# Sau khi build xong copy abigen vào thư mục /usr/local/bin

Nếu bạn chạy lệnh trên báo lỗi dạng:

github.com/ethereum/go-ethereum/cmd/abigen: go build github.com/ethereum/go-ethereum/cmd/abigen: copying /tmp/go-build1832924738/b001/exe/a.out: open abigen: permission denied

Thì bạn phải chuyển sang quyền sudo để build:

sudo go build ./cmd/abigen

Nếu bạn lại nhận lỗi khác:

sudo: go: command not found

Lỗi này do ở account root, biến môi trường PATH chưa có đường dẫn đến Go. Chúng ta sẽ làm thủ công như sau:

# Chuyển sang user root
sudo -i
export PATH=$PATH:/usr/local/go/bin

# Vào lại thư mục mã nguồn
cd /home/ubuntu/go/pkg/mod/github.com/ethereum/go-ethereum@v1.13.1
go build ./cmd/abigen

# Tệp abigen sinh ra ngay thư mục hiện tại
# Kiểm tra lại bằng lệnh dưới
abigen --version

Bây giờ chúng ta sử dụng công cụ này để tương tác với Uniswap Pair V3 0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640 (USDC / ETH). Đầu tiên chúng ta sẽ copy ABI của contract về lưu trong tệp UniswapPairV3.abi, sau đó chúng ta đánh lệnh sau:

# Sinh tệp UniswapPairV3.go
abigen --abi UniswapPairV3.abi --pkg main --type UniswapPairV3 --out UniswapPairV3.go

Tệp này khá lớn, bạn có thể xem tại UniswapPairV3.go. Chú ý trong tệp này chúng ta sẽ thấy có 3 hàm mà chúng ta sẽ dùng:

func NewUniswapPairV3(address common.Address, backend bind.ContractBackend) (*UniswapPairV3, error) {
    // ...
}

func (_UniswapPairV3 *UniswapPairV3Caller) Slot0(opts *bind.CallOpts) (struct {
	SqrtPriceX96               *big.Int
	Tick                       *big.Int
	ObservationIndex           uint16
	ObservationCardinality     uint16
	ObservationCardinalityNext uint16
	FeeProtocol                uint8
	Unlocked                   bool
}, error) {
    // ...
}

func (_UniswapPairV3 *UniswapPairV3Caller) Liquidity(opts *bind.CallOpts) (*big.Int, error) {
    // ...
}

Bây giờ ta tạo tệp call-smartcontract-02.go sử dụng các hàm trong tệp UniswapPairV3.go:

package main

import (
    "fmt"
    "log"

    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/ethereum/go-ethereum/common"
)

func main() {
    // Connect to RPC
    client, err := ethclient.Dial("https://rpc.ankr.com/eth")
    if err != nil {
        log.Fatalf("Failed to connect to the Ethereum network: %v", err)
        return
    }
    fmt.Println("Success! you are connected to the Ethereum Network")
    fmt.Printf("EthClient: %+v\n", client)
    fmt.Println("------------------------------------------------------")

    // Create UniswapPairV3
    pairV3, err := NewUniswapPairV3(common.HexToAddress("0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640"), client)
    slot0, err := pairV3.Slot0(nil)
    liquidity, err := pairV3.Liquidity(nil)
    fmt.Println("Slot0:", slot0)
    fmt.Println("Liquidity:", liquidity)
    fmt.Println("------------------------------------------------------")
}

Chạy bằng lệnh sau:

go run call-smartcontract-02.go UniswapPairV3.go

Output như dưới:

Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00011e3f0}
------------------------------------------------------
Slot0: {1979924313596069596897820693414386 202534 29 722 722 0 true}
Liquidity: 40784110340099305750
------------------------------------------------------

Sử dụng Multicall3

Multicall3 là contract hết sức tiện lợi khi cần lấy dữ liệu trên Blockchain. Để sử dụng Multicall3 trên Go, ta làm như sau. Đầu tiên ta vào 0xcA11bde05977b3631167028862bE2a173976CA11, xuống phần Contract ABI, ta chỉ lấy ABI cho hàm aggregate3, vì chúng ta chỉ sử dụng hàm này thôi. Ta tạo tệp Multicall3.abi và copy ABI của aggregate3 vào như sau (Chú ý bạn phải đổi stateMutability từ payable thành view):

[
    {
        "inputs":[
            {
                "components":[
                    {
                        "internalType":"address",
                        "name":"target",
                        "type":"address"
                    },
                    {
                        "internalType":"bool",
                        "name":"allowFailure",
                        "type":"bool"
                    },
                    {
                        "internalType":"bytes",
                        "name":"callData",
                        "type":"bytes"
                    }
                ],
                "internalType":"struct Multicall3.Call3[]",
                "name":"calls",
                "type":"tuple[]"
            }
        ],
        "name":"aggregate3",
        "outputs":[
            {
                "components":[
                    {
                        "internalType":"bool",
                        "name":"success",
                        "type":"bool"
                    },
                    {
                        "internalType":"bytes",
                        "name":"returnData",
                        "type":"bytes"
                    }
                ],
                "internalType":"struct Multicall3.Result[]",
                "name":"returnData",
                "type":"tuple[]"
            }
        ],
        "stateMutability":"view",
        "type":"function"
    }
]

Bây giờ ta đánh lệnh sau để sinh ra tệp Multicall3.go

abigen --abi Multicall3.abi --pkg main --type Multicall3 --out Multicall3.go

Bây giờ ta viết tệp use-multicall.go để sử dụng các hàm trong Multicall3.go được sinh ra ở trên:

package main

import (
        "log"
        "strings"
        "math/big"

        "github.com/ethereum/go-ethereum/accounts/abi"
        "github.com/ethereum/go-ethereum/common"
        "github.com/ethereum/go-ethereum/ethclient"
)

var Encode_GetBlockNumber []byte = common.Hex2Bytes("42cbb15c")
var Encode_GetCurrentBlockTimestamp []byte = common.Hex2Bytes("0f28c97d")
var Encode_GetReserves []byte = common.Hex2Bytes("0902f1ac")

var rpcClient *ethclient.Client = nil
var multicall3Decoder *abi.ABI = nil

var Multicall3Addr common.Address

const DecodeAbi = `[
        {
                "inputs": [],
                "name": "getBlockNumber",
                "outputs": [
                        {
                                "internalType": "uint256",
                                "name": "blockNumber",
                                "type": "uint256"
                        }
                ],
                "stateMutability": "view",
                "type": "function"
        },
        {
                "inputs": [],
                "name": "getCurrentBlockTimestamp",
                "outputs": [
                        {
                                "internalType": "uint256",
                                "name": "timestamp",
                                "type": "uint256"
                        }
                ],
                "stateMutability": "view",
                "type": "function"
        },
        {
                "inputs": [],
                "name": "getReserves",
                "outputs": [
                        {
                                "internalType": "uint112",
                                "name": "_reserve0",
                                "type": "uint112"
                        },
                        {
                                "internalType": "uint112",
                                "name": "_reserve1",
                                "type": "uint112"
                        },
                        {
                                "internalType": "uint32",
                                "name": "_blockTimestampLast",
                                "type": "uint32"
                        }
                ],
                "stateMutability": "view",
                "type": "function"
        }
]`

func InitMulticall3(network string, rpcNode string) *Multicall3 {
        // Init addresses
        Multicall3Addr = common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11")

        // Create Multicall3 ABI
        if multicall3Decoder == nil {
                mAbi, err := abi.JSON(strings.NewReader(DecodeAbi))
                if err != nil {
                        log.Fatalf("Unable to create ABI: %v", err)
                }
                multicall3Decoder = &mAbi
        }

        // Connect to RPC
        client, err := ethclient.Dial(rpcNode)
        if err != nil {
                log.Fatalf("Failed to connect RPC %s: %v", rpcNode, err)
        }
        log.Printf("Success to connect RPC %s\n", rpcNode)
        rpcClient = client

        // Create multicall3
        multicall, err := NewMulticall3(Multicall3Addr, rpcClient)
        if err != nil {
                log.Fatalf("Failed to create Multicall3: %v", err)
        }
        return multicall
}

func DecodeBlockNumber(result Multicall3Result) uint64 {
        if !result.Success {
                return 0
        }
        data, err := multicall3Decoder.Unpack("getBlockNumber", result.ReturnData)
        if err != nil {
                log.Printf("Decode output of getBlockNumber() ERROR: %v\n", err)
                return 0
        }
        blockNumber := data[0].(*big.Int)
        return blockNumber.Uint64()
}

func DecodeBlockTime(result Multicall3Result) uint64 {
        if !result.Success {
                return 0
        }
        data, err := multicall3Decoder.Unpack("getCurrentBlockTimestamp", result.ReturnData)
        if err != nil {
                log.Printf("Decode output of data() ERROR: %v\n", err)
                return 0
        }
        blockTime := data[0].(*big.Int)
        return blockTime.Uint64()
}

func DecodeReserves(result Multicall3Result) (reserve0 string, reserve1 string, ok bool) {
        ok = false
        if result.Success {
                data, err := multicall3Decoder.Unpack("getReserves", result.ReturnData)
                if err == nil {
                        reserve0 = (data[0].(*big.Int)).String()
                        reserve1 = (data[1].(*big.Int)).String()
                        ok = true
                }
        }

        return reserve0, reserve1, ok
}

func main() {
    // Init multicall
    multicall := InitMulticall3("BASE", "https://mainnet.base.org")

    // Init args
    args := make([]Multicall3Call3, 3)
    args[0].Target = Multicall3Addr
    args[0].AllowFailure = false
    args[0].CallData = Encode_GetBlockNumber
    args[1].Target = Multicall3Addr
    args[1].AllowFailure = false
    args[1].CallData = Encode_GetCurrentBlockTimestamp
    args[2].Target = common.HexToAddress("0xB4885Bc63399BF5518b994c1d0C153334Ee579D0")
    args[2].AllowFailure = false
    args[2].CallData = Encode_GetReserves


    // Call to multicall
    results, err := multicall.Aggregate3(nil, args)
    if err != nil {
        log.Fatalf("Call aggregate3() ERROR: %+v\n", err)
        return
    }
    // fmt.Println("Results:", results)

    // Decode data
    blockNumber := DecodeBlockNumber(results[0])
    blockTime := DecodeBlockTime(results[1])
    reserve0, reserve1, ok := DecodeReserves(results[2])
    if !ok {
        log.Println("Unable to decode reserves")
        return
    }
    log.Println("Block Number:", blockNumber)
    log.Println("Block Time:", blockTime)
    log.Println("Reserve0:", reserve0, "- Reserve1:", reserve1)
}

Lệnh chạy như sau:

go run use-multicall.go Multicall3.go

Kết quả hiển thị màn hình:

2024/01/10 03:44:50 Success to connect RPC https://mainnet.base.org
2024/01/10 03:44:50 Block Number: 9034471
2024/01/10 03:44:50 Block Time: 1704858289
2024/01/10 03:44:50 Reserve0: 2552028515455613855143 - Reserve1: 6008243004271

Ghi dữ liệu vào blockchain

Để tiết kiệm tiền, các giao dịch ghi dữ liệu tôi sẽ chạy trên Goerli Testnet

Chuyển ETH giữa các ví

Chuyển ETH giữa các ví bạn có thể xem hướng dẫn trong bài: Transferring ETH. Mã nguồn tệp transfer-eth.go như dưới:

package main

import (
    "context"
    "fmt"
    "log"
    "math/big"
    "crypto/ecdsa"

    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/core/types"
)

func main() {
    client, err := ethclient.Dial("https://rpc.ankr.com/eth_goerli")
    if err != nil {
        log.Fatalf("Failed to connect to the Ethereum network: %v", err)
    } else {
        fmt.Println("Success! you are connected to the Ethereum Network")
        fmt.Printf("EthClient: %+v\n", client)
    }

    // Load private key
    // Account: 0x96216849c49358B10257cb55b28eA603c874b05E
    privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19")
    publicKey := privateKey.Public()
    publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
    if !ok {
        log.Fatal("error casting public key to ECDSA")
    }
    fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
    fmt.Println("From Address:", fromAddress.Hex())

    // Get nonce
    nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Nonce:", nonce)

    // Transfer value
    value := big.NewInt(1000000000000) // in wei (0.000001 eth)
    fmt.Println("Value:", value)

    // Gas Limit
    gasLimit := uint64(30000)                // in units
    fmt.Println("Gas Limit:", gasLimit)

    // Gas Price
    gasPrice, err := client.SuggestGasPrice(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Gas Price:", gasPrice)

    // Chain ID
    chainID, err := client.NetworkID(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("ChainID:", chainID)

    // To Address
    toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
    fmt.Println("To Address:", toAddress.Hex())

    // Create transaction
    var data []byte
    tx := types.NewTransaction(nonce, toAddress, value, gasLimit, gasPrice, data)

    // Sign transaction
    signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
    if err != nil {
        log.Fatal(err)
    }

    // Send transaction
    err = client.SendTransaction(context.Background(), signedTx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("TxId:", signedTx.Hash().Hex())
}

Kết quả sau khi chạy (Xem giao dịch):

Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
From Address: 0x96216849c49358B10257cb55b28eA603c874b05E
Nonce: 645
Value: 1000000000000
Gas Limit: 30000
Gas Price: 49
ChainID: 5
To Address: 0x4592D8f8D7B001e72Cb26A73e4Fa1806a51aC79d
TxId: 0xb55dfb72a9f7a51bfdd9cee1281a5c10e96b3ea3ff4915746180e47bae8bebb0

Chuyển token giữa các ví

Chuyển token giữa các ví bạn có thể xem bài viết: Transferring Tokens. Do lỗi phát sinh nên tôi có thay đổi chút để chạy được:

  • Tôi không sử dụng thư viện sha3 do báo lỗi không có thư viện này (Có thể liên quan tới version thư viện của ethereum)
  • Tôi fix cứng Gas Limit do hàm EstimateGas() có vấn đề

Toàn bộ code trong tệp transfer-token.go:

package main

import (
    "context"
    "fmt"
    "log"
    "math/big"
    "crypto/ecdsa"

//    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/common/hexutil"
)

func main() {
    client, err := ethclient.Dial("https://rpc.ankr.com/eth_goerli")
    if err != nil {
        log.Fatalf("Failed to connect to the Ethereum network: %v", err)
    } else {
        fmt.Println("Success! you are connected to the Ethereum Network")
        fmt.Printf("EthClient: %+v\n", client)
    }

    // Load private key
    // Account: 0x96216849c49358B10257cb55b28eA603c874b05E
    privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19")
    publicKey := privateKey.Public()
    publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
    if !ok {
        log.Fatal("error casting public key to ECDSA")
    }
    fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
    fmt.Println("From Address:", fromAddress.Hex())

    // Get nonce
    nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Nonce:", nonce)

    // Transfer value
    value := big.NewInt(0)
    fmt.Println("Value:", value)

    // Gas Price
    gasPrice, err := client.SuggestGasPrice(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Gas Price:", gasPrice)

    // Chain ID
    chainID, err := client.NetworkID(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("ChainID:", chainID)

    // To Address
    toAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
    tokenAddress := common.HexToAddress("0x326C977E6efc84E512bB9C30f76E30c160eD06FB")
    fmt.Println("To Address:", toAddress.Hex())
    fmt.Println("Token Address:", tokenAddress.Hex())

    // Transfer amount
    amount := big.NewInt(1000000000000000)    // 0.001
    fmt.Println("Amount:", amount)

    // Generate 4 bytes function signature
    transferFnSignature := []byte("transfer(address,uint256)")
    hash := crypto.Keccak256(transferFnSignature)
    methodID := []byte{hash[0], hash[1], hash[2], hash[3]}
    fmt.Println("Method ID:", hexutil.Encode(methodID)) // 0xa9059cbb

    // Generate data
    var data []byte
    data = append(data, methodID...)
    paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)
    data = append(data, paddedAddress...)
    paddedAmount := common.LeftPadBytes(amount.Bytes(), 32)
    data = append(data, paddedAmount...)
    fmt.Println("Data:", hexutil.Encode(data))

    // Estimate gas
    //gasLimit, err := client.EstimateGas(context.Background(), ethereum.CallMsg{
    //    To:   &tokenAddress,
    //    Data: data,
    //})
    //if err != nil {
    //    log.Fatal("Gas Limit: ", err)
    //}
    gasLimit := uint64(120000)
    fmt.Println("Gas Limit:", gasLimit)

    // Create transaction
    tx := types.NewTransaction(nonce, tokenAddress, value, gasLimit, gasPrice, data)

    // Sign transaction
    signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
    if err != nil {
        log.Fatal(err)
    }

    // Send transaction
    err = client.SendTransaction(context.Background(), signedTx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("TxId:", signedTx.Hash().Hex())
}

Kết quả sau khi chay (Xem giao dịch):

Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
From Address: 0x96216849c49358B10257cb55b28eA603c874b05E
Nonce: 650
Value: 0
Gas Price: 28
ChainID: 5
To Address: 0x4592D8f8D7B001e72Cb26A73e4Fa1806a51aC79d
Token Address: 0x326C977E6efc84E512bB9C30f76E30c160eD06FB
Amount: 1000000000000000
Method ID: 0xa9059cbb
Data: 0xa9059cbb0000000000000000000000004592d8f8d7b001e72cb26a73e4fa1806a51ac79d00000000000000000000000000000000000000000000000000038d7ea4c68000
Gas Limit: 120000
TxId: 0xc917e323dad00f9a5569e1b94f5b71ba980fd1be4dd7df907d20398630a7cad9

Swap token trên Pool V3

Trong ví dụ này tôi sẽ swap từ ETH sang UNI, thông tin chi tiết swap sẽ thực hiện như sau:

Nếu bạn muốn thử swap trên pool khác, bạn có thể lấy thông tin pool trong bài viết: Hướng dẫn triển khai Flashloan / Flashswap sử dụng Uniswap V3 trên Goerli Testnet

Ta tạo tệp swap-token.go để thực hiện swap token:

package main

import (
    "context"
    "fmt"
    "log"
    "math/big"
    "crypto/ecdsa"

//    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/common/hexutil"
)

func main() {
    client, err := ethclient.Dial("https://rpc.ankr.com/eth_goerli")
    if err != nil {
        log.Fatalf("Failed to connect to the Ethereum network: %v", err)
    } else {
        fmt.Println("Success! you are connected to the Ethereum Network")
        fmt.Printf("EthClient: %+v\n", client)
    }

    // Load private key
    // Account: 0x96216849c49358B10257cb55b28eA603c874b05E
    privateKey, err := crypto.HexToECDSA("fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19")
    publicKey := privateKey.Public()
    publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
    if !ok {
        log.Fatal("error casting public key to ECDSA")
    }
    fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
    fmt.Println("From Address:", fromAddress.Hex())

    // Get nonce
    nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Nonce:", nonce)

    // Transfer value
    value := big.NewInt(1000000000000000)   // 0.001 ETH
    fmt.Println("Value:", value)

    // Gas Price
    gasPrice, err := client.SuggestGasPrice(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Gas Price:", gasPrice)

    // Chain ID
    chainID, err := client.NetworkID(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("ChainID:", chainID)

    // To Address
    recipientAddress := common.HexToAddress("0x4592d8f8d7b001e72cb26a73e4fa1806a51ac79d")
    swapRouter := common.HexToAddress("0xE592427A0AEce92De3Edee1F18E0157C05861564")
    tokenIn := common.HexToAddress("0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6")  // WETH
    tokenOut := common.HexToAddress("0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984")  // UNI
    poolFee := big.NewInt(500)
    fmt.Println("Recipient Address:", recipientAddress.Hex())
    fmt.Println("Swap Router:", swapRouter.Hex())
    fmt.Println("Token In:", tokenIn.Hex())
    fmt.Println("Token Out:", tokenOut.Hex())
    fmt.Println("Pool Fee:", poolFee)

    // Generate 4 bytes function signature
    transferFnSignature := []byte("exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))")
    hash := crypto.Keccak256(transferFnSignature)
    methodID := []byte{hash[0], hash[1], hash[2], hash[3]}
    fmt.Println("Method ID:", hexutil.Encode(methodID)) // 0x414bf389

    // Generate data
    var data []byte
    data = append(data, methodID...)
    tempData := common.LeftPadBytes(tokenIn.Bytes(), 32)
    data = append(data, tempData...)
    tempData = common.LeftPadBytes(tokenOut.Bytes(), 32)
    data = append(data, tempData...)
    tempData = common.LeftPadBytes(poolFee.Bytes(), 32)
    data = append(data, tempData...)
    tempData = common.LeftPadBytes(recipientAddress.Bytes(), 32)
    data = append(data, tempData...)
    tempData = common.LeftPadBytes(big.NewInt(2999999999).Bytes(), 32)
    data = append(data, tempData...)
    tempData = common.LeftPadBytes(value.Bytes(), 32)
    data = append(data, tempData...)
    tempData = common.LeftPadBytes(big.NewInt(0).Bytes(), 32)
    data = append(data, tempData...)
    tempData = common.LeftPadBytes(big.NewInt(0).Bytes(), 32)
    data = append(data, tempData...)
    fmt.Println("Data:", hexutil.Encode(data))

    // Fix gas
    gasLimit := uint64(200000)
    fmt.Println("Gas Limit:", gasLimit)

    // Create transaction
    tx := types.NewTransaction(nonce, swapRouter, value, gasLimit, gasPrice, data)

    // Sign transaction
    signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
    if err != nil {
        log.Fatal(err)
    }

    // Send transaction
    err = client.SendTransaction(context.Background(), signedTx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("TxId:", signedTx.Hash().Hex())
}

Kết quả sau khi chạy như sau (Xem giao dịch):

Success! you are connected to the Ethereum Network
EthClient: &{c:0xc00015c090}
From Address: 0x96216849c49358B10257cb55b28eA603c874b05E
Nonce: 651
Value: 1000000000000000
Gas Price: 63
ChainID: 5
Recipient Address: 0x4592D8f8D7B001e72Cb26A73e4Fa1806a51aC79d
Swap Router: 0xE592427A0AEce92De3Edee1F18E0157C05861564
Token In: 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6
Token Out: 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984
Pool Fee: 500
Method ID: 0x414bf389
Data: 0x414bf389000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d60000000000000000000000001f9840a85d5af5bf1d1762f925bdaddc4201f98400000000000000000000000000000000000000000000000000000000000001f40000000000000000000000004592d8f8d7b001e72cb26a73e4fa1806a51ac79d00000000000000000000000000000000000000000000000000000000b2d05dff00000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Gas Limit: 200000
TxId: 0xe4065836134889e078d13541970c40ec83ebef64241f14fba89f5398479ac028

Lắng nghe sự kiện từ Blockchain

Lắng nghe sự kiện khi có block mới

Chi tiết hướng dẫn, bạn tham khảo bài viết: Subscribing to New Blocks. Trong ví dụ dưới, tôi sẽ lắng nghe sự kiện có block mới trên Base blockchain. Chúng ta cần xác định block mới nhất là block nào và dữ liệu đang trễ bao nhiều so với hiện tại, do đó tôi sửa lại mã nguồn. Trong ví dụ này tôi sử dụng node nội bộ nên link websocket là ws://localhost:8546, bạn không có node nội bộ, bạn có thể sử dụng public websocket như wss://base.publicnode.com. Mã nguồn tệp block_subscribe.go như sau:

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/ethclient"
)

func main() {
    client, err := ethclient.Dial("ws://localhost:8546")
    // client, err := ethclient.Dial("wss://base.publicnode.com")
    if err != nil {
        log.Fatal("Connect websocket: ", err)
    }
    fmt.Println("Success! you are connected to the Base Network")
    fmt.Printf("EthClient: %+v\n", client)

    headers := make(chan *types.Header)
    sub, err := client.SubscribeNewHead(context.Background(), headers)
    if err != nil {
        log.Fatal("Subcribe New head: ", err)
    }

    for {
        select {
        case err := <-sub.Err():
            log.Fatal("Data Error", err)
        case header := <-headers:
            fmt.Println("----------------------------------")
            fmt.Println("  Block Number:", header.Number)
            fmt.Println("  Block Time:", header.Time)
            fmt.Println("  Block Hash:", header.Hash().Hex())
            fmt.Println("  Delay Time (Ms)", time.Now().UnixMilli() - 1000*int64(header.Time))
            fmt.Println("----------------------------------")
        }
    }
}

Kết quả sau khi chạy chương trình như sau:

Success! you are connected to the Base Network
EthClient: &{c:0xc00015c360}
----------------------------------
  Block Number: 4591872
  Block Time: 1695973091
  Block Hash: 0x1cc41c80d7b4837b74d40b889edd891679022f8dee314811aa9be285a6391bed
  Delay Time (Ms) 43248
----------------------------------
----------------------------------
  Block Number: 4591873
  Block Time: 1695973093
  Block Hash: 0x38d8c6ca14e5770dd9e21b388902753d4cf930f547c7ec200877b71d2800193f
  Delay Time (Ms) 41294
----------------------------------
----------------------------------
  Block Number: 4591874
  Block Time: 1695973095
  Block Hash: 0xe9680a7ccd0d8fc96f9cf42d2d55d0185c3fc2329341af441c232db410416c9b
  Delay Time (Ms) 39427
----------------------------------

Lắng nghe Event Logs

Phần này tôi tham khảo Subscribing to Event LogsReading Event Logs. Dựa vào hướng dẫn, tôi xây dựng ví dụ lắng nghe hai sự kiện:

  • Sự kiện Sync (Trên các Pool của Uniswap V2)
  • Sự kiện Swap (Trên các Pool của Uniswap V3)

Để cập nhật thông tin mới nhất các pool. Mã nguồn đóng gói trong tệp event_subscribe.go như dưới:

package main

import (
    "context"
    "fmt"
    "log"
    "strings"

    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/ethclient"
    "github.com/ethereum/go-ethereum/accounts/abi"
)

func main() {
    // client, err := ethclient.Dial("ws://localhost:8546")
    client, err := ethclient.Dial("wss://base.publicnode.com")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Success! you are connected to the Base Network")
    fmt.Printf("EthClient: %+v\n", client)

    // Event ABI
    eventABI := "[{\"anonymous\": false,\"inputs\": [{\"indexed\": false,\"internalType\": \"uint112\",\"name\": \"reserve0\",\"type\": \"uint112\"},{\"indexed\": false,\"internalType\": \"uint112\",\"name\": \"reserve1\",\"type\": \"uint112\"}],\"name\": \"Sync\",\"type\": \"event\"},{\"anonymous\": false,\"inputs\": [{\"indexed\": true,\"internalType\": \"address\",\"name\": \"sender\",\"type\": \"address\"},{\"indexed\": true,\"internalType\": \"address\",\"name\": \"recipient\",\"type\": \"address\"},{\"indexed\": false,\"internalType\": \"int256\",\"name\": \"amount0\",\"type\": \"int256\"},{\"indexed\": false,\"internalType\": \"int256\",\"name\": \"amount1\",\"type\": \"int256\"},{\"indexed\": false,\"internalType\": \"uint160\",\"name\": \"sqrtPriceX96\",\"type\": \"uint160\"},{\"indexed\": false,\"internalType\": \"uint128\",\"name\": \"liquidity\",\"type\": \"uint128\"},{\"indexed\": false,\"internalType\": \"int24\",\"name\": \"tick\",\"type\": \"int24\"}],\"name\": \"Swap\",\"type\": \"event\"}]"
    eventAbi, err := abi.JSON(strings.NewReader(eventABI))
    if err != nil {
        log.Fatal("Event ABI ERROR: ", err)
    }

    // Events:
    //     Sync event for Pair V2
    //     Swap event for Pair V3
    var syncTopic = common.HexToHash("0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1")
    var swapV3Topic = common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67")

    // Create filter
    topics := [][]common.Hash{{syncTopic, swapV3Topic}}
    query := ethereum.FilterQuery{
        Topics: topics,
    }

    logs := make(chan types.Log)
    sub, err := client.SubscribeFilterLogs(context.Background(), query, logs)
    if err != nil {
        log.Fatal(err)
    }

    eventSyncDataMap := map[string]interface{}{}
    eventSwapDataMap := map[string]interface{}{}
    for {
        select {
        case err := <-sub.Err():
            log.Fatal(err)
        case vLog := <-logs:
            //fmt.Println(vLog) // Print log data to debug
            if (vLog.Topics[0]==syncTopic) {
                // Sync event for Pair V2
                fmt.Println("Sync event for V2:")
                fmt.Println("    Pair:", vLog.Address.Hex())
                err := eventAbi.UnpackIntoMap(eventSyncDataMap, "Sync", vLog.Data)
                if err!=nil {
                    fmt.Println("    Parse data error:", err)
                } else {
                    fmt.Println("    Reserves:", eventSyncDataMap)
                }
            } else if (vLog.Topics[0]==swapV3Topic) {
                // Swap event for Pair V3
                fmt.Println("Swap event for V3:")
                fmt.Println("    Pair:", vLog.Address.Hex())
                err := eventAbi.UnpackIntoMap(eventSwapDataMap, "Swap", vLog.Data)
                if err!=nil {
                    fmt.Println("    Parse data error:", err, common.Bytes2Hex(vLog.Data))
                } else {
                    fmt.Println("    SwapInfo:", eventSwapDataMap)
                }
            }
            //fmt.Println(vLog) // pointer to event log
        }
    }
}
Success! you are connected to the Base Network
EthClient: &{c:0xc00016a1b0}
Swap event for V3:
    Pair: 0x4C36388bE6F416A29C8d8Eee81C771cE6bE14B18
    SwapInfo: map[amount0:128000000000000000 amount1:-214596119 liquidity:324535014823069391 sqrtPriceX96:3244820116208715158447683 tick:-202071]
Sync event for V2:
    Pair: 0x6D3c5a4a7aC4B1428368310E4EC3bB1350d01455
    Reserves: map[reserve0:1045335009077065580521023 reserve1:1045741040809]
Swap event for V3:
    Pair: 0x3B8000CD10625ABdC7370fb47eD4D4a9C6311fD5
    SwapInfo: map[amount0:5049996066912133 amount1:-8466734 liquidity:20979686887511447 sqrtPriceX96:3244229835509471844999839 tick:-202075]

Làm việc với thư viện mã hóa trong Crypto

Khi làm việc với Blockchain và Crypto thì chắc chắn bạn phải cần làm việc với Mã hóa / Giải mã sử dụng khóa bất đối xứng. Bạn tham khảo thư viện crypto/rsa trong Go.

Bây giờ chúng ta viết tệp rsa.go thực hiện các việc sau:

  • Sinh ra cặp khóa RSA Private/Public key 4096 bit và thực hiện lưu key này xuống tệp PEM (keys/private.pemkeys/public.pem)
  • Mã hóa đoạn văn bản sử dụng Public key được load lên từ tệp keys/public.pem
  • Giải mã văn bản sử dụng Private key được load lên từ tệp keys/private.pem

Chi tiết mã nguồn như dưới:

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/hex"
	"encoding/pem"
	"fmt"
	"log"
	"os"
)

func GenerateKey() *rsa.PrivateKey {
	var size int = 4096
	priv, err := rsa.GenerateKey(rand.Reader, size)
	if err != nil {
		log.Printf("GenerateKey(%d): %v\n", size, err)
		return nil
	}
	if bits := priv.N.BitLen(); bits != size {
		log.Printf("Key too short (%d vs %d)\n", bits, size)
		return nil
	}
	return priv
}

func SavePrivateKey(fileName string, key *rsa.PrivateKey) bool {
	outFile, err := os.Create(fileName)
	if err != nil {
		log.Println("Error to create file:", fileName, err)
		return false
	}
	defer outFile.Close()

	var privateKey = &pem.Block{
		Type:  "PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(key),
	}

	err = pem.Encode(outFile, privateKey)
	if err != nil {
		log.Println("Error to save key to file:", fileName, err)
		return false
	}
	return true
}

func PrivateKey2Bytes(priv *rsa.PrivateKey) []byte {
	data := x509.MarshalPKCS1PrivateKey(priv)
	if data == nil {
		fmt.Println("Unable to encode private key")
		return nil
	}
	return data
}

func Bytes2PrivateKey(data []byte) *rsa.PrivateKey {
	privKey, err := x509.ParsePKCS1PrivateKey(data)
	if err != nil {
		fmt.Println("Unable to decode public key", err)
		return nil
	}
	return privKey
}

func PublicKey2Bytes(pub *rsa.PublicKey) []byte {
	data := x509.MarshalPKCS1PublicKey(pub)
	if data == nil {
		fmt.Println("Unable to encode public key")
		return nil
	}
	return data
}

func Bytes2PublicKey(data []byte) *rsa.PublicKey {
	pubKey, err := x509.ParsePKCS1PublicKey(data)
	if err != nil {
		fmt.Println("Unable to decode public key", err)
		return nil
	}
	return pubKey
}

// keyType: "PUBLIC KEY" / "PRIVATE KEY"
func SaveKeyToFile(fileName string, data []byte, keyType string) bool {
	var pemkey = &pem.Block{
		Type:  keyType,
		Bytes: data,
	}

	pemfile, err := os.Create(fileName)
	if err != nil {
		log.Println("Error to create file:", fileName, err)
		return false
	}
	defer pemfile.Close()

	err = pem.Encode(pemfile, pemkey)
	if err != nil {
		log.Println("Error to save key to file:", fileName, err)
		return false
	}
	return true
}

func LoadKeyFromFile(fileName string) []byte {
	data, err := os.ReadFile(fileName)
	if err != nil {
		log.Println("Error to read file:", fileName, err)
		return nil
	}
	block, _ := pem.Decode(data)
	if block == nil {
		log.Println("Error to decode key from file:", fileName, err)
		return nil
	}
	return block.Bytes
}

func ShowHelp() {
	fmt.Println("Commands:")
	fmt.Println("    go run rsa.go generate-key")
	fmt.Println("    go run rsa.go encrypt \"Testing data\"")
	fmt.Println("    go run rsa.go decrypt <data>")
}

// Main function
func main() {
	if len(os.Args) < 2 {
		ShowHelp()
		return
	}

	privateKeyFile := "keys/private.pem"
	publicKeyFile := "keys/public.pem"
	arg := os.Args[1]
	if arg == "generate-key" {
		os.Mkdir("keys", 0750)

		// Generate key
		privKey := GenerateKey()

		// Save private key
		privData := PrivateKey2Bytes(privKey)
		fmt.Println("PrivateKey:", hex.EncodeToString(privData))
		SaveKeyToFile(privateKeyFile, privData, "PRIVATE KEY")
		fmt.Println("Save private key to file:", privateKeyFile)

		// Save public key
		pubData := PublicKey2Bytes(&privKey.PublicKey)
		fmt.Println("PublicKey:", hex.EncodeToString(pubData))
		SaveKeyToFile(publicKeyFile, pubData, "PUBLIC KEY")
		fmt.Println("Save public key to file:", publicKeyFile)
	} else if arg == "encrypt" {
		// Encrypt message
		if len(os.Args) < 3 {
			ShowHelp()
			return
		}
		message := os.Args[2]
		pubData := LoadKeyFromFile(publicKeyFile)
		pubKey := Bytes2PublicKey(pubData)
		if pubKey == nil {
			return
		}
		data, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, []byte(message))
		if err != nil {
			fmt.Println("Unable to encrypt message", message, err)
		} else {
			strHexa := hex.EncodeToString(data)
			fmt.Println("Encrypted Data:", strHexa)
		}
	} else if arg == "decrypt" {
		// Decrypt message
		if len(os.Args) < 3 {
			ShowHelp()
			return
		}
		strHexa := os.Args[2]
		encryptedData, err := hex.DecodeString(strHexa)
		if err != nil {
			fmt.Println("Unable to convert hexa to bytes", strHexa)
			return
		}
		privData := LoadKeyFromFile(privateKeyFile)
		privKey := Bytes2PrivateKey(privData)
		if privKey == nil {
			return
		}
		data, err := rsa.DecryptPKCS1v15(nil, privKey, encryptedData)
		if err != nil {
			fmt.Println("Unable to decrypt message:", err)
		} else {
			message := string(data)
			fmt.Println("Message:", message)
		}
	} else {
		ShowHelp()
	}
}

Các bước để chạy như sau:

# B1: Sinh cặp khóa RSA
go run rsa.go generate-key

# B2: Mã hóa dữ liệu sử dụng Public key được sinh ra ở B1
# Mặc dù cùng 1 message đầu vào, bạn chạy các lần khác nhau cho kết quả khác nhau do cơ chế làm nhiều đầu ra
# Nhưng khi bạn giải mã đều cho cùng 1 kết quả nếu cùng 1 message đầu vào
go run rsa.go encrypt "Testing data"

# B3: Giải mã dữ liệu sử dụng Private key được sinh ra ở B1
# Thay cipher-data bằng dữ liệu đã được mã hóa ở B2
go run rsa.go decrypt <cipher-data>

Lập trình Go kết nối đến một số dịch vụ khác

Kết nối tới MongoDb

Chi tiết tài liệu kết nối tới MongoDb, bạn xem tại: MongoDB Go Driver. Thư viện chúng ta sử dụng sẽ là mongo.

Kết nối tới Redis

Việc kết nối và sử dụng Redis cũng khá đơn giản sử dụng thư việ go-redis, bạn có thể xem chi tiết tại: Connect your Go application to a Redis database. Chi tiết tài liệu có thể xem tại: Go Redis Guide

Thực hiện Set / Get dữ liệu trên Redis

Theo hướng dẫn trang này tôi viết tệp redis-test.go như sau:

package main

import (
    "context"
    "fmt"
    "log"
    "github.com/redis/go-redis/v9"
)

func main() {
    client := redis.NewClient(&redis.Options{
        Addr:     "127.0.0.1:6379",
        Password: "xxxxxxxxxxxxx",
        DB:       0,
    })
    if client==nil {
        log.Fatal("Unable to create Redis Client!!!");
    }

    ctx := context.Background()
    redisKey := "go_testing_key"
    redisValue := "Only for testing on Go"
    err := client.Set(ctx, redisKey, redisValue, 0).Err()
    if err != nil {
        log.Printf("Unable to set data to key %s: %+v\n", redisKey, err);
    }

    val, err := client.Get(ctx, redisKey).Result()
    switch {
        case err == redis.Nil:
            fmt.Printf("The key does not exist: %s\n", redisKey)
        case err != nil:
            fmt.Printf("Unable to get data for key %s: %+v\n", redisKey, err)
        case val == "":
            fmt.Println("The value is empty")
        default:
            fmt.Printf("The value for key %s: %s\n", redisKey, val)
    }
}

Chúng ta khởi tạo project và tải thư viện bằng 2 lệnh dưới:

go mod init example
go get github.com/redis/go-redis/v9

Sau đó chúng ta chạy bằng lệnh sau:

go run redis-test.go

# Output:
# go_testing_key: Only for testing on Go

Sau khi thực hiện một thao tác sẽ trả về một đối tượng:

  • Set/Get: Trả về đối tượng. Bạn có thể gọi hàm Err(), Result() của đối tượng này để lấy thông tin
  • Do: Trả về đối tượng Cmd, bạn gọi hàm hỗ trợ để lấy thông tin. Xem thêm: Executing unsupported commands

Các lệnh hay dùng với Redis:

# Lệnh lưu dữ liệu lên Redis
client.Set(ctx, redisKey, redisValue, 0).Err()

# Lệnh lưu dữ liệu lên Redis nếu chưa có, nếu đã có giá trị thì không làm gì
# SETNX: Viết tắt của SET - Not - eXists
client.SetNx(ctx, redisKey, redisValue, 0).Err()

# Thực hiện lệnh bất kỳ
# Số lượng tham số phụ thuộc vào command: set / get /...
client.Do(ctx, command, param1, param2, ...).Result()

# Lệnh lấy dữ liệu từ Redis
client.Get(ctx, redisKey).Result()

#...

Thực hiện Pub / Sub trên Redis

Để trao đổi dữ liệu Realtime, người ta hay sử dụng cơ chế Pub/Sub trên Redis. Bây giờ ta viết tệp redis-pubsub.go thực hiện như sau:

  • Tạo luồng phụ để nhận dữ liệu từ channel tên là “go_testing_channel“. Mỗi khi nhận được sẽ in ra nội dung nhận được ra màn hình.
  • Luồng chính ta thực hiện publish dữ liệu tới channel trên.
package main

import (
    "context"
    "fmt"
    "log"
    "strconv"
    "time"
    "github.com/redis/go-redis/v9"
)

func createRedisClient() *redis.Client {
    client := redis.NewClient(&redis.Options{
        Addr:     "127.0.0.1:6379",
        Password: "xxxxxxxxxxxxx",
        DB:       0,
    })
    if client==nil {
        log.Fatal("Unable to create Redis Client!!!");
    }
    return client
}

func listenOnChannel(channel string) {
    // Create PubSub, is a pointer to an object of redis.PubSub
    client := createRedisClient()
    ctx := context.Background()
    pubsub := client.Subscribe(ctx, channel)

    // Close the subscription when we are done.
    defer pubsub.Close()

    // Receive a message
    for {
        msg, err := pubsub.ReceiveMessage(ctx)
        if err != nil {
            panic(err)
        }
        fmt.Println("[Data Received]", msg.Channel, msg.Payload)
    }
}

func main() {
    // Subcribe on channel
    channel := "go_testing_channel"
    go listenOnChannel(channel)

    // Public messages to the channel
    client := createRedisClient()
    ctx := context.Background()
    for count := 0; count < 100; count++ {
        data := "The value " + strconv.Itoa(count)
        err := client.Publish(ctx, channel, data).Err()
        if err != nil {
            panic(err)
        }
        fmt.Println("[Data Published]", channel, data)
        time.Sleep(time.Second)
    }
}

Ta đánh lệnh sau để chạy:

go run redis-pubsub.go

Ta được kết quả như sau:

Kết quả khi chạy tệp redis-pubsub.go

Đóng gói để sử dụng trong ứng dụng

Bây giờ tôi sẽ đóng gói lại một số thao tác cơ bản để sử dụng trong ứng dụng thông qua tệp redis.go:

package database

import (
    "context"
    "log"
    "github.com/redis/go-redis/v9"

    "zethyr.go/models"
)

func CreateRedisClient(config *models.AppConfig) *redis.Client {
    client := redis.NewClient(&redis.Options{
        Addr:     config.RedisAddr,
        Password: config.RedisPwd,
        DB:       0,
    })
    if client==nil {
        log.Fatalln("Unable to create Redis Client!!!");
    }
    return client
}

func RedisSet(client *redis.Client, redisKey string, redisValue string) bool {
    ctx := context.Background()
    err := client.Set(ctx, redisKey, redisValue, 0).Err()
    if err != nil {
        log.Printf("Unable to set data to key %s: %+v\n", redisKey, err);
        return false
    }
    return true
}

func RedisGet(client *redis.Client, redisKey string) string {
    ctx := context.Background()
    val, err := client.Get(ctx, redisKey).Result()
    switch {
        case err == redis.Nil:
            log.Printf("The redis key does not exist: %s\n", redisKey)
            val = ""
        case err != nil:
            log.Printf("Unable to get data for redis key %s: %+v\n", redisKey, err)
            val = ""
        case val == "":
            log.Printf("The value for redis %s is empty\n", redisKey)
        //default:
        //    log.Printf("The value for redis key %s: %s\n", redisKey, val)
    }
    return val
}

func RedisPub(client *redis.Client, redisChannel string, redisValue string) bool {
    ctx := context.Background()
    err := client.Publish(ctx, redisChannel, redisValue).Err()
    if err != nil {
        log.Printf("Error to public data to channel %s: %+v\n", redisChannel, err)
        return false
    }
    return true
}

func RedisSub(client *redis.Client, redisChannel string) *redis.PubSub {
    ctx := context.Background()
    pubsub := client.Subscribe(ctx, redisChannel)
    return pubsub
}

// An small example to use Redis PubSub
func RedisSubUse(pubsub *redis.PubSub) {
    // Close the subscription when we are done.
    defer pubsub.Close()

    // Receive a message in a loop
    ctx := context.Background()
    for {
        msg, err := pubsub.ReceiveMessage(ctx)
        if err != nil {
            log.Printf("Error to receive a message from channel: %+v\n", err)
        } else {
            log.Println("[Redis Channel Received]", msg.Channel, msg.Payload)
        }
    }
}

Vấn đề truy cập đồng thời dữ liệu trên Go

Vấn đề truy cập đồng thời dữ liệu tại 1 thời điểm sẽ gặp phải khi bạn cần chạy nhiều luồng song song trong khi lại sử dụng chung một nguồn dữ liệu. Chi tiết bạn xem bài viết: Concurrent map access in Go

Vấn đề sử dụng đồng thời dữ liệu trong map

Sẽ có vấn đề xảy ra khi chúng ta sử dụng map được truy cập đồng thời từ nhiều luồng khác nhau. Dưới đây, tôi viết ví dụ use-map-01.go tạo 2 luồng:

  • Luồng phụ: Cứ 30ms thực hiện đọc dữ liệu từ map và hiển thị
  • Luồng chính: Cứ 1ms thực hiện tăng dữ liệu trong map lên 1 đơn vị
package main

import (
	"fmt"
	"time"
)

var showCounter int = 0
var mapData map[int]int = make(map[int]int)

func InitMap() {
	for i := 0; i < 20; i++ {
		mapData[i] = i
	}
}

func ShowMap() {
	showCounter++
	str := fmt.Sprintf("[%d] ", showCounter)
	for key, value := range mapData {
		tmp := fmt.Sprintf("%d->%d ", key, value)
		str += tmp
	}
	fmt.Println(str)
}

func ShowMapThread() {
	for {
		ShowMap()
		time.Sleep(30 * time.Millisecond)
	}

}

func WriteMapRandom() {
	for {
		for key, _ := range mapData {
			mapData[key]++
		}
		time.Sleep(1 * time.Millisecond)
	}
}

func main() {
	InitMap()
	ShowMap()
	go ShowMapThread()
	WriteMapRandom()
}

Khi chạy ví dụ này, chưa đến 15s bạn sẽ nhận được thông báo lỗi:

[65] 2->1705 4->1707 6->1709 10->1713 13->1716 15->1718 19->1722 0->1703 1->1704 5->1708 7->1710 11->1714 14->1717 18->1721 3->1706 8->1711 9->1712 12->1715 16->1719 17->1720
[66] 6->1737 10->1741 13->1744 15->1746 19->1750 2->1733 4->1735 5->1736 7->1738 11->1742 14->1745 0->1731 1->1732 18->1749 9->1740 12->1743 16->1747 17->1748 3->1734 8->1739
[67] 7->1764 11->1768 14->1771 0->1757 1->1758 5->1762 18->1775 12->1769 16->1773 17->1774 3->1760 8->1765 9->1766 10->1767 13->1770 15->1772 19->1776 2->1759 4->1761 6->1763
fatal error: concurrent map iteration and map write

goroutine 18 [running]:
main.ShowMap()
        /home/arbitest/go-apps/examples/use-map-01.go:20 +0x16b
main.ShowMapThread()
        /home/arbitest/go-apps/examples/use-map-01.go:29 +0x19
created by main.main
        /home/arbitest/go-apps/examples/use-map-01.go:47 +0x68

goroutine 1 [sleep]:
time.Sleep(0xf4240)
        /usr/local/go/src/runtime/time.go:195 +0x135
main.WriteMapRandom(...)
        /home/arbitest/go-apps/examples/use-map-01.go:40
main.main()
        /home/arbitest/go-apps/examples/use-map-01.go:48 +0x75
exit status 2

Như bạn đã biết map là kiểu dữ liệu “not safe for concurrent use” vì vậy mà phát sinh lỗi bộ nhớ trên.

Giải pháp truy cập đồng thời trên map

Như vậy chúng ta cần có một giải pháp giúp truy cập map một cách an toàn và tránh được xung đột. Có nhiều giải pháp khác nhau giúp bạn làm được điều này:

GP1: Sử dụng sync.RWMutex

Đây là cách thông dụng nhất, có thể áp dụng cho rất nhiều trường hợp khác nhau, không chỉ mới map. Chúng ta dùng cơ chế Lock/Unlock để đảm bảo an toàn khi truy cập vào một tài nguyên nào đó. Cách sử dụng cơ bản như sau:

var contexts = make(map[string]cronMetadata)
var mutex = &sync.RWMutex{}

// ...

// Read data
mutex.Lock()
contexts[keyName] = *request
mutex.Unlock()

// Store data
mutex.RLock()
var cronScaler = contexts[in.GetName()]
mutex.RUnlock()

Tôi viết use-map-02.go với nghiệp vụ hoàn toàn giống với use-map-01.go nhưng sử dụng thêm sync.RWMutex:

package main

import (
	"fmt"
	"sync"
	"time"
)

var showCounter int = 0
var mapData map[int]int = make(map[int]int)
var lock = sync.RWMutex{}

func InitMap() {
	for i := 0; i < 20; i++ {
		mapData[i] = i
	}
}

func ShowMap() {
	lock.RLock()
	defer lock.RUnlock()

	showCounter++
	str := fmt.Sprintf("[%d] ", showCounter)
	for key, value := range mapData {
		tmp := fmt.Sprintf("%d->%d ", key, value)
		str += tmp
	}
	fmt.Println(str)
}

func ShowMapThread() {
	for {
		ShowMap()
		time.Sleep(30 * time.Millisecond)
	}

}

func WriteMapRandom() {
	for {
		lock.Lock()
		for key, _ := range mapData {
			mapData[key]++
		}
		lock.Unlock()
		time.Sleep(1 * time.Millisecond)
	}
}

func main() {
	InitMap()
	ShowMap()
	go ShowMapThread()
	WriteMapRandom()
}

Tôi đã chạy trong khoảng vài giờ đồng hồ và không có lỗi nào phát sinh.

GP2: Sử dụng sync.Map

sync.Map là kiểu dữ liệu map nhưng hỗ trợ an toàn khi truy cập đồng thời. Cách sử dụng trong code như sau:

var contexts = sync.Map{}

// ....

contexts.Store(keyName, *request)

//retrieve data
cronContext, _ := contexts.Load(keyName)

Dùng sync.Map có thể áp dụng cho nhiều kiểu dữ liệu khác nhau, nhưng bạn phải thêm phần chuyển đổi dữ liệu.

Tôi viết use-map-03.go với nghiệp vụ giống use-map-01.go nhưng đổi từ sử dụng kiểu dữ liệu map sang kiểu sync.Map:

package main

import (
	"fmt"
	"sync"
	"time"
)

var showCounter int = 0
var mapData = sync.Map{}

func InitMap() {
	for i := 0; i < 20; i++ {
		mapData.Store(i, i)
	}
}

func ShowMap() {
	showCounter++
	str := fmt.Sprintf("[%d] ", showCounter)
	for i := 0; i < 20; i++ {
		value, _ := mapData.Load(i)
		tmp := fmt.Sprintf("%d->%d ", i, value)
		str += tmp
	}
	fmt.Println(str)
}

func ShowMapThread() {
	for {
		ShowMap()
		time.Sleep(30 * time.Millisecond)
	}

}

func WriteMapRandom() {
	for {
		for i := 0; i < 20; i++ {
			value, _ := mapData.Load(i)
			v := int(value.(int))
			v++
			mapData.Store(i, v)
		}
		time.Sleep(1 * time.Millisecond)
	}
}

func main() {
	InitMap()
	ShowMap()
	go ShowMapThread()
	WriteMapRandom()
}

Nhưng để ý kỹ thì so với use-map-02.go sẽ có khác khi thực hiện:

  • Trong use-map-02.go, khi đang xử lý trong hàm ShowMap() thì không có lệnh ghi nào được thực hiện, chỉ khi nào hết hàm ShowMap(), lock mới được mở và lệnh ghi dữ liệu mới được thực hiện.
  • Trong use-map-03.go, khi đang xử lý trong hàm ShowMap() thì vẫn có lệnh ghi được thực hiện. Việc khóa chỉ được thực hiện ở phần đọc và ghi map mà thôi.

Một số lỗi phát sinh

Unrecognized field ‘apiVersion’

Lỗi này do khi khởi tạo Mongo thì xác định chính xác version api nhưng mà trên server lại không hỗ trợ:

serverAPI := options.ServerAPI(options.ServerAPIVersion1)
opts := options.Client().ApplyURI(appData.Config.MongoUri).SetServerAPIOptions(serverAPI)

Chỉ cần sửa lại bỏ thiết lập serverAPI là xong:

opts := options.Client().ApplyURI(appData.Config.MongoUri)

Tại sao sử dụng thư viện ethclient trên Go gọi hàm tới Multicall báo lỗi Revert khi sử dụng với Hardhat Forking: “Error: Transaction reverted without a reason string

Tôi có chuyển code sử dụng Multicall từ NodeJs sang Go, trong quá trình làm việc thì tôi có sử dụng Hardhat để fork mainnet ở một block nào đó. Trên NodeJs thì tôi sử dụng Web3 bình thường, còn trên Go tôi sử dụng thư viện ethclient. Sau khi code xong thì có vấn đề phát sinh, cụ thể là:

  • Sử dụng NodeJs thì gọi bình thường tới cả Base Mainnet và Hardhat Forking
  • Sử dụng Go thì gọi tới Base Mainet thì OKIE nhưng gọi tới Hardhat Forking thì báo lỗi Revert
    Error: Transaction reverted without a reason string

Sau thời gian nghiên cứu và tìm hiểu vấn đề liên quan đến tên trường trong dữ liệu RPC gửi đi, trên ethclient gửi trường “input“, còn thư viện trên NodeJs thì gửi trường “data“. Các server mainnet đều hỗ trợ cả hai trường này, nhưng hardhat chỉ hỗ trợ tên trường là data. Vì thế để fix lỗi này ta bắt buộc phải sửa thư viện ethclient trong Go. Chúng ta tìm file ethclient.go, trên máy local thì nó ở đâu đó trong thư mục:
~/go/pkg/mod/github.com/ethereum/go-ethereum@v1.13.1/ethclient/ethclient.go
Phụ thuộc vào phiên bản bạn sử dụng.

Ta sửa lại thay “input” bằng “data” ở dòng code như ảnh dưới là xong. Sau khi sửa xong đảm bảo ứng dụng bạn trên Go có kết nối được với cả Hardhat Forking và Mainnet bình thường.

Hot fix thư viện ethclient để hỗ trợ Hardhat Forking

Hot fix thư viện ethclient để hỗ trợ Hardhat Forking

Tham khảo:

Bài viết này có hữu ích với bạn?

Kích vào một biểu tượng ngôi sao để đánh giá bài viết!

Xếp hạng trung bình 5 / 5. Số phiếu: 89

Bài viết chưa có đánh giá! Hãy là người đầu tiên đánh giá bài viết này.

Trả lời

Giao diện bởi Anders Norén