LapTrinhBlockchain

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

Kiến thức lập trình, Lập trình Blockchain, Lập Trình DApp, Lập trình GoLang, Lập trình NodeJs

Hướng dẫn đóng gói các giao dịch TRON sử dụng ngôn ngữ Go (go lang)

Hướng dẫn đóng gói các giao dịch TRON sử dụng ngôn ngữ Go (go lang)

Hướng dẫn đóng gói các giao dịch TRON sử dụng ngôn ngữ Go (go lang)

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

Trong phần này tôi sử dụng tài khoản TCuWX3Az4D23vWKcDU442eXFQBm2rsCRke với private key là “9499BF78CE4A0217EC48F69AFDBAC19E10E10ACA14C8B278F08A213FA92FC498“. Đây là tài khoản dùng để test trên mạng lưới Nile Testnet, tuyệt đối không được sử dụng tài khoản này trên Mainnet. Trên Testnet, để có thể chạy test được, bạn phải chuyển ít TRX trên Nile Testnet vào địa chỉ này trước nhé. Bạn có thể lấy TRX miễn phí từ địa chỉ: https://nileex.io/join/getJoinPage

Bài viết này tôi tập trung vào giải pháp tạo ra một giao dịch đóng gói đầy đủ bao gồm cả chữ ký sử dụng ngôn ngữ Go. Với dữ liệu đóng gói này, chúng ta có thể sử dụng bất kỳ ngôn ngữ nào để đẩy giao dịch đó lên blockchain.

Tại sao cần đóng gói giao dịch bằng ngôn ngữ Go

Như bạn đã biết thì hiện tại NodeJs đang sử dụng phổ biến, được sử dụng nhiều bởi sự linh hoạt và việc xây dựng ứng dụng nhanh, đặc biệt rất hiệu quả đối với bài toán mà cấu trúc dữ liệu chưa rõ ràng và thay đổi liên tục. Do đó NodeJs được sử dụng nhiều để code các DApp. Nhưng có vấn đề là các thao tác gửi lên blockchain thì cần có Private Key để ký lên giao dịch đó, mà NodeJs là ngôn ngữ thông dịch nên khi triển khai trên server bạn phải triển khai cả code của nó. Điều này đồng nghĩa với việc nếu server bị hack thì Private Key sẽ rất dễ bị lộ, cho dù bạn có mã hóa kiểu gì trong NodeJs đi nữa.

Như vậy để bảo vệ được Private Key, chúng ta nên xây dựng ứng dụng bằng các ngôn ngữ biên dịch, ứng dụng không thể dịch ngược từ tệp thực thi thành mã nguồn được. Ngôn ngữ đó có thể C/C++, Go,… và Go là lựa chọn ưu tiên hàng đầu, vì trên Go có rất nhiều thư viện về crypto và blockchain, hơn nữa các dự án hàng đầu trong thế giới crypto đều được viết bằng Go.

Nhưng việc chuyển toàn bộ DApp của bạn từ NodeJs sang Go thực sự tốn rất nhiều thời gian và nguồn lực. Nên theo mình giải pháp tốt nhất để bảo mật Private Key là tách DApp thành 2 phần:

  • Riêng các phần liên quan đến sử dụng Private Key như ký giao dịch thì mới thực hiện trên Go
  • Các phần nghiệp vụ vẫn giữ trên NodeJs, khi cần đóng gói giao dịch thì gọi sang module trên Go để thực hiện.

Nguyên tắc đóng gói giao dịch và lựa chọn thư viện trên Go

Về cơ bản nguyên tắc chung đóng gói gồm:

  • B1: Tạo giao dịch với thông tin cần thiết. Từ thông tin giao dịch này ta encode để được dữ liệu d0 dạng mảng byte.
  • B2: Từ dữ liệu d0, ta thực hiện băm bằng SHA256 ta được dữ liệu hash, là Transaction Hash hay TxId của giao dịch.
  • B3: Ta dùng Private Key để ký lên dữ liệu hash ta được s, đây là chữa ký của giao dịch.
  • B4: Ta kết hợp giữa d0 s ta được d1, đây là dữ liệu đóng gói cho toàn bộ giao dịch. Gửi d1 lên blockchain thì giao dịch sẽ được thực hiện.

Và tôi đã tìm được một thư viện tron-sdk để làm được việc này, thư viện này là một phần của thư viện go-wallet-sdk của OKX (OKEX) lên khá uy tín. Trong thư viện này có một số hàm quan trọng sau (Xem thêm tron_test.go):

  • Hàm SignStart(): Hàm này thực hiện băm giao dịch bằng SHA256 để được hash.
  • Hàm Sign(): Hàm này dùng để ký hash tạo được ở trên
  • Hàm SignEnd(): Hàm này đóng gọi giao dịch bao gồm cả chữ ký.

Thêm nữa, trong thông tin giao dịch trên TRON, bạn cần phải cung cấp refBlock (Block tham chiếu), bạn có thể lấy từ thông tin block mới nhất trên Nile Scan. Nếu thông tin refBlock lớn hơn block hiện tại, hoặc nhỏ hơn block hiện tại 65535 block thì bạn sẽ nhận được lỗi “Tapos check error“.

