github.com/status-im/status-go@v1.1.0/services/rpcfilters/api.go (about) 1 package rpcfilters 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "sync" 8 "time" 9 10 "github.com/pborman/uuid" 11 12 ethereum "github.com/ethereum/go-ethereum" 13 "github.com/ethereum/go-ethereum/common" 14 "github.com/ethereum/go-ethereum/core/types" 15 "github.com/ethereum/go-ethereum/eth/filters" 16 "github.com/ethereum/go-ethereum/log" 17 getrpc "github.com/ethereum/go-ethereum/rpc" 18 ) 19 20 const ( 21 defaultFilterLivenessPeriod = 5 * time.Minute 22 defaultLogsPeriod = 3 * time.Second 23 defaultLogsQueryTimeout = 10 * time.Second 24 ) 25 26 var ( 27 errFilterNotFound = errors.New("filter not found") 28 ) 29 30 type filter interface { 31 add(interface{}) error 32 pop() interface{} 33 stop() 34 deadline() *time.Timer 35 } 36 37 type ChainEvent interface { 38 Start() error 39 Stop() 40 Subscribe() (id int, ch interface{}) 41 Unsubscribe(id int) 42 } 43 44 // PublicAPI represents filter API that is exported to `eth` namespace 45 type PublicAPI struct { 46 filtersMu sync.Mutex 47 filters map[getrpc.ID]filter 48 49 // filterLivenessLoop defines how often timeout loop is executed 50 filterLivenessLoop time.Duration 51 // filter liveness increased by this period when changes are requested 52 filterLivenessPeriod time.Duration 53 54 client func() ContextCaller 55 chainID func() uint64 56 57 latestBlockChangedEvent *latestBlockChangedEvent 58 transactionSentToUpstreamEvent *transactionSentToUpstreamEvent 59 } 60 61 // NewPublicAPI returns a reference to the PublicAPI object 62 func NewPublicAPI(s *Service) *PublicAPI { 63 api := &PublicAPI{ 64 filters: make(map[getrpc.ID]filter), 65 latestBlockChangedEvent: s.latestBlockChangedEvent, 66 transactionSentToUpstreamEvent: s.transactionSentToUpstreamEvent, 67 68 client: func() ContextCaller { return s.rpc.RPCClient() }, 69 chainID: func() uint64 { return s.rpc.RPCClient().UpstreamChainID }, 70 filterLivenessLoop: defaultFilterLivenessPeriod, 71 filterLivenessPeriod: defaultFilterLivenessPeriod + 10*time.Second, 72 } 73 go api.timeoutLoop(s.quit) 74 return api 75 } 76 77 func (api *PublicAPI) timeoutLoop(quit chan struct{}) { 78 for { 79 select { 80 case <-quit: 81 return 82 case <-time.After(api.filterLivenessLoop): 83 api.filtersMu.Lock() 84 for id, f := range api.filters { 85 deadline := f.deadline() 86 if deadline == nil { 87 continue 88 } 89 select { 90 case <-deadline.C: 91 delete(api.filters, id) 92 f.stop() 93 default: 94 continue 95 } 96 } 97 api.filtersMu.Unlock() 98 } 99 } 100 } 101 102 func (api *PublicAPI) NewFilter(crit filters.FilterCriteria) (getrpc.ID, error) { 103 id := getrpc.ID(uuid.New()) 104 ctx, cancel := context.WithCancel(context.Background()) 105 f := &logsFilter{ 106 id: id, 107 crit: ethereum.FilterQuery(crit), 108 originalCrit: ethereum.FilterQuery(crit), 109 done: make(chan struct{}), 110 timer: time.NewTimer(api.filterLivenessPeriod), 111 ctx: ctx, 112 cancel: cancel, 113 logsCache: newCache(defaultCacheSize), 114 } 115 api.filtersMu.Lock() 116 api.filters[id] = f 117 api.filtersMu.Unlock() 118 go pollLogs(api.client(), api.chainID(), f, defaultLogsQueryTimeout, defaultLogsPeriod) 119 return id, nil 120 } 121 122 // NewBlockFilter is an implemenation of `eth_newBlockFilter` API 123 // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newblockfilter 124 func (api *PublicAPI) NewBlockFilter() getrpc.ID { 125 api.filtersMu.Lock() 126 defer api.filtersMu.Unlock() 127 128 f := newHashFilter() 129 id := getrpc.ID(uuid.New()) 130 131 api.filters[id] = f 132 133 go func() { 134 id, si := api.latestBlockChangedEvent.Subscribe() 135 s, ok := si.(chan common.Hash) 136 if !ok { 137 panic("latestBlockChangedEvent returned wrong type") 138 } 139 140 defer api.latestBlockChangedEvent.Unsubscribe(id) 141 142 for { 143 select { 144 case hash := <-s: 145 if err := f.add(hash); err != nil { 146 log.Error("error adding value to filter", "hash", hash, "error", err) 147 } 148 case <-f.done: 149 return 150 } 151 } 152 153 }() 154 155 return id 156 } 157 158 // NewPendingTransactionFilter is an implementation of `eth_newPendingTransactionFilter` API 159 // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newpendingtransactionfilter 160 func (api *PublicAPI) NewPendingTransactionFilter() getrpc.ID { 161 api.filtersMu.Lock() 162 defer api.filtersMu.Unlock() 163 164 f := newHashFilter() 165 id := getrpc.ID(uuid.New()) 166 167 api.filters[id] = f 168 169 go func() { 170 id, si := api.transactionSentToUpstreamEvent.Subscribe() 171 s, ok := si.(chan *PendingTxInfo) 172 if !ok { 173 panic("transactionSentToUpstreamEvent returned wrong type") 174 } 175 defer api.transactionSentToUpstreamEvent.Unsubscribe(id) 176 177 for { 178 select { 179 case hash := <-s: 180 if err := f.add(hash); err != nil { 181 log.Error("error adding value to filter", "hash", hash, "error", err) 182 } 183 case <-f.done: 184 return 185 } 186 } 187 }() 188 189 return id 190 191 } 192 193 // UninstallFilter is an implemenation of `eth_uninstallFilter` API 194 // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_uninstallfilter 195 func (api *PublicAPI) UninstallFilter(id getrpc.ID) bool { 196 api.filtersMu.Lock() 197 f, found := api.filters[id] 198 if found { 199 delete(api.filters, id) 200 } 201 api.filtersMu.Unlock() 202 203 if found { 204 f.stop() 205 } 206 207 return found 208 } 209 210 // GetFilterLogs returns the logs for the filter with the given id. 211 // If the filter could not be found an empty array of logs is returned. 212 // 213 // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterlogs 214 func (api *PublicAPI) GetFilterLogs(ctx context.Context, id getrpc.ID) ([]types.Log, error) { 215 api.filtersMu.Lock() 216 f, exist := api.filters[id] 217 api.filtersMu.Unlock() 218 if !exist { 219 return []types.Log{}, errFilterNotFound 220 } 221 logs, ok := f.(*logsFilter) 222 if !ok { 223 return []types.Log{}, fmt.Errorf("filter with ID %v is not of logs type", id) 224 } 225 ctx, cancel := context.WithTimeout(ctx, defaultLogsQueryTimeout) 226 defer cancel() 227 rst, err := getLogs(ctx, api.client(), api.chainID(), logs.originalCrit) 228 return rst, err 229 } 230 231 // GetFilterChanges returns the hashes for the filter with the given id since 232 // last time it was called. This can be used for polling. 233 // 234 // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterchanges 235 func (api *PublicAPI) GetFilterChanges(id getrpc.ID) (interface{}, error) { 236 api.filtersMu.Lock() 237 defer api.filtersMu.Unlock() 238 239 if f, found := api.filters[id]; found { 240 deadline := f.deadline() 241 if deadline != nil { 242 if !deadline.Stop() { 243 // timer expired but filter is not yet removed in timeout loop 244 // receive timer value and reset timer 245 // see https://golang.org/pkg/time/#Timer.Reset 246 <-deadline.C 247 } 248 deadline.Reset(api.filterLivenessPeriod) 249 } 250 rst := f.pop() 251 if rst == nil { 252 return []interface{}{}, nil 253 } 254 return rst, nil 255 } 256 return []interface{}{}, errFilterNotFound 257 }