From 7943c71912efc7313ca796311df44629c022289c Mon Sep 17 00:00:00 2001 From: Bas van Kervel Date: Thu, 5 Oct 2017 13:36:12 +0200 Subject: [PATCH] cmd/abigen, accounts/abi: add support for log subscriptions --- accounts/abi/bind/backend.go | 6 +++ accounts/abi/bind/base.go | 18 +++++++- accounts/abi/bind/bind.go | 13 ++++++ accounts/abi/bind/template.go | 79 ++++++++++++++++++++++++++++++++--- mobile/bind.go | 2 +- 5 files changed, 109 insertions(+), 9 deletions(-) diff --git a/accounts/abi/bind/backend.go b/accounts/abi/bind/backend.go index 25b61928e1..cfdf2f12c7 100644 --- a/accounts/abi/bind/backend.go +++ b/accounts/abi/bind/backend.go @@ -90,8 +90,14 @@ type ContractTransactor interface { SendTransaction(ctx context.Context, tx *types.Transaction) error } +// ContractEventer defines methods to listen for events raised by the contract. +type ContractEventer interface { + SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) +} + // ContractBackend defines the methods needed to work with contracts on a read-write basis. type ContractBackend interface { ContractCaller ContractTransactor + ContractEventer } diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go index b40bd65e80..b661499075 100644 --- a/accounts/abi/bind/base.go +++ b/accounts/abi/bind/base.go @@ -63,16 +63,18 @@ type BoundContract struct { abi abi.ABI // Reflect based ABI to access the correct Ethereum methods caller ContractCaller // Read interface to interact with the blockchain transactor ContractTransactor // Write interface to interact with the blockchain + eventer ContractEventer // Listen for events raised by the contract } // NewBoundContract creates a low level contract interface through which calls // and transactions may be made through. -func NewBoundContract(address common.Address, abi abi.ABI, caller ContractCaller, transactor ContractTransactor) *BoundContract { +func NewBoundContract(address common.Address, abi abi.ABI, caller ContractCaller, transactor ContractTransactor, eventer ContractEventer) *BoundContract { return &BoundContract{ address: address, abi: abi, caller: caller, transactor: transactor, + eventer: eventer, } } @@ -80,7 +82,7 @@ func NewBoundContract(address common.Address, abi abi.ABI, caller ContractCaller // deployment address with a Go wrapper. func DeployContract(opts *TransactOpts, abi abi.ABI, bytecode []byte, backend ContractBackend, params ...interface{}) (common.Address, *types.Transaction, *BoundContract, error) { // Otherwise try to deploy the contract - c := NewBoundContract(common.Address{}, abi, backend, backend) + c := NewBoundContract(common.Address{}, abi, backend, backend, backend) input, err := c.abi.Pack("", params...) if err != nil { @@ -225,6 +227,18 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i return signedTx, nil } +// SubscribeFilterLogs creates a log subscription that streams logs raised by this contract +// with topics matching +func (c *BoundContract) SubscribeFilterLogs(opts *CallOpts, topics [][]common.Hash, ch chan<- types.Log) (ethereum.Subscription, error) { + ctx := ensureContext(opts.Context) + q := ethereum.FilterQuery{ + Addresses: []common.Address{c.address}, + Topics: topics, + } + + return c.eventer.SubscribeFilterLogs(ctx, q, ch) +} + func ensureContext(ctx context.Context) context.Context { if ctx == nil { return context.TODO() diff --git a/accounts/abi/bind/bind.go b/accounts/abi/bind/bind.go index f587580884..0766830b73 100644 --- a/accounts/abi/bind/bind.go +++ b/accounts/abi/bind/bind.go @@ -67,6 +67,7 @@ func Bind(types []string, abis []string, bytecodes []string, pkg string, lang La var ( calls = make(map[string]*tmplMethod) transacts = make(map[string]*tmplMethod) + events = make(map[string]*tmplEvent) ) for _, original := range evmABI.Methods { // Normalize the method for capital cases and non-anonymous inputs/outputs @@ -94,6 +95,16 @@ func Bind(types []string, abis []string, bytecodes []string, pkg string, lang La transacts[original.Name] = &tmplMethod{Original: original, Normalized: normalized, Structured: structured(original)} } } + + for _, e := range evmABI.Events { + events[e.Name] = &tmplEvent{ + Name: e.Name, + Inputs: e.Inputs, + ID: fmt.Sprintf("%x", e.Id()), + Anonymous: e.Anonymous, + } + } + contracts[types[i]] = &tmplContract{ Type: capitalise(types[i]), InputABI: strings.Replace(strippedABI, "\"", "\\\"", -1), @@ -101,6 +112,7 @@ func Bind(types []string, abis []string, bytecodes []string, pkg string, lang La Constructor: evmABI.Constructor, Calls: calls, Transacts: transacts, + Events: events, // TODO: add event subscription support to the java template } } // Generate the contract template data content and render it @@ -116,6 +128,7 @@ func Bind(types []string, abis []string, bytecodes []string, pkg string, lang La "capitalise": capitalise, "decapitalise": decapitalise, } + tmpl := template.Must(template.New("").Funcs(funcs).Parse(tmplSource[lang])) if err := tmpl.Execute(buffer, data); err != nil { return "", err diff --git a/accounts/abi/bind/template.go b/accounts/abi/bind/template.go index d07610e7c2..3a57442bda 100644 --- a/accounts/abi/bind/template.go +++ b/accounts/abi/bind/template.go @@ -16,7 +16,12 @@ package bind -import "github.com/ethereum/go-ethereum/accounts/abi" +import ( + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" +) // tmplData is the data structure required to fill the binding template. type tmplData struct { @@ -32,6 +37,7 @@ type tmplContract struct { Constructor abi.Method // Contract constructor for deploy parametrization Calls map[string]*tmplMethod // Contract calls that only read state data Transacts map[string]*tmplMethod // Contract calls that write state data + Events map[string]*tmplEvent // Contract events } // tmplMethod is a wrapper around an abi.Method that contains a few preprocessed @@ -42,6 +48,39 @@ type tmplMethod struct { Structured bool // Whether the returns should be accumulated into a contract } +// tmplEvent is a wrapper around an abi.Event that contains a few preprocessed +// and cached data fields. +type tmplEvent struct { + Name string + Inputs []abi.Argument + ID string + Anonymous bool +} + +func (e tmplEvent) Canonical() string { + var ( + args []string + anonymous string + ) + + for _, a := range e.Inputs { + indexed := "" + if a.Indexed { + indexed = " indexed" + } + if a.Name != "" { + args = append(args, fmt.Sprintf("%s%s %s", a.Type, indexed, a.Name)) + } else { + args = append(args, fmt.Sprintf("%s%s", a.Type, indexed)) + } + } + + if e.Anonymous { + anonymous = " anonymous" + } + return fmt.Sprintf("%s(%s)%s", e.Name, strings.Join(args, ", "), anonymous) +} + // tmplSource is language to template mapping containing all the supported // programming languages the package can generate to. var tmplSource = map[Lang]string{ @@ -83,6 +122,7 @@ package {{.Package}} type {{.Type}} struct { {{.Type}}Caller // Read-only binding to the contract {{.Type}}Transactor // Write-only binding to the contract + {{.Type}}Eventer // Event listener binding to the contract } // {{.Type}}Caller is an auto generated read-only Go binding around an Ethereum contract. @@ -95,6 +135,12 @@ package {{.Package}} contract *bind.BoundContract // Generic contract wrapper for the low level calls } + // {{.Type}}Eventer is an auto generated write-only Go binding around an Ethereum contract. + type {{.Type}}Eventer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls + address common.Address // Contract address + } + // {{.Type}}Session is an auto generated Go binding around an Ethereum contract, // with pre-set call and transact options. type {{.Type}}Session struct { @@ -134,7 +180,7 @@ package {{.Package}} // New{{.Type}} creates a new instance of {{.Type}}, bound to a specific deployed contract. func New{{.Type}}(address common.Address, backend bind.ContractBackend) (*{{.Type}}, error) { - contract, err := bind{{.Type}}(address, backend, backend) + contract, err := bind{{.Type}}(address, backend, backend, backend) if err != nil { return nil, err } @@ -143,7 +189,7 @@ package {{.Package}} // New{{.Type}}Caller creates a new read-only instance of {{.Type}}, bound to a specific deployed contract. func New{{.Type}}Caller(address common.Address, caller bind.ContractCaller) (*{{.Type}}Caller, error) { - contract, err := bind{{.Type}}(address, caller, nil) + contract, err := bind{{.Type}}(address, caller, nil, nil) if err != nil { return nil, err } @@ -152,20 +198,29 @@ package {{.Package}} // New{{.Type}}Transactor creates a new write-only instance of {{.Type}}, bound to a specific deployed contract. func New{{.Type}}Transactor(address common.Address, transactor bind.ContractTransactor) (*{{.Type}}Transactor, error) { - contract, err := bind{{.Type}}(address, nil, transactor) + contract, err := bind{{.Type}}(address, nil, transactor, nil) if err != nil { return nil, err } return &{{.Type}}Transactor{contract: contract}, nil } + // New{{.Type}}Eventer creates a new listen only instance of {{.Type}}, bound to a specific deployed contract. + func New{{.Type}}Eventer(address common.Address, eventer bind.ContractEventer) (*{{.Type}}Eventer, error) { + contract, err := bind{{.Type}}(address, nil, nil, eventer) + if err != nil { + return nil, err + } + return &{{.Type}}Eventer{contract: contract, address: address}, nil + } + // bind{{.Type}} binds a generic wrapper to an already deployed contract. - func bind{{.Type}}(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor) (*bind.BoundContract, error) { + func bind{{.Type}}(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, eventer bind.ContractEventer) (*bind.BoundContract, error) { parsed, err := abi.JSON(strings.NewReader({{.Type}}ABI)) if err != nil { return nil, err } - return bind.NewBoundContract(address, parsed, caller, transactor), nil + return bind.NewBoundContract(address, parsed, caller, transactor, eventer), nil } // Call invokes the (constant) contract method with params as input values and @@ -263,6 +318,18 @@ package {{.Package}} return _{{$contract.Type}}.Contract.{{.Normalized.Name}}(&_{{$contract.Type}}.TransactOpts {{range $i, $_ := .Normalized.Inputs}}, {{.Name}}{{end}}) } {{end}} + + {{range .Events}} + // Solidity: event {{.Canonical}} + // + // {{if not .Anonymous}}Note: this method will fill in the Event ID topic{{end}} + func (_{{$contract.Type}} *{{$contract.Type}}Caller) Subscribe{{.Name}}(opts *bind.CallOpts, ch chan<- types.Log, topics ...[]common.Hash) (ethereum.Subscription, error) { + {{if not .Anonymous}}id := []common.Hash{common.HexToHash("{{.ID}}")} + topics = append([][]common.Hash{id}, topics...) + {{end}} + return _{{$contract.Type}}.contract.SubscribeFilterLogs(opts, topics, ch) + } + {{end}} {{end}} ` diff --git a/mobile/bind.go b/mobile/bind.go index 084d6ef4c7..fbf930725d 100644 --- a/mobile/bind.go +++ b/mobile/bind.go @@ -138,7 +138,7 @@ func BindContract(address *Address, abiJSON string, client *EthereumClient) (con return nil, err } return &BoundContract{ - contract: bind.NewBoundContract(address.address, parsed, client.client, client.client), + contract: bind.NewBoundContract(address.address, parsed, client.client, client.client, client.client), address: address.address, }, nil }