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 }