昨天講述了怎麼使用Go-ethereum對鏈上進行交易,還不太清楚的可以點這邊觀看,而今天則是要介紹該怎麼使用Go-ethereum與鏈上的合約做操作,我們都知道要與鏈上合約做操作的時候,主要需要兩個東西,一個就是合約的地址,另一個是合約的ABI,因為要跟鏈上合約互動,等於是做一個收款人地址是合約地址的交易,而要呼叫合約哪個函數則是要透過交易帶入的額外資料來決定,至於怎麼編碼跟解碼那些資料則需要靠合約的ABI。
透過昨天的教學,我們已經會做一般鏈上的交易了,所以意味著我們只要能填入NewTransaction裡面的data就可以與合約互動了,而我們發現在"github.com/ethereum/go-ethereum/accounts/abi"
在這裡面有提供將ABI編碼的東西,但是每次都還要按照合約的格式定義出他每個不同函式的型別,相當麻煩,但是沒有關係,Go-ethereum這邊有提供一個相當好用的工具叫做Abigen,當我們在安裝Go-ethereum時,有順便安裝Developer Tools的話,這個工具就包含在裡面了。
Abigen是其中一個由Go-ethereum提供的開發工具之一,他的功用是可以把智能合約的ABI檔轉換成Golang的程式包,他可以把智能合約轉換成一個struct,你只要把他新增出來,就可以很好的對鏈上合約進行操作,假設你能額外提供智能合約的二進位檔,他還可以幫你輕鬆部屬智能合約到鏈上,那該怎麼做呢?接下來我一步一步帶著你們做。
首先,要先有一個智能合約,我為了示範,就隨便寫一個檔名叫做test.sol的智能合約出來,智能合約內容如下:
// SPDX-License-Identifier: MIT
//
pragma solidity ^0.8.0;
contract MyContract {
address private owner;
uint256 number;
uint256 private amount;
event Receipt(address indexed sender, uint256 indexed amount);
constructor(uint256 _number) {
owner = msg.sender;
number = _number;
amount = 0;
}
function getNumber() external view returns (uint256) {
return number;
}
function getAmount() external view returns (uint256) {
return amount;
}
function donate() external payable {
emit Receipt(msg.sender, msg.value);
amount += msg.value;
}
function withdraw(uint256 value) external {
require(msg.sender == owner);
require(amount >= value);
amount -= value;
payable(msg.sender).transfer(value);
}
function changeNumber(uint256 newNumber) external {
number = newNumber;
}
}
有了這個合約之後,需要有他的ABI,如果需要部署他,還需要有他的二進位檔,因此我們需要Solidity的編譯器將我們所需要的東西編譯出來,這邊可以用線上版本的Remix IDE或者是安裝solc來進行編譯,因為我的作業系統是Windows,安裝solc相當麻煩,但是我可以用solc在npm版本的輕量版本的solc,如果需要安裝,則需要有Node.js環境,在command line下以下指令:
$ npm install solc
如果電腦作業系統是Linux或者Mac,可以使用以下指令安裝solc:
$ sudo apt-get install
這兩個不同的點在於,如果是使用前者的話,指令就不可以打solc,而是要打solcjs,兩者在功能上略有不同,我是用前者做示範。
有了Solidity的編譯器後,下指令分別得到他的二進位檔跟ABI,分別下以下兩個指令:
$ solcjs --abi test.sol
$ solcjs --bin test.sol
這時候會得到兩個檔案分別是test_sol_MyContract.abi跟test_sol_MyContract.bin,有了這兩個之後,就可以使用Abigen來產生一個Golang的程式碼檔案,在command line下以下指令:
$ abigen --bin="test.bin" --abi="abi.json" --pkg="contracts" --out="contract.go" --type="MyContract"
這邊帶入參數說明如下:
輸入完指令後,可以發現生成一個檔名為contract.go,因為我將他的package名稱設為contracts而非main,所以要新增一個名叫contracts的資料夾給他。接著只要在主程式中import contracts這個package就可以使用MyContract這個型別了。
當我們仔細觀察contract.go這支檔案,可以發現裡面有個函式叫做DeployMyContract
,詳細內容如下:
func contracts.DeployMyContract(auth *bind.TransactOpts, backend bind.ContractBackend, _number *big.Int) (common.Address, *types.Transaction, *contracts.MyContract, error)
可以發現他需要帶入三個參數分別是auth、backend跟_number,如果說夠仔細的話,可以發現這個_number就是我合約中的建構子所要帶入的參數,所以這個函式就是在部屬合約的,只要在程式中呼叫他,就可以幫你部屬合約,而這邊的backend要帶入的就是昨天講到連線的client,至於auth就是要用要部屬的帳戶的私鑰來生成,這個auth主要是用來做簽章的,而要新增一個TransactOpts型別的變數,靠的是"github.com/ethereum/go-ethereum/accounts/abi/bind"
裡面提供的func bind.NewKeyedTransactorWithChainID(key *ecdsa.PrivateKey, chainID *big.Int) (*bind.TransactOpts, error)
一定要用帶有chainID的這個,原因昨天也說過,現在以太坊交易簽章都是走EIP-155,這是要有chainId的,透過這種方式就可以拿到他需要帶入的TransactOpts型別的變數,都用好後,他會回傳部署上去的合約地址,這個東西要記錄下來,還有這次的交易,以及一個MyContract的東西,和error,如果部署完後想要直接跟合約互動,可以直接使用那個回傳回來的MyContract型別的變數,但這邊我只講部屬所以這東西還沒有用處。
程式碼如下:
package main
import (
"context"
"fmt"
"math/big"
"專案名稱/contracts"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
client, err := ethclient.Dial("連線網址,如果是要連你的私有鏈的話,就是 http://127.0.0.1:[你設定的PORT]")
if err != nil {
panic(err)
}
chainID, err := client.NetworkID(context.TODO())
if err != nil {
panic(err)
}
deployer_prk, err := crypto.HexToECDSA("要部屬合約的帳戶私鑰16進位數字碼(不含0x字首)")
if err != nil {
panic(err)
}
auth, err := bind.NewKeyedTransactorWithChainID(deployer_prk, chainID)
if err != nil {
panic(err)
}
addr, _, _, err := contracts.DeployMyContract(auth, client, big.NewInt(12)) // 這邊_number我是隨便帶等等會用
if err != nil {
panic(err)
}
// 輸出合約地址
fmt.Println(addr.Hex())
}
剛剛說到如果合約部署的時候,會給一個MyContract型別的變數,基本上跟合約互動都是透過那個變數,如果不是部屬合約的程式,那麼就要透過contract.go裡面的函式func contracts.NewMyContract(address common.Address, backend bind.ContractBackend) (*contracts.MyContract, error)
來新增出一個合約的變數,要帶入的參數分別是合約地址與連線的client,而這邊變數就可以隨便你用了,你可以發現他已經定義好原本你合約裡面寫好的所有公開的function,而這邊要提醒的是,如果是呼叫view function的話,bind.CallOpts這裡就可以不用帶,這個的功能跟剛剛部署合約的一模一樣,主要是用來簽章用的,以下是我寫出一個小程式跟我示範的合約做互動:
package main
import (
"context"
"fmt"
"math/big"
"專案名稱/contracts"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
client, err := ethclient.Dial("連線網址,如果是要連你的私有鏈的話,就是 http://127.0.0.1:[你設定的PORT]")
if err != nil {
panic(err)
}
chainID, err := client.NetworkID(context.TODO())
if err != nil {
panic(err)
}
deployer_prk, err := crypto.HexToECDSA("部屬合約的帳戶私鑰16進位數字碼(不含0x字首)")
if err != nil {
panic(err)
}
auth, err := bind.NewKeyedTransactorWithChainID(deployer_prk, chainID)
if err != nil {
panic(err)
}
contract, err := contracts.NewMyContract(common.HexToAddress("合約的地址(含0x字首)"), client)
if err != nil {
panic(err)
}
number, err := contract.GetNumber(nil)
if err != nil {
panic(err)
}
fmt.Println(number)
_, err = contract.ChangeNumber(auth, big.NewInt(567))
if err != nil {
panic(err)
}
}
以上是使用得到合約裡面的number,以及修改合約的number做示範,如果是遇到payable的function,則需要在auth裡面的Value更改成要付出的金額,當然單位是wei,而這個MyContract型別的變數可以透過Filter來聽取鏈上該合約發出的Event,這也是一個常用的功能,以上便是使用Go-ethereum搭配Abigen做出可以與合約互動的程式的介紹。