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

     1  package provider
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base"
    14  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config"
    15  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/rpc"
    16  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/types"
    17  	"golang.org/x/time/rate"
    18  )
    19  
    20  // const covalentFirstPage = 0
    21  const covalentRequestsPerSecond = 5
    22  const covalentBaseUrl = "https://api.covalenthq.com/v1/[{CHAIN}]/address/[{ADDRESS}]/transactions_v3/page/[{PAGE}]/"
    23  
    24  var tbChainToCovalent = map[string]string{
    25  	"mainnet": "eth-mainnet",
    26  }
    27  
    28  func covalentPrepareQuery(q *Query) (result *Query) {
    29  	result = q.Dup()
    30  	result.Resources = []string{"covalent"}
    31  	return
    32  }
    33  
    34  type CovalentProvider struct {
    35  	printProgress bool
    36  	conn          *rpc.Connection
    37  	limiter       *rate.Limiter
    38  	baseUrl       string
    39  	chain         string
    40  	apiKey        string
    41  }
    42  
    43  func NewCovalentProvider(conn *rpc.Connection, chain string) (p *CovalentProvider, err error) {
    44  	apiKey := config.GetKey("covalent").ApiKey
    45  	if apiKey == "" {
    46  		err = errors.New("missing Covalent API key")
    47  		return
    48  	}
    49  	p = &CovalentProvider{
    50  		conn:    conn,
    51  		chain:   chain,
    52  		apiKey:  apiKey,
    53  		baseUrl: covalentBaseUrl,
    54  	}
    55  	p.printProgress = true
    56  	p.limiter = rate.NewLimiter(covalentRequestsPerSecond, covalentRequestsPerSecond)
    57  
    58  	return
    59  }
    60  
    61  func (p *CovalentProvider) PrintProgress() bool {
    62  	return p.printProgress
    63  }
    64  
    65  func (p *CovalentProvider) SetPrintProgress(print bool) {
    66  	p.printProgress = print
    67  }
    68  
    69  func (p *CovalentProvider) NewPaginator(query *Query) Paginator {
    70  	pageNumber := query.StartPage
    71  	return NewPageNumberPaginator(pageNumber, pageNumber, int(query.PerPage))
    72  }
    73  
    74  func (p *CovalentProvider) TransactionsByAddress(ctx context.Context, query *Query, errorChan chan error) (txChan chan types.Slurp) {
    75  	txChan = make(chan types.Slurp, providerChannelBufferSize)
    76  
    77  	prepQuery := covalentPrepareQuery(query)
    78  	slurpedChan := fetchAndFilterData(ctx, p, prepQuery, errorChan, p.fetchData)
    79  	go func() {
    80  		defer close(txChan)
    81  		for {
    82  			select {
    83  			case <-ctx.Done():
    84  				return
    85  			case item, ok := <-slurpedChan:
    86  				if !ok {
    87  					return
    88  				}
    89  				txChan <- *item.Transaction
    90  			}
    91  		}
    92  	}()
    93  
    94  	return
    95  }
    96  
    97  func (p *CovalentProvider) Appearances(ctx context.Context, query *Query, errorChan chan error) (appChan chan types.Appearance) {
    98  	appChan = make(chan types.Appearance, providerChannelBufferSize)
    99  
   100  	prepQuery := covalentPrepareQuery(query)
   101  	slurpedChan := fetchAndFilterData(ctx, p, prepQuery, errorChan, p.fetchData)
   102  	go func() {
   103  		defer close(appChan)
   104  		for {
   105  			select {
   106  			case <-ctx.Done():
   107  				return
   108  			case item, ok := <-slurpedChan:
   109  				if !ok {
   110  					return
   111  				}
   112  				appChan <- *item.Appearance
   113  			}
   114  		}
   115  	}()
   116  
   117  	return
   118  }
   119  
   120  func (p *CovalentProvider) Count(ctx context.Context, query *Query, errorChan chan error) (monitorChan chan types.Monitor) {
   121  	prepQuery := covalentPrepareQuery(query)
   122  	slurpedChan := fetchAndFilterData(ctx, p, prepQuery, errorChan, p.fetchData)
   123  	return countSlurped(ctx, query, slurpedChan)
   124  }
   125  
   126  type covalentResponseBody struct {
   127  	Data covalentResponseData `json:"data"`
   128  }
   129  
   130  type covalentResponseData struct {
   131  	Items []covalentTransaction `json:"items"`
   132  	Links *covalentLinks        `json:"links"`
   133  }
   134  
   135  type covalentTransaction struct {
   136  	BlockHeight   *int       `json:"block_height,omitempty"`
   137  	BlockHash     *string    `json:"block_hash,omitempty"`
   138  	TxHash        *string    `json:"tx_hash,omitempty"`
   139  	TxOffset      *int       `json:"tx_offset,omitempty"`
   140  	Successful    *bool      `json:"successful,omitempty"`
   141  	From          *string    `json:"from_address,omitempty"`
   142  	To            *string    `json:"to_address,omitempty"`
   143  	Value         *base.Wei  `json:"value,omitempty"`
   144  	GasSpent      *int64     `json:"gas_spent,omitempty"`
   145  	GasPrice      *int64     `json:"gas_price,omitempty"`
   146  	BlockSignedAt *time.Time `json:"block_signed_at,omitempty"`
   147  }
   148  
   149  func (c *covalentTransaction) Slurp() (s types.Slurp) {
   150  	to := ""
   151  	if c.To != nil {
   152  		to = *c.To
   153  	}
   154  	s = types.Slurp{
   155  		BlockHash:        base.HexToHash(*c.BlockHash),
   156  		BlockNumber:      base.Blknum(*c.BlockHeight),
   157  		From:             base.HexToAddress(*c.From),
   158  		Gas:              base.Gas(*c.GasSpent),
   159  		IsError:          !(*c.Successful),
   160  		Timestamp:        base.Timestamp(c.BlockSignedAt.Unix()),
   161  		To:               base.HexToAddress(to),
   162  		TransactionIndex: base.Txnum(*c.TxOffset),
   163  		Value:            *c.Value,
   164  	}
   165  
   166  	return
   167  }
   168  
   169  func (c *covalentTransaction) Appearance(address base.Address) (a types.Appearance) {
   170  	return types.Appearance{
   171  		Address:          address,
   172  		BlockNumber:      uint32(*c.BlockHeight),
   173  		TransactionIndex: uint32(*c.TxOffset),
   174  	}
   175  }
   176  
   177  type covalentLinks struct {
   178  	Prev string `json:"prev,omitempty"`
   179  	Next string `json:"next,omitempty"`
   180  }
   181  
   182  func (e *CovalentProvider) fetchData(ctx context.Context, address base.Address, paginator Paginator, _ string) (data []SlurpedPageItem, count int, err error) {
   183  	if e.baseUrl == "" {
   184  		e.baseUrl = covalentBaseUrl
   185  	}
   186  
   187  	pageNumber, ok := paginator.Page().(int)
   188  	if !ok {
   189  		err = errors.New("cannot get page number")
   190  		return
   191  	}
   192  
   193  	url, err := e.url(address, pageNumber)
   194  	if err != nil {
   195  		return
   196  	}
   197  
   198  	var response covalentResponseBody
   199  	request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   200  	if err != nil {
   201  		return
   202  	}
   203  	request.SetBasicAuth(e.apiKey, "")
   204  	request.Header.Set("Content-Type", "application/json")
   205  
   206  	resp, err := http.DefaultClient.Do(request)
   207  	if err != nil {
   208  		return
   209  	}
   210  	if resp.StatusCode != http.StatusOK {
   211  		err = fmt.Errorf("covalent responded with: %s", resp.Status)
   212  		paginator.SetDone(true)
   213  		return
   214  	}
   215  	defer resp.Body.Close()
   216  	respBytes, err := io.ReadAll(resp.Body)
   217  	if err != nil {
   218  		return
   219  	}
   220  	if err = json.Unmarshal(respBytes, &response); err != nil {
   221  		return
   222  	}
   223  
   224  	data = make([]SlurpedPageItem, 0, len(response.Data.Items))
   225  	for _, covalentTx := range response.Data.Items {
   226  		appearance := covalentTx.Appearance(address)
   227  		slurpedTx := covalentTx.Slurp()
   228  		data = append(data, SlurpedPageItem{
   229  			Appearance:  &appearance,
   230  			Transaction: &slurpedTx,
   231  		})
   232  	}
   233  	// update paginator
   234  	paginator.SetDone(response.Data.Links.Next == "")
   235  
   236  	count = len(data)
   237  	return
   238  }
   239  
   240  func (p *CovalentProvider) url(address base.Address, pageNumber int) (url string, err error) {
   241  	covalentChain := tbChainToCovalent[p.chain]
   242  	if covalentChain == "" {
   243  		err = fmt.Errorf("cannot find covalent chain ID for chain %s", p.chain)
   244  		return
   245  	}
   246  
   247  	url = p.baseUrl
   248  	url = strings.Replace(url, "[{CHAIN}]", covalentChain, -1)
   249  	url = strings.Replace(url, "[{ADDRESS}]", address.Hex(), -1)
   250  	url = strings.Replace(url, "[{PAGE}]", fmt.Sprint(pageNumber), -1)
   251  	return
   252  }