Đóng gói một số giao dịch trên Go

Đóng gói giao dịch chuyển TRX bằng ngôn ngữ Go

Bây giờ chúng ta sẽ thực hiện đóng gói 1 giao dịch chuyển 0.01 TRX từ địa chỉ TCuWX3Az4D23vWKcDU442eXFQBm2rsCRke tới địa chỉ TWXKV19GnweWQzFPJckKHbCgeSRUMMowHz.

Đầu tiên chúng ta vào Nile Scan để lấy thông tin block mới nhất:

Bạn sẽ thấy thông tin này được inline trực tiếp trong code. Bạn cũng thế viết code để lấy thông tin này, nhưng để đơn giản tôi sẽ inline trực tiếp trong code. Khi các bạn chạy nhớ cập nhật lại thông tin này.

Trong thư viện có sẵn hàm newTransfer() để tạo một giao dịch transfer TRX, nhưng do tên hàm bắt đầu bằng chữ thường nên nó là hàm private, chỉ sử dụng được trong cùng package, package khác không thể gọi được. Vì thế tôi sẽ viết lại hàm này. Ngoài ra bạn có thể sử dụng hàm tron.ParseTxStr() để chuyển dữ liệu mảng byte sang thông tin giao dịch.

Bây giờ chúng ta sẽ tạo tệp transfer-trx.go với nội dung như sau:

// https://pkg.go.dev/github.com/okx/go-wallet-sdk/coins/tron
// https://github.com/okx/go-wallet-sdk/blob/main/coins/tron/tron_test.go
package main

import (
        "encoding/binary"
        "encoding/hex"
        "fmt"
        "time"

        "github.com/btcsuite/btcd/btcec/v2"
        "github.com/golang/protobuf/proto"
        "github.com/golang/protobuf/ptypes"
        "github.com/okx/go-wallet-sdk/coins/tron"
        "github.com/okx/go-wallet-sdk/coins/tron/pb"
)

func NewTransfer(fromAddress string, toAddress string, amount int64, refBlockBytes string, refBlockHash string, expiration int64, timestamp int64) (string, error) {
        owner, err := tron.GetAddressHash(fromAddress)
        if err != nil {
                return "", err
        }
        to, err := tron.GetAddressHash(toAddress)
        if err != nil {
                return "", err
        }
        transferContract := &pb.TransferContract{OwnerAddress: owner, ToAddress: to, Amount: amount}
        param, err := ptypes.MarshalAny(transferContract)
        if err != nil {
                return "", err
        }
        contract := &pb.Transaction_Contract{Type: pb.Transaction_Contract_TransferContract, Parameter: param}
        raw := new(pb.TransactionRaw)
        refBytes, err := hex.DecodeString(refBlockBytes)
        if err != nil {
                return "", err
        }
        raw.RefBlockBytes = refBytes
        refHash, err := hex.DecodeString(refBlockHash)
        if err != nil {
                return "", err
        }
        raw.RefBlockHash = refHash
        raw.Expiration = expiration
        raw.Timestamp = timestamp
        raw.Contract = []*pb.Transaction_Contract{contract}
        trans := pb.Transaction{RawData: raw}
        data, err := proto.Marshal(&trans)
        if err != nil {
                return "", err
        }
        return hex.EncodeToString(data), nil
}

func TransferTrx() {
        currentTime := time.Now()
        pkHex, _ := hex.DecodeString("9499BF78CE4A0217EC48F69AFDBAC19E10E10ACA14C8B278F08A213FA92FC498")
        pk, _ := btcec.PrivKeyFromBytes(pkHex)
        pubKey := pk.PubKey()
        fromAddr := tron.GetAddress(pubKey)
        fmt.Printf("From Address: %s\n", fromAddr)
        fmt.Println("----------------------------------")

        k1 := make([]byte, 8)
        binary.BigEndian.PutUint64(k1, 45900131)
        k2, _ := hex.DecodeString("0000000002bc61630b86cbb9989fa37dbfaa95b01e2c1201a4c723d636041e8c")
        d0, _ := NewTransfer(
                fromAddr,
                "TWYrgz7RDP2NpumQRPY1jBmPKLWVSnrzWZ",
                int64(10000), // 0.01 TRX
                hex.EncodeToString(k1[6:8]),
                hex.EncodeToString(k2[8:16]),
                currentTime.Unix()*1000+3600*1000,
                currentTime.UnixMilli())
        //fmt.Printf("Transfer Raw Data: %s\n", d0)
        fmt.Println("----------------------------------")

        txData0, _ := tron.ParseTxStr(d0)
        fmt.Printf("Transaction Info0: %v\n", txData0)
        fmt.Println("----------------------------------")

        txId, _ := tron.SignStart(d0)
        fmt.Printf("TxId: %s\n", txId)
        fmt.Println("----------------------------------")

        s, _ := tron.Sign(txId, pk)
        fmt.Printf("Signature: %v\n", s)
        fmt.Println("----------------------------------")

        d1, _ := tron.SignEnd(d0, s)
        fmt.Printf("Signed Transaction: %s\n", d1)
        fmt.Println("----------------------------------")

        txData1, _ := tron.ParseTxStr(d1)
        fmt.Printf("Transaction Info1: %v\n", txData1)
        fmt.Println("----------------------------------")
}

