9

面向 Ethers 的 Go 以太坊开发非权威指南

 1 year ago
source link: https://blog.dteam.top/posts/2024-02/go-ethereum-cookbook.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

面向 Ethers 的 Go 以太坊开发非权威指南

胡键 Posted at — Feb 16, 2024 阅读 2

最近心血来潮,使用 go 对以太坊开发进行了一番简单探索,特撰此文以记录与 ehters 使用上的差异。

废话不多书,直接上代码 ?。本文代码主要参考 go-ethereum 文档,补充了文档中未提及的袭击。

关键类库:

  • go-ethereum
  • go-ethereum-hdwallet
ethclient.Dial(link)

此处的连接可以是普通 http urlws url,但与 ethers 不同,如果要订阅事件,必须使用 ws url

  1. chain、block、tx、balance 和 gas 信息等可以直接从 ethclient.Client 中获得,自行查看文档。一个比较 tricky 的地方是从 tx 中获得 from 的信息,见下面的代码:
types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx)

其中 tx 是 types.Transaction 类型

  1. wallet 创建,三种情况:
  • random wallet
crypto.GenerateKey()
crypto.HexToECDSA(privateKey)
  • 基于某 wallet 派生,此处用到上面列出的 go-ethereum-hdwallet
sk, _ := crypto.HexToECDSA(privateKey)
wallet, _ := hdwallet.NewFromSeed(sk.D.Bytes())
path := hdwallet.MustParseDerivationPath("m/44'/60'/0'/0/" + fmt.Sprintf("%d", rand.Intn(10)))
account, _ := wallet.Derive(path, false)
derivedSk, _ := wallet.PrivateKey(account)

获得 wallet 信息

func printPrivateKey(sk *ecdsa.PrivateKey) {
    fmt.Printf("Private key: %s\n", common.Bytes2Hex(sk.D.Bytes()))
    fmt.Printf("Public key: %s\n", common.Bytes2Hex(crypto.FromECDSAPub(&sk.PublicKey)[1:]))
    fmt.Printf("Address: %s\n", crypto.PubkeyToAddress(sk.PublicKey).Hex())
}
func Parse(bnValue string, decimal int) *big.Int {
    amount := new(big.Float)
    amount.SetString(bnValue)
    amount = amount.Mul(amount, big.NewFloat(math.Pow10(decimal)))
    result, _ := new(big.Int).SetString(amount.String(), 10)
    return result
}

func Format(bnValue string, decimal int) string {
    amount := new(big.Float)
    amount.SetString(bnValue)
    amount = amount.Quo(amount, big.NewFloat(math.Pow10(decimal)))
    result, _ := amount.Float64()
    return fmt.Sprintf("%f", result)
}

Contract 交互

基本流程:

  1. 获得 abi
  2. 使用 abigen 执行对应的命令以生成 contract 对应的 go class
  3. 使用生成的 class 即可

以 USDC 为例,对应的生成命令:

abigen --abi=usdc_abi.json --pkg=usdc --out=./internal/contracts/udsc.go

使用生成的类:

address := common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
u, _ := usdc.NewUsdc(address, finger.Client)
name, _ := u.Name(nil)
symbol, _ := u.Symbol(nil)
totalSupply, _ := u.TotalSupply(nil)
decimal, _ := u.Decimals(nil)
fmt.Printf("name: %s\n", name)
fmt.Printf("symbol: %s\n", symbol)
fmt.Printf("total supply: %s\n", commons.Format(totalSupply.String(), int(decimal)))

Log 和 Event

对于特定的 dapp 大多都是订阅和查询特定 contract 的 log 和 event,这也是本节代码的覆盖的内容。至于自由式查询和监听,请自行参考文后链接。

订阅事件:

// 注意此处使用了 ws link
u, _ := usdc.NewUsdc(address, finger.WsClient)
transfer := make(chan *usdc.UsdcTransfer)
sub, _ := u.WatchTransfer(nil, transfer, nil, nil)

defer sub.Unsubscribe()
defer close(transfer)

times := 0
for {
    select {
    case err := <-sub.Err():
        panic(err)
    case t := <-transfer:
        times++
        fmt.Printf("%d ------\n", times)
        fmt.Printf("%s == %s usdc ==> %s with\n", t.From.Hex(), commons.Format(t.Value.String(), 6), t.To.Hex())
        fmt.Printf("tx hash: %s\n", t.Raw.TxHash.Hex())

        if times == 3 {
            return nil
        }
    }
}

