github.com/amazechain/amc@v0.1.3/internal/api/filters/api.go (about) 1 package filters 2 3 import ( 4 "context" 5 "fmt" 6 "github.com/amazechain/amc/common/block" 7 "github.com/amazechain/amc/common/types" 8 mvm_types "github.com/amazechain/amc/internal/avm/types" 9 "github.com/amazechain/amc/modules/rpc/jsonrpc" 10 "sync" 11 "time" 12 ) 13 14 // filter is a helper struct that holds metadata information over the filter type 15 // and associated subscription in the event system. 16 type filter struct { 17 typ Type 18 deadline *time.Timer // filter is inactiv when deadline triggers 19 hashes []types.Hash 20 crit FilterCriteria 21 logs []*block.Log 22 s *Subscription // associated subscription in event system 23 } 24 25 // FilterAPI offers support to create and manage filters. This will allow external clients to retrieve various 26 // information related to the Ethereum protocol such als blocks, transactions and logs. 27 type FilterAPI struct { 28 api Api 29 events *EventSystem 30 filtersMu sync.Mutex 31 filters map[jsonrpc.ID]*filter 32 timeout time.Duration 33 } 34 35 // NewFilterAPI returns a new FilterAPI instance. 36 func NewFilterAPI(api Api, timeout time.Duration) *FilterAPI { 37 filterAPI := &FilterAPI{ 38 api: api, 39 events: NewEventSystem(api), 40 filters: make(map[jsonrpc.ID]*filter), 41 timeout: timeout, 42 } 43 go filterAPI.timeoutLoop(timeout) 44 45 return filterAPI 46 } 47 48 // timeoutLoop runs at the interval set by 'timeout' and deletes filters 49 // that have not been recently used. It is started when the API is created. 50 func (filterApi *FilterAPI) timeoutLoop(timeout time.Duration) { 51 var toUninstall []*Subscription 52 ticker := time.NewTicker(timeout) 53 defer ticker.Stop() 54 for { 55 <-ticker.C 56 filterApi.filtersMu.Lock() 57 for id, f := range filterApi.filters { 58 select { 59 case <-f.deadline.C: 60 toUninstall = append(toUninstall, f.s) 61 delete(filterApi.filters, id) 62 default: 63 continue 64 } 65 } 66 filterApi.filtersMu.Unlock() 67 68 // Unsubscribes are processed outside the lock to avoid the following scenario: 69 // event loop attempts broadcasting events to still active filters while 70 // Unsubscribe is waiting for it to process the uninstall request. 71 for _, s := range toUninstall { 72 s.Unsubscribe() 73 } 74 toUninstall = nil 75 } 76 } 77 78 // NewPendingTransactionFilter creates a filter that fetches pending transaction hashes 79 // as transactions enter the pending state. 80 // 81 // It is part of the filter package because this filter can be used through the 82 // `eth_getFilterChanges` polling method that is also used for log filters. 83 func (filterApi *FilterAPI) NewPendingTransactionFilter() jsonrpc.ID { 84 var ( 85 pendingTxs = make(chan []types.Hash) 86 pendingTxSub = filterApi.events.SubscribePendingTxs(pendingTxs) 87 ) 88 89 filterApi.filtersMu.Lock() 90 filterApi.filters[pendingTxSub.ID] = &filter{typ: PendingTransactionsSubscription, deadline: time.NewTimer(filterApi.timeout), hashes: make([]types.Hash, 0), s: pendingTxSub} 91 filterApi.filtersMu.Unlock() 92 93 go func() { 94 for { 95 select { 96 case ph := <-pendingTxs: 97 filterApi.filtersMu.Lock() 98 if f, found := filterApi.filters[pendingTxSub.ID]; found { 99 f.hashes = append(f.hashes, ph...) 100 } 101 filterApi.filtersMu.Unlock() 102 case <-pendingTxSub.Err(): 103 filterApi.filtersMu.Lock() 104 delete(filterApi.filters, pendingTxSub.ID) 105 filterApi.filtersMu.Unlock() 106 return 107 } 108 } 109 }() 110 111 return pendingTxSub.ID 112 } 113 114 // NewPendingTransactions creates a subscription that is triggered each time a transaction 115 // enters the transaction pool and was signed from one of the transactions this nodes manages. 116 func (filterApi *FilterAPI) NewPendingTransactions(ctx context.Context) (*jsonrpc.Subscription, error) { 117 notifier, supported := jsonrpc.NotifierFromContext(ctx) 118 if !supported { 119 return &jsonrpc.Subscription{}, jsonrpc.ErrNotificationsUnsupported 120 } 121 122 rpcSub := notifier.CreateSubscription() 123 124 go func() { 125 txHashes := make(chan []types.Hash, 128) 126 pendingTxSub := filterApi.events.SubscribePendingTxs(txHashes) 127 128 for { 129 select { 130 case hashes := <-txHashes: 131 // To keep the original behaviour, send a single tx hash in one notification. 132 // TODO(rjl493456442) Send a batch of tx hashes in one notification 133 for _, h := range hashes { 134 notifier.Notify(rpcSub.ID, h) 135 } 136 case <-rpcSub.Err(): 137 pendingTxSub.Unsubscribe() 138 return 139 case <-notifier.Closed(): 140 pendingTxSub.Unsubscribe() 141 return 142 } 143 } 144 }() 145 146 return rpcSub, nil 147 } 148 149 // NewBlockFilter creates a filter that fetches blocks that are imported into the chain. 150 // It is part of the filter package since polling goes with eth_getFilterChanges. 151 func (filterApi *FilterAPI) NewBlockFilter() jsonrpc.ID { 152 var ( 153 headers = make(chan block.IHeader) 154 headerSub = filterApi.events.SubscribeNewHeads(headers) 155 ) 156 157 filterApi.filtersMu.Lock() 158 filterApi.filters[headerSub.ID] = &filter{typ: BlocksSubscription, deadline: time.NewTimer(filterApi.timeout), hashes: make([]types.Hash, 0), s: headerSub} 159 filterApi.filtersMu.Unlock() 160 161 go func() { 162 for { 163 select { 164 case h := <-headers: 165 filterApi.filtersMu.Lock() 166 if f, found := filterApi.filters[headerSub.ID]; found { 167 f.hashes = append(f.hashes, h.Hash()) 168 } 169 filterApi.filtersMu.Unlock() 170 case <-headerSub.Err(): 171 filterApi.filtersMu.Lock() 172 delete(filterApi.filters, headerSub.ID) 173 filterApi.filtersMu.Unlock() 174 return 175 } 176 } 177 }() 178 179 return headerSub.ID 180 } 181 182 // NewHeads send a notification each time a new (header) block is appended to the chain. 183 func (filterApi *FilterAPI) NewHeads(ctx context.Context) (*jsonrpc.Subscription, error) { 184 notifier, supported := jsonrpc.NotifierFromContext(ctx) 185 if !supported { 186 return &jsonrpc.Subscription{}, jsonrpc.ErrNotificationsUnsupported 187 } 188 189 rpcSub := notifier.CreateSubscription() 190 191 go func() { 192 headers := make(chan block.IHeader) 193 headersSub := filterApi.events.SubscribeNewHeads(headers) 194 for { 195 select { 196 case h := <-headers: 197 notifier.Notify(rpcSub.ID, mvm_types.FromAmcHeader(h)) 198 case <-rpcSub.Err(): 199 headersSub.Unsubscribe() 200 return 201 case <-notifier.Closed(): 202 headersSub.Unsubscribe() 203 return 204 } 205 } 206 }() 207 208 return rpcSub, nil 209 } 210 211 // Logs creates a subscription that fires for all new log that match the given filter criteria. 212 func (filterApi *FilterAPI) Logs(ctx context.Context, crit FilterCriteria) (*jsonrpc.Subscription, error) { 213 notifier, supported := jsonrpc.NotifierFromContext(ctx) 214 if !supported { 215 return &jsonrpc.Subscription{}, jsonrpc.ErrNotificationsUnsupported 216 } 217 218 var ( 219 rpcSub = notifier.CreateSubscription() 220 matchedLogs = make(chan []*block.Log) 221 ) 222 223 logsSub, err := filterApi.events.SubscribeLogs(crit, matchedLogs) 224 if err != nil { 225 return nil, err 226 } 227 228 go func() { 229 for { 230 select { 231 case logs := <-matchedLogs: 232 for _, log := range logs { 233 log := log 234 notifier.Notify(rpcSub.ID, &log) 235 } 236 case <-rpcSub.Err(): // client send an unsubscribe request 237 logsSub.Unsubscribe() 238 return 239 case <-notifier.Closed(): // connection dropped 240 logsSub.Unsubscribe() 241 return 242 } 243 } 244 }() 245 246 return rpcSub, nil 247 } 248 249 // NewFilter creates a new filter and returns the filter id. It can be 250 // used to retrieve logs when the state changes. This method cannot be 251 // used to fetch logs that are already stored in the state. 252 // 253 // Default criteria for the from and to block are "latest". 254 // Using "latest" as block number will return logs for mined blocks. 255 // Using "pending" as block number returns logs for not yet mined (pending) blocks. 256 // In case logs are removed (chain reorg) previously returned logs are returned 257 // again but with the removed property set to true. 258 // 259 // In case "fromBlock" > "toBlock" an error is returned. 260 func (filterApi *FilterAPI) NewFilter(crit FilterCriteria) (jsonrpc.ID, error) { 261 logs := make(chan []*block.Log) 262 logsSub, err := filterApi.events.SubscribeLogs(crit, logs) 263 if err != nil { 264 return "", err 265 } 266 267 filterApi.filtersMu.Lock() 268 filterApi.filters[logsSub.ID] = &filter{typ: LogsSubscription, crit: crit, deadline: time.NewTimer(filterApi.timeout), logs: make([]*block.Log, 0), s: logsSub} 269 filterApi.filtersMu.Unlock() 270 271 go func() { 272 for { 273 select { 274 case l := <-logs: 275 filterApi.filtersMu.Lock() 276 if f, found := filterApi.filters[logsSub.ID]; found { 277 f.logs = append(f.logs, l...) 278 } 279 filterApi.filtersMu.Unlock() 280 case <-logsSub.Err(): 281 filterApi.filtersMu.Lock() 282 delete(filterApi.filters, logsSub.ID) 283 filterApi.filtersMu.Unlock() 284 return 285 } 286 } 287 }() 288 289 return logsSub.ID, nil 290 } 291 292 // GetLogs returns logs matching the given argument that are stored within the state. 293 func (filterApi *FilterAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*block.Log, error) { 294 var filter *Filter 295 if crit.BlockHash != (types.Hash{}) { 296 // Block filter requested, construct a single-shot filter 297 filter = NewBlockFilter(filterApi.api, crit.BlockHash, crit.Addresses, crit.Topics) 298 } else { 299 // Convert the RPC block numbers into internal representations 300 begin := jsonrpc.LatestBlockNumber.Int64() 301 if crit.FromBlock != nil { 302 begin = crit.FromBlock.Int64() 303 } 304 end := jsonrpc.LatestBlockNumber.Int64() 305 if crit.ToBlock != nil { 306 end = crit.ToBlock.Int64() 307 } 308 // Construct the range filter 309 filter = NewRangeFilter(filterApi.api, begin, end, crit.Addresses, crit.Topics) 310 } 311 // Run the filter and return all the logs 312 logs, err := filter.Logs(ctx) 313 if err != nil { 314 return nil, err 315 } 316 return returnLogs(logs), err 317 } 318 319 // UninstallFilter removes the filter with the given filter id. 320 func (filterApi *FilterAPI) UninstallFilter(id jsonrpc.ID) bool { 321 filterApi.filtersMu.Lock() 322 f, found := filterApi.filters[id] 323 if found { 324 delete(filterApi.filters, id) 325 } 326 filterApi.filtersMu.Unlock() 327 if found { 328 f.s.Unsubscribe() 329 } 330 331 return found 332 } 333 334 // GetFilterLogs returns the logs for the filter with the given id. 335 // If the filter could not be found an empty array of logs is returned. 336 func (filterApi *FilterAPI) GetFilterLogs(ctx context.Context, id jsonrpc.ID) ([]*block.Log, error) { 337 filterApi.filtersMu.Lock() 338 f, found := filterApi.filters[id] 339 filterApi.filtersMu.Unlock() 340 341 if !found || f.typ != LogsSubscription { 342 return nil, fmt.Errorf("filter not found") 343 } 344 345 var filter *Filter 346 if f.crit.BlockHash != (types.Hash{}) { 347 // Block filter requested, construct a single-shot filter 348 filter = NewBlockFilter(filterApi.api, f.crit.BlockHash, f.crit.Addresses, f.crit.Topics) 349 } else { 350 // Convert the RPC block numbers into internal representations 351 begin := jsonrpc.LatestBlockNumber.Int64() 352 if f.crit.FromBlock != nil { 353 begin = f.crit.FromBlock.Int64() 354 } 355 end := jsonrpc.LatestBlockNumber.Int64() 356 if f.crit.ToBlock != nil { 357 end = f.crit.ToBlock.Int64() 358 } 359 // Construct the range filter 360 filter = NewRangeFilter(filterApi.api, begin, end, f.crit.Addresses, f.crit.Topics) 361 } 362 // Run the filter and return all the logs 363 logs, err := filter.Logs(ctx) 364 if err != nil { 365 return nil, err 366 } 367 return returnLogs(logs), nil 368 } 369 370 // GetFilterChanges returns the logs for the filter with the given id since 371 // last time it was called. This can be used for polling. 372 // 373 // For pending transaction and block filters the result is []types.Hash. 374 // (pending)Log filters return []Log. 375 func (filterApi *FilterAPI) GetFilterChanges(id jsonrpc.ID) (interface{}, error) { 376 filterApi.filtersMu.Lock() 377 defer filterApi.filtersMu.Unlock() 378 379 if f, found := filterApi.filters[id]; found { 380 if !f.deadline.Stop() { 381 // timer expired but filter is not yet removed in timeout loop 382 // receive timer value and reset timer 383 <-f.deadline.C 384 } 385 f.deadline.Reset(filterApi.timeout) 386 387 switch f.typ { 388 case PendingTransactionsSubscription, BlocksSubscription: 389 hashes := f.hashes 390 f.hashes = nil 391 return returnHashes(hashes), nil 392 case LogsSubscription, MinedAndPendingLogsSubscription: 393 logs := f.logs 394 f.logs = nil 395 return returnLogs(logs), nil 396 } 397 } 398 399 return []interface{}{}, fmt.Errorf("filter not found") 400 } 401 402 // returnHashes is a helper that will return an empty hash array case the given hash array is nil, 403 // otherwise the given hashes array is returned. 404 func returnHashes(hashes []types.Hash) []types.Hash { 405 if hashes == nil { 406 return []types.Hash{} 407 } 408 return hashes 409 } 410 411 // returnLogs is a helper that will return an empty log array in case the given logs array is nil, 412 // otherwise the given logs array is returned. 413 func returnLogs(logs []*block.Log) []*block.Log { 414 if logs == nil { 415 return []*block.Log{} 416 } 417 return logs 418 }