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  }