github.com/fff-chain/go-fff@v0.0.0-20220726032732-1c84420b8a99/eth/filters/fffapi.go (about) 1 // Copyright 2015 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package filters 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "math/big" 25 "sync" 26 "time" 27 28 "github.com/fff-chain/go-fff" 29 "github.com/fff-chain/go-fff/common" 30 "github.com/fff-chain/go-fff/common/gopool" 31 "github.com/fff-chain/go-fff/core/types" 32 "github.com/fff-chain/go-fff/ethdb" 33 "github.com/fff-chain/go-fff/event" 34 "github.com/fff-chain/go-fff/rpc" 35 ) 36 37 // PublicFilterAPI offers support to create and manage filters. This will allow external clients to retrieve various 38 // information related to the Ethereum protocol such als blocks, transactions and logs. 39 type FFFPublicFilterAPI struct { 40 backend Backend 41 mux *event.TypeMux 42 quit chan struct{} 43 chainDb ethdb.Database 44 events *EventSystem 45 filtersMu sync.Mutex 46 filters map[rpc.ID]*filter 47 timeout time.Duration 48 rangeLimit bool 49 } 50 51 // NewPublicFilterAPI returns a new PublicFilterAPI instance. 52 func NewFFFPublicFilterAPI(backend Backend, lightMode bool, timeout time.Duration, rangeLimit bool) *PublicFilterAPI { 53 api := &PublicFilterAPI{ 54 backend: backend, 55 chainDb: backend.ChainDb(), 56 events: NewEventSystem(backend, lightMode), 57 filters: make(map[rpc.ID]*filter), 58 timeout: timeout, 59 rangeLimit: rangeLimit, 60 } 61 go api.timeoutLoop(timeout) 62 63 return api 64 } 65 66 // timeoutLoop runs at the interval set by 'timeout' and deletes filters 67 // that have not been recently used. It is started when the API is created. 68 func (api *FFFPublicFilterAPI) timeoutLoop(timeout time.Duration) { 69 var toUninstall []*Subscription 70 ticker := time.NewTicker(timeout) 71 defer ticker.Stop() 72 for { 73 <-ticker.C 74 api.filtersMu.Lock() 75 for id, f := range api.filters { 76 select { 77 case <-f.deadline.C: 78 toUninstall = append(toUninstall, f.s) 79 delete(api.filters, id) 80 default: 81 continue 82 } 83 } 84 api.filtersMu.Unlock() 85 86 // Unsubscribes are processed outside the lock to avoid the following scenario: 87 // event loop attempts broadcasting events to still active filters while 88 // Unsubscribe is waiting for it to process the uninstall request. 89 for _, s := range toUninstall { 90 s.Unsubscribe() 91 } 92 toUninstall = nil 93 } 94 } 95 96 // NewPendingTransactionFilter creates a filter that fetches pending transaction hashes 97 // as transactions enter the pending state. 98 // 99 // It is part of the filter package because this filter can be used through the 100 // `eth_getFilterChanges` polling method that is also used for log filters. 101 // 102 // https://eth.wiki/json-rpc/API#eth_newpendingtransactionfilter 103 func (api *FFFPublicFilterAPI) NewPendingTransactionFilter() rpc.ID { 104 var ( 105 pendingTxs = make(chan []common.Hash) 106 pendingTxSub = api.events.SubscribePendingTxs(pendingTxs) 107 ) 108 api.filtersMu.Lock() 109 api.filters[pendingTxSub.ID] = &filter{typ: PendingTransactionsSubscription, deadline: time.NewTimer(api.timeout), hashes: make([]common.Hash, 0), s: pendingTxSub} 110 api.filtersMu.Unlock() 111 112 gopool.Submit(func() { 113 for { 114 select { 115 case ph := <-pendingTxs: 116 api.filtersMu.Lock() 117 if f, found := api.filters[pendingTxSub.ID]; found { 118 f.hashes = append(f.hashes, ph...) 119 } 120 api.filtersMu.Unlock() 121 case <-pendingTxSub.Err(): 122 api.filtersMu.Lock() 123 delete(api.filters, pendingTxSub.ID) 124 api.filtersMu.Unlock() 125 return 126 } 127 } 128 }) 129 130 return pendingTxSub.ID 131 } 132 133 // NewPendingTransactions creates a subscription that is triggered each time a transaction 134 // enters the transaction pool and was signed from one of the transactions this nodes manages. 135 func (api *FFFPublicFilterAPI) NewPendingTransactions(ctx context.Context) (*rpc.Subscription, error) { 136 notifier, supported := rpc.NotifierFromContext(ctx) 137 if !supported { 138 return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported 139 } 140 141 rpcSub := notifier.CreateSubscription() 142 143 gopool.Submit(func() { 144 txHashes := make(chan []common.Hash, 128) 145 pendingTxSub := api.events.SubscribePendingTxs(txHashes) 146 147 for { 148 select { 149 case hashes := <-txHashes: 150 // To keep the original behaviour, send a single tx hash in one notification. 151 // TODO(rjl493456442) Send a batch of tx hashes in one notification 152 for _, h := range hashes { 153 notifier.Notify(rpcSub.ID, h) 154 } 155 case <-rpcSub.Err(): 156 pendingTxSub.Unsubscribe() 157 return 158 case <-notifier.Closed(): 159 pendingTxSub.Unsubscribe() 160 return 161 } 162 } 163 }) 164 165 return rpcSub, nil 166 } 167 168 // NewBlockFilter creates a filter that fetches blocks that are imported into the chain. 169 // It is part of the filter package since polling goes with eth_getFilterChanges. 170 // 171 // https://eth.wiki/json-rpc/API#eth_newblockfilter 172 func (api *FFFPublicFilterAPI) NewBlockFilter() rpc.ID { 173 var ( 174 headers = make(chan *types.Header) 175 headerSub = api.events.SubscribeNewHeads(headers) 176 ) 177 178 api.filtersMu.Lock() 179 api.filters[headerSub.ID] = &filter{typ: BlocksSubscription, deadline: time.NewTimer(api.timeout), hashes: make([]common.Hash, 0), s: headerSub} 180 api.filtersMu.Unlock() 181 182 gopool.Submit(func() { 183 for { 184 select { 185 case h := <-headers: 186 api.filtersMu.Lock() 187 if f, found := api.filters[headerSub.ID]; found { 188 f.hashes = append(f.hashes, h.Hash()) 189 } 190 api.filtersMu.Unlock() 191 case <-headerSub.Err(): 192 api.filtersMu.Lock() 193 delete(api.filters, headerSub.ID) 194 api.filtersMu.Unlock() 195 return 196 } 197 } 198 }) 199 200 return headerSub.ID 201 } 202 203 // NewHeads send a notification each time a new (header) block is appended to the chain. 204 func (api *FFFPublicFilterAPI) NewHeads(ctx context.Context) (*rpc.Subscription, error) { 205 notifier, supported := rpc.NotifierFromContext(ctx) 206 if !supported { 207 return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported 208 } 209 210 rpcSub := notifier.CreateSubscription() 211 212 gopool.Submit(func() { 213 headers := make(chan *types.Header) 214 headersSub := api.events.SubscribeNewHeads(headers) 215 216 for { 217 select { 218 case h := <-headers: 219 notifier.Notify(rpcSub.ID, h) 220 case <-rpcSub.Err(): 221 headersSub.Unsubscribe() 222 return 223 case <-notifier.Closed(): 224 headersSub.Unsubscribe() 225 return 226 } 227 } 228 }) 229 230 return rpcSub, nil 231 } 232 233 // Logs creates a subscription that fires for all new log that match the given filter criteria. 234 func (api *FFFPublicFilterAPI) Logs(ctx context.Context, crit FilterCriteria) (*rpc.Subscription, error) { 235 notifier, supported := rpc.NotifierFromContext(ctx) 236 if !supported { 237 return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported 238 } 239 240 var ( 241 rpcSub = notifier.CreateSubscription() 242 matchedLogs = make(chan []*types.Log) 243 ) 244 245 logsSub, err := api.events.SubscribeLogs(ethereum.FilterQuery(crit), matchedLogs) 246 if err != nil { 247 return nil, err 248 } 249 250 gopool.Submit(func() { 251 252 for { 253 select { 254 case logs := <-matchedLogs: 255 for _, log := range logs { 256 notifier.Notify(rpcSub.ID, &log) 257 } 258 case <-rpcSub.Err(): // client send an unsubscribe request 259 logsSub.Unsubscribe() 260 return 261 case <-notifier.Closed(): // connection dropped 262 logsSub.Unsubscribe() 263 return 264 } 265 } 266 }) 267 268 return rpcSub, nil 269 } 270 271 // FilterCriteria represents a request to create a new filter. 272 // Same as ethereum.FilterQuery but with UnmarshalJSON() method. 273 type FFFFilterCriteria ethereum.FFFFilterQuery 274 275 // NewFilter creates a new filter and returns the filter id. It can be 276 // used to retrieve logs when the state changes. This method cannot be 277 // used to fetch logs that are already stored in the state. 278 // 279 // Default criteria for the from and to block are "latest". 280 // Using "latest" as block number will return logs for mined blocks. 281 // Using "pending" as block number returns logs for not yet mined (pending) blocks. 282 // In case logs are removed (chain reorg) previously returned logs are returned 283 // again but with the removed property set to true. 284 // 285 // In case "fromBlock" > "toBlock" an error is returned. 286 // 287 // https://eth.wiki/json-rpc/API#eth_newfilter 288 func (api *FFFPublicFilterAPI) NewFilter(crit FFFFilterCriteria) (rpc.ID, error) { 289 a := new(FilterCriteria) 290 291 a.Topics = crit.Topics 292 a.BlockHash = crit.BlockHash 293 a.Addresses = common.FFFAddressesToAddresses(crit.Addresses) 294 a.FromBlock = crit.FromBlock 295 a.ToBlock = crit.ToBlock 296 297 logs := make(chan []*types.Log) 298 logsSub, err := api.events.SubscribeLogs(ethereum.FilterQuery(*a), logs) 299 if err != nil { 300 return "", err 301 } 302 303 api.filtersMu.Lock() 304 api.filters[logsSub.ID] = &filter{typ: LogsSubscription, crit: *a, deadline: time.NewTimer(api.timeout), logs: make([]*types.Log, 0), s: logsSub} 305 api.filtersMu.Unlock() 306 307 gopool.Submit(func() { 308 for { 309 select { 310 case l := <-logs: 311 api.filtersMu.Lock() 312 if f, found := api.filters[logsSub.ID]; found { 313 f.logs = append(f.logs, l...) 314 } 315 api.filtersMu.Unlock() 316 case <-logsSub.Err(): 317 api.filtersMu.Lock() 318 delete(api.filters, logsSub.ID) 319 api.filtersMu.Unlock() 320 return 321 } 322 } 323 }) 324 325 return logsSub.ID, nil 326 } 327 328 // GetLogs returns logs matching the given argument that are stored within the state. 329 // 330 // https://eth.wiki/json-rpc/API#eth_getlogs 331 func (api *FFFPublicFilterAPI) GetLogs(ctx context.Context, crit FFFFilterCriteria) ([]*types.Log, error) { 332 333 334 335 var filter *Filter 336 if crit.BlockHash != nil { 337 // Block filter requested, construct a single-shot filter 338 filter = NewBlockFilter(api.backend, *crit.BlockHash, common.FFFAddressesToAddresses( crit.Addresses), crit.Topics) 339 } else { 340 // Convert the RPC block numbers into internal representations 341 begin := rpc.LatestBlockNumber.Int64() 342 if crit.FromBlock != nil { 343 begin = crit.FromBlock.Int64() 344 } 345 end := rpc.LatestBlockNumber.Int64() 346 if crit.ToBlock != nil { 347 end = crit.ToBlock.Int64() 348 } 349 // Construct the range filter 350 filter = NewRangeFilter(api.backend, begin, end, common.FFFAddressesToAddresses( crit.Addresses), crit.Topics, api.rangeLimit) 351 } 352 // Run the filter and return all the logs 353 logs, err := filter.Logs(ctx) 354 if err != nil { 355 return nil, err 356 } 357 return returnLogs(logs), err 358 } 359 360 // UninstallFilter removes the filter with the given filter id. 361 // 362 // https://eth.wiki/json-rpc/API#eth_uninstallfilter 363 func (api *FFFPublicFilterAPI) UninstallFilter(id rpc.ID) bool { 364 api.filtersMu.Lock() 365 f, found := api.filters[id] 366 if found { 367 delete(api.filters, id) 368 } 369 api.filtersMu.Unlock() 370 if found { 371 f.s.Unsubscribe() 372 } 373 374 return found 375 } 376 377 // GetFilterLogs returns the logs for the filter with the given id. 378 // If the filter could not be found an empty array of logs is returned. 379 // 380 // https://eth.wiki/json-rpc/API#eth_getfilterlogs 381 func (api *FFFPublicFilterAPI) GetFilterLogs(ctx context.Context, id rpc.ID) ([]*types.Log, error) { 382 api.filtersMu.Lock() 383 f, found := api.filters[id] 384 api.filtersMu.Unlock() 385 386 if !found || f.typ != LogsSubscription { 387 return nil, fmt.Errorf("filter not found") 388 } 389 390 var filter *Filter 391 if f.crit.BlockHash != nil { 392 // Block filter requested, construct a single-shot filter 393 filter = NewBlockFilter(api.backend, *f.crit.BlockHash, f.crit.Addresses, f.crit.Topics) 394 } else { 395 // Convert the RPC block numbers into internal representations 396 begin := rpc.LatestBlockNumber.Int64() 397 if f.crit.FromBlock != nil { 398 begin = f.crit.FromBlock.Int64() 399 } 400 end := rpc.LatestBlockNumber.Int64() 401 if f.crit.ToBlock != nil { 402 end = f.crit.ToBlock.Int64() 403 } 404 // Construct the range filter 405 filter = NewRangeFilter(api.backend, begin, end, f.crit.Addresses, f.crit.Topics, api.rangeLimit) 406 } 407 // Run the filter and return all the logs 408 logs, err := filter.Logs(ctx) 409 if err != nil { 410 return nil, err 411 } 412 return returnLogs(logs), nil 413 } 414 415 // GetFilterChanges returns the logs for the filter with the given id since 416 // last time it was called. This can be used for polling. 417 // 418 // For pending transaction and block filters the result is []common.Hash. 419 // (pending)Log filters return []Log. 420 // 421 // https://eth.wiki/json-rpc/API#eth_getfilterchanges 422 func (api *FFFPublicFilterAPI) GetFilterChanges(id rpc.ID) (interface{}, error) { 423 api.filtersMu.Lock() 424 defer api.filtersMu.Unlock() 425 426 if f, found := api.filters[id]; found { 427 if !f.deadline.Stop() { 428 // timer expired but filter is not yet removed in timeout loop 429 // receive timer value and reset timer 430 <-f.deadline.C 431 } 432 f.deadline.Reset(api.timeout) 433 434 switch f.typ { 435 case PendingTransactionsSubscription, BlocksSubscription: 436 hashes := f.hashes 437 f.hashes = nil 438 return returnHashes(hashes), nil 439 case LogsSubscription, MinedAndPendingLogsSubscription: 440 logs := f.logs 441 f.logs = nil 442 return returnLogs(logs), nil 443 } 444 } 445 446 return []interface{}{}, fmt.Errorf("filter not found") 447 } 448 449 450 451 // UnmarshalJSON sets *args fields with given data. 452 func (args *FFFFilterCriteria) UnmarshalJSON(data []byte) error { 453 type input struct { 454 BlockHash *common.Hash `json:"blockHash"` 455 FromBlock *rpc.BlockNumber `json:"fromBlock"` 456 ToBlock *rpc.BlockNumber `json:"toBlock"` 457 Addresses interface{} `json:"address"` 458 Topics []interface{} `json:"topics"` 459 } 460 461 var raw input 462 if err := json.Unmarshal(data, &raw); err != nil { 463 return err 464 } 465 466 if raw.BlockHash != nil { 467 if raw.FromBlock != nil || raw.ToBlock != nil { 468 // BlockHash is mutually exclusive with FromBlock/ToBlock criteria 469 return fmt.Errorf("cannot specify both BlockHash and FromBlock/ToBlock, choose one or the other") 470 } 471 args.BlockHash = raw.BlockHash 472 } else { 473 if raw.FromBlock != nil { 474 args.FromBlock = big.NewInt(raw.FromBlock.Int64()) 475 } 476 477 if raw.ToBlock != nil { 478 args.ToBlock = big.NewInt(raw.ToBlock.Int64()) 479 } 480 } 481 482 args.Addresses = []common.FFFAddress{} 483 484 if raw.Addresses != nil { 485 // raw.Address can contain a single address or an array of addresses 486 switch rawAddr := raw.Addresses.(type) { 487 case []interface{}: 488 for i, addr := range rawAddr { 489 if strAddr, ok := addr.(string); ok { 490 addr, err := decodeAddress(strAddr) 491 if err != nil { 492 return fmt.Errorf("invalid address at index %d: %v", i, err) 493 } 494 args.Addresses = append(args.Addresses, common.AddressToFFFAddress(addr)) 495 } else { 496 return fmt.Errorf("non-string address at index %d", i) 497 } 498 } 499 case string: 500 addr, err := decodeAddress(rawAddr) 501 if err != nil { 502 return fmt.Errorf("invalid address: %v", err) 503 } 504 args.Addresses = []common.FFFAddress{common.AddressToFFFAddress(addr)} 505 default: 506 return errors.New("invalid addresses in query") 507 } 508 } 509 510 // topics is an array consisting of strings and/or arrays of strings. 511 // JSON null values are converted to common.Hash{} and ignored by the filter manager. 512 if len(raw.Topics) > 0 { 513 args.Topics = make([][]common.Hash, len(raw.Topics)) 514 for i, t := range raw.Topics { 515 switch topic := t.(type) { 516 case nil: 517 // ignore topic when matching logs 518 519 case string: 520 // match specific topic 521 top, err := decodeTopic(topic) 522 if err != nil { 523 return err 524 } 525 args.Topics[i] = []common.Hash{top} 526 527 case []interface{}: 528 // or case e.g. [null, "topic0", "topic1"] 529 for _, rawTopic := range topic { 530 if rawTopic == nil { 531 // null component, match all 532 args.Topics[i] = nil 533 break 534 } 535 if topic, ok := rawTopic.(string); ok { 536 parsed, err := decodeTopic(topic) 537 if err != nil { 538 return err 539 } 540 args.Topics[i] = append(args.Topics[i], parsed) 541 } else { 542 return fmt.Errorf("invalid topic(s)") 543 } 544 } 545 default: 546 return fmt.Errorf("invalid topic(s)") 547 } 548 } 549 } 550 551 return nil 552 }