文章目录
- golang与以太坊交互
- 什么是go-ethereum
- 与节点交互前的准备
- 使用golang与以太坊区块链交互
- 查询账户的余额
- 使用golang生成以太坊账户
- 使用golang生成以太坊钱包
- 使用golang在账户之间转移eth
- 安装使用solc和abigen
- 生成bin和abi文件
- 生成go文件
- 使用golang在测试网上部署智能合约
- 使用goalng与智能合约进行交互
- 使用golang在Etherscan上验证合约
golang与以太坊交互
阅读此文章前,需要你了解go,solidity,ethereum相关基础知识。
Golang基础入门-****博客
go语言–区块链学习(三)_go区块链合约-****博客
MetaMask安装及使用(全网最全!!!)_matemask-****博客
Solidity基础(详细易懂!!!)_solidity教程-****博客
在以太坊测试网上部署合约_将智能合约部署至以太坊测试网-****博客
此篇文章参考于01-Interact with Ethereum blockchain using Golang (youtube.com),博主在视频代码的基础上进行了些改进,修改了一些被弃用的包和函数,并结合自己的理解写下了这篇博客。
什么是go-ethereum
Geth (go-ethereum) 是以Go语言实现的以太坊——通往去中心化网络的门户。
自始至终,Geth 一直是以太坊的核心部分。作为最早的以太坊实现之一,Geth 经历了最多的实战检验和测试。
Geth 是一个以太坊执行客户端,负责处理交易、智能合约的部署和执行,内置了被称为以太坊虚拟机的嵌入式计算机。将 Geth 与共识客户端一起运行,可以将一台计算机转变为以太坊节点。
与节点交互前的准备
前面提到,将 Geth 与共识客户端一起运行,可以将一台计算机转变为以太坊节点,这样做,可以为我们带来什么?
运行自己的节点使您能够以真正私密、自给自足和无需信任的方式使用以太坊。您无需信任接收到的信息,因为可以通过您的 Geth 实例自行验证数据。
这样你就可以直接利用自己的节点提供的 rpc 地址直接与以太坊网络进行交互了。但是,对于一般的个人开发者来说,运营一台以太坊节点服务器,成本可能有些许高昂。
并且,由于高昂的 gas 费用,我们在开发过程中的测试,不可能是在以太坊主网上测试的。我们在实际开发过程中,可以启动本地 Geth 测试网或者用 Ganache 快速启动一条测试链,做本地开发测试,但是这并不能涵盖所有的以太坊网络上的可能性。
为了模拟更真实的以太坊网络环境,帮助我们更好地去构建我们的 dapp,我们可以选择与以太坊测试网络进行交互,做开发测试。
如何与测试网交互?同样,将自己的设备作为以太坊节点(测试网节点)还是过于繁琐,下面介绍一项第三方托管服务。
Infura 是一项托管服务,提供安全可靠的访问多种区块链网络的能力,帮助开发者摆脱管理区块链基础设施的复杂性,让他们专注于构建创新的 Web3 应用程序。
Infura 充当了连接应用程序与区块链网络的重要桥梁,为开发者提供强大的API以与区块链进行交互、部署和管理智能合约等功能。无论您是构建去中心化应用程序(Dapp)、加密钱包还是交易所,Infura都提供了必要的基础设施和工具,帮助创建高质量、可靠的 Web3 应用程序。
首先,我们来到 Infura 的官网(不要开代理):Ethereum API | IPFS API & Gateway | ETH Nodes as a Service | Infura
注册登录,选择以太坊服务,选择免费的 Infura 服务。
接下来,会进入到我们的面板,点击 My First Key,查看你的 api 密钥。
如果一开始注册过程中未选择以太坊的服务,也不要紧,这里勾选一下就好了,然后点击 Active Endpoints。
然后你就可以看到你可以使用的 rpc 地址了,这里我们只需要主网和 sepolia 测试网的就可以了。
使用golang与以太坊区块链交互
接下来,我们将利用 Infura 提供的 rpc 地址,去获取 sepolia 测试网上当前的区块高度。
代码:
package main
import (
"context"
"fmt"
"github.com/ethereum/go-ethereum/ethclient" // 导入以太坊客户端库
"log"
)
var infuraURL = "https://sepolia.infura.io/v3/********************************" // Infura提供的API URL
func main() {
// 使用Infura URL连接以太坊客户端
client, err := ethclient.DialContext(context.Background(), infuraURL)
if err != nil {
log.Fatalf("Error to create a ether client:%v", err) // 如果连接失败,打印错误并终止程序
}
defer client.Close() // 程序结束前关闭客户端连接
// 获取最新区块
block, err := client.BlockByNumber(context.Background(), nil)
if err != nil {
log.Fatalf("Error to get a block:%v", err) // 如果获取区块失败,打印错误并终止程序
}
fmt.Println(block.Number()) // 打印区块号
}
然后我们编译运行一下代码。
可以看到,区块链浏览器上显示的最后一个区块也是6248296。TESTNET Sepolia (ETH) Blockchain Explorer (etherscan.io)
查询账户的余额
接下来,我们去获取我们的账户余额,打开我们的metamask,复制我们的账户地址,写入代码,编译运行。
代码:
package main
import (
"context"
"fmt"
"github.com/ethereum/go-ethereum/common" // 导入以太坊常用函数库
"github.com/ethereum/go-ethereum/ethclient" // 导入以太坊客户端库
"log"
"math"
"math/big"
)
var infuraURL = "https://sepolia.infura.io/v3/********************************" // Infura提供的API URL
func main() {
// 使用Infura URL连接以太坊客户端
client, err := ethclient.DialContext(context.Background(), infuraURL)
if err != nil {
log.Fatalf("Error to create a ether client:%v", err) // 如果连接失败,打印错误并终止程序
}
defer client.Close() // 程序结束前关闭客户端连接
// 要查询余额的以太坊地址
addr := "0x****************************************"
address := common.HexToAddress(addr)
// 获取地址的余额
balance, err := client.BalanceAt(context.Background(), address, nil)
if err != nil {
log.Fatalf("Error to get the balance:%v", err) // 如果获取余额失败,打印错误并终止程序
}
fmt.Println("The balance:", balance)
// 将余额转换为以太单位(从wei到ether)
// 1 ether = 10^18 wei
// 将balance转换为big.Float类型以处理大数
fBalance := new(big.Float)
fBalance.SetString(balance.String())
// 打印原始的big.Float格式的余额
fmt.Println("Balance as big.Float:", fBalance)
// 计算以ether为单位的余额
balanceEther := new(big.Float).Quo(fBalance, big.NewFloat(math.Pow10(18)))
fmt.Println("Balance in ether:", balanceEther)
}
这样,我们就可以看到我们的账户余额了。
使用golang生成以太坊账户
当您想要创建一个账户时,大多数库会为您生成一个随机的私钥。
私钥由64个十六进制字符组成,并且可以用密码加密。
公钥是使用椭圆曲线数字签名算法从私钥生成的。您可以通过取公钥的 Keccak-256 哈希的最后20个字节并在开头添加 0x 来获得您账户的公共地址。
接下来是使用 golang 生成一个账户的代码。
代码:
package main
import (
"fmt"
"github.com/ethereum/go-ethereum/common/hexutil" // 导入以太坊的hex编解码包
"github.com/ethereum/go-ethereum/crypto" // 导入以太坊的加密库
"log"
)
func main() {
// 生成一个新的以太坊私钥
pvk, err := crypto.GenerateKey()
if err != nil {
log.Fatal(err) // 如果生成私钥时出现错误,打印错误信息并退出程序
}
// 将私钥转换为字节格式,并以hex编码方式打印出来
pData := crypto.FromECDSA(pvk)
fmt.Println(hexutil.Encode(pData))
// 将公钥部分(从私钥派生而来)转换为字节格式,并以hex编码方式打印出来
puData := crypto.FromECDSAPub(&pvk.PublicKey)
fmt.Println(hexutil.Encode(puData))
// 使用公钥生成对应的以太坊地址,并以hex编码方式打印出来
fmt.Println(crypto.PubkeyToAddress(pvk.PublicKey).Hex())
}
获得的账户私钥是可以直接导入你的 metamask 的,不用担心会生成一个被使用过的账户。
(这里的私钥只是作为演示,我是不用的,我没打码,大家不要直接拿来用,造成的财产损失,博主概不负责,大家请妥善保管好自己的私钥)
使用golang生成以太坊钱包
除了使用 crypto.GenerateKey() 生成以太坊账户的私钥之外,还有一种更安全的方式,即使用 keystore 库。这个库将账户的私钥安全地存储在操作系统的文件系统中。私钥通常以 JSON 格式编码,并使用密码加密,以确保在存储和传输过程中的安全性。
设计 keystore 库的初衷之一是安全地管理以太坊账户的私钥,并避免将私钥直接硬编码在代码中。直接在代码中使用私钥存在许多安全风险,例如私钥泄露或意外提交到版本控制系统中,这可能导致资产损失或被恶意利用。使用 keystore 可以将私钥安全地存储在操作系统的文件系统中,并使用密码加密,只在必要时才解密和使用私钥,从而增强了私钥管理的安全性。
keystore 库使得以太坊开发者能更便捷地管理私钥和账户,提供了一种安全且标准化的方法来处理与以太坊账户相关的加密操作和文件存储需求。
代码:
package main
import (
"fmt"
"github.com/ethereum/go-ethereum/accounts/keystore" // 导入以太坊账户管理和密钥存储库
"log"
)
func main() {
// 创建一个新的 keystore 实例,将密钥存储在当前目录下的 "wallet" 文件夹中
key := keystore.NewKeyStore("./wallet", keystore.StandardScryptN, keystore.StandardScryptP)
password := "password" // 设置用于加密私钥的密码
// 使用指定的密码创建一个新的以太坊账户
a, err := key.NewAccount(password)
if err != nil {
log.Fatal(err) // 如果创建账户时出现错误,打印错误信息并退出程序
}
// 打印新创建账户的以太坊地址
fmt.Println(a.Address.Hex())
}
这样我们就得到了一个加密后的账户,可以看到,在我们的 wallet 文件夹下,有我们的密钥文件。
因为 goland 默认用 .txt 格式打开这个密钥文件,所以格式不如人意,我们可以在 Settings->File Types->JSON 里面添加文件名通配类型。
UTC--*-*-*T*-*-*.*Z--*
或
UTC--*
完成之后,可以看到我们的文件显示,已经变成高亮了。
接下来按住Ctrl+Alt+L,格式化代码,就可以看的更舒服一点了。
然后,接下来,我们来使用这个密钥文件,从里面拿出我们的私钥、公钥和钱包地址。
代码:
package main
import (
"fmt"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"log"
"os"
)
func main() {
//key := keystore.NewKeyStore("./wallet", keystore.StandardScryptN, keystore.StandardScryptP)
password := "password"
//a, err := key.NewAccount(password)
//if err != nil {
// log.Fatal(err)
//}
//fmt.Println(a.Address)
// 使用已存在的密钥文件进行解密
b, err := os.ReadFile("./wallet/UTC--****-**-**T**-**-**.**********--****************************************") // 读取密钥文件
if err != nil {
log.Fatal(err)
}
key, err := keystore.DecryptKey(b, password) // 解密密钥文件
if err != nil {
log.Fatal(err)
}
// 获取私钥的字节表示,并打印出来
pData := crypto.FromECDSA(key.PrivateKey)
fmt.Println("Priv", hexutil.Encode(pData))
// 获取公钥的字节表示,并打印出来
pData = crypto.FromECDSAPub(&key.PrivateKey.PublicKey)
fmt.Println("Pub", hexutil.Encode(pData))
// 获取以太坊地址,并打印出来
fmt.Println("Add", crypto.PubkeyToAddress(key.PrivateKey.PublicKey).Hex())
}
编译运行后我们就可以拿到我们的私钥、公钥和账户地址了。
使用golang在账户之间转移eth
我们先使用 keystore 创建两个以太坊账户,也可以只创建一个,因为上一步已经创建了一个账户。
代码:
package main
import (
"github.com/ethereum/go-ethereum/accounts/keystore"
"log"
)
func main() {
// 用 keystore 创建两个以太坊账户的密钥文件
ks := keystore.NewKeyStore("./wallet", keystore.StandardScryptN, keystore.StandardScryptP)
_, err := ks.NewAccount("password")
if err != nil {
log.Fatal(err)
}
_, err = ks.NewAccount("password")
if err != nil {
log.Fatal(err)
}
}
然后我们使用 metamask 往一个账户里面转点 SepoliaETH。
记得转点,就直接往账户地址里面转就行了。
代码:
package main
import (
"context"
"fmt"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"log"
"math/big"
"os"
)
var url = "https://sepolia.infura.io/v3/********************************" // Infura提供的API URL
func main() {
// 客户端连接到以太坊节点
client, err := ethclient.Dial(url)
if err != nil {
log.Fatal(err)
}
// a1,a2为刚刚创建的两个以太坊账户地址
a1 := common.HexToAddress("****************************************")
a2 := common.HexToAddress("****************************************")
// 查询第一个地址的余额
b1, err := client.BalanceAt(context.Background(), a1, nil)
if err != nil {
log.Fatal(err)
}
// 查询第二个地址的余额
b2, err := client.BalanceAt(context.Background(), a2, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("Balance 1:", b1)
fmt.Println("Balance 2:", b2)
// 获取第一个地址的待处理交易数量(nonce)
nonce, err := client.PendingNonceAt(context.Background(), a1)
if err != nil {
log.Fatal(err)
}
// 设置要发送的以太币数量(1 ether = 1000000000000000000 wei)
amount := big.NewInt(1000000000000000)
// 获取推荐的 gas 价格
gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
log.Fatal(err)
}
// 创建新的交易
tx := types.NewTx(&types.LegacyTx{
Nonce: nonce,
To: &a2,
Value: amount,
Gas: 21000,
GasPrice: gasPrice,
Data: nil,
})
// 获取当前链的 ID
chainID, err := client.NetworkID(context.Background())
if err != nil {
log.Fatal(err)
}
// 从密钥文件中解密私钥
b, err := os.ReadFile("./wallet/UTC--****-**-**T**-**-**.**********--****************************************") // 有余额的账户的密钥文件
if err != nil {
log.Fatal(err)
}
key, err := keystore.DecryptKey(b, "password")
if err != nil {
log.Fatal(err)
}
// 使用私钥对交易进行签名
tx, err = types.SignTx(tx, types.NewEIP155Signer(chainID), key.PrivateKey)
if err != nil {
log.Fatal(err)
}
// 发送交易到以太坊网络
err = client.SendTransaction(context.Background(), tx)
if err != nil {
log.Fatal(err)
}
// 打印该交易的交易哈希
fmt.Printf("tx semt: %s\n", tx.Hash().Hex())
// 打印交易后,两个账户的剩余余额
fmt.Println("Balance 1:", b1)
fmt.Println("Balance 2:", b2)
}
下面是结果截图,我用的是我之前测试的账户地址。
我们也可以去区块链浏览器上查看这笔交易。TESTNET Sepolia (ETH) Blockchain Explorer (etherscan.io)
知识点补充:
nonce
在以太坊中,nonce(Number used ONCE,一次性数字)是与每个发送者账户相关联的整数,用于确保交易顺序和唯一性。每个账户的 nonce 值是账户发送的交易数量加一,从零开始。这意味着每笔交易必须使用正确的 nonce ,以确保它们按正确的顺序执行且不会被重放。
具体来说:
- Nonce 的作用:Nonce 确保在发送交易时,每笔交易都有唯一的标识符。它防止了重放攻击,因为同样的交易数据使用不同的 nonce 会被认为是不同的交易。
- 获取 Nonce:通过调用以太坊客户端的 PendingNonceAt 方法,可以获取指定账户当前待处理的 nonce 值。这个方法返回的 nonce 是在该账户发送的交易队列中尚未被打包进块中的数量。在创建新交易时,通常会使用此 nonce 值加一作为新交易的 nonce。
例如,对于以下代码片段:
nonce, err := client.PendingNonceAt(context.Background(), a1)
这段代码会查询账户 a1 当前的待处理交易数量(即nonce),并将其赋值给 nonce 变量。
gas费
在以太坊中,gasPrice 是指愿意支付每单位 gas 的以太币数量,用于衡量交易的成本。Gas 本质上是执行智能合约或发送交易所需的计算资源。GasPrice 决定了矿工愿意为每单位 gas 支付多少以太币来处理你的交易。
具体来说:
- GasPrice 的作用:GasPrice 影响到你的交易被矿工选择打包进区块的速度。较高的 GasPrice 意味着交易更有可能快速被矿工处理,因为矿工有动机选择收益更高的交易。
- 获取推荐 GasPrice:以太坊客户端提供了一个方法 SuggestGasPrice,用于推荐当前网络上合理的 GasPrice。这个推荐的 GasPrice 通常是基于当前网络上最近几个区块中包含交易的 GasPrice 的中位数或平均值。
例如,对于以下代码片段:
gasPrice, err := client.SuggestGasPrice(context.Background())
这段代码会调用以太坊客户端的 SuggestGasPrice 方法来获取当前推荐的 GasPrice,并将其赋值给 gasPrice 变量。
安装使用solc和abigen
什么是 solc,什么是 abigen?
Solc 是 Solidity 编译器的命令行接口。Solidity 是一种用于编写智能合约的高级语言,solc 则是将 Solidity 代码编译成 Ethereum 虚拟机(EVM)可以执行的字节码的工具。solc 提供了将 Solidity 代码转换为 EVM 字节码的功能,这些字节码可以部署到以太坊区块链上执行。
Abigen 是一个工具,用于从 Solidity 合约 ABI 文件生成 Go 语言绑定代码。ABI(Application Binary Interface)文件定义了合约与外部世界的接口规范,包括合约的方法、参数和返回值类型等信息。Abigen 接收 ABI 文件作为输入,并生成相应的 Go 语言代码,这些代码可以用于与 Solidity 合约进行交互,方便在 Go 语言中调用和操作以太坊智能合约。
solc下载:
Release Version 0.8.26 · ethereum/solidity (github.com)
找到所需要的系统版本下载安装就可以了,这里就以windows举例。
下载后,将 solc-windows.exe 改名为 solc.exe,没有为什么,只是为了输入命令方便。
然后将其放入一个已经配置在系统环境变量中的路径下,这一点很重要,这样无论在哪个路径下都可以使用 solc 命令了。
abigen下载:
如果你已经执行过下面的命令,没执行过,那就是前面的代码没有验证,只要跑过前面的代码,这个包都是已经拉过的。
go get github.com/ethereum/go-ethereum
那么请打开你的 cmd,输入 go env,找到 GOMODCACHE 路径,打开它,找到 github.com\ethereum 下的名称为 go-ethereum 的文件夹,可能会为 go-ethereum@版本号。
一般就在 %UserProfile%\go\pkg\mod\github.com\ethereum 下面
然后打开 cmd,切换进 go-ethereum目录,然后输入以下命令。
go run build/ci.go install ./cmd/abigen
这样我们就可以在 go-ethereum/build/bin 下找到 abigen.exe 了。
然后将其放入一个已经配置在系统环境变量中的路径下,这一点很重要,这样无论在哪个路径下都可以使用 abigen 命令了。
补充:
这里我就随便创建了一个 D:/cmd 的文件夹,然后把 solc.exe 和 abigen.exe 放进去,然后在系统环境变量中配置了一下这个路径。
生成bin和abi文件
首先,随便写一个 todo.sol 文件,放在 contract 文件夹中。
代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;
contract Todo{
address owner;
Task[] tasks;
struct Task{
string content;
bool status;
}
constructor(){
owner = msg.sender;
}
modifier isOwner(){
require(owner == msg.sender);
_;
}
function add(string memory _content) public isOwner {
tasks.push(Task(_content,false));
}
function get(uint _id) public isOwner view returns (Task memory) {
return tasks[_id];
}
function list() public isOwner view returns (Task[] memory){
return tasks;
}
function update(uint _id, string memory _content) public isOwner {
tasks[_id].content = _content;
}
function remove(uint _id) public isOwner {
for(uint i = _id; i<tasks.length -1; i++){
tasks[i] = tasks[i+1];
}
tasks.pop();
}
}
然后打开终端,输入:
solc --bin --abi contract/todo.sol -o build
这样,我们就可以在 build 文件夹下,找到我们生成的 abi 和 bin 文件。
补充:
goland 安装 solidity 插件,在 settings->Plugins,实际上没有啥用,就是图一个好看,真正编写 solidity 还是得看 remix。
生成go文件
我们现在有了 abi,bin 文件,也有了abigen工具,接下来我们就可以生成相应的 go 文件了。
先创建一个 gen 文件夹。
打开命令行,输入:
abigen -bin build/Todo.bin -abi build/Todo.abi -pkg todo -out gen/todo.go
有了这个go文件,我们就可以调用和操作以太坊智能合约了。
使用golang在测试网上部署智能合约
接下来,我们使用之前创建的账户去调用 todo.DeployTodo() 函数就可以了。(需要有足够的SepoliaETH)
代码:
package main
import (
"context"
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
todo "go-ether-learn/gen"
"log"
"math/big"
"os"
)
func main() {
// 读取以太坊钱包文件
b, err := os.ReadFile("./wallet/UTC--****-**-**T**-**-**.**********--****************************************")
if err != nil {
log.Fatal(err)
}
// 解密钱包文件,获取私钥
key, err := keystore.DecryptKey(b, "password")
if err != nil {
log.Fatal(err)
}
// 连接以太坊节点
client, err := ethclient.Dial("https://sepolia.infura.io/v3/********************************")
if err != nil {
log.Fatal(err)
}
defer client.Close()
// 获取钱包地址
add := crypto.PubkeyToAddress(