过滤日志:

u, _ := usdc.NewUsdc(address, finger.Client)
transfers, _ := u.FilterTransfer(nil, []common.Address{common.HexToAddress(from)}, nil)
for transfers.Next() {
    fmt.Printf("transfered %s usdc ==> %s\n", commons.Format(transfers.Event.Value.String(), 6), transfers.Event.To.Hex())
    fmt.Printf("tx hash: %s\n", transfers.Event.Raw.TxHash.Hex())
}

这里需要特别留意,正确的代码必须是用 go 生成的签名完全等于同样输入下 ethers 的输出。否则,两者写的代码根本无法配合使用,而这种配合的几率很高!并且,这也是文后链接未明确提到的地方。

personal_sign 签名的生成和验证:

func PersonalSign(message string, privateKey *ecdsa.PrivateKey) []byte {
    // 使用了 accounts.TextHash 来生成 hash,因为 ethereum 的固定前缀。
    signature, err := crypto.Sign(accounts.TextHash([]byte(message)), privateKey)
    if nil != err {
        panic(err)
    }

    // 注意此处操作
    signature[64] += 27

    return signature
}

func VerfiyPersonalSign(message string, signature []byte, publicKey *ecdsa.PublicKey) string {
    signature[64] -= 27
    sigPk, err := crypto.Ecrecover(accounts.TextHash([]byte(message)), signature)
    if nil != err {
        panic(err)
    }

    if !bytes.Equal(sigPk, crypto.FromECDSAPub(publicKey)) {
        panic("invalid signature")
    }

    return crypto.PubkeyToAddress(*publicKey).Hex()
}

EIP712 签名目前已经普及,因此没有理由不提供对应的 helper:

func EIP712Sign(typedData apitypes.TypedData, privateKey *ecdsa.PrivateKey) []byte {
    // 使用了特定的 hash 函数,理由同上
    typedHash, _, _ := apitypes.TypedDataAndHash(typedData)
    signature, err := crypto.Sign(typedHash, privateKey)
    if nil != err {
        panic(err)
    }

    // 同上
    signature[64] += 27

    return signature
}

func VerifyEIP712Sign(typedData apitypes.TypedData, signature []byte, publicKey *ecdsa.PublicKey) string {
    signature[64] -= 27
    typedHash, _, _ := apitypes.TypedDataAndHash(typedData)
    sigPk, err := crypto.Ecrecover(typedHash, signature)
    if nil != err {
        panic(err)
    }

    if !bytes.Equal(sigPk, crypto.FromECDSAPub(publicKey)) {
        panic("invalid signature")
    }

    return crypto.PubkeyToAddress(*publicKey).Hex()
}

你以为这就结束了吗?非也,对于 EIP712 签名有一个非常 tricky 的地方,如果不注意,很容易导致 ethers 签出来的和上面的代码输出不同。这还不是算法不对,至于原因,已经写在下面的注释里了:

// Note: the order of the fields in "Types" matters !!!
// If you want to get the same signature in ethers.js, the order of fields in both places must be the same.
// For "EIP712Domain", the order suggested by EIP Spec is below:
// ---
// 1. name
// 2. version
// 3. chainId
// 4. verifyingContract
// 5. salt
eip712Data := apitypes.TypedData{
    Types: apitypes.Types{
        "Person": []apitypes.Type{
            {Name: "name", Type: "string"},
            {Name: "age", Type: "uint256"},
        },
        "EIP712Domain": []apitypes.Type{
            {Name: "name", Type: "string"},
            {Name: "version", Type: "string"},
            {Name: "chainId", Type: "uint256"},
        },
    },
    PrimaryType: "Person",
    Domain: apitypes.TypedDataDomain{
        Name:    "EIP712 Example",
        Version: "1",
        ChainId: math.NewHexOrDecimal256(1),
    },
    Message: apitypes.TypedDataMessage{
        "name": someone.Name,
        "age":  math.NewHexOrDecimal256(someone.Age),
    },
}

剩下的就简单了,将此 typedata 带入两函数即可。

至此,典型场景代码示例已经罗列完毕,至于使用体验,就留给各位看官自行判断了。

觉得有帮助的话,不妨考虑购买付费文章来支持我们 ? :

付费文章

</div


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK