add generic log watching/filtering. skip adding generic call/transact (issue with interface regarding contract methods that don't take arguments)
This commit is contained in:
parent
69e6f2932d
commit
28d57002ff
|
@ -460,9 +460,9 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i
|
||||||
return signedTx, nil
|
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.
|
// 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...)
|
return c.filterLogs(opts, eventID, query...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -125,7 +125,9 @@ var (
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_{{$contract.Type}} *{{$contract.Type}}) Unpack{{.Normalized.Name}}Event(log *types.Log) (*{{$contract.Type}}{{.Normalized.Name}}, error) {
|
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 {
|
if log.Topics[0] != _{{$contract.Type}}.abi.Events[event].ID {
|
||||||
return nil, errors.New("event signature mismatch")
|
return nil, errors.New("event signature mismatch")
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,7 +134,9 @@ func CBasic1EventID() common.Hash {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_C *C) UnpackBasic1Event(log *types.Log) (*CBasic1, error) {
|
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 {
|
if log.Topics[0] != _C.abi.Events[event].ID {
|
||||||
return nil, errors.New("event signature mismatch")
|
return nil, errors.New("event signature mismatch")
|
||||||
}
|
}
|
||||||
|
@ -169,7 +171,9 @@ func CBasic2EventID() common.Hash {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_C *C) UnpackBasic2Event(log *types.Log) (*CBasic2, error) {
|
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 {
|
if log.Topics[0] != _C.abi.Events[event].ID {
|
||||||
return nil, errors.New("event signature mismatch")
|
return nil, errors.New("event signature mismatch")
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,12 @@ package v2
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"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/accounts/abi/bind"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/event"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -212,3 +215,149 @@ func LinkAndDeploy(auth *bind.TransactOpts, backend bind.ContractBackend, deploy
|
||||||
|
|
||||||
return res, nil
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -269,7 +269,6 @@ func TestDeploymentWithOverrides(t *testing.T) {
|
||||||
t.Fatalf("expected internal call count of 6. got %d.", internalCallCount.Uint64())
|
t.Fatalf("expected internal call count of 6. got %d.", internalCallCount.Uint64())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvents(t *testing.T) {
|
func TestEvents(t *testing.T) {
|
||||||
// test watch/filter logs method on a contract that emits various kinds of events (struct-containing, etc.)
|
// test watch/filter logs method on a contract that emits various kinds of events (struct-containing, etc.)
|
||||||
txAuth, backend, err := testSetup()
|
txAuth, backend, err := testSetup()
|
||||||
|
@ -305,31 +304,30 @@ func TestEvents(t *testing.T) {
|
||||||
t.Fatalf("error getting contract abi: %v", err)
|
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{
|
watchOpts := &bind.WatchOpts{
|
||||||
Start: nil,
|
Start: nil,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
}
|
}
|
||||||
chE1, sub1, err := boundContract.WatchLogsForId(watchOpts, events.CBasic1EventID(), nil)
|
sub1, err := WatchEvents(&boundContract, *abi, watchOpts, events.CBasic1EventID(), ctrct.UnpackBasic1Event, newCBasic1Ch)
|
||||||
if err != nil {
|
sub2, err := WatchEvents(&boundContract, *abi, watchOpts, events.CBasic2EventID(), ctrct.UnpackBasic2Event, newCBasic2Ch)
|
||||||
t.Fatalf("WatchLogsForId with event type 1 failed: %v", err)
|
|
||||||
}
|
|
||||||
defer sub1.Unsubscribe()
|
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()
|
defer sub2.Unsubscribe()
|
||||||
|
|
||||||
packedCallData, err := ctrct.PackEmitMulti()
|
crtctInstance := &ContractInstance{
|
||||||
if err != nil {
|
Address: res.Addrs[events.CMetaData.Pattern],
|
||||||
t.Fatalf("failed to pack EmitMulti arguments")
|
Backend: backend,
|
||||||
}
|
}
|
||||||
tx, err := boundContract.RawTransact(txAuth, packedCallData)
|
packedInput, _ := ctrct.PackEmitMulti()
|
||||||
|
tx, err := Transact(crtctInstance, txAuth, packedInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to submit transaction: %v", err)
|
t.Fatalf("failed to send transaction: %v", err)
|
||||||
}
|
}
|
||||||
backend.Commit()
|
backend.Commit()
|
||||||
if _, err := bind.WaitMined(context.Background(), backend, tx); err != nil {
|
if _, err := bind.WaitMined(context.Background(), backend, tx); err != nil {
|
||||||
|
@ -341,22 +339,15 @@ func TestEvents(t *testing.T) {
|
||||||
e2Count := 0
|
e2Count := 0
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-timeout.C:
|
case _ = <-newCBasic1Ch:
|
||||||
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)
|
|
||||||
}
|
|
||||||
e1Count++
|
e1Count++
|
||||||
case log := <-chE2:
|
case _ = <-newCBasic2Ch:
|
||||||
if _, err := ctrct.UnpackBasic2Event(&log); err != nil {
|
|
||||||
t.Fatalf("failed to unpack basic2 type event: %v", err)
|
|
||||||
}
|
|
||||||
e2Count++
|
e2Count++
|
||||||
|
case _ = <-timeout.C:
|
||||||
|
goto done
|
||||||
|
}
|
||||||
|
if e1Count == 2 && e2Count == 1 {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
done:
|
done:
|
||||||
|
@ -373,33 +364,39 @@ done:
|
||||||
Start: 0,
|
Start: 0,
|
||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
}
|
}
|
||||||
chE1, sub1, err = boundContract.FilterLogsByID(filterOpts, events.CBasic1EventID(), nil)
|
unpackBasic := func(raw *types.Log) (*events.CBasic1, error) {
|
||||||
if err != nil {
|
return &events.CBasic1{
|
||||||
t.Fatalf("failed to filter logs for event type 1: %v", err)
|
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)
|
unpackBasic2 := func(raw *types.Log) (*events.CBasic2, error) {
|
||||||
if err != nil {
|
return &events.CBasic2{
|
||||||
t.Fatalf("failed to filter logs for event type 2: %v", err)
|
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
|
e1Count = 0
|
||||||
e2Count = 0
|
e2Count = 0
|
||||||
for {
|
for it.Next() {
|
||||||
select {
|
e1Count++
|
||||||
case <-timeout.C:
|
}
|
||||||
goto done2
|
for it2.Next() {
|
||||||
case <-chE1:
|
e2Count++
|
||||||
e1Count++
|
|
||||||
case <-chE2:
|
|
||||||
e2Count++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
done2:
|
|
||||||
if e1Count != 2 {
|
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 {
|
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 {
|
for _, match := range matches {
|
||||||
f, _ := os.Stat(match)
|
f, _ := os.Stat(match)
|
||||||
if f.IsDir() {
|
if f.IsDir() {
|
||||||
fmt.Printf("match %s\n", f.Name())
|
|
||||||
dirs = append(dirs, 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)
|
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 {
|
for name, contract := range contracts {
|
||||||
// fully qualified name is of the form <solFilePath>:<type>
|
// fully qualified name is of the form <solFilePath>:<type>
|
||||||
nameParts := strings.Split(name, ":")
|
nameParts := strings.Split(name, ":")
|
||||||
|
|
Loading…
Reference in New Issue