最近心血来潮,使用 go 对以太坊开发进行了一番简单探索,特撰此文以记录与 ehters 使用上的差异。
废话不多书,直接上代码 😜。本文代码主要参考 go-ethereum 文档,补充了文档中未提及的袭击。
关键类库:
ethclient.Dial(link)
此处的连接可以是普通 http url
和 ws url
,但与 ethers 不同,如果要订阅事件,必须使用 ws url
。
ethclient.Client
中获得,自行查看文档。一个比较 tricky 的地方是从 tx 中获得 from 的信息,见下面的代码:types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx)
其中 tx 是 types.Transaction
类型
crypto.GenerateKey()
crypto.HexToECDSA(privateKey)
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)
}
基本流程:
abi
abigen
执行对应的命令以生成 contract 对应的 go 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)))
对于特定的 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
带入两函数即可。
至此,典型场景代码示例已经罗列完毕,至于使用体验,就留给各位看官自行判断了。
觉得有帮助的话,不妨考虑购买付费文章来支持我们 🙂 :
付费文章