github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/pkg/rpc/provider/provider.go (about)

     1  package provider
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sync"
     8  
     9  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base"
    10  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/logger"
    11  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/types"
    12  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/utils"
    13  )
    14  
    15  // When providers create a new buffered channel, they set this as buffer size.
    16  const providerChannelBufferSize = 50
    17  
    18  // SlurpedPageItem carries Transaction (if available) and Appearance (to give more info
    19  // about Transaction, like Address). Either Transaction or Appearance comes from 3rd party
    20  // RPC provider. For example, from Etherscan we get Transaction, but from Key - Appearance.
    21  type SlurpedPageItem struct {
    22  	Appearance  *types.Appearance
    23  	Transaction *types.Slurp
    24  }
    25  
    26  // Provider is an abstraction over different RPC services, like Etherscan or Key
    27  type Provider interface {
    28  	// TransactionsByAddress returns a channel that will be populated with transactions
    29  	TransactionsByAddress(context.Context, *Query, chan error) chan types.Slurp
    30  
    31  	// Appearances returns a channel that will be populated with appearances
    32  	Appearances(context.Context, *Query, chan error) chan types.Appearance
    33  
    34  	// Count returns a channel that will be populated with monitors, one for each address.
    35  	// These monitors will only have NRecords set.
    36  	Count(context.Context, *Query, chan error) chan types.Monitor
    37  
    38  	// NewPaginator creates and returns Paginator that should be used for given service
    39  	NewPaginator(*Query) Paginator
    40  
    41  	// PrintProgress returns true if Provider should print progress bar
    42  	PrintProgress() bool
    43  	SetPrintProgress(bool)
    44  }
    45  
    46  // fetchPageFunc is a function that gets a page of data from 3rd party provider
    47  type fetchPageFunc func(ctx context.Context, address base.Address, paginator Paginator, resource string) ([]SlurpedPageItem, int, error)
    48  
    49  // fetchAndFilterData gets all pages available for every query.Address and every query.Resource and push individual page items to a channel.
    50  // It is a helper that encapsulates logic shared by multiple providers
    51  func fetchAndFilterData(ctx context.Context, provider Provider, query *Query, errorChan chan error, fetchPage fetchPageFunc) (resultChan chan SlurpedPageItem) {
    52  	resultChan = make(chan SlurpedPageItem, providerChannelBufferSize)
    53  
    54  	totalFetched := 0
    55  	totalFiltered := 0
    56  
    57  	go func() {
    58  		defer close(resultChan)
    59  
    60  		for _, address := range query.Addresses {
    61  			for _, resource := range query.Resources {
    62  				bar := logger.NewBar(logger.BarOptions{
    63  					Type:    logger.Expanding,
    64  					Enabled: provider.PrintProgress(),
    65  					Prefix:  fmt.Sprintf("%s %s", utils.FormattedHash(false, address.String()), resource),
    66  				})
    67  
    68  				// Get a paginator so we can iterate
    69  				paginator := provider.NewPaginator(query)
    70  
    71  				for !paginator.Done() {
    72  					select {
    73  					case <-ctx.Done():
    74  						return
    75  					default:
    76  						if err := paginator.NextPage(); err != nil {
    77  							errorChan <- err
    78  							return
    79  						}
    80  						page, fetched, err := fetchPage(ctx, address, paginator, resource)
    81  						totalFetched += fetched
    82  						if err != nil {
    83  							errorChan <- err
    84  							// we break here because if cannot fetch the page, we don't
    85  							// know if we're finished (we can get into infinite loop)
    86  							paginator.SetDone(true)
    87  							break
    88  						}
    89  
    90  						for _, slurpedItem := range page {
    91  							// Make sure we want this block
    92  							if ok, err := query.InRange(base.Blknum(slurpedItem.Appearance.BlockNumber)); !ok {
    93  								if err != nil {
    94  									errorChan <- err
    95  								}
    96  								continue
    97  							}
    98  							totalFiltered++
    99  							bar.Tick()
   100  
   101  							resultChan <- slurpedItem
   102  						}
   103  					}
   104  				}
   105  				bar.Finish(true /* newLine */)
   106  			}
   107  			if totalFiltered == 0 {
   108  				msg := fmt.Sprintf("zero transactions reported, %d fetched", totalFetched)
   109  				errorChan <- errors.New(msg)
   110  			}
   111  		}
   112  	}()
   113  
   114  	return
   115  }
   116  
   117  // countSlurped turns a channel of SlurpedPageItem into a channel of Monitors.
   118  // It is a helper that encapsulates logic shared by multiple providers
   119  func countSlurped(ctx context.Context, query *Query, slurpedChan chan SlurpedPageItem) (monitorChan chan types.Monitor) {
   120  	monitorChan = make(chan types.Monitor)
   121  
   122  	recordCount := make(map[base.Address]types.Monitor, len(query.Addresses))
   123  	var mu sync.Mutex
   124  
   125  	go func() {
   126  		defer close(monitorChan)
   127  		for {
   128  			select {
   129  			case <-ctx.Done():
   130  				return
   131  			case slurpedTx, ok := <-slurpedChan:
   132  				if !ok {
   133  					for _, monitor := range recordCount {
   134  						monitorChan <- monitor
   135  					}
   136  					return
   137  				}
   138  				mu.Lock()
   139  				address := slurpedTx.Appearance.Address
   140  				monitor := recordCount[address]
   141  				recordCount[address] = types.Monitor{
   142  					Address:  address,
   143  					NRecords: monitor.NRecords + 1,
   144  				}
   145  				mu.Unlock()
   146  			}
   147  		}
   148  	}()
   149  
   150  	return
   151  }