106-Ethereum钱包开发
0x-wen

Ethereum钱包开发

基于go实现以太坊离线钱包

主要依赖仓库列表:

1
2
3
4
5
    github.com/ethereum/go-ethereum v1.14.4

​ github.com/tyler-smith/go-bip32 v1.0.0

​ github.com/tyler-smith/go-bip39 v1.1.0

1.非确定性钱包

地址独立,无关联性,但需要备份所有地址的私钥

  • 第一步生成私钥 GenerateKey -> ecdsa.PrivateKey

  • 第二步通过私钥获取公钥 privKey.PublicKey

  • 第三步公钥通过Keccak256算法得到地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func GeneratePrivKey() {
// 生成随机私钥
privKey, _ := crypto.GenerateKey()
// 将私钥转换为字节序列
privKeyBytes := privKey.D.Bytes()
// 将私钥字节序列转换为十六进制字符串
privKeyHex := hex.EncodeToString(privKeyBytes)
// 打印私钥的十六进制表示
fmt.Println(privKeyHex)

// 提取私钥对应的公钥
PublicKey := privKey.PublicKey
// 将公钥转换为以太坊地址
addr := crypto.PubkeyToAddress(PublicKey)
fmt.Println(addr)
}

通过secp256k1算法生成公私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func createPrivKey() {
curve := secp256k1.S256()
b := make([]byte, curve.Params().N.BitLen()/8)
io.ReadFull(rand.Reader, b)
key := new(big.Int).SetBytes(b)
fmt.Println("key:", len(key.Bytes()))
fmt.Println("key:", hex.EncodeToString(key.Bytes()))
// 使用私钥计算公钥,X和Y是公钥的坐标
x, y := curve.ScalarBaseMult(key.Bytes())

// 获取公钥的字节表示形式
publicKey := ecdsa.PublicKey{Curve: curve, X: x, Y: y}
pubKeyBytes := crypto.FromECDSAPub(&publicKey)
// 输出公钥
fmt.Println("pubKey:", hex.EncodeToString(pubKeyBytes))

compressPubKey := crypto.Keccak256(pubKeyBytes[1:])
addr := common.BytesToAddress(compressPubKey[12:])

// 输出以太坊地址
fmt.Println("addr:", addr.String())
}

2.分层确定性钱包

生成一个BIP39兼容的助记词

1
2
3
4
5
6
7
8
9
10
11
func bip39Mnemonic() (seed []byte) {
// 生成256位的随机熵,用于创建BIP39助记词。即 128 个 16 进制字符
entropy, _ := bip39.NewEntropy(128)
mnemonic, _ := bip39.NewMnemonic(entropy)
fmt.Println("助记词:", mnemonic)

// 由助记词生成种子(Seed), password为空可兼容其他钱包
seed = bip39.NewSeed(mnemonic, "")
fmt.Println("New seed:", seed)
return
}

根据BIP39随机种子字节生成BIP32钱包

  • 通过种子得到MasterKey
  • 通过主私钥派生子私钥 masterKey.NewChildKey
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func bip32HD(seed []byte) {
// 由种子生成主账户扩展私钥(私钥和链码)
masterKey, _ := bip32.NewMasterKey(seed) //从0开始,主账户密钥 masterKey 序号是 0,这个就叫做索引号(32 位)
fmt.Println("masterKey:", masterKey)

childKey1, _ := masterKey.NewChildKey(1)

// 用主账户公钥 派生 子账户公钥
publicKey := masterKey.PublicKey()
childPubKey1, _ := publicKey.NewChildKey(1)

// 第一种: 通过扩展公钥派生出子账户公钥,扩展私钥派生出子账户私钥,实现公私钥解耦
// 第二种: 强化派生限制父公钥派生子公钥,严格通过 扩展私钥 -> 子私钥 -> 子公钥
pubK1, _ := childPubKey1.Serialize()
pubK2, _ := childKey1.PublicKey().Serialize()
// 当前通过 扩展公钥 -> 子公钥[1] == 对应子私钥[1] -> 子公钥, [1]表示 childIdx 一致
if !bytes.Equal(pubK1, pubK2) {
fmt.Println("pubK1 != pubK2")
}

// 强化派生
// 索引号在 0 和 2^31–1(0x0 to 0x7FFFFFFF)之间的只用于常规派生。
// 索引号在 2^31 和 2^32– 1(0x80000000 to 0xFFFFFFFF)之间的只用于强化派生。
childKeyPro, _ := masterKey.NewChildKey(bip32.FirstHardenedChild)
pubKPro, _ := childKeyPro.PublicKey().Serialize()
fmt.Println("pubKPro:", pubKPro)

childPubKeyPro, _ := publicKey.NewChildKey(bip32.FirstHardenedChild)
fmt.Println("childPubKeyPro:", childPubKeyPro) // childPubKeyPro: <nil>
}

