Refactored the subscription with the new unpacking

This commit is contained in:
Max Wolter 2016-08-15 20:47:52 +02:00
parent 6fb1523944
commit 4812a4f49e

View file

@ -60,9 +60,6 @@ type TransactOpts struct {
// SubscribeOpts is the collection of options to fine tune the contract subscription // SubscribeOpts is the collection of options to fine tune the contract subscription
// and unsubscription requests. // and unsubscription requests.
type SubscribeOpts struct { type SubscribeOpts struct {
History bool // Whether to also retrieve events from the past
FromBlock *big.Int // Block at which we want to start retrieving past events
Context context.Context // Network context to support cancellation and timeouts (nil = no timeout) Context context.Context // Network context to support cancellation and timeouts (nil = no timeout)
} }
@ -87,8 +84,9 @@ type BoundContract struct {
latestHasCode uint32 // Cached verification that the latest state contains code for this contract latestHasCode uint32 // Cached verification that the latest state contains code for this contract
pendingHasCode uint32 // Cached verification that the pending state contains code for this contract pendingHasCode uint32 // Cached verification that the pending state contains code for this contract
subscriptions map[chan<- vm.Log]ethereum.Subscription subscriptions map[uint32]ethereum.Subscription
errors map[chan<- vm.Log]error errors map[uint32]error
nextID uint32
} }
// NewBoundContract creates a low level contract interface through which calls // NewBoundContract creates a low level contract interface through which calls
@ -98,6 +96,8 @@ func NewBoundContract(address common.Address, abi abi.ABI, backend ContractBacke
address: address, address: address,
abi: abi, abi: abi,
backend: backend, backend: backend,
subscriptions: make(map[uint32]ethereum.Subscription),
errors: make(map[uint32]error),
} }
} }
@ -261,6 +261,18 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i
// the given start & end blocks. // the given start & end blocks.
func (c *BoundContract) Events(opts *EventOpts, name string, output interface{}, topics ...[]common.Hash) error { func (c *BoundContract) Events(opts *EventOpts, name string, output interface{}, topics ...[]common.Hash) error {
// check that output is a pointer
ptr := reflect.ValueOf(output)
if ptr.Kind() != reflect.Ptr {
return fmt.Errorf("need pointer to slice as output parameter, have %T", output)
}
// check that it points to a slice
slice := ptr.Elem()
if slice.Kind() != reflect.Slice {
return fmt.Errorf("need pointer to slice as output parameter, have %T", output)
}
// get the event so we can encode the name into the first topic // get the event so we can encode the name into the first topic
event, ok := c.abi.Events[name] event, ok := c.abi.Events[name]
if !ok { if !ok {
@ -269,18 +281,6 @@ func (c *BoundContract) Events(opts *EventOpts, name string, output interface{},
names := []common.Hash{event.Id()} names := []common.Hash{event.Id()}
topics = append([][]common.Hash{names}, topics...) topics = append([][]common.Hash{names}, topics...)
// check that output is a pointer
ptr := reflect.ValueOf(output)
if ptr.Kind() != reflect.Ptr {
return fmt.Errorf("need pointer to slice as output, have %T", output)
}
// check that it points to a slice
slice := ptr.Elem()
if slice.Kind() != reflect.Slice {
return fmt.Errorf("need pointer to slice as output, have %T", output)
}
// create the filter query and retrieve the events through the backend // create the filter query and retrieve the events through the backend
filterQuery := ethereum.FilterQuery{ filterQuery := ethereum.FilterQuery{
FromBlock: opts.FromBlock, FromBlock: opts.FromBlock,
@ -315,102 +315,99 @@ func (c *BoundContract) Events(opts *EventOpts, name string, output interface{},
// Subscribe subscribes to the contract for the given topics. Subscription events // Subscribe subscribes to the contract for the given topics. Subscription events
// will be submitted to the provided channel. Upon cancelation of the subscription, // will be submitted to the provided channel. Upon cancelation of the subscription,
// the channel will be closed. A channel should only be used for ones subscription. // the channel will be closed. A channel should only be used for one subscription.
func (c *BoundContract) Subscribe(opts *SubscribeOpts, name string, channel chan<- vm.Log, topics ...[]common.Hash) error { func (c *BoundContract) Subscribe(opts *SubscribeOpts, name string, output interface{}, topics ...[]common.Hash) (uint32, error) {
// check if we already have a subscription on this channel // check that output is a pointer
_, ok := c.subscriptions[channel] channel := reflect.ValueOf(output)
if ok { if channel.Kind() != reflect.Chan {
return fmt.Errorf("cannot reuse channel for multiple subscriptions") return 0, fmt.Errorf("need channel as output parameter, have %T", output)
} }
message := channel.Elem()
// Build the filter query with our desired options and topics // get the event so we can encode the name into the first topic
event, ok := c.abi.Events[name]
if !ok {
return 0, fmt.Errorf("unknown event name: %v", name)
}
names := []common.Hash{event.Id()}
topics = append([][]common.Hash{names}, topics...)
// create the filter query and retrieve the events through the backend
filterQuery := ethereum.FilterQuery{ filterQuery := ethereum.FilterQuery{
FromBlock: opts.FromBlock, FromBlock: nil,
ToBlock: nil, ToBlock: nil,
Addresses: []common.Address{c.address}, Addresses: []common.Address{c.address},
Topics: topics, Topics: topics,
} }
// If we don't want to miss any real-time event logs, we need to subscribe right away // create a channel of log messages that we can decode into events
tempChannel := make(chan vm.Log) logs := make(chan vm.Log)
tempSub, err := c.backend.SubscribeFilterLogs(opts.Context, filterQuery, tempChannel) subscription, err := c.backend.SubscribeFilterLogs(opts.Context, filterQuery, logs)
if err != nil { if err != nil {
return fmt.Errorf("could not create subscription (%v)", err) return 0, fmt.Errorf("could not subscribe (%v)", err)
} }
id := c.nextID
c.subscriptions[id] = subscription
c.nextID++
// Get the desired historical data and feed it into the subscription channel // retrieve logs and transcribe them into typed events
if opts.History { go func() {
var logs []vm.Log Loop:
logs, err = c.backend.FilterLogs(opts.Context, filterQuery)
if err != nil {
return fmt.Errorf("could not retrieve history of subscription events")
}
for _, log := range logs {
channel <- log
}
}
// Feed the real-time events that happenend in the meantime to the subscription channel
// and create the new subscription as soon as none are left
var subscription ethereum.Subscription
Feed:
for { for {
select { select {
case log := <-tempChannel: case err, ok = <-subscription.Err():
channel <- log
default: // error was closed, so we shut down
subscription, err = c.backend.SubscribeFilterLogs(opts.Context, filterQuery, channel) if !ok {
tempSub.Unsubscribe() break Loop
for _ = range tempSub.Err() {
} }
// otherwise just save the error for this subscription
c.errors[id] = err
// receive log and transcribe
case log := <-logs:
item := reflect.New(message.Type())
c.abi.Unpack(item.Interface(), name, log.Data)
channel.Send(item)
}
}
// drain the logs channel; this might not always work correctly because we
// are not sure all logs were put in the channel already
Drain: Drain:
for { for {
select { select {
case <-tempChannel: case log := <-logs:
item := reflect.New(message.Type())
c.abi.Unpack(item.Interface(), name, log.Data)
channel.Send(item)
default: default:
break Drain break Drain
} }
} }
close(tempChannel)
break Feed
}
}
// check if the subscription succeeded // close the output channel
if err != nil { channel.Close()
return fmt.Errorf("could not create subscription (%v)", err) }()
}
c.subscriptions[channel] = subscription
// Read from the error channel in the background return id, nil
// When we receive an error, we save it with the channel index
// When the error channel is closed, it indicates an ended subscription
// We then close the subscription channel to notify the consumer and remove
// the subscription from our map
go func(errChannel <-chan error) {
for err := range errChannel {
c.errors[channel] = err
}
close(channel)
delete(c.subscriptions, channel)
}(subscription.Err())
return nil
} }
// Error returns the error that was encountered for the subscription on the given // Error returns the error that was encountered for the subscription on the given
// channel. It will reset the error upon retrieval. // channel. It will reset the error upon retrieval.
func (c *BoundContract) Error(channel chan<- vm.Log) error { func (c *BoundContract) Error(id uint32) error {
err := c.errors[channel] err := c.errors[id]
delete(c.errors, channel) delete(c.errors, id)
return err return err
} }
// Unsubscribe cancels the subscription if one exists on the given channel. The // Unsubscribe cancels the subscription if one exists on the given channel. The
// subscription channel will be closed once the subscription was successfully // subscription channel will be closed once the subscription was successfully
// canceled. // canceled.
func (c *BoundContract) Unsubscribe(channel chan<- vm.Log) error { func (c *BoundContract) Unsubscribe(id uint32) error {
subscription, ok := c.subscriptions[channel] subscription, ok := c.subscriptions[id]
if !ok { if !ok {
return fmt.Errorf("subscription doesn't exist or already closed") return fmt.Errorf("subscription doesn't exist or already closed")
} }