转发请注明出处:https://blog.csdn.net/ahy231/article/details/114112638
序
网上关于 go
语言开发 DApp
的教程较少,因此我只能通过官方文档来系统学习 go
语言的 DApp
开发。这篇文章是我对 https://geth.ethereum.org/docs/dapp/native-bindings 这篇文档的翻译,如有不符之处,欢迎斧正。
译文
Go 语言合约绑定
[请注意,event
尚未实现,因为它们需要一些仍在审查中的RPC订阅功能。]
以太坊平台最初的路线图和梦想是以各种语言提供一致协议的可靠、高性能的客户端实现,这将为 JavaScript dapp
提供一个RPC接口进行通信,朝着Mist浏览器的方向发展,用户可以通过它与区块链进行交互。
尽管这是一个主流采用的坚实计划,并涵盖了人们提出的大量用例(主要是人们手动与区块链交互的情况),但它避开了服务器端(后端、全自动、devops)用例,在这些用例中,由于 JavaScript
的动态性,JavaScript
通常不是首选语言。
本页介绍了服务器端本机 Dapps
的概念:任何以太坊契约的 Go
语言绑定都是编译时类型安全、高性能的,最重要的是,可以从契约 ABI
和可选的 EVM
字节码完全自动生成。
这个页面是以一种更为初学者友好的教程风格编写的,以使人们更容易从编写 Go-native-dapp
开始。所使用的概念将随着开发人员的需要或遇到而逐步引入。但是,我们假设读者对以太坊非常熟悉,对其稳定性有相当的了解,并且可以编写代码。
代币合同
为了避免陷入无用的学术示例的谬误,我们将以官方令牌契约作为引入 Go
本机绑定的基础。如果你对合同不熟悉,浏览一下链接页面就足够了,细节现在还不相关。简而言之,契约实现了一个可以部署在以太坊之上的定制代币代码。为了确保本教程在链接网站发生变化时不会过时,Token
契约的 Solidity
源代码在 token.sol。
Go 绑定生成器
通过以太坊客户端公开的RPC接口,已经可以与来自 Go
的以太坊区块链上的合约(或事实上的任何其他语言)进行交互。然而,编写样例代码,将良好的 Go
语言结构转换为 RPC
调用并返回,这是非常耗时的,而且也是非常脆弱的:实现中的 bug
只能在运行时检测到,而且几乎不可能调用合约,因为即使是稳定性上的微小变化,移植到 Go
上也会非常痛苦。
为了避免所有这些混乱,go-ethereum
实现引入了一个源代码生成器,它可以将 ethereum-ABI
定义转换为易于使用、类型安全的 go
包。假设您设置了有效的 Go
开发环境,安装了 godep
,并且正确签出了 go-ethereum
存储库,则可以使用以下方法构建生成器:
$ cd $GOPATH/src/github.com/ethereum/go-ethereum
$ godep go install ./cmd/abigen
生成绑定
生成到以太坊合约的 Go
绑定所需的一件重要事情是合约的 ABI
定义 JSON
文件。对于我们的代币合约教程,您可以通过自己(例如通过 @chriseth
的在线 Solidity compiler )编译稳固性代码来获得,也可以下载我们预先编译的 token.abi。
要生成绑定,只需调用:
$ abigen --abi token.abi --pkg main --type Token --out token.go
这里标识是:
- --abi: Mandatory path to the contract ABI to bind to
- --pkg: Mandatory Go package name to place the Go code into
- --type: Optional Go type name to assign to the binding struct
- --out: Optional output path for the generated Go source file (not set = stdout)
这将为代币合约生成一个类型安全的 Go
绑定。生成的代码将类似于 token.go,但请自行生成,因为这将随发电机投入更多工作而改变。
访问以太坊合约
要与部署在区块链上的合约进行交互,您需要知道合约本身的地址,并需要指定访问以太坊的后端。绑定生成程序提供开箱即用的RPC后端,通过该后端,您可以通过 IPC
、HTTP
或 WebSockets
连接到现有的以太坊节点。
我们将使用部署在测试链上的基金会的Unicorn token
合约来演示调用合约方法。其部署在地址 0X21E6FC92F93C8A1BB41E61EE64E64E1F88A54D3576
。
要运行以下代码段,请确保运行一个 Geth
实例并将其连接到部署上述合约的现代测试网络。此外,请将以下 IPC
套接字的路径更新为您自己的本地 Geth
节点报告的一。
package main
import (
"fmt"
"log"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
// Create an IPC based RPC connection to a remote node
conn, err := ethclient.Dial("/home/karalabe/.ethereum/testnet/geth.ipc")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
}
// Instantiate the contract and display its name
token, err := NewToken(common.HexToAddress("0x21e6fc92f93c8a1bb41e2be64b4e1f88a54d3576"), conn)
if err != nil {
log.Fatalf("Failed to instantiate a Token contract: %v", err)
}
name, err := token.Name(nil)
if err != nil {
log.Fatalf("Failed to retrieve token name: %v", err)
}
fmt.Println("Token name:", name)
}
输出:
Token name: Testnet Unicorn
如果查看为读取代币名称而调用的方法 token.Name(nil)
,它需要传递一个参数,即使原始的稳固性合同不需要任何参数。这是一 *bind.CallOpts
类型,可用于微调呼叫。
Pending
:是进入待定合同状态还是当前稳定状态GasLimit
:对呼叫可能消耗的计算资源设置限制
与以太坊合约进行交易
调用改变合同状态(即交易)的方法涉及更多,因为实时交易需要授权并广播到网络。与在我们所连接的节点中存储帐户和密匙的传统方式相反,Go
绑定要求在本地签署事务,而不将其委托给远程节点。这样做是为了促进以太坊社区的整体方向,在以太坊社区中,账户对 DAPP
是保密的,而不是在 DAPP
之间共享(默认情况下)。
因此,为了允许与合约进行交易,代码需要实现一个方法,该方法提供一个输入交易、签署该交易并返回一个授权输出交易。由于大多数用户的密匙采用 Web3
秘密存储格式,因此 bind
包包含一个小型实用方法( bind.NewTransactor(keyjson, passphrase)
),可以从一个密匙文件和相关密码创建一个授权的交易者,而无需用户自己实现密匙签名。
更改先前的代码段以将一个 unicorn
发送到零地址:
package main
import (
"fmt"
"log"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
const key = `paste the contents of your *testnet* key json here`
func main() {
// Create an IPC based RPC connection to a remote node and instantiate a contract binding
conn, err := ethclient.Dial("/home/karalabe/.ethereum/testnet/geth.ipc")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
}
token, err := NewToken(common.HexToAddress("0x21e6fc92f93c8a1bb41e2be64b4e1f88a54d3576"), conn)
if err != nil {
log.Fatalf("Failed to instantiate a Token contract: %v", err)
}
// Create an authorized transactor and spend 1 unicorn
auth, err := bind.NewTransactor(strings.NewReader(key), "my awesome super secret password")
if err != nil {
log.Fatalf("Failed to create authorized transactor: %v", err)
}
tx, err := token.Transfer(auth, common.HexToAddress("0x0000000000000000000000000000000000000000"), big.NewInt(1))
if err != nil {
log.Fatalf("Failed to request token transfer: %v", err)
}
fmt.Printf("Transfer pending: 0x%x\n", tx.Hash())
}
输出:
Transfer pending: 0x4f4aaeb29ed48e88dd653a81f0b05d4df64a86c99d4e83b5bfeb0f0006b0e55b
注意,很有可能您将没有任何可用的测试链 unicorn
,因此上述程序将失败并出现错误。发送至少 2.014 测试网以太到 foundation testnet tipjar
与上一节中仅读取合约状态的方法调用类似,事务方法也需要一个强制的第一参数 *bind.TransactOpts
类型,其授权交易并可能对其进行微调:
From
:用于调用方法的帐户的地址(强制)Signer
:在广播事务前在本地签署事务的方法(强制)Nonce
:用于交易排序的账户随机数(可选)GasLimit
:对呼叫可能消耗的计算资源设置限制(可选)GasPrice
:显式设置Gas
价格以运行交易(可选)Value
:随方法调用转移的任何资金(可选)
如果使用 bind.NewTransactor
函数生成身份验证选项,两个强制的参数会被 bind
包自动填写。如果未设置,nonce和gas相关的域,它们将由绑定自动导出。未设定的 Value
假定为零。
预先配置的合约 Session
如前二节所述,读取和状态修改合约调用都需要一个强制的第一参数,该参数既可以授权也可以微调一些内部参数。然而,大多数情况下,我们希望使用相同的参数并使用相同的账户进行交易,因此始终构建调用、交易选项或将其与绑定一起传递可能变得不易。
为避免这些情况,generator
还创建可预先配置调优和授权参数的专用包装器,允许在不需要额外参数的情况下调用所有已定义的方法。
这些名称与原始合约类型名称类似,仅以 Sessions
作为其后缀:
// Wrap the Token contract instance into a session
session := &TokenSession{
Contract: token,
CallOpts: bind.CallOpts{
Pending: true,
},
TransactOpts: bind.TransactOpts{
From: auth.From,
Signer: auth.Signer,
GasLimit: big.NewInt(3141592),
},
}
// Call the previous methods without the option parameters
session.Name()
session.Transfer("0x0000000000000000000000000000000000000000"), big.NewInt(1))
在以太坊上部署合约
与现有合约互动固然不错,但让我们更进一步,在以太坊区块链上部署一份全新合约!然而,要做到这一点,我们用于生成绑定的合约 ABI
是不够的。我们也需要已编译的字节码以允许部署它。
要获得该位元组码,请返回可用于生成该位元组码的在线编译程序,或下载我们的 token.bin 为了创建部署代码,还需要重新运行包含了字节码的 Go generator
:
$ abigen --abi token.abi --pkg main --type Token --out token.go --bin token.bin
这将产生类似于 token.go 。如果快速浏览此文件,将发现与先前的代码相比,新注入的额外 DeployToken
函数。除了由 Solidity
指定的所有参数外,它还需要常规授权选项来部署与之签订的合同,以及通过以太坊后端来部署合同。
综上所述,结果是:
package main
import (
"fmt"
"log"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/ethclient"
)
const key = `paste the contents of your *testnet* key json here`
func main() {
// Create an IPC based RPC connection to a remote node and an authorized transactor
conn, err := rpc.NewIPCClient("/home/karalabe/.ethereum/testnet/geth.ipc")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
}
auth, err := bind.NewTransactor(strings.NewReader(key), "my awesome super secret password")
if err != nil {
log.Fatalf("Failed to create authorized transactor: %v", err)
}
// Deploy a new awesome contract for the binding demo
address, tx, token, err := DeployToken(auth, conn), new(big.Int), "Contracts in Go!!!", 0, "Go!")
if err != nil {
log.Fatalf("Failed to deploy new token contract: %v", err)
}
fmt.Printf("Contract pending deploy: 0x%x\n", address)
fmt.Printf("Transaction waiting to be mined: 0x%x\n\n", tx.Hash())
// Don't even wait, check its presence in the local pending state
time.Sleep(250 * time.Millisecond) // Allow it to be processed by the local node :P
name, err := token.Name(&bind.CallOpts{Pending: true})
if err != nil {
log.Fatalf("Failed to retrieve pending name: %v", err)
}
fmt.Println("Pending name:", name)
}
代码按预期执行:它要求在以太坊区块链上创建一个全新的代币合约,我们可以等待该合约被挖掘,或如上述代码中所述,在挂起状态下对其开始调用方法:)
Contract pending deploy: 0x46506d900559ad005feb4645dcbb2dbbf65e19cc
Transaction waiting to be mined: 0x6a81231874edd2461879b7280ddde1a857162a744e3658ca7ec276984802183b
Pending name: Contracts in Go!!!
直接绑定 Solidty
如果您一直遵循本教程,直至目前为止,您可能已经意识到每个合约修改都需要重新编译,所产生的 ABI
和 bytecodes
(特别是如果需要多个合约)将单独保存到文件中,然后为其执行绑定。这在第n次迭代后可能会变得相当烦人,因此 abigen
命令支持直接从 Solidity
源文件( --sol
)进行绑定,后者第一次将源代码(通过 --solc
,默认为 solc
)编译到其组成组件中并使用该组件进行绑定。
绑定 官方代币合约 token.sol
然后将需要运行:
$ abigen --sol token.sol --pkg main --out token.go
注:从稳固性( --sol
)进行构建与单独设置绑定组件( --abi
,--bin
和 --type
)是互斥的,因为所有组件均从稳固性代码中提取并直接生成构建结果。
直接从 Solidity
构建一个合约有一个好的副作用,即包含在一个 Solidity
源文件中的所有合约都是构建和绑定的,因此,如果您的文件包含多个合约源,它们中的每一个都可以从 Go
代码中获得。示例 Token solidity
文件对应的结果是 token.go 。
项目集成(即go generate)
abigen
命令是以一种与现有 Go
工具链完美结合的方式发出的:我们可以利用 Go generate
来记下所有细节,而不必记下将以太坊合约绑定到 Go
项目中所需的确切命令。
//go:generate abigen --sol token.sol --pkg main --out token.go
之后,无论何时修改 Solidity
合约,我们都可以简单地在包上调用 go generate
(甚至是通过 go generate ./...
),而无需记忆并运行上述命令,它将为我们正确地生成新的绑定。
区块链模拟器
能够从本机 Go
代码中部署和访问已经部署的 Ethereum
契约是一个非常强大的特性,但是开发本机代码有一个方面甚至测试链都不适合:自动单元测试。使用 go-ethereum
内部构造可以创建测试链并验证它们,但是使用这种低级机制进行高级合约测试是不可行的。
为了解决最后一个使本机 DApps
难以运行(和测试)的问题,我们还实现了一个模拟区块链,可以将其设置为本机契约的后端,就像实时RPC后端一样:backends.NewSimulatedBackend(genesisAccounts)
。
package main
import (
"fmt"
"log"
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/crypto"
)
func main() {
// Generate a new random account and a funded simulator
key, _ := crypto.GenerateKey()
auth := bind.NewKeyedTransactor(key)
sim := backends.NewSimulatedBackend(core.GenesisAccount{Address: auth.From, Balance: big.NewInt(10000000000)})
// Deploy a token contract on the simulated blockchain
_, _, token, err := DeployMyToken(auth, sim, new(big.Int), "Simulated blockchain tokens", 0, "SBT")
if err != nil {
log.Fatalf("Failed to deploy new token contract: %v", err)
}
// Print the current (non existent) and pending name of the contract
name, _ := token.Name(nil)
fmt.Println("Pre-mining name:", name)
name, _ = token.Name(&bind.CallOpts{Pending: true})
fmt.Println("Pre-mining pending name:", name)
// Commit all pending transactions in the simulator and print the names again
sim.Commit()
name, _ = token.Name(nil)
fmt.Println("Post-mining name:", name)
name, _ = token.Name(&bind.CallOpts{Pending: true})
fmt.Println("Post-mining pending name:", name)
}
输出:
Pre-mining name:
Pre-mining pending name: Simulated blockchain tokens
Post-mining name: Simulated blockchain tokens
Post-mining pending name: Simulated blockchain tokens
注意,我们不必等待本地私有链矿工或测试链矿工来收集当前挂起的事务。当我们决定挖掘下一个块时,我们只需 Commit()
模拟器。