结合BIP44创建钱包

  • masterKey 兼容BIP44协议创建eth钱包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func createWallet(seed []byte) {
// 创建子账户, 为了保护主私钥安全,所有主私钥派生的第一级账户,都采用强化派生。
masterKey, _ := bip32.NewMasterKey(seed)
// 以太坊的币种类型是60, 以路径(path: "m/44'/60'/0'/0/0")为例
key, _ := masterKey.NewChildKey(bip32.FirstHardenedChild + 44) // 强化派生 对应 purpose'
key, _ = key.NewChildKey(bip32.FirstHardenedChild + uint32(60)) // 强化派生 对应 coin_type'
key, _ = key.NewChildKey(bip32.FirstHardenedChild + uint32(0)) // 强化派生 对应 account'
key, _ = key.NewChildKey(uint32(0)) // 常规派生 对应 change
key, _ = key.NewChildKey(uint32(0)) // 常规派生 对应 address_index

fmt.Println("privKey:", key.String())

// 将key转换为十六进制字符串
hexKey := fmt.Sprintf("%x", key.Key)
fmt.Println("Hex Key:", hexKey)

// 生成地址
pubKey, _ := crypto.DecompressPubkey(key.PublicKey().Key)
address := crypto.PubkeyToAddress(*pubKey).Hex()
fmt.Println("address:", address)
}

发送交易与查询

1.查询链上数据

  • 创建一个rpc节点客户端,通过客户端提供的方法进行查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// client的代码去 https://dashboard.alchemy.com/ 点击APIKEY后复制
func newClient() (*ethclient.Client, error) {
// client, err := ethclient.Dial("https://eth-mainnet.g.alchemy.com/v2/k7J02LbbJiACCe52gTgZ64sY-sj-AZux")
client, err := ethclient.Dial("https://eth-sepolia.g.alchemy.com/v2/k7J02LbbJiACCe52gTgZ64sY-sj-AZux")
if err != nil {
log.Fatal(err)
}

// Get the balance of an account
account := common.HexToAddress(formAddr)
balance, err := client.BalanceAt(context.Background(), account, nil)
if err != nil {
log.Fatal(err)
}

fmt.Printf("Account balance: %d\n", balance)

// Get the latest known block
block, err := client.BlockByNumber(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Latest block: %d\n", block.Number().Uint64())
return client, nil
}

2.发送交易至链上(SendTransaction)

  • 构造交易 types.NewTransaction
  • 使用私钥对交易进行签名 types.SignTx
  • 广播交易至节点 cli.SendTransaction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
func transaction() {
privKey, _ := crypto.HexToECDSA(privKeyHex)
formAddr := crypto.PubkeyToAddress(privKey.PublicKey)
fmt.Println("formAddr:", formAddr.Hex())
cli, _ := newClient()

// 构造交易数据
nonce, err := cli.NonceAt(context.Background(), formAddr, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("nonce:", nonce)
toAddress := common.HexToAddress(toAddr)
amount := big.NewInt(1000000000000000)
gasLimit := uint64(21000) // 标准交易的 gas 限制
gasPrice := big.NewInt(200000000000) // gas 价格,你需要根据网络情况调整
// gasPrice, err := cli.SuggestGasTipCap(context.Background())
// if err != nil {
// log.Fatal(err)
// }
fmt.Println("gasPrice:", gasPrice)
tx := types.NewTransaction(nonce, toAddress, amount, gasLimit, gasPrice, nil)

chainID, err := cli.NetworkID(context.Background())
if err != nil {
log.Fatal(err)
}
// 对交易进行签名
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privKey)
if err != nil {
log.Fatal(err)
}
// 将签名后的数据通过clp编码为字节数组,前端eth_sendRawTransaction接口中params参数
encodedTx, err := rlp.EncodeToBytes(signedTx)
if err != nil {
log.Fatal(err)
}
params := hex.EncodeToString(encodedTx)
fmt.Printf("eth_sendRawTransaction Params: %s\n", params)
// 广播交易到测试节点
if err = cli.SendTransaction(context.Background(), signedTx); err != nil {
log.Fatal(err)
}
fmt.Printf("Transaction Hash: 0x%x\n", signedTx.Hash())
}

API相关接口

参考api文档: https://docs.alchemy.com/reference/eth-gettransactionbyhash

发送交易:params参数获取可参考transaction方法

1
2
3
4
5
6
7
8
9
10
11
12
13
curl --request POST \
--url https://eth-sepolia.g.alchemy.com/v2/k7J02LbbJiACCe52gTgZ64sY-sj-AZux \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '
{
"id": 1,
"jsonrpc": "2.0",
"params": [
"0xf86f04852e90edd0008252089475751bf3a86ea2f19660229c112df7dad84b8c0287038d7ea4c68000808401546d72a0d7618c5a377ae77c40c0e092c05545bbb24aacfff02569d74a1ab3bf27620d0ba02b5e3fe241f53d44a00691084d9e0d08475ce136b92da53b8b0b9b4fed285167"
],
"method": "eth_sendRawTransaction"
}'

根据txhash查询交易信息

1
2
3
4
5
6
7
8
9
10
11
12
13
curl --request POST \
--url https://eth-sepolia.g.alchemy.com/v2/k7J02LbbJiACCe52gTgZ64sY-sj-AZux \
--header 'Content-Type: application/json' \
--header 'Cookie: _cfuvid=wq9cOWHZrQJC_tuTXZLmlWkdI8GPYot1N2.FsyNsrbw-1716723624624-0.0.1.1-604800000' \
--data '
{
"id": 1,
"jsonrpc": "2.0",
"method": "eth_getTransactionByHash",
"params": [
"0x18fb59de0ac1ab62401e08f6260c0fd838a35f113da467671c894c65d1b1ccc6"
]
}'
由 Hexo 驱动 & 主题 Keep
总字数 41.3k