func main() {
        TransferTrx()
}

Bây giờ chúng ta chạy tệp này bằng lệnh:

# Cập nhật thư viện (Chỉ cần làm 1 lần)
go mod init tron-go
go mod tidy

# Chạy ứng dụng
go run transfer-trx.go

Sau khi chạy ta được dữ liệu d1 (Signed Transaction): 0a84010a02616322080b86cbb9989fa37d40d0b5ceb4ec315a66080112620a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412310a1541203555fb15bebbdc1a8551a8164e2f6ecebff67a121541e1bfb3fe1dc96903414e8d158b98e52586f3fa4818904e70eedff2b2ec311241269d97ca94717606310b451b3a50f12ed0165fdcb96d52d036d12a69a2edc2024e6eca447185fe55f66b199f03c967ebad4bb0965e99f04136bedd7c7c20139e01

Đóng gói giao dịch transfer TRX

Bây giờ chúng ta viết tệp public-transaction.js bằng NodeJs với nội dung như sau:

// Must using tronweb@5.0.0 trở lên
const TronWeb = require('tronweb');

const nodeLink = "https://nile.trongrid.io";

function getTronWeb() {
    let opts = {
        fullNode: nodeLink,
        solidityNode: nodeLink,
        eventServer: nodeLink,
        privateKey: ""
    };
    let tronWeb = new TronWeb(opts);
    return tronWeb;
}

async function publicTransaction(signedData) {
    try {
        let tronWeb = getTronWeb();
        let transaction = await tronWeb.trx.sendHexTransaction(signedData);
        console.log("Transaction:", transaction);
    } catch(ex) {
        console.error("Unable to send signed transaction", ex);
    }
}

async function main() {
    if (process.argv.length<3) {
        console.log("Please run command as below:");
        console.log("    node public-transaction.js <Signed-Data>");
        return 0;
    }

    let signedData = process.argv[2];
    console.log("Signed Data", signedData);
    await publicTransaction(signedData);
}

main();

Chúng ta chạy bằng lệnh sau để đẩy giao dịch lên blockchain:

# Cài đặt thư viện, chỉ cần làm 1 lần
npm i tronweb@5.0.0

# Chạy file để public giao dịch
node public-transaction.js 0a84010a02616322080b86cbb9989fa37d40d0b5ceb4ec315a66080112620a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412310a1541203555fb15bebbdc1a8551a8164e2f6ecebff67a121541e1bfb3fe1dc96903414e8d158b98e52586f3fa4818904e70eedff2b2ec311241269d97ca94717606310b451b3a50f12ed0165fdcb96d52d036d12a69a2edc2024e6eca447185fe55f66b199f03c967ebad4bb0965e99f04136bedd7c7c20139e01

Ta được giao dịch 069e4e55131435012fe6d2b8b82bfd555e5f6c3ddedb07a333402250ca127260

Đẩy giao dịch Transfer TRX lên Nile blockchain

Đóng gói giao dịch chuyển token TRC20 bằng ngôn ngữ Go

Đầu tiên chúng ta cần có TRC20, chúng ta vào link https://nileex.io/join/getJoinPage và thực hiện claim token BTT. Ta có contract token BTT trên Nile: TVSvjZdyDSNocHm7dP3jvCmMNsCnMTPa5W. Bạn nhớ dùng ví trên để claim, hoặc claim bằng ví khác sau đó chuyển BTT vào ví TCuWX3Az4D23vWKcDU442eXFQBm2rsCRke

Chúng ta viết tệp transfer-trc20.go với mã lệnh như sau:

// https://pkg.go.dev/github.com/okx/go-wallet-sdk/coins/tron
// https://github.com/okx/go-wallet-sdk/blob/main/coins/tron/tron_test.go
package main

import (
        "encoding/binary"
        "encoding/hex"
        "fmt"
        "math/big"
        "time"

        "github.com/btcsuite/btcd/btcec/v2"
        "github.com/golang/protobuf/proto"
        "github.com/golang/protobuf/ptypes"
        "github.com/okx/go-wallet-sdk/coins/tron"
        "github.com/okx/go-wallet-sdk/coins/tron/pb"
        "github.com/okx/go-wallet-sdk/coins/tron/token"
)

