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  }