DTeam 技术日志

Doer、Delivery、Dream

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

胡键 Posted at — Feb 16, 2024 阅读

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

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

关键类库:

基本操作

  1. 连接
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 创建,三种情况:
crypto.GenerateKey()
crypto.HexToECDSA(privateKey)
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())
}
  1. 数字转换
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 带入两函数即可。

写到最后

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

参考

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

付费文章

友情链接


相关文章