func NewTRC20TokenTransfer(fromAddress string, toAddress string, contractAddress string, amount *big.Int, feeLimit int64, refBlockBytes string, refBlockHash string, expiration int64, timestamp int64) (string, error) {
        raw := new(pb.TransactionRaw)
        refBytes, err := hex.DecodeString(refBlockBytes)
        if err != nil {
                return "", err
        }
        raw.RefBlockBytes = refBytes
        refHash, err := hex.DecodeString(refBlockHash)
        if err != nil {
                return "", err
        }
        raw.RefBlockHash = refHash
        raw.Expiration = expiration
        raw.Timestamp = timestamp

        fromAddressHash, err := tron.GetAddressHash(fromAddress)
        if err != nil {
                return "", err
        }
        toAddressHash, err := tron.GetAddressHash(toAddress)
        if err != nil {
                return "", err
        }
        contractAddressHash, err := tron.GetAddressHash(contractAddress)
        if err != nil {
                return "", err
        }
        input, err := token.Transfer(hex.EncodeToString(toAddressHash), amount)
        if err != nil {
                return "", err
        }
        transferContract := &pb.TriggerSmartContract{OwnerAddress: fromAddressHash, ContractAddress: contractAddressHash, CallValue: 0, CallTokenValue: 0, Data: input}
        param, err := ptypes.MarshalAny(transferContract)
        if err != nil {
                return "", err
        }

        contract := &pb.Transaction_Contract{Type: pb.Transaction_Contract_TriggerSmartContract, Parameter: param}
        raw.FeeLimit = feeLimit

        raw.Contract = []*pb.Transaction_Contract{contract}
        trans := pb.Transaction{RawData: raw}
        data, err := proto.Marshal(&trans)
        if err != nil {
                return "", err
        }
        return hex.EncodeToString(data), nil
}

func TransferTrc20() {
        currentTime := time.Now()
        pkHex, _ := hex.DecodeString("9499BF78CE4A0217EC48F69AFDBAC19E10E10ACA14C8B278F08A213FA92FC498")
        pk, _ := btcec.PrivKeyFromBytes(pkHex)
        pubKey := pk.PubKey()
        fromAddr := tron.GetAddress(pubKey)
        fmt.Printf("From Address: %s\n", fromAddr)
        fmt.Println("----------------------------------")

        // Amount: 8 BTT (8*10^18)
        amount := big.NewInt(10)
        amount.Exp(amount, big.NewInt(18), nil)
        amount.Mul(amount, big.NewInt(8))

        k1 := make([]byte, 8)
        binary.BigEndian.PutUint64(k1, 45903239)
        k2, _ := hex.DecodeString("0000000002bc6d873f0c8de58a60c81183362cd78a7bd9528e783fecdf111be4")
        contractAddress := "TVSvjZdyDSNocHm7dP3jvCmMNsCnMTPa5W" // BTT
        d0, _ := NewTRC20TokenTransfer(
                fromAddr,
                "TWXKV19GnweWQzFPJckKHbCgeSRUMMowHz",
                contractAddress,
                amount,
                int64(100000000),
                hex.EncodeToString(k1[6:8]),
                hex.EncodeToString(k2[8:16]),
                currentTime.Unix()*1000+3600*1000,
                currentTime.UnixMilli())
        fmt.Printf("Transfer Raw Data: %s\n", d0)
        fmt.Println("----------------------------------")

        txData0, _ := tron.ParseTxStr(d0)
        fmt.Printf("Transaction Info0: %v\n", txData0)
        fmt.Println("----------------------------------")

        txId, _ := tron.SignStart(d0)
        fmt.Printf("TxId: %s\n", txId)
        fmt.Println("----------------------------------")

        s, _ := tron.Sign(d0, pk)
        fmt.Printf("Signature: %v\n", s)
        d1, _ := tron.SignEnd(d0, s)
        fmt.Printf("Signed Transaction: %s\n", d1)
        fmt.Println("----------------------------------")

        txData1, _ := tron.ParseTxStr(d1)
        fmt.Printf("Transaction Info1: %v\n", txData1)
        fmt.Println("----------------------------------")
}

func main() {
        TransferTrc20()
}

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

go run transfer-trc20.go

Sau khi chạy ta được dữ liệu d1 (Signed Transaction):

0ad3010a0270012208ff0df89c11ae035240e0e7f2b9ec315aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a1541203555fb15bebbdc1a8551a8164e2f6ecebff67a121541d5a8752a5fc64a1ce09df7b195df6b669d09271f2244a9059cbb000000000000000000000041e1753cb82b1a4ab5b872e44a3de4bac5530298740000000000000000000000000000000000000000000000006f05b59d3b20000070f18c97b8ec31900180c2d72f124125fba935e01699597d470cffabaa9d9ad5b61a8ef63d9cc57691145c5a778f387ec0b5685686a353378011d692dd5ce2aa9b83a77a2378c5bb9ba97abca9897701

Ta chạy ứng dụng node để gửi giao dịch lên blockchain:

node public-transaction.js 0ad3010a0270012208ff0df89c11ae035240e0e7f2b9ec315aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a1541203555fb15bebbdc1a8551a8164e2f6ecebff67a121541d5a8752a5fc64a1ce09df7b195df6b669d09271f2244a9059cbb000000000000000000000041e1753cb82b1a4ab5b872e44a3de4bac5530298740000000000000000000000000000000000000000000000006f05b59d3b20000070f18c97b8ec31900180c2d72f124125fba935e01699597d470cffabaa9d9ad5b61a8ef63d9cc57691145c5a778f387ec0b5685686a353378011d692dd5ce2aa9b83a77a2378c5bb9ba97abca9897701

Ta được giao dịch b05b3234bc235c7435d5f3ef2e4e65622361c340871b0ab3a87995e408a0332e

Đóng gói giao dịch swap bằng ngôn ngữ Go

Bây giờ chúng ta sẽ thực hiện một giao dịch swap từ 100 TRX sang USDT trên SunswapRouterV2 thông qua hàm swapExactETHForTokens(). Chúng ta cần biết một số địa chỉ:

Đầu tiên chúng ta nhớ chuyển 200 TRX tới địa chỉ TCuWX3Az4D23vWKcDU442eXFQBm2rsCRke, sau đó chúng ta encode hàm swapExactETHForTokens():
swapExactETHForTokens(0, [TYsbWxNnyTgsZaTFaue9hqpxkU3Fkco94a, TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf], TCuWX3Az4D23vWKcDU442eXFQBm2rsCRke, “9999999999999”)
Ta được:
7ff36ab500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000203555fb15bebbdc1a8551a8164e2f6ecebff67a000000000000000000000000000000000000000000000000000009184e729fff0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000fb3b3134f13ccd2c81f4012e53024e8135d58fee000000000000000000000000eca9bc828a3005b9a3b909f2cc5c2a54794de05f
Dữ liệu này sẽ được sử dụng trong code.

Bây giờ ta tạo tệp tron-swap.go với mã lệnh như sau:

// https://pkg.go.dev/github.com/okx/go-wallet-sdk/coins/tron
// https://github.com/okx/go-wallet-sdk/blob/main/coins/tron/tron_test.go
package main

import (
        "encoding/binary"
        "encoding/hex"
        "fmt"
        "time"

        "github.com/btcsuite/btcd/btcec/v2"
        "github.com/golang/protobuf/proto"
        "github.com/golang/protobuf/ptypes"
        "github.com/okx/go-wallet-sdk/coins/tron"
        "github.com/okx/go-wallet-sdk/coins/tron/pb"
)

func NewTransaction(fromAddress string, contractAddress string, callValue int64, inputData string, feeLimit int64, refBlockBytes string, refBlockHash string, expiration int64, timestamp int64) (string, error) {
        raw := new(pb.TransactionRaw)
        refBytes, err := hex.DecodeString(refBlockBytes)
        if err != nil {
                return "", err
        }
        raw.RefBlockBytes = refBytes
        refHash, err := hex.DecodeString(refBlockHash)
        if err != nil {
                return "", err
        }
        raw.RefBlockHash = refHash
        raw.Expiration = expiration
        raw.Timestamp = timestamp

        fromAddressHash, err := tron.GetAddressHash(fromAddress)
        if err != nil {
                return "", err
        }
        contractAddressHash, err := tron.GetAddressHash(contractAddress)
        if err != nil {
                return "", err
        }
        input, err := hex.DecodeString(inputData)
        if err != nil {
                return "", err
        }
        transactionContract := &pb.TriggerSmartContract{OwnerAddress: fromAddressHash, ContractAddress: contractAddressHash, CallValue: callValue, CallTokenValue: 0, Data: input}
        param, err := ptypes.MarshalAny(transactionContract)
        if err != nil {
                return "", err
        }

        contract := &pb.Transaction_Contract{Type: pb.Transaction_Contract_TriggerSmartContract, Parameter: param}
        raw.FeeLimit = feeLimit

        raw.Contract = []*pb.Transaction_Contract{contract}
        trans := pb.Transaction{RawData: raw}
        data, err := proto.Marshal(&trans)
        if err != nil {
                return "", err
        }
        return hex.EncodeToString(data), nil
}

// Swap from TRX to USDT on SunSwapV2 on Nile
func SwapToken() {
        currentTime := time.Now()
        pkHex, _ := hex.DecodeString("9499BF78CE4A0217EC48F69AFDBAC19E10E10ACA14C8B278F08A213FA92FC498")
        pk, _ := btcec.PrivKeyFromBytes(pkHex)
        pubKey := pk.PubKey()
        fromAddr := tron.GetAddress(pubKey)
        fmt.Printf("From Address: %s\n", fromAddr)
        fmt.Println("----------------------------------")

        // swapExactETHForTokens(0, [TYsbWxNnyTgsZaTFaue9hqpxkU3Fkco94a, TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf], TCuWX3Az4D23vWKcDU442eXFQBm2rsCRke, "9999999999999")
        data := "7ff36ab500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000203555fb15bebbdc1a8551a8164e2f6ecebff67a000000000000000000000000000000000000000000000000000009184e729fff0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000fb3b3134f13ccd2c81f4012e53024e8135d58fee000000000000000000000000eca9bc828a3005b9a3b909f2cc5c2a54794de05f"

        k1 := make([]byte, 8)
        binary.BigEndian.PutUint64(k1, 45905108)
        k2, _ := hex.DecodeString("0000000002bc74d4cc668e76adb0c91a03ee8136b0a315dc3d49a8d0a0f76d73")
        contractAddress := "TMn1qrmYUMSTXo9babrJLzepKZoPC7M6Sy" // UniswapV2Router02
        d0, _ := NewTransaction(
                fromAddr,
                contractAddress,
                int64(100000000), // 100 TRX
                data,
                int64(75000000), // Fee Limit: 75 TRX
                hex.EncodeToString(k1[6:8]),
                hex.EncodeToString(k2[8:16]),
                currentTime.Unix()*1000+3600*1000,
                currentTime.UnixMilli())
        fmt.Printf("Transaction Raw Data: %s\n", d0)
        fmt.Println("----------------------------------")

        txData0, _ := tron.ParseTxStr(d0)
        fmt.Printf("Transaction Info0: %v\n", txData0)
        fmt.Println("----------------------------------")

        txId, _ := tron.SignStart(d0)
        fmt.Printf("TxId: %s\n", txId)
        fmt.Println("----------------------------------")

        s, _ := tron.Sign(txId, pk)
        fmt.Printf("Signature: %v\n", s)
        fmt.Println("----------------------------------")

        d1, _ := tron.SignEnd(d0, s)
        fmt.Printf("Signed Transaction: %s\n", d1)
        fmt.Println("----------------------------------")

        txData1, _ := tron.ParseTxStr(d1)
        fmt.Printf("Transaction Info1: %v\n", txData1)
        fmt.Println("----------------------------------")
}

