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

     1  package provider
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"strings"
     7  
     8  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base"
     9  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config"
    10  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/rpc"
    11  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/rpc/query"
    12  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/types"
    13  	"golang.org/x/time/rate"
    14  )
    15  
    16  const alchemyFirstPage = ""
    17  const alchemyRequestsPerSecond = 5 // 330 Compute Units per second
    18  // const alchemyMaxPerPage = 1000
    19  const alchemyBaseUrl = "https://eth-mainnet.g.alchemy.com/v2/"
    20  
    21  func alchemyPrepareQuery(q *Query) (result *Query, err error) {
    22  	result = q.Dup()
    23  	result.Resources = make([]string, 0, len(q.Resources)*2)
    24  	for _, resource := range q.Resources {
    25  		cat := alchemyCategory(resource)
    26  		if cat == "" {
    27  			// Ignore unknown categories, so the user can simply use
    28  			// `slurp --types all`
    29  			continue
    30  		}
    31  		result.Resources = append(result.Resources, cat+":to", cat+":from")
    32  	}
    33  
    34  	return
    35  }
    36  
    37  func alchemyCategory(requestType string) (category string) {
    38  	switch requestType {
    39  	case "ext":
    40  		category = "external"
    41  	case "int":
    42  		category = "internal"
    43  	case "token":
    44  		category = "erc20"
    45  	case "nfts":
    46  		category = "erc721"
    47  	case "1155":
    48  		category = "erc1155"
    49  	}
    50  	return
    51  }
    52  
    53  type AlchemyProvider struct {
    54  	printProgress            bool
    55  	perPage                  int //nolint:unused
    56  	conn                     *rpc.Connection
    57  	limiter                  *rate.Limiter
    58  	baseUrl                  string
    59  	chain                    string
    60  	getTransactionAppearance func(hash string) (types.Appearance, error)
    61  }
    62  
    63  func NewAlchemyProvider(conn *rpc.Connection, chain string) (p *AlchemyProvider, err error) {
    64  	apiKey := config.GetKey("alchemy").ApiKey
    65  	if apiKey == "" {
    66  		err = errors.New("missing Alchemy API key")
    67  		return
    68  	}
    69  
    70  	p = &AlchemyProvider{
    71  		conn:    conn,
    72  		chain:   chain,
    73  		baseUrl: alchemyBaseUrl + apiKey,
    74  	}
    75  	p.printProgress = true
    76  	p.limiter = rate.NewLimiter(alchemyRequestsPerSecond, alchemyRequestsPerSecond)
    77  	p.getTransactionAppearance = p.defaultGetTransactionAppearance
    78  
    79  	return
    80  }
    81  
    82  func (p *AlchemyProvider) PrintProgress() bool {
    83  	return p.printProgress
    84  }
    85  
    86  func (p *AlchemyProvider) SetPrintProgress(print bool) {
    87  	p.printProgress = print
    88  }
    89  
    90  func (p *AlchemyProvider) NewPaginator(query *Query) Paginator {
    91  	pageId := query.StartPageId
    92  	if pageId == "" {
    93  		pageId = alchemyFirstPage
    94  	}
    95  
    96  	return NewPageIdPaginator(pageId, pageId, int(query.PerPage))
    97  }
    98  
    99  func (p *AlchemyProvider) TransactionsByAddress(ctx context.Context, query *Query, errorChan chan error) (txChan chan types.Slurp) {
   100  	txChan = make(chan types.Slurp, providerChannelBufferSize)
   101  
   102  	prepQuery, err := alchemyPrepareQuery(query)
   103  	if err != nil {
   104  		errorChan <- err
   105  		return
   106  	}
   107  	slurpedChan := fetchAndFilterData(ctx, p, prepQuery, errorChan, p.fetchData)
   108  	go func() {
   109  		defer close(txChan)
   110  		for {
   111  			select {
   112  			case <-ctx.Done():
   113  				return
   114  			case item, ok := <-slurpedChan:
   115  				if !ok {
   116  					return
   117  				}
   118  				txChan <- *item.Transaction
   119  			}
   120  		}
   121  	}()
   122  
   123  	return
   124  }
   125  
   126  func (p *AlchemyProvider) Appearances(ctx context.Context, query *Query, errorChan chan error) (appChan chan types.Appearance) {
   127  	appChan = make(chan types.Appearance, providerChannelBufferSize)
   128  
   129  	prepQuery, err := alchemyPrepareQuery(query)
   130  	if err != nil {
   131  		errorChan <- err
   132  		return
   133  	}
   134  	slurpedChan := fetchAndFilterData(ctx, p, prepQuery, errorChan, p.fetchData)
   135  	go func() {
   136  		defer close(appChan)
   137  		for {
   138  			select {
   139  			case <-ctx.Done():
   140  				return
   141  			case item, ok := <-slurpedChan:
   142  				if !ok {
   143  					return
   144  				}
   145  				appChan <- *item.Appearance
   146  			}
   147  		}
   148  	}()
   149  
   150  	return
   151  }
   152  
   153  func (p *AlchemyProvider) Count(ctx context.Context, query *Query, errorChan chan error) (monitorChan chan types.Monitor) {
   154  	prepQuery, err := alchemyPrepareQuery(query)
   155  	if err != nil {
   156  		errorChan <- err
   157  		return
   158  	}
   159  	slurpedChan := fetchAndFilterData(ctx, p, prepQuery, errorChan, p.fetchData)
   160  	return countSlurped(ctx, query, slurpedChan)
   161  }
   162  
   163  type alchemyRequestParam struct {
   164  	ToBlock     string   `json:"toBlock,omitempty"`
   165  	ToAddress   string   `json:"toAddress,omitempty"`
   166  	FromAddress string   `json:"fromAddress,omitempty"`
   167  	Category    []string `json:"category,omitempty"`
   168  	PageKey     string   `json:"pageKey,omitempty"`
   169  }
   170  
   171  type alchemyResponseBody struct {
   172  	Transfers []AlchemyTx `json:"transfers"`
   173  	PageKey   string      `json:"pageKey"`
   174  }
   175  
   176  type AlchemyTx struct {
   177  	BlockNumber string `json:"blockNum"`
   178  	Hash        string `json:"hash"`
   179  	From        string `json:"from"`
   180  	To          string `json:"to"`
   181  }
   182  
   183  func (tx *AlchemyTx) SimpleSlurp() (s types.Slurp, err error) {
   184  	s = types.Slurp{
   185  		BlockNumber: base.MustParseBlknum(tx.BlockNumber),
   186  		Hash:        base.HexToHash(tx.Hash),
   187  		From:        base.HexToAddress(tx.From),
   188  		To:          base.HexToAddress(tx.To),
   189  	}
   190  
   191  	return
   192  }
   193  
   194  func (e *AlchemyProvider) fetchData(ctx context.Context, address base.Address, paginator Paginator, categoryToken string) (data []SlurpedPageItem, count int, err error) {
   195  	pageKey, ok := paginator.Page().(string)
   196  	if !ok {
   197  		err = errors.New("cannot get page id")
   198  		return
   199  	}
   200  
   201  	// categoryToken has form of alchemyCategory[:from|to]
   202  	category := strings.Split(categoryToken, ":")
   203  
   204  	method := "alchemy_getAssetTransfers"
   205  	requestParam := alchemyRequestParam{
   206  		ToBlock:  "latest",
   207  		Category: []string{category[0]},
   208  		PageKey:  pageKey,
   209  	}
   210  	if len(category) > 1 && category[1] == "to" {
   211  		requestParam.ToAddress = address.Hex()
   212  	} else {
   213  		requestParam.FromAddress = address.Hex()
   214  	}
   215  	params := query.Params{requestParam}
   216  
   217  	var response *alchemyResponseBody
   218  	if response, err = query.QueryUrl[alchemyResponseBody](e.baseUrl, method, params); err != nil {
   219  		return
   220  	}
   221  
   222  	// log.Printf("Got: %+v\n", response)
   223  
   224  	data = make([]SlurpedPageItem, 0, len(response.Transfers))
   225  	for _, alchemyTx := range response.Transfers {
   226  		app, err := e.getTransactionAppearance(alchemyTx.Hash)
   227  		if err != nil {
   228  			return []SlurpedPageItem{}, 0, err
   229  		}
   230  		tx, err := alchemyTx.SimpleSlurp()
   231  		if err != nil {
   232  			return []SlurpedPageItem{}, 0, err
   233  		}
   234  		tx.TransactionIndex = base.Txnum(app.TransactionIndex)
   235  		data = append(data, SlurpedPageItem{
   236  			Appearance: &types.Appearance{
   237  				TransactionIndex: app.TransactionIndex,
   238  				BlockNumber:      app.BlockNumber,
   239  				Address:          address,
   240  			},
   241  			Transaction: &tx,
   242  		})
   243  	}
   244  	// update paginator
   245  	_ = paginator.SetNextPage(response.PageKey)
   246  	paginator.SetDone(response.PageKey == "")
   247  
   248  	count = len(data)
   249  	return
   250  }
   251  
   252  func (p *AlchemyProvider) defaultGetTransactionAppearance(hash string) (types.Appearance, error) {
   253  	return p.conn.GetTransactionAppByHash(hash)
   254  }