github.com/0xsequence/ethkit@v1.25.0/ethreceipts/subscription.go (about)

     1  package ethreceipts
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math/big"
     7  	"sync"
     8  
     9  	"github.com/goware/channel"
    10  	"github.com/goware/superr"
    11  )
    12  
    13  type Subscription interface {
    14  	TransactionReceipt() <-chan Receipt
    15  	Done() <-chan struct{}
    16  	Unsubscribe()
    17  
    18  	Filters() []Filterer
    19  	AddFilter(filters ...FilterQuery)
    20  	RemoveFilter(filter Filterer)
    21  	ClearFilters()
    22  }
    23  
    24  var _ Subscription = &subscriber{}
    25  
    26  type subscriber struct {
    27  	listener    *ReceiptsListener
    28  	ch          channel.Channel[Receipt]
    29  	done        chan struct{}
    30  	unsubscribe func()
    31  	filters     []Filterer
    32  	finalizer   *finalizer
    33  	mu          sync.Mutex
    34  }
    35  
    36  type registerFilters struct {
    37  	subscriber *subscriber
    38  	filters    []Filterer
    39  }
    40  
    41  func (s *subscriber) TransactionReceipt() <-chan Receipt {
    42  	return s.ch.ReadChannel()
    43  }
    44  
    45  func (s *subscriber) Done() <-chan struct{} {
    46  	return s.done
    47  }
    48  
    49  func (s *subscriber) Unsubscribe() {
    50  	s.unsubscribe()
    51  }
    52  
    53  func (s *subscriber) Filters() []Filterer {
    54  	s.mu.Lock()
    55  	defer s.mu.Unlock()
    56  	filters := make([]Filterer, len(s.filters))
    57  	copy(filters, s.filters)
    58  	return filters
    59  }
    60  
    61  func (s *subscriber) AddFilter(filterQueries ...FilterQuery) {
    62  	if len(filterQueries) == 0 {
    63  		return
    64  	}
    65  
    66  	s.mu.Lock()
    67  	defer s.mu.Unlock()
    68  
    69  	filters := make([]Filterer, len(filterQueries))
    70  	for i, query := range filterQueries {
    71  		filterer, ok := query.(Filterer)
    72  		if !ok {
    73  			panic("ethreceipts: unexpected")
    74  		}
    75  		filters[i] = filterer
    76  	}
    77  
    78  	s.filters = append(s.filters, filters...)
    79  
    80  	// TODO: maybe add non-blocking push structure like in relayer queue
    81  	s.listener.registerFiltersCh <- registerFilters{subscriber: s, filters: filters}
    82  }
    83  
    84  func (s *subscriber) RemoveFilter(filter Filterer) {
    85  	s.mu.Lock()
    86  	defer s.mu.Unlock()
    87  
    88  	for i, f := range s.filters {
    89  		if f == filter {
    90  			s.filters = append(s.filters[:i], s.filters[i+1:]...)
    91  			return
    92  		}
    93  	}
    94  }
    95  
    96  func (s *subscriber) ClearFilters() {
    97  	s.mu.Lock()
    98  	defer s.mu.Unlock()
    99  	s.filters = s.filters[:0]
   100  }
   101  
   102  func (s *subscriber) matchFilters(ctx context.Context, filterers []Filterer, receipts []Receipt) ([]bool, error) {
   103  	oks := make([]bool, len(filterers))
   104  
   105  	for _, receipt := range receipts {
   106  		for i, filterer := range filterers {
   107  			matched, err := filterer.Match(ctx, receipt)
   108  			if err != nil {
   109  				return oks, superr.New(ErrFilterMatch, err)
   110  			}
   111  
   112  			if !matched {
   113  				// skip, not a match
   114  				continue
   115  			}
   116  
   117  			// its a match
   118  			oks[i] = true
   119  			receipt := receipt // copy
   120  			receipt.Filter = filterer
   121  
   122  			// fetch transaction receipt if its not been marked as reorged
   123  			if !receipt.Reorged {
   124  				r, err := s.listener.fetchTransactionReceipt(ctx, receipt.TransactionHash(), true)
   125  				if err != nil {
   126  					// TODO: is this fine to return error..? its a bit abrupt.
   127  					// Options are to set FailedFetch bool on the Receipt, and still send to s.ch,
   128  					// or just log the error and continue to the next receipt
   129  					return oks, superr.Wrap(fmt.Errorf("failed to fetch txn %s receipt", receipt.TransactionHash()), err)
   130  				}
   131  				receipt.receipt = r
   132  				receipt.logs = r.Logs
   133  			}
   134  
   135  			// Finality enqueue if filter asked to Finalize, and receipt isn't already final
   136  			if !receipt.Reorged && !receipt.Final && filterer.Options().Finalize {
   137  				s.finalizer.enqueue(filterer.FilterID(), receipt, receipt.BlockNumber())
   138  			}
   139  
   140  			// LimitOne will auto unsubscribe now if were not also waiting for finalizer,
   141  			// and if the returned txn isn't one that has been reorged
   142  			//
   143  			// NOTE: when Finalize is set, we don't want to remove this filter until the txn finalizes,
   144  			// because its possible that it can reorg and we have to fetch it again after being re-mined.
   145  			// So we only remove the filter now if the filter finalizer isn't used, otherwise the
   146  			// finalizer will remove the LimitOne filter
   147  			toFinalize := filterer.Options().Finalize && !receipt.Final
   148  			if !receipt.Reorged && filterer.Options().LimitOne && !toFinalize {
   149  				s.RemoveFilter(receipt.Filter)
   150  			}
   151  
   152  			// Check if receipt is already final, in case comes from cache when
   153  			// previously final was not toggled.
   154  			if s.listener.isBlockFinal(receipt.BlockNumber()) {
   155  				receipt.Final = true
   156  			}
   157  
   158  			// Broadcast to subscribers
   159  			s.ch.Send(receipt)
   160  		}
   161  	}
   162  
   163  	return oks, nil
   164  }
   165  
   166  func (s *subscriber) finalizeReceipts(blockNum *big.Int) error {
   167  	// check subscriber finalizer
   168  	finalizer := s.finalizer
   169  	if finalizer.len() == 0 {
   170  		return nil
   171  	}
   172  
   173  	finalTxns := finalizer.dequeue(blockNum)
   174  	if len(finalTxns) == 0 {
   175  		// no matching txns which have been finalized
   176  		return nil
   177  	}
   178  
   179  	// dispatch to subscriber finalized receipts
   180  	for _, x := range finalTxns {
   181  		if x.receipt.Reorged {
   182  			// for removed receipts, just skip
   183  			continue
   184  		}
   185  
   186  		// mark receipt as final, and send the receipt payload to the subscriber
   187  		x.receipt.Final = true
   188  
   189  		// send to the subscriber
   190  		s.ch.Send(x.receipt)
   191  
   192  		// Automatically remove filters for finalized txn hashes, as they won't come up again.
   193  		filter := x.receipt.Filter
   194  		if filter != nil && (filter.Cond().TxnHash != nil || filter.Options().LimitOne) {
   195  			s.RemoveFilter(filter)
   196  		}
   197  	}
   198  
   199  	return nil
   200  }