From 28d57002ff3866c5cc8dfa5df55285530c346aac Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Tue, 3 Dec 2024 13:25:46 +0700 Subject: [PATCH] add generic log watching/filtering. skip adding generic call/transact (issue with interface regarding contract methods that don't take arguments) --- accounts/abi/bind/base.go | 4 +- accounts/abi/bind/source2.go.tpl | 4 +- .../abi/bind/testdata/v2/events/bindings.go | 8 +- accounts/abi/bind/v2/lib.go | 149 ++++++++++++++++++ accounts/abi/bind/v2/lib_test.go | 98 ++++++------ 5 files changed, 206 insertions(+), 57 deletions(-) diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go index 2ae92391a3..2380d137c7 100644 --- a/accounts/abi/bind/base.go +++ b/accounts/abi/bind/base.go @@ -460,9 +460,9 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i return signedTx, nil } -// FilterLogsByID filters contract logs for past blocks, returning the necessary +// FilterLogsById filters contract logs for past blocks, returning the necessary // channels to construct a strongly typed bound iterator on top of them. -func (c *BoundContract) FilterLogsByID(opts *FilterOpts, eventID common.Hash, query ...[]interface{}) (<-chan types.Log, event.Subscription, error) { +func (c *BoundContract) FilterLogsById(opts *FilterOpts, eventID common.Hash, query ...[]interface{}) (<-chan types.Log, event.Subscription, error) { return c.filterLogs(opts, eventID, query...) } diff --git a/accounts/abi/bind/source2.go.tpl b/accounts/abi/bind/source2.go.tpl index 34bf50b8ea..990988bbf1 100644 --- a/accounts/abi/bind/source2.go.tpl +++ b/accounts/abi/bind/source2.go.tpl @@ -125,7 +125,9 @@ var ( } func (_{{$contract.Type}} *{{$contract.Type}}) Unpack{{.Normalized.Name}}Event(log *types.Log) (*{{$contract.Type}}{{.Normalized.Name}}, error) { - event := "{{.Normalized.Name}}" + // TODO: okay to index by the original name here? I think so because we assume that the abi json is well-formed. + // and we only need normalized name when dealing with generated go symbols. + event := "{{.Original.Name}}" if log.Topics[0] != _{{$contract.Type}}.abi.Events[event].ID { return nil, errors.New("event signature mismatch") } diff --git a/accounts/abi/bind/testdata/v2/events/bindings.go b/accounts/abi/bind/testdata/v2/events/bindings.go index 24c5c4da36..c8c3944d95 100644 --- a/accounts/abi/bind/testdata/v2/events/bindings.go +++ b/accounts/abi/bind/testdata/v2/events/bindings.go @@ -134,7 +134,9 @@ func CBasic1EventID() common.Hash { } func (_C *C) UnpackBasic1Event(log *types.Log) (*CBasic1, error) { - event := "Basic1" + // TODO: okay to index by the original name here? I think so because we assume that the abi json is well-formed. + // and we only need normalized name when dealing with generated go symbols. + event := "basic1" if log.Topics[0] != _C.abi.Events[event].ID { return nil, errors.New("event signature mismatch") } @@ -169,7 +171,9 @@ func CBasic2EventID() common.Hash { } func (_C *C) UnpackBasic2Event(log *types.Log) (*CBasic2, error) { - event := "Basic2" + // TODO: okay to index by the original name here? I think so because we assume that the abi json is well-formed. + // and we only need normalized name when dealing with generated go symbols. + event := "basic2" if log.Topics[0] != _C.abi.Events[event].ID { return nil, errors.New("event signature mismatch") } diff --git a/accounts/abi/bind/v2/lib.go b/accounts/abi/bind/v2/lib.go index e2658c2357..1be51fae4e 100644 --- a/accounts/abi/bind/v2/lib.go +++ b/accounts/abi/bind/v2/lib.go @@ -19,9 +19,12 @@ package v2 import ( "encoding/hex" "fmt" + "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" "regexp" "strings" ) @@ -212,3 +215,149 @@ func LinkAndDeploy(auth *bind.TransactOpts, backend bind.ContractBackend, deploy return res, nil } + +// TODO: this will be generated as part of the bindings, contain the ABI (or metadata object?) and errors +type ContractInstance struct { + Address common.Address + Backend bind.ContractBackend +} + +// TODO: adding docs soon (jwasinger) +func FilterEvents[T any](instance *ContractInstance, opts *bind.FilterOpts, eventID common.Hash, unpack func(*types.Log) (*T, error), topics ...[]any) (*EventIterator[T], error) { + backend := instance.Backend + c := bind.NewBoundContract(instance.Address, abi.ABI{}, backend, backend, backend) + logs, sub, err := c.FilterLogsById(opts, eventID, topics...) + if err != nil { + return nil, err + } + return &EventIterator[T]{unpack: unpack, logs: logs, sub: sub}, nil +} + +// WatchEvents causes logs emitted with a given event id from a specified +// contract to be intercepted, unpacked, and forwarded to sink. If +// unpack returns an error, the returned subscription is closed with the +// error. +func WatchEvents[T any](instance *ContractInstance, abi abi.ABI, opts *bind.WatchOpts, eventID common.Hash, unpack func(*types.Log) (*T, error), sink chan<- *T, topics ...[]any) (event.Subscription, error) { + backend := instance.Backend + c := bind.NewBoundContract(instance.Address, abi, backend, backend, backend) + logs, sub, err := c.WatchLogsForId(opts, eventID, topics...) + 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 + ev, err := unpack(&log) + if err != nil { + fmt.Printf("unpack err: %v", err) + return err + } + + select { + case sink <- ev: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// EventIterator is returned from FilterLogs and is used to iterate over the raw logs and unpacked data for events. +type EventIterator[T any] struct { + event *T // event containing the contract specifics and raw log + + unpack func(*types.Log) (*T, error) // Unpack function for the event + + logs <-chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for solc_errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Value returns the current value of the iterator, or nil if there isn't one. +func (it *EventIterator[T]) Value() *T { + return it.event +} + +// 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 *EventIterator[T]) 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: + res, err := it.unpack(&log) + if err != nil { + it.fail = err + return false + } + it.event = res + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + res, err := it.unpack(&log) + if err != nil { + it.fail = err + return false + } + it.event = res + 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 *EventIterator[T]) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *EventIterator[T]) Close() error { + it.sub.Unsubscribe() + return nil +} + +// Transact creates and submits a transaction to the bound contract instance +// using the provided abi-encoded input (or nil). +func Transact(instance *ContractInstance, opts *bind.TransactOpts, packedInput []byte) (*types.Transaction, error) { + var ( + addr = instance.Address + backend = instance.Backend + ) + c := bind.NewBoundContract(addr, abi.ABI{}, backend, backend, backend) + return c.RawTransact(opts, packedInput) +} + +// Call performs an eth_call on the given bound contract instance, using the +// provided abi-encoded input (or nil). +func Call(instance *ContractInstance, opts *bind.CallOpts, packedInput []byte) ([]byte, error) { + backend := instance.Backend + c := bind.NewBoundContract(instance.Address, abi.ABI{}, backend, backend, backend) + return c.CallRaw(opts, packedInput) +} diff --git a/accounts/abi/bind/v2/lib_test.go b/accounts/abi/bind/v2/lib_test.go index fc96d1a811..bedd368216 100644 --- a/accounts/abi/bind/v2/lib_test.go +++ b/accounts/abi/bind/v2/lib_test.go @@ -269,7 +269,6 @@ func TestDeploymentWithOverrides(t *testing.T) { t.Fatalf("expected internal call count of 6. got %d.", internalCallCount.Uint64()) } } - func TestEvents(t *testing.T) { // test watch/filter logs method on a contract that emits various kinds of events (struct-containing, etc.) txAuth, backend, err := testSetup() @@ -305,31 +304,30 @@ func TestEvents(t *testing.T) { t.Fatalf("error getting contract abi: %v", err) } - boundContract := bind.NewBoundContract(res.Addrs[events.CMetaData.Pattern], *abi, backend, backend, backend) + boundContract := ContractInstance{ + res.Addrs[events.CMetaData.Pattern], + backend, + } + newCBasic1Ch := make(chan *events.CBasic1) + newCBasic2Ch := make(chan *events.CBasic2) watchOpts := &bind.WatchOpts{ Start: nil, Context: context.Background(), } - chE1, sub1, err := boundContract.WatchLogsForId(watchOpts, events.CBasic1EventID(), nil) - if err != nil { - t.Fatalf("WatchLogsForId with event type 1 failed: %v", err) - } + sub1, err := WatchEvents(&boundContract, *abi, watchOpts, events.CBasic1EventID(), ctrct.UnpackBasic1Event, newCBasic1Ch) + sub2, err := WatchEvents(&boundContract, *abi, watchOpts, events.CBasic2EventID(), ctrct.UnpackBasic2Event, newCBasic2Ch) defer sub1.Unsubscribe() - - chE2, sub2, err := boundContract.WatchLogsForId(watchOpts, events.CBasic2EventID(), nil) - if err != nil { - t.Fatalf("WatchLogsForId with event type 2 failed: %v", err) - } defer sub2.Unsubscribe() - packedCallData, err := ctrct.PackEmitMulti() - if err != nil { - t.Fatalf("failed to pack EmitMulti arguments") + crtctInstance := &ContractInstance{ + Address: res.Addrs[events.CMetaData.Pattern], + Backend: backend, } - tx, err := boundContract.RawTransact(txAuth, packedCallData) + packedInput, _ := ctrct.PackEmitMulti() + tx, err := Transact(crtctInstance, txAuth, packedInput) if err != nil { - t.Fatalf("failed to submit transaction: %v", err) + t.Fatalf("failed to send transaction: %v", err) } backend.Commit() if _, err := bind.WaitMined(context.Background(), backend, tx); err != nil { @@ -341,22 +339,15 @@ func TestEvents(t *testing.T) { e2Count := 0 for { select { - case <-timeout.C: - goto done - case err := <-sub1.Err(): - t.Fatalf("received err from sub1: %v", err) - case err := <-sub2.Err(): - t.Fatalf("received err from sub2: %v", err) - case log := <-chE1: - if _, err := ctrct.UnpackBasic1Event(&log); err != nil { - t.Fatalf("failed to unpack basic1 type event: %v", err) - } + case _ = <-newCBasic1Ch: e1Count++ - case log := <-chE2: - if _, err := ctrct.UnpackBasic2Event(&log); err != nil { - t.Fatalf("failed to unpack basic2 type event: %v", err) - } + case _ = <-newCBasic2Ch: e2Count++ + case _ = <-timeout.C: + goto done + } + if e1Count == 2 && e2Count == 1 { + break } } done: @@ -373,33 +364,39 @@ done: Start: 0, Context: context.Background(), } - chE1, sub1, err = boundContract.FilterLogsByID(filterOpts, events.CBasic1EventID(), nil) - if err != nil { - t.Fatalf("failed to filter logs for event type 1: %v", err) + unpackBasic := func(raw *types.Log) (*events.CBasic1, error) { + return &events.CBasic1{ + Id: (new(big.Int)).SetBytes(raw.Topics[0].Bytes()), + Data: (new(big.Int)).SetBytes(raw.Data), + }, nil } - chE2, sub2, err = boundContract.FilterLogsByID(filterOpts, events.CBasic2EventID(), nil) - if err != nil { - t.Fatalf("failed to filter logs for event type 2: %v", err) + unpackBasic2 := func(raw *types.Log) (*events.CBasic2, error) { + return &events.CBasic2{ + Flag: false, // TODO: how to unpack different types to go types? this should be exposed via abi package. + Data: (new(big.Int)).SetBytes(raw.Data), + }, nil + } + it, err := FilterEvents[events.CBasic1](crtctInstance, filterOpts, events.CBasic1EventID(), unpackBasic) + if err != nil { + t.Fatalf("error filtering logs %v\n", err) + } + it2, err := FilterEvents[events.CBasic2](crtctInstance, filterOpts, events.CBasic2EventID(), unpackBasic2) + if err != nil { + t.Fatalf("error filtering logs %v\n", err) } - timeout.Reset(2 * time.Second) e1Count = 0 e2Count = 0 - for { - select { - case <-timeout.C: - goto done2 - case <-chE1: - e1Count++ - case <-chE2: - e2Count++ - } + for it.Next() { + e1Count++ + } + for it2.Next() { + e2Count++ } -done2: if e1Count != 2 { - t.Fatalf("incorrect results from filter logs: expected event type 1 count to be 2. got %d", e1Count) + t.Fatalf("expected e1Count of 2 from filter call. got %d", e1Count) } if e2Count != 1 { - t.Fatalf("incorrect results from filter logs: expected event type 2 count to be 1. got %d", e2Count) + t.Fatalf("expected e2Count of 1 from filter call. got %d", e1Count) } } @@ -409,7 +406,6 @@ func TestBindingGeneration(t *testing.T) { for _, match := range matches { f, _ := os.Stat(match) if f.IsDir() { - fmt.Printf("match %s\n", f.Name()) dirs = append(dirs, f.Name()) } } @@ -433,8 +429,6 @@ func TestBindingGeneration(t *testing.T) { t.Fatalf("Failed to read contract information from json output: %v", err) } - fmt.Println(dir) - fmt.Printf("number of contracts: %d\n", len(contracts)) for name, contract := range contracts { // fully qualified name is of the form : nameParts := strings.Split(name, ":")