用Go语言开发以太坊合约

转发请注明出处: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后端,通过该后端,您可以通过 IPCHTTPWebSockets 连接到现有的以太坊节点。

我们将使用部署在测试链上的基金会的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

如果您一直遵循本教程,直至目前为止,您可能已经意识到每个合约修改都需要重新编译,所产生的 ABIbytecodes(特别是如果需要多个合约)将单独保存到文件中,然后为其执行绑定。这在第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() 模拟器。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值