2024. 11. 13. 22:45ㆍBlockchain/Smart contract
보통 Solidity 스마트 컨트랙트를 다룰때는 JS로 되어있는 web3.js나 ethers.js를 사용하여 interaction 하고, 배포시에는 hardhat, truffle을 사용한다. 이들은 모두 javascript 기반이다. 다른 언어를 사용하여 프로그램을 개발한다면 언어 호환에 대한 고민이 있을 수 있다. Go-ethereum에서는 Golang을 기반으로 하는 서버 등의 프로그램에서 다른 언어로 이루어진 라이브러리를 사용하지 않고도 쉽고 편리하게 Solidity 컨트랙트를 다룰 수 있게 하는 기능을 제공하기 때문에 이번 페이지에서는 해당 기능을 소개하고 사용하는 방법에 대해 작성하여 한다.
Bind package
Geth는 "bind" package가 존재한다. 해당 패키지의 directory는 "github.com/ethereum/go-ethereum/accounts/abi/bind"이다. Bind 패키지를 사용하면 컨트랙트의 ABI와 bytecode로 Go wrapper를 생성한다. 이는 일정한 template을 가지고 있어서 생성 및 동작 규칙만 알고 있다면 모든 Solidity 컨트랙트에 대해 다룰 수 있다.
컨트랙트 데이터 추출
Binding을 통한 Go wrapper를 생성하기 위해서는 가장 먼저 컨트랙트의 데이터를 뽑아내야 한다. 예시로 사용할 컨트랙트는 Remix에 기본적으로 제공하는 2_Owner.sol
이다. 해당 컨트랙트는 remix.ethereum.org에 접속하면 바로 확인가능하다.
컨트랙트는 아래와 같다.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
/**
* @title Owner
* @dev Set & change owner
*/
contract Owner {
address private owner;
// event for EVM logging
event OwnerSet(address indexed oldOwner, address indexed newOwner);
// modifier to check if caller is owner
modifier isOwner() {
// If the first argument of 'require' evaluates to 'false', execution terminates and all
// changes to the state and to Ether balances are reverted.
// This used to consume all gas in old EVM versions, but not anymore.
// It is often a good idea to use 'require' to check if functions are called correctly.
// As a second argument, you can also provide an explanation about what went wrong.
require(msg.sender == owner, "Caller is not owner");
_;
}
/**
* @dev Set contract deployer as owner
*/
constructor() {
console.log("Owner contract deployed by:", msg.sender);
owner = msg.sender; // 'msg.sender' is sender of current call, contract deployer for a constructor
emit OwnerSet(address(0), owner);
}
/**
* @dev Change owner
* @param newOwner address of new owner
*/
function changeOwner(address newOwner) public isOwner {
emit OwnerSet(owner, newOwner);
owner = newOwner;
}
/**
* @dev Return owner address
* @return address of owner
*/
function getOwner() external view returns (address) {
return owner;
}
}
2_Owner.sol
은 위와 같이 아주 간단한 컨트랙트 코드이다. 이 컨트랙트의 ABI와 bytecode를 remix를 이용하여 추출한다. Remix를 이용하여 ABI와 bytecode를 가져오는 방법은 이전 게시글에서 확인 가능하다.
추출한 데이터는 아래와 같다.
- ABI
// abi.json
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "oldOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnerSet",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "changeOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
}
]
- Bytecode
// bytecode.json
{
"contractBytecode":"608060405...00081a0033"
}
Go wrapper 생성하기
컨트랙트 데이터를 이용하여 Go wrapper를 생성하는 코드는 아래와 같다.
packge client
import (
"encoding/json"
"io"
"os"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
)
type ContractBytecodeStruct struct {
ContractBytecode string `json:"contractByteCode"`
}
func GolangBindingOwner() error {
abiDir := "./ABI/PATH/abi.json"
bytecodeDir := "./BYTECODE/PATH/bytecode.json"
var bytecodeJsonStruct types.ContractBytecodeStruct
jsonData, err := os.Open(bytecodeDir)
if err != nil {
return err
}
byteValue, err := io.ReadAll(jsonData)
if err != nil {
return err
}
err = json.Unmarshal(byteValue, &bytecodeJsonStruct)
if err != nil {
return err
}
f, err := os.ReadFile(abiDir)
if err != nil {
return err
}
abi := string(f)
// Bind ABI and bytecode
bind, err := bind.Bind(
[]string{"Owner"},
[]string{abi},
[]string{bytecodeJsonStruct.ContractBytecode},
nil,
"client",
bind.LangGo,
nil,
nil,
)
if err != nil {
return err
}
f, err := os.Create("./client/form_owner.go")
if err != nil {
return err
}
defer f.Close()
_, err = io.WriteString(f, bind)
if err != nil {
return err
}
return nil
}
추출한 컨트랙트 데이터를 파싱하고 해당 데이터들을 bind.Bind()
함수에 input value로 입력한다. 기본적인 wrapper를 생성할 때 필수적으로 필요하지 않는 값들은 nil
로 입력되며, 위 코드에 입력된 input들은 mandatory로 생각하면 된다.
첫번째 입력 []string{"Owner"}
는 wrapper 내에 생성되는 컨트랙트 핸들러들을 구분짓기 위한 type이며, 두번째/세번째 입력은 해당 컨트랙트의 ABI와 bytecode를 []string
값으로 받는다. List index 별로 구분지어진 핸들러에 속한다. 다섯번째 입력 "client"
는 생성되는 Go wrapper의 package 명을 지정한다. 여섯번째 입력 bind.LangGo
는 Go언어 wrapper를 사용한다는 뜻이다.
Go wrapper는 파일형태로 생성되기 때문에 파일 디렉토리 및 파일명을 지정해주어야 한다. 위의 예시에서는 "./client/form_owner.go"
로 지정했는데, 패키지 명은 보통 폴더명과 동일하게 지정해주기 때문에 ./client
폴더 내에 파일이 생성된다. Golang에서는 파일명은 사실 크게 중요하지는 않기 때문에 적절한 명칭으로 파일명을 지정한다.
최종적으로 위 function을 실행하면 내가 지정한 디렉토리에 파일이 생성된다.
Go wrapper 파일 생성
Bind 패키지를 통해 생성한 Go wrapper는 아래와 같다.
// Code generated - DO NOT EDIT.
// This file is a generated binding and any manual changes will be lost.
package client
import (
"errors"
"math/big"
"strings"
ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
)
// Reference imports to suppress errors if they are not otherwise used.
var (
_ = errors.New
_ = big.NewInt
_ = strings.NewReader
_ = ethereum.NotFound
_ = bind.Bind
_ = common.Big1
_ = types.BloomLookup
_ = event.NewSubscription
)
// OwnerMetaData contains all meta data concerning the Owner contract.
var OwnerMetaData = &bind.MetaData{
ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"oldOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnerSet\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"changeOwner\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getOwner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]",
Bin: "608060405...00081a0033",
}
// OwnerABI is the input ABI used to generate the binding from.
// Deprecated: Use OwnerMetaData.ABI instead.
var OwnerABI = OwnerMetaData.ABI
// OwnerBin is the compiled bytecode used for deploying new contracts.
// Deprecated: Use OwnerMetaData.Bin instead.
var OwnerBin = OwnerMetaData.Bin
// DeployOwner deploys a new Ethereum contract, binding an instance of Owner to it.
func DeployOwner(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *Owner, error) {
parsed, err := OwnerMetaData.GetAbi()
if err != nil {
return common.Address{}, nil, nil, err
}
if parsed == nil {
return common.Address{}, nil, nil, errors.New("GetABI returned nil")
}
address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(OwnerBin), backend)
if err != nil {
return common.Address{}, nil, nil, err
}
return address, tx, &Owner{OwnerCaller: OwnerCaller{contract: contract}, OwnerTransactor: OwnerTransactor{contract: contract}, OwnerFilterer: OwnerFilterer{contract: contract}}, nil
}
// Owner is an auto generated Go binding around an Ethereum contract.
type Owner struct {
OwnerCaller // Read-only binding to the contract
OwnerTransactor // Write-only binding to the contract
OwnerFilterer // Log filterer for contract events
}
// OwnerCaller is an auto generated read-only Go binding around an Ethereum contract.
type OwnerCaller struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// OwnerTransactor is an auto generated write-only Go binding around an Ethereum contract.
type OwnerTransactor struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// OwnerFilterer is an auto generated log filtering Go binding around an Ethereum contract events.
type OwnerFilterer struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// OwnerSession is an auto generated Go binding around an Ethereum contract,
// with pre-set call and transact options.
type OwnerSession struct {
Contract *Owner // Generic contract binding to set the session for
CallOpts bind.CallOpts // Call options to use throughout this session
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
}
// OwnerCallerSession is an auto generated read-only Go binding around an Ethereum contract,
// with pre-set call options.
type OwnerCallerSession struct {
Contract *OwnerCaller // Generic contract caller binding to set the session for
CallOpts bind.CallOpts // Call options to use throughout this session
}
// OwnerTransactorSession is an auto generated write-only Go binding around an Ethereum contract,
// with pre-set transact options.
type OwnerTransactorSession struct {
Contract *OwnerTransactor // Generic contract transactor binding to set the session for
TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session
}
// OwnerRaw is an auto generated low-level Go binding around an Ethereum contract.
type OwnerRaw struct {
Contract *Owner // Generic contract binding to access the raw methods on
}
// OwnerCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract.
type OwnerCallerRaw struct {
Contract *OwnerCaller // Generic read-only contract binding to access the raw methods on
}
// OwnerTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract.
type OwnerTransactorRaw struct {
Contract *OwnerTransactor // Generic write-only contract binding to access the raw methods on
}
// NewOwner creates a new instance of Owner, bound to a specific deployed contract.
func NewOwner(address common.Address, backend bind.ContractBackend) (*Owner, error) {
contract, err := bindOwner(address, backend, backend, backend)
if err != nil {
return nil, err
}
return &Owner{OwnerCaller: OwnerCaller{contract: contract}, OwnerTransactor: OwnerTransactor{contract: contract}, OwnerFilterer: OwnerFilterer{contract: contract}}, nil
}
// NewOwnerCaller creates a new read-only instance of Owner, bound to a specific deployed contract.
func NewOwnerCaller(address common.Address, caller bind.ContractCaller) (*OwnerCaller, error) {
contract, err := bindOwner(address, caller, nil, nil)
if err != nil {
return nil, err
}
return &OwnerCaller{contract: contract}, nil
}
// NewOwnerTransactor creates a new write-only instance of Owner, bound to a specific deployed contract.
func NewOwnerTransactor(address common.Address, transactor bind.ContractTransactor) (*OwnerTransactor, error) {
contract, err := bindOwner(address, nil, transactor, nil)
if err != nil {
return nil, err
}
return &OwnerTransactor{contract: contract}, nil
}
// NewOwnerFilterer creates a new log filterer instance of Owner, bound to a specific deployed contract.
func NewOwnerFilterer(address common.Address, filterer bind.ContractFilterer) (*OwnerFilterer, error) {
contract, err := bindOwner(address, nil, nil, filterer)
if err != nil {
return nil, err
}
return &OwnerFilterer{contract: contract}, nil
}
// bindOwner binds a generic wrapper to an already deployed contract.
func bindOwner(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) {
parsed, err := abi.JSON(strings.NewReader(OwnerABI))
if err != nil {
return nil, err
}
return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil
}
// Call invokes the (constant) contract method with params as input values and
// sets the output to result. The result type might be a single field for simple
// returns, a slice of interfaces for anonymous returns and a struct for named
// returns.
func (_Owner *OwnerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error {
return _Owner.Contract.OwnerCaller.contract.Call(opts, result, method, params...)
}
// Transfer initiates a plain transaction to move funds to the contract, calling
// its default method if one is available.
func (_Owner *OwnerRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
return _Owner.Contract.OwnerTransactor.contract.Transfer(opts)
}
// Transact invokes the (paid) contract method with params as input values.
func (_Owner *OwnerRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
return _Owner.Contract.OwnerTransactor.contract.Transact(opts, method, params...)
}
// Call invokes the (constant) contract method with params as input values and
// sets the output to result. The result type might be a single field for simple
// returns, a slice of interfaces for anonymous returns and a struct for named
// returns.
func (_Owner *OwnerCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error {
return _Owner.Contract.contract.Call(opts, result, method, params...)
}
// Transfer initiates a plain transaction to move funds to the contract, calling
// its default method if one is available.
func (_Owner *OwnerTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) {
return _Owner.Contract.contract.Transfer(opts)
}
// Transact invokes the (paid) contract method with params as input values.
func (_Owner *OwnerTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) {
return _Owner.Contract.contract.Transact(opts, method, params...)
}
// GetOwner is a free data retrieval call binding the contract method 0x893d20e8.
//
// Solidity: function getOwner() view returns(address)
func (_Owner *OwnerCaller) GetOwner(opts *bind.CallOpts) (common.Address, error) {
var out []interface{}
err := _Owner.contract.Call(opts, &out, "getOwner")
if err != nil {
return *new(common.Address), err
}
out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address)
return out0, err
}
// GetOwner is a free data retrieval call binding the contract method 0x893d20e8.
//
// Solidity: function getOwner() view returns(address)
func (_Owner *OwnerSession) GetOwner() (common.Address, error) {
return _Owner.Contract.GetOwner(&_Owner.CallOpts)
}
// GetOwner is a free data retrieval call binding the contract method 0x893d20e8.
//
// Solidity: function getOwner() view returns(address)
func (_Owner *OwnerCallerSession) GetOwner() (common.Address, error) {
return _Owner.Contract.GetOwner(&_Owner.CallOpts)
}
// ChangeOwner is a paid mutator transaction binding the contract method 0xa6f9dae1.
//
// Solidity: function changeOwner(address newOwner) returns()
func (_Owner *OwnerTransactor) ChangeOwner(opts *bind.TransactOpts, newOwner common.Address) (*types.Transaction, error) {
return _Owner.contract.Transact(opts, "changeOwner", newOwner)
}
// ChangeOwner is a paid mutator transaction binding the contract method 0xa6f9dae1.
//
// Solidity: function changeOwner(address newOwner) returns()
func (_Owner *OwnerSession) ChangeOwner(newOwner common.Address) (*types.Transaction, error) {
return _Owner.Contract.ChangeOwner(&_Owner.TransactOpts, newOwner)
}
// ChangeOwner is a paid mutator transaction binding the contract method 0xa6f9dae1.
//
// Solidity: function changeOwner(address newOwner) returns()
func (_Owner *OwnerTransactorSession) ChangeOwner(newOwner common.Address) (*types.Transaction, error) {
return _Owner.Contract.ChangeOwner(&_Owner.TransactOpts, newOwner)
}
// OwnerOwnerSetIterator is returned from FilterOwnerSet and is used to iterate over the raw logs and unpacked data for OwnerSet events raised by the Owner contract.
type OwnerOwnerSetIterator struct {
Event *OwnerOwnerSet // Event containing the contract specifics and raw log
contract *bind.BoundContract // Generic contract to use for unpacking event data
event string // Event name to use for unpacking event data
logs chan types.Log // Log channel receiving the found contract events
sub ethereum.Subscription // Subscription for errors, completion and termination
done bool // Whether the subscription completed delivering logs
fail error // Occurred error to stop iteration
}
// Next advances the iterator to the subsequent event, returning whether there
// are any more events found. In case of a retrieval or parsing error, false is
// returned and Error() can be queried for the exact failure.
func (it *OwnerOwnerSetIterator) Next() bool {
// If the iterator failed, stop iterating
if it.fail != nil {
return false
}
// If the iterator completed, deliver directly whatever's available
if it.done {
select {
case log := <-it.logs:
it.Event = new(OwnerOwnerSet)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
default:
return false
}
}
// Iterator still in progress, wait for either a data or an error event
select {
case log := <-it.logs:
it.Event = new(OwnerOwnerSet)
if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil {
it.fail = err
return false
}
it.Event.Raw = log
return true
case err := <-it.sub.Err():
it.done = true
it.fail = err
return it.Next()
}
}
// Error returns any retrieval or parsing error occurred during filtering.
func (it *OwnerOwnerSetIterator) Error() error {
return it.fail
}
// Close terminates the iteration process, releasing any pending underlying
// resources.
func (it *OwnerOwnerSetIterator) Close() error {
it.sub.Unsubscribe()
return nil
}
// OwnerOwnerSet represents a OwnerSet event raised by the Owner contract.
type OwnerOwnerSet struct {
OldOwner common.Address
NewOwner common.Address
Raw types.Log // Blockchain specific contextual infos
}
// FilterOwnerSet is a free log retrieval operation binding the contract event 0x342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a735.
//
// Solidity: event OwnerSet(address indexed oldOwner, address indexed newOwner)
func (_Owner *OwnerFilterer) FilterOwnerSet(opts *bind.FilterOpts, oldOwner []common.Address, newOwner []common.Address) (*OwnerOwnerSetIterator, error) {
var oldOwnerRule []interface{}
for _, oldOwnerItem := range oldOwner {
oldOwnerRule = append(oldOwnerRule, oldOwnerItem)
}
var newOwnerRule []interface{}
for _, newOwnerItem := range newOwner {
newOwnerRule = append(newOwnerRule, newOwnerItem)
}
logs, sub, err := _Owner.contract.FilterLogs(opts, "OwnerSet", oldOwnerRule, newOwnerRule)
if err != nil {
return nil, err
}
return &OwnerOwnerSetIterator{contract: _Owner.contract, event: "OwnerSet", logs: logs, sub: sub}, nil
}
// WatchOwnerSet is a free log subscription operation binding the contract event 0x342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a735.
//
// Solidity: event OwnerSet(address indexed oldOwner, address indexed newOwner)
func (_Owner *OwnerFilterer) WatchOwnerSet(opts *bind.WatchOpts, sink chan<- *OwnerOwnerSet, oldOwner []common.Address, newOwner []common.Address) (event.Subscription, error) {
var oldOwnerRule []interface{}
for _, oldOwnerItem := range oldOwner {
oldOwnerRule = append(oldOwnerRule, oldOwnerItem)
}
var newOwnerRule []interface{}
for _, newOwnerItem := range newOwner {
newOwnerRule = append(newOwnerRule, newOwnerItem)
}
logs, sub, err := _Owner.contract.WatchLogs(opts, "OwnerSet", oldOwnerRule, newOwnerRule)
if err != nil {
return nil, err
}
return event.NewSubscription(func(quit <-chan struct{}) error {
defer sub.Unsubscribe()
for {
select {
case log := <-logs:
// New log arrived, parse the event and forward to the user
event := new(OwnerOwnerSet)
if err := _Owner.contract.UnpackLog(event, "OwnerSet", log); err != nil {
return err
}
event.Raw = log
select {
case sink <- event:
case err := <-sub.Err():
return err
case <-quit:
return nil
}
case err := <-sub.Err():
return err
case <-quit:
return nil
}
}
}), nil
}
// ParseOwnerSet is a log parse operation binding the contract event 0x342827c97908e5e2f71151c08502a66d44b6f758e3ac2f1de95f02eb95f0a735.
//
// Solidity: event OwnerSet(address indexed oldOwner, address indexed newOwner)
func (_Owner *OwnerFilterer) ParseOwnerSet(log types.Log) (*OwnerOwnerSet, error) {
event := new(OwnerOwnerSet)
if err := _Owner.contract.UnpackLog(event, "OwnerSet", log); err != nil {
return nil, err
}
event.Raw = log
return event, nil
}
이러한 내용을 담은 파일이 생성되고 개발자는 이 함수들을 호출하여 Solidity 컨트랙트와 상호작용 할 수 있다. Bind()
함수의 입력으로 사용된 컨트랙트 type인 Owner
가 DeployOwner()
처럼 모든 insatance의 명칭에 포함되어 컨트랙트 별로 handle을 위한 함수를 구분짓는다.
각각의 파라미터들을 살펴보자.
ABI & Bytecode
Wrapper에는 가장 먼저 var OwnerMetaData = &bind.MetaData{}
구조체에 컨트랙트 데이터가 포함되어 있음을 확인할 수 있다. 이 구조체는 ABI와 bytecode의 string 데이터를 OwnerMetaData.ABI
나 OwnerMetaData.Bin
을 사용하여 직접적으로 불러올 수 있을 뿐만 아니라 abi.ABI
structure로 ABI의 데이터(Construtor, Methods, Events, Errors 등)를 자체적으로 파싱하여 사용할 수 있다. bind.Metadata
의 구조는 아래와 같다.
// "github.com/ethereum/go-ethereum/accounts/abi/bind/base.go"
// MetaData collects all metadata for a bound contract.
type MetaData struct {
mu sync.Mutex
Sigs map[string]string
Bin string
ABI string
ab *abi.ABI
}
// "github.com/ethereum/go-ethereum/accounts/abi/abi.go"
// The ABI holds information about a contract's context and available
// invokable methods. It will allow you to type check function calls and
// packs data accordingly.
type ABI struct {
Constructor Method
Methods map[string]Method
Events map[string]Event
Errors map[string]Error
// Additional "special" functions introduced in solidity v0.6.0.
// It's separated from the original default fallback. Each contract
// can only define one fallback and receive function.
Fallback Method // Note it's also used to represent legacy fallback before v0.6.0
Receive Method
}
The ABI holds information about a contract's context and available invokable methods. It will allow you to type check function calls and packs data accordingly.
func (abi *ABI) EventByID(topic common.Hash) (*Event, error)
func (abi *ABI) HasFallback() bool
func (abi *ABI) HasReceive() bool
func (abi *ABI) MethodById(sigdata []byte) (*Method, error)
func (abi ABI) Pack(name string, args ...interface{}) ([]byte, error)
func (abi *ABI) UnmarshalJSON(data []byte) error
func (abi ABI) Unpack(name string, data []byte) ([]interface{}, error)
func (abi ABI) UnpackIntoInterface(v interface{}, name string, data []byte) error
func (abi ABI) UnpackIntoMap(v map[string]interface{}, name string, data []byte) (err error)
func (abi ABI) getArguments(name string, data []byte) (Arguments, error)
MetaData
의 ab *abi.ABI
를 이용하여 다양한 기능을 수행할 수 있다.
- Hex value로 리턴되는 컨트랙트 데이터의 packaging/unpackaging
- Constructor, Methods 등의 컨트랙트 파싱
- 데이터의 json marshaling/unmarshaling
Deploy
Go wrapper는 hardhat처럼 컨트랙트를 EVM 기반 블록체인에 배포하는 기능을 내포하고 있다. Go wrapper 파일에서 확인 가능한 DeployOwner()
를 사용하면 된다. Deploy시에는 트랜잭션을 전달할 sender의 정보를 포함한 bind.TransactOpts
가 필요하고, 배포시 사용될 bind.ContractBackend
가 필요하다.
DeployOwner()
의 사용방법에 대한 예시 코드는 아래와 같다.
import (
"context"
"fmt"
"net/http"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
)
func DeployContract() error {
rpcUrl := TARGET_EVM_RPC_URL
ctx := context.Background()
privKey := SENDER_PRIVATE_KEY
chainId := TARGET_EVM_CHAIN_ID
evmclient, err := NewEvmClient(rpcUrl, ctx)
if err != nil {
return err
}
privKeyCrypto, err := crypto.HexToECDSA(privKey)
if err != nil {
return err
}
contractAuth, err := bind.NewKeyedTransactorWithChainID(privKeyCrypto, chainId)
if err != nil {
return err
}
contractAuth.Nonce = nonceBigInt
contractAuth.Value = big.NewInt(0)
contractAuth.GasLimit = gasLimitUint64
contractAuth.GasPrice = gasPriceBigInt
contractAddr, tx, Owner, err := DeployOwner(contractAuth, evmclient)
if err != nil {
return err
}
...
return nil
}
func newEvmClient(evmRpcUrl string, ctx context.Context) (*ethclient.Client, error) {
httpDefaultTransport := http.DefaultTransport
defaultTransport, ok := httpDefaultTransport.(*http.Transport)
if !ok {
return fmt.Errorf("default transport pointer err")
}
defaultTransport.DisableKeepAlives = true
httpClient := &http.Client{Transport: defaultTransport}
rpcClient, err := erpc.DialHTTPWithClient(evmRpcUrl, httpClient)
if err != nil {
return err
}
return ethclient.NewClient(rpcClient), nil
}
위에 작성된 예시 코드를 보면 DeployOwner()
를 어떻게 사용하는지 대략 짐작이 될것이다. 배포 컨트랙트를 전송할 sender의 private key를 이용하여 contractAuth
를 만들고, 트랜잭션 세부 옵션들을 지정한다. 그리고 타겟 EVM 체인에 연결하여 통신을 담당할 client인 evmclient
를 생성하여 각각을 DeployOwner()
의 입력값으로 넣는다.
이 함수를 수행하면 내부 기능을 통해 컨트랙트 배포를 진행하고 컨트랙트 주소, 배포 컨트랙트의 정보 등을 출력값으로 확인할 수 있다. 출력값 중 하나인 Owner
는 다음 섹션에 설명될 것이다.
Transactor, Caller, and Filterer
출력값중 하나인 Owner
는 아래와 같은 구조를 가진다.
// Owner is an auto generated Go binding around an Ethereum contract.
type Owner struct {
OwnerCaller // Read-only binding to the contract
OwnerTransactor // Write-only binding to the contract
OwnerFilterer // Log filterer for contract events
}
// OwnerCaller is an auto generated read-only Go binding around an Ethereum contract.
type OwnerCaller struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// OwnerTransactor is an auto generated write-only Go binding around an Ethereum contract.
type OwnerTransactor struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
// OwnerFilterer is an auto generated log filtering Go binding around an Ethereum contract events.
type OwnerFilterer struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
Owner
에 포함된 파라미터들은 각각의 이름에서 유추할 수 있듯이, transactor는 트랜잭션을 전송하고, caller는 컨트랙트 데이터를 조회하며, filterer는 contract의 event와 관련된 기능을 수행한다.
Transactor
먼저 transactor를 확인해보자. Transactor를 초기화 하는 방법은 아래와 같다.
import (
"github.com/ethereum/go-ethereum/common"
)
func setTransactor(contractAddr common.Address) error {
evmclient, err := NewEvmClient(rpcUrl, ctx)
if err != nil {
return err
}
ownerTransactor, err := NewOwnerTransactor(contractAddr, evmclient)
if err != nil {
return err
}
...
}
생성된 ownerTransactor
는 컨트랙트에서 정의된 모든 transaction 전송 method를 실행할 수 있다. 2_Owner.sol
컨트랙트에서는 function changeOwner(addres newOwner) public isOwner
라는 트랜잭션 수행 method가 존재한다. Transactor를 통해 트랜잭션 전송하는 방법은 아래와 같다.
tx, err := ownerTransactor.ChangeOwner(opts, newOwner)
해당 함수를 수행하면 changeOwner
를 위한 input 데이터 newOwner
를 지정하여 컨트랙트를 실행한다. 참고적으로 opts
는 deploy 섹션에서 언급된 contractAuth
와 동일하게 생성하면 되고, gas limit/price나 nonce들을 따로 지정하지 않았다면 client에서 자동으로 확인하여 적절한 값을 입력하게 된다.
Caller
다음으로 caller를 확인해보자. Caller를 초기화 하는 방법은 아래와 같다.
import (
"github.com/ethereum/go-ethereum/common"
)
func setCaller(contractAddr common.Address) error {
evmclient, err := NewEvmClient(rpcUrl, ctx)
if err != nil {
return err
}
ownerCaller, err := NewOwnerCaller(contractAddr, evmclient)
if err != nil {
return err
}
...
}
Transactor의 초기화와 동일하다. 마찬가지로 컨트랙트가 보유한 조회 함수인 getOwner()
를 아래와 같이 실행할 수 있다.
owner, err := ownerCaller.GetOwner()
Filterer
Filterer는 컨트랙트에서 지정된 event가 발생할 때 event를 디텍팅하고 조회하는 역할을 수행한다. 2_Owner.sol
은 changeOwner()
함수가 실행될 때 emit OwnerSet(owner, newOwner)
를 통해 event가 발생된다. Filterer의 초기화는 아래와 같다.
import (
"github.com/ethereum/go-ethereum/common"
)
func setFilterer(contractAddr common.Address) error {
evmclient, err := NewEvmClient(rpcUrl, ctx)
if err != nil {
return err
}
ownerCaller, err := NewOwnerFilterer(contractAddr, evmclient)
if err != nil {
return err
}
...
}
Transactor, caller, filterer 모두 초기화 하는 방법은 같다. Filterer는 아래와 같은 3가지 함수를 갖는다.
type OwnerFilterer struct {
contract *bind.BoundContract // Generic contract wrapper for the low level calls
}
OwnerFilterer is an auto generated log filtering Go binding around an Ethereum contract events.
func (_Owner *OwnerFilterer) FilterOwnerSet(opts *bind.FilterOpts, oldOwner []common.Address, newOwner []common.Address) (*OwnerOwnerSetIterator, error)
func (_Owner *OwnerFilterer) ParseOwnerSet(log types.Log) (*OwnerOwnerSet, error)
func (_Owner *OwnerFilterer) WatchOwnerSet(opts *bind.WatchOpts, sink chan<- *OwnerOwnerSet, oldOwner []common.Address, newOwner []common.Address) (event.Subscription, error)
Filterer 함수의 각각의 기능은 아래와 같다.
FilterOwnerSet
: EVM RPC URL을 사용하여, EVM client가 요청할 시OwnerSet
event에 대한 데이터들을 조회한다.WatchOwnerSet
: EVM Websocket URL을 사용하여, 지속적인 websocket 연결로OwnerSet
event가 발생할 때 마다 detecting하여 조회한다.ParseOwnerSet
: 트랜잭션 로그 중에서OwnerSet
이벤트에 대한 내용을 파싱한다.
위와 같은 기능 때문에, 실시간으로 event의 생성을 탐지하고, 프로세스를 처리하기 위해서는 타겟 EVM 체인의 websocket URL을 EVM client에 넣어서 지속적인 연결을 유지하고, WatchOwnerSet()
을 사용해야 한다. EVM RPC URL만 EVM client에 존재할 시에는 Watch
가 동작하지 않는다.
나머지 2개는 함수명의 input과 output을 보면 어떻게 동작할지 쉽게 유추할 수 있으니 WatchOwnerSet()
만 동작 시키는 방법을 확인한다.
func watch() error {
newChan := make(chan *OwnerOwnerSet)
sub, err := ownerFilterer.WatchOwnerSet(nil, newChan)
if err != nil {
return err
}
for {
select {
case <-quitChannel:
sub.Unsubscribe()
case watchEvent := <-newChan:
...
}
}
}
WatchOwnerSet()
의 input으로 받는 channel을 생성하고 함수에 입력한다. OwnerSet
event가 발생할 때 마다 Go wrapper의 내부에서 channel로 데이터를 전달하기 때문에 개발자가 event를 실시간으로 수신하고 데이터를 처리할 수 있다.
Conclusion
Geth에서는 Go wrapper를 통하여 Golang 개발자에게 Solidity 컨트랙트의 이용에 대한 편의성을 제공해준다. 필자도 Web3js나 ethersjs를 자주 사용하기도 하고 Javascript를 사용하는 개발자가 훨씬 많기 때문에 JS 라이브러리를 이용한 dApp들이 많지만 Golang 개발자를 위한 스마트 컨트랙트 툴이 존재하기 때문에 Golang 개발자들은 Geth의 Go wrapper를 통해 다른 언어를 쓰지 않고도 쉽고 간편하게 컨트랙트를 다룰 수 있다.
'Blockchain > Smart contract' 카테고리의 다른 글
CosmWasm 컨트랙트 개발 시 전략과 신경 쓰면 좋은 점 (1) | 2023.11.01 |
---|---|
Remix로 contract 만들고 배포하기 (0) | 2023.07.11 |
CosmWasm 기본 개념 및 구조 (0) | 2023.07.06 |
CosmWasm Contract Migration (0) | 2023.06.15 |
CosmWasm smart contract 배포하기 (0) | 2023.06.14 |