We had some methods to call/transact:
// 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, input []byte) (*types.Transaction, error) {
var (
addr = instance.Address
backend = instance.Backend
)
c := bind.NewBoundContract(addr, abi.ABI{}, backend, backend, backend)
return c.RawTransact(opts, input)
}
// 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, input []byte) ([]byte, error) {
backend := instance.Backend
c := bind.NewBoundContract(instance.Address, abi.ABI{}, backend, backend, backend)
return c.CallRaw(opts, input)
}
These are obviously useless: We can just use CallRaw
/RawTransact
diretly (these are two methods I added that are the same as existing Call
, Transact
but take abi-encoded inputs).
Then we had some utilities for watching/filtering logs that were added by Sina in the PoC (note that the *ById
methods were added to the BoundContract
interface by me because I emit the event id instead of name in the bindings... I'm going to just export the name and revert these method additions, but that's beside the point):
Filtering:
// FilterEvents returns an iterator for filtering events that match the query
// parameters (filter range, eventID, topics). If unpack returns an error,
// the iterator value is not updated, and the iterator is stopped with the
// returned error.
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
}
// EventIterator is returned from FilterEvents 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
}
Why do we do all this boilerplate just to wrap a channel/subscription returned by FilterLogsByID
into an opinionated iterator struct? Let the user call FilterLogsByID
themselves and know to pass the correct event id and unpack func (both of which are generated in the bindings).
Similarly, we had a method to watch logs:
// WatchLogs 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 WatchLogs[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 {
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
}
Again, we are just wrapping a call to an existing v1 method on BoundContract
, and the generics don't really give us any guarantees here: the user still needs to ensure that the event id and unpack function they pass correspond to the same event type.