func main() {
        SwapToken()
}

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

go run tron-swap.go

Sau khi chạy ta được dữ liệu d1 (Signed Transaction):

0afa020a0274d42208cc668e76adb0c91a409880c0bcec315ad502081f12d0020a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e7472616374129a020a1541203555fb15bebbdc1a8551a8164e2f6ecebff67a12154181839e7bccdc7d5f50419bc34209d8ae5969ef661880c2d72f22e4017ff36ab500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000203555fb15bebbdc1a8551a8164e2f6ecebff67a000000000000000000000000000000000000000000000000000009184e729fff0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000fb3b3134f13ccd2c81f4012e53024e8135d58fee000000000000000000000000eca9bc828a3005b9a3b909f2cc5c2a54794de05f7092a4e4baec319001c0d1e1231241f6b3c6f4a966ddf2024a8dc61bad55281e980609b87032d3308138ca790202745789f86120f2b7b8af71962a0eacf53758686a9ca9ffed05c632c588bc2619d001

Ta chạy ứng dụng node để gửi giao dịch lên blockchain:

node public-transaction.js 0afa020a0274d42208cc668e76adb0c91a409880c0bcec315ad502081f12d0020a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e7472616374129a020a1541203555fb15bebbdc1a8551a8164e2f6ecebff67a12154181839e7bccdc7d5f50419bc34209d8ae5969ef661880c2d72f22e4017ff36ab500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000203555fb15bebbdc1a8551a8164e2f6ecebff67a000000000000000000000000000000000000000000000000000009184e729fff0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000fb3b3134f13ccd2c81f4012e53024e8135d58fee000000000000000000000000eca9bc828a3005b9a3b909f2cc5c2a54794de05f7092a4e4baec319001c0d1e1231241f6b3c6f4a966ddf2024a8dc61bad55281e980609b87032d3308138ca790202745789f86120f2b7b8af71962a0eacf53758686a9ca9ffed05c632c588bc2619d001

Ta được giao dịch: 56051aa9e249279877b8bd49d228a17ba782bebee49e2bfa1b3007917da3be26

Tạo giao dịch bằng NodeJs và ký giao dịch bằng Go

Một cách đơn giản khác là chúng ta tạo thông tin giao dịch trên NodeJs bằng TransactionBuilder trên TronWeb, và thực hiện ký giao dịch trên GoLang. Các bước thực hiện:

  • B1: Tạo giao dịch trên NodeJs bằng TransactionBuilder trên TronWeb => Sau khi tạo xong, trong thông tin giao dịch transaction chúng ta có TxId, nó chính là hash của giao dịch.
  • B2: Trên Go chúng ta thực hiện ký lên TxId này ta thu được Signature => Gửi thông tin này cho NodeJs
  • B3: Trên NodeJs, chúng ta gắn Signature vào transaction => Ta được một giao dịch đã được ký.
  • B4: Trên NodeJs, chúng ta sử dụng hàm sendRawTransaction() để gửi giao dịch nên blockchain.

Khi mới bắt đầu tôi gặp vấn đề “Lỗi SIGERROR do chữ ký trên GoLang và chữ ký trên NodeJs khác nhau” (Xem chi lỗi phần dưới), sau khi vấn đề được giải quyết mọi thứ khá đơn giản.

Đầu tiên trên NodeJs chúng ta tạo tệp test-sign-txid.js với nội dung như sau:

// Must using tronweb@5.0.0 trở lên
const TronWeb = require('tronweb');

const nodeLink = "https://nile.trongrid.io";

function getTronWeb() {
    let opts = {
        fullNode: nodeLink,
        solidityNode: nodeLink,
        eventServer: nodeLink,
        privateKey: ""
    };
    let tronWeb = new TronWeb(opts);
    return tronWeb;
}

async function buildTransaction() {
    try {
        let tronWeb = getTronWeb();
        let transaction = await tronWeb.transactionBuilder.sendTrx("TWXKV19GnweWQzFPJckKHbCgeSRUMMowHz", "500000", "TCuWX3Az4D23vWKcDU442eXFQBm2rsCRke");
        transaction = await tronWeb.transactionBuilder.extendExpiration(transaction, 3600);
        console.log("Transaction", JSON.stringify(transaction, null, 4));
    } catch(ex) {
        console.error("Unable to build transaction", ex);
    }
}

async function sendTransaction() {
    try {
        let tronWeb = getTronWeb();
        let transaction = {
            "txID": "d881918e173354293ad33bc9c33a65e37d4a0119c7f5c26f925339108e04bed7",
            "raw_data": {
                "contract": [
                    {
                        "parameter": {
                            "value": {
                                "amount": 500000,
                                "owner_address": "41203555fb15bebbdc1a8551a8164e2f6ecebff67a",
                                "to_address": "41e1753cb82b1a4ab5b872e44a3de4bac553029874"
                            },
                            "type_url": "type.googleapis.com/protocol.TransferContract"
                        },
                        "type": "TransferContract"
                    }
                ],
                "ref_block_bytes": "38cf",
                "ref_block_hash": "43167de6019aaea5",
                "expiration": 1715768109000,
                "timestamp": 1715764449836
            },
            "raw_data_hex": "0a0238cf220843167de6019aaea540c8cffeddf7315a67080112630a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412320a1541203555fb15bebbdc1a8551a8164e2f6ecebff67a121541e1753cb82b1a4ab5b872e44a3de4bac55302987418a0c21e70aca49fdcf731",
            "visible": false
        };

        // Update signature to transaction
        let signature = "cb5d8fa68d97f5958fe6ed940e11615915102958592a0993232a1bc278133f98732e6e8645c4aa333747eebe0590a00dc339f1fadca4f211d380e10b016dd7a500";
        transaction.signature = [ signature ];

        // Send transaction
        let result = await tronWeb.trx.sendRawTransaction(transaction);
        console.log("Result:", result);
    } catch(ex) {
        console.error("Unable to send transaction", ex);
    }
}

async function main() {
    if (process.argv.length<3) {
        console.log("Please run command as below:");
        console.log("    node test-sign-txid.js <Type>");
        return 0;
    }

    let type = process.argv[2];
        if (type==1) {
                await sendTransaction();
        } else {
                await buildTransaction();
        }
}

main();

Sau đó chúng ta đánh lệnh sau để sinh thông tin giao dịch và TxId:

node test-sign-txid.js 0
-------------------------
Transaction {
    "txID": "d881918e173354293ad33bc9c33a65e37d4a0119c7f5c26f925339108e04bed7",
    "raw_data": {
        "contract": [
            {
                "parameter": {
                    "value": {
                        "amount": 500000,
                        "owner_address": "41203555fb15bebbdc1a8551a8164e2f6ecebff67a",
                        "to_address": "41e1753cb82b1a4ab5b872e44a3de4bac553029874"
                    },
                    "type_url": "type.googleapis.com/protocol.TransferContract"
                },
                "type": "TransferContract"
            }
        ],
        "ref_block_bytes": "38cf",
        "ref_block_hash": "43167de6019aaea5",
        "expiration": 1715768109000,
        "timestamp": 1715764449836
    },
    "raw_data_hex": "0a0238cf220843167de6019aaea540c8cffeddf7315a67080112630a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412320a1541203555fb15bebbdc1a8551a8164e2f6ecebff67a121541e1753cb82b1a4ab5b872e44a3de4bac55302987418a0c21e70aca49fdcf731",
    "visible": false
}

Bây giờ trên Go ta tạo tệp sign-txid.go với mã nguồn như sau (Nhớ cập nhật TxId ở trên vào):

// https://pkg.go.dev/github.com/okx/go-wallet-sdk/coins/tron
// https://github.com/okx/go-wallet-sdk/blob/main/coins/tron/tron_test.go
package main

import (
        "encoding/hex"
        "fmt"

        "github.com/btcsuite/btcd/btcec/v2"
        "github.com/okx/go-wallet-sdk/coins/tron"
)

func SignTxId(txId string) {
        // Convert Pk from string to struct
        pkHex, _ := hex.DecodeString("9499BF78CE4A0217EC48F69AFDBAC19E10E10ACA14C8B278F08A213FA92FC498")
        pk, _ := btcec.PrivKeyFromBytes(pkHex)
        pubKey := pk.PubKey()
        fromAddr := tron.GetAddress(pubKey)
        fmt.Printf("From Address: %s\n", fromAddr)
        fmt.Println("----------------------------------")

        // Sign on TxId
        s, _ := tron.Sign(txId, pk)
        fmt.Printf("TxId: %s\n", txId)
        fmt.Printf("Raw Signature: %s\n", s)
        fmt.Println("----------------------------------")

        // Standardize the signature
        signature, _ := hex.DecodeString(s)
        var sig []byte
        sig = append(sig, signature[1:33]...)
        sig = append(sig, signature[33:65]...)
        sig = append(sig, signature[0]-27)
        s1 := hex.EncodeToString(sig)
        fmt.Printf("Standard Signature: %s\n", s1)
        fmt.Println("----------------------------------")
}

func main() {
        SignTxId("d881918e173354293ad33bc9c33a65e37d4a0119c7f5c26f925339108e04bed7")
}

Sau đó ta chạy lệnh sau để ký vào TxId:

go run sign-txid.go
-------------------------------
-------------------------------
From Address: TCuWX3Az4D23vWKcDU442eXFQBm2rsCRke
----------------------------------
TxId: d881918e173354293ad33bc9c33a65e37d4a0119c7f5c26f925339108e04bed7
Raw Signature: 1bcb5d8fa68d97f5958fe6ed940e11615915102958592a0993232a1bc278133f98732e6e8645c4aa333747eebe0590a00dc339f1fadca4f211d380e10b016dd7a5
----------------------------------
Standard Signature: cb5d8fa68d97f5958fe6ed940e11615915102958592a0993232a1bc278133f98732e6e8645c4aa333747eebe0590a00dc339f1fadca4f211d380e10b016dd7a500
----------------------------------

Ta lấy giá trị “Standard Signature” vào trong biến signature và dữ liệu Transaction vào biến transaction trong hàm sendTransaction() trong tệp test-sign-txid.js sau đó chạy lệnh sau:

node test-sign-txid.js 1
----------------------------
Result: {
  result: true,
  txid: 'd881918e173354293ad33bc9c33a65e37d4a0119c7f5c26f925339108e04bed7',
  transaction: {
    txID: 'd881918e173354293ad33bc9c33a65e37d4a0119c7f5c26f925339108e04bed7',
    raw_data: {
      contract: [Array],
      ref_block_bytes: '38cf',
      ref_block_hash: '43167de6019aaea5',
      expiration: 1715768109000,
      timestamp: 1715764449836
    },
    raw_data_hex: '0a0238cf220843167de6019aaea540c8cffeddf7315a67080112630a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412320a1541203555fb15bebbdc1a8551a8164e2f6ecebff67a121541e1753cb82b1a4ab5b872e44a3de4bac55302987418a0c21e70aca49fdcf731',
    visible: false,
    signature: [
      'cb5d8fa68d97f5958fe6ed940e11615915102958592a0993232a1bc278133f98732e6e8645c4aa333747eebe0590a00dc339f1fadca4f211d380e10b016dd7a500'
    ]
  }
}

Bạn xem giao dịch d881918e173354293ad33bc9c33a65e37d4a0119c7f5c26f925339108e04bed7 trên NileScan.

Và bạn làm tương tự với các giao dịch khác.

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

Lỗi SIGERROR do chữ ký trên GoLang và chữ ký trên NodeJs khác nhau

Để đơn giản hơn bài toán, hầu hết thao tác tôi sử dụng NodeJs, trên Go chỉ cần dùng Private Key để ký lên TxId được tạo ra trên NodeJs. Nhưng sau khi kiểm tra tôi gặp vấn đề là chữ ký trên Go và trên Node khác nhau, thường khác nhau ở Byte đầu và Byte cuối. Cụ thể như sau:

Với TxId là 21987f164ceccb5f28dc3187156041ab201e194c14e77e0b3c92d653204d32e8 thì:
Signature trên NodeJs: a097dfd9818d8b1d9e36022509443e767738e2270cdf5bc38a4b67789de12f1c171c562f7d9593da47cc6ccc0ab04c725dc904d567ac5a1cfda27e479b06cab901
Signature trên GoLang: 1ca097dfd9818d8b1d9e36022509443e767738e2270cdf5bc38a4b67789de12f1c171c562f7d9593da47cc6ccc0ab04c725dc904d567ac5a1cfda27e479b06cab9
Trường hợp này nếu trên NodeJs ta bỏ 2 byte cuối đi và trên GoLang ta bỏ byte đầu đi thì ta được dữ liệu khớp nhau.

Với TxId là 1ec74b87a63442e689baf459fa84cfd30f6b02a9ce173c0fe00a131b15917513 thì:
Signature trên NodeJs: e254a3d94d29ccd89a3f23f62a65fdf5e8ae396f78d111412862dfb8fed99200bacc323f61f1da02acedb8dc80ea8e89bf31152df401a50300d8c82c19858fbf00
Signature trên GoLang: 1ce254a3d94d29ccd89a3f23f62a65fdf5e8ae396f78d111412862dfb8fed992004533cdc09e0e25fd531247237f157174fb7dc7b8bb46fb38bef99660b6b0b182
Trường hợp này phần dữ liệu sau sai lệch khá nhiều.

Tôi tìm trên mạng thấy có bài viết nói về vấn đề chữ ký khác nhau giữa trên go-ethereum và nodejs: go-ethereum sign provides different signature than nodejs ethers

Tôi tìm hiểu sau hơn trong code thư viện xem có cách nào để khớp 2 Signature này với nhau không

Cuối cùng tôi tìm ra giải pháp trong hàm SignEnd() trên Go lang. Nếu tôi biến đổi như trong code (Xem ảnh dưới) thì sẽ cho ra Signature khớp với trên NodeJs. Cụ thể thì Signature trên NodeJs sẽ bao gồm:

  • Lấy 32 byte từ byte số 1 của Signature trên Golang
  • Nối với 32 byte tiếp tính từ byte số 33
  • Nối tiếp với byte đầu tiên sau khi đã trừ đi 27
Cách biến đổi Signature trong hàm SignEnd()

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: 68

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