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

     1  package provider
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"net/http"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base"
    13  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config"
    14  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/debug"
    15  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/logger"
    16  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/rpc"
    17  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/types"
    18  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/utils"
    19  	"golang.org/x/time/rate"
    20  )
    21  
    22  const etherscanFirstPage = 1
    23  const etherscanRequestsPerSecond = 5
    24  const etherscanMaxPerPage = 3000
    25  
    26  var etherscanBaseUrl = "https://api.etherscan.io"
    27  
    28  type EtherscanProvider struct {
    29  	printProgress bool
    30  	perPage       int
    31  	baseUrl       string
    32  	conn          *rpc.Connection
    33  	limiter       *rate.Limiter
    34  	// TODO: BOGUS - clean raw
    35  	convertSlurpType func(address string, requestType string, trans *types.Slurp) (types.Slurp, error)
    36  	apiKey           string
    37  }
    38  
    39  func NewEtherscanProvider(conn *rpc.Connection) (p *EtherscanProvider, err error) {
    40  	apiKey := config.GetKey("etherscan").ApiKey
    41  	if apiKey == "" {
    42  		err = errors.New("missing Etherscan API key")
    43  		return
    44  	}
    45  
    46  	p = &EtherscanProvider{
    47  		conn:    conn,
    48  		perPage: etherscanMaxPerPage,
    49  		baseUrl: etherscanBaseUrl,
    50  		apiKey:  apiKey,
    51  	}
    52  	p.printProgress = true
    53  	p.limiter = rate.NewLimiter(etherscanRequestsPerSecond, etherscanRequestsPerSecond)
    54  	p.convertSlurpType = p.defaultConvertSlurpType
    55  
    56  	return
    57  }
    58  
    59  func (p *EtherscanProvider) PrintProgress() bool {
    60  	return p.printProgress
    61  }
    62  
    63  func (p *EtherscanProvider) SetPrintProgress(print bool) {
    64  	p.printProgress = print
    65  }
    66  
    67  func (p *EtherscanProvider) NewPaginator(query *Query) Paginator {
    68  	pageNumber := query.StartPage
    69  	if pageNumber == 0 {
    70  		pageNumber = etherscanFirstPage
    71  	}
    72  	perPageValue := query.PerPage
    73  	if perPageValue == 0 {
    74  		perPageValue = etherscanMaxPerPage
    75  	}
    76  	return NewPageNumberPaginator(pageNumber, pageNumber, int(perPageValue))
    77  }
    78  
    79  func (p *EtherscanProvider) TransactionsByAddress(ctx context.Context, query *Query, errorChan chan error) (txChan chan types.Slurp) {
    80  	txChan = make(chan types.Slurp, providerChannelBufferSize)
    81  
    82  	slurpedChan := fetchAndFilterData(ctx, p, query, errorChan, p.fetchData)
    83  	go func() {
    84  		defer close(txChan)
    85  		for {
    86  			select {
    87  			case <-ctx.Done():
    88  				return
    89  			case item, ok := <-slurpedChan:
    90  				if !ok {
    91  					return
    92  				}
    93  				txChan <- *item.Transaction
    94  			}
    95  		}
    96  	}()
    97  
    98  	return
    99  }
   100  
   101  func (p *EtherscanProvider) Appearances(ctx context.Context, query *Query, errorChan chan error) (appChan chan types.Appearance) {
   102  	appChan = make(chan types.Appearance, providerChannelBufferSize)
   103  
   104  	slurpedChan := fetchAndFilterData(ctx, p, query, errorChan, p.fetchData)
   105  	go func() {
   106  		defer close(appChan)
   107  		for {
   108  			select {
   109  			case <-ctx.Done():
   110  				return
   111  			case item, ok := <-slurpedChan:
   112  				if !ok {
   113  					return
   114  				}
   115  				appChan <- *item.Appearance
   116  			}
   117  		}
   118  	}()
   119  
   120  	return
   121  }
   122  
   123  func (p *EtherscanProvider) Count(ctx context.Context, query *Query, errorChan chan error) (monitorChan chan types.Monitor) {
   124  	slurpedChan := fetchAndFilterData(ctx, p, query, errorChan, p.fetchData)
   125  	return countSlurped(ctx, query, slurpedChan)
   126  }
   127  
   128  type etherscanResponseBody struct {
   129  	Message string        `json:"message"`
   130  	Result  []types.Slurp `json:"result"`
   131  	Status  string        `json:"status"`
   132  }
   133  
   134  func (p *EtherscanProvider) fetchData(ctx context.Context, address base.Address, paginator Paginator, requestType string) (data []SlurpedPageItem, count int, err error) {
   135  	url, err := p.url(address.String(), paginator, requestType)
   136  	if err != nil {
   137  		return []SlurpedPageItem{}, 0, err
   138  	}
   139  
   140  	if err = p.limiter.Wait(ctx); err != nil {
   141  		return
   142  	}
   143  
   144  	debug.DebugCurlStr(url)
   145  	fromEs := etherscanResponseBody{}
   146  	sleepyTime := 50 * time.Millisecond
   147  
   148  	attempts := 0
   149  	var ret []SlurpedPageItem
   150  	for {
   151  		attempts++
   152  		if len(ret) > 0 || attempts > 3 {
   153  			paginator.SetDone(len(ret) < paginator.PerPage())
   154  			return ret, len(ret), nil
   155  		}
   156  		resp, err := http.Get(url)
   157  		if err != nil {
   158  			if attempts > 3 {
   159  				return ret, len(ret), err
   160  			}
   161  		}
   162  
   163  		defer resp.Body.Close()
   164  		if resp.StatusCode != http.StatusOK {
   165  			if attempts > 3 {
   166  				return ret, len(ret), fmt.Errorf("etherscan API error: %s", resp.Status)
   167  			}
   168  			time.Sleep(sleepyTime)
   169  			sleepyTime *= 2
   170  		}
   171  		// Check server response
   172  		decoder := json.NewDecoder(resp.Body)
   173  		if err = decoder.Decode(&fromEs); err != nil {
   174  			if attempts > 3 {
   175  				if fromEs.Message == "NOTOK" {
   176  					return ret, len(ret), fmt.Errorf("provider responded with: %s %s", url, fromEs.Message)
   177  				}
   178  				return ret, len(ret), fmt.Errorf("decoder failed: %w", err)
   179  			}
   180  			time.Sleep(sleepyTime)
   181  			sleepyTime *= 2
   182  		}
   183  
   184  		if fromEs.Message == "NOTOK" {
   185  			if attempts > 3 {
   186  				// Etherscan sends 200 OK responses even if there's an error. We want to cache the error
   187  				// response so we don't keep asking Etherscan for the same address. The user may later
   188  				// remove empty ABIs with chifra abis --decache.
   189  				if !utils.IsFuzzing() {
   190  					logger.Warn("provider responded with:", url, fromEs.Message, strings.Repeat(" ", 40))
   191  				}
   192  				return ret, len(ret), nil
   193  				// } else if fromEs.Message != "OK" {
   194  				// 	logger.Warn("URL:", url)
   195  				// 	logger.Warn("provider responded with:", url, fromEs.Message)
   196  			}
   197  			time.Sleep(sleepyTime)
   198  			sleepyTime *= 2
   199  		}
   200  
   201  		for _, trans := range fromEs.Result {
   202  			if transaction, err := p.convert(address.String(), requestType, &trans); err != nil {
   203  				return nil, 0, err
   204  			} else {
   205  				ret = append(ret, SlurpedPageItem{
   206  					Appearance: &types.Appearance{
   207  						Address:          address,
   208  						BlockNumber:      uint32(transaction.BlockNumber),
   209  						TransactionIndex: uint32(transaction.TransactionIndex),
   210  					},
   211  					Transaction: &transaction,
   212  				})
   213  			}
   214  		}
   215  	}
   216  }
   217  
   218  // convert translate Slurp to Slurp. By default it uses `defaultConvertSlurpType`, but this can be changed, e.g. in tests
   219  func (p *EtherscanProvider) convert(address string, requestType string, trans *types.Slurp) (types.Slurp, error) {
   220  	return p.convertSlurpType(address, requestType, trans)
   221  }
   222  
   223  func (p *EtherscanProvider) defaultConvertSlurpType(address string, requestType string, trans *types.Slurp) (types.Slurp, error) {
   224  	s := types.Slurp{
   225  		Hash:             trans.Hash,
   226  		BlockHash:        trans.BlockHash,
   227  		BlockNumber:      trans.BlockNumber,
   228  		TransactionIndex: trans.TransactionIndex,
   229  		Timestamp:        trans.Timestamp,
   230  		From:             trans.From,
   231  		To:               trans.To,
   232  		Gas:              trans.Gas,
   233  		GasPrice:         trans.GasPrice,
   234  		GasUsed:          trans.GasUsed,
   235  		Input:            trans.Input,
   236  		Value:            trans.Value,
   237  		ContractAddress:  trans.ContractAddress,
   238  		HasToken:         requestType == "nfts" || requestType == "token" || requestType == "1155",
   239  	}
   240  	// s.IsError = trans.TxReceiptStatus == "0"
   241  
   242  	if requestType == "int" {
   243  		// We use a weird marker here since Etherscan doesn't send the transaction id for internal txs and we don't
   244  		// want to make another RPC call. We tried (see commented code), but EtherScan balks with a weird message
   245  		app, _ := p.conn.GetTransactionAppByHash(s.Hash.Hex())
   246  		s.TransactionIndex = base.Txnum(app.TransactionIndex)
   247  	} else if requestType == "miner" {
   248  		s.BlockHash = base.HexToHash("0xdeadbeef")
   249  		s.TransactionIndex = types.BlockReward
   250  		s.From = base.BlockRewardSender
   251  		// TODO: This is only correct for Eth mainnet
   252  		s.Value.SetString("5000000000000000000", 0)
   253  		s.To = base.HexToAddress(address)
   254  	} else if requestType == "uncles" {
   255  		s.BlockHash = base.HexToHash("0xdeadbeef")
   256  		s.TransactionIndex = types.UncleReward
   257  		s.From = base.UncleRewardSender
   258  		// TODO: This is only correct for Eth mainnet
   259  		s.Value.SetString("3750000000000000000", 0)
   260  		s.To = base.HexToAddress(address)
   261  	} else if requestType == "withdrawals" {
   262  		s.BlockHash = base.HexToHash("0xdeadbeef")
   263  		s.TransactionIndex = types.WithdrawalAmt
   264  		s.From = base.WithdrawalSender
   265  		s.ValidatorIndex = trans.ValidatorIndex
   266  		s.WithdrawalIndex = trans.WithdrawalIndex
   267  		s.Value = trans.Amount
   268  		s.To = base.HexToAddress(address)
   269  	}
   270  	return s, nil
   271  }
   272  
   273  func (p *EtherscanProvider) url(value string, paginator Paginator, requestType string) (string, error) {
   274  	var actions = map[string]string{
   275  		"ext":         "txlist",
   276  		"int":         "txlistinternal",
   277  		"token":       "tokentx",
   278  		"nfts":        "tokennfttx",
   279  		"1155":        "token1155tx",
   280  		"miner":       "getminedblocks&blocktype=blocks",
   281  		"uncles":      "getminedblocks&blocktype=uncles",
   282  		"byHash":      "eth_getTransactionByHash",
   283  		"withdrawals": "txsBeaconWithdrawal&startblock=0&endblock=999999999",
   284  	}
   285  
   286  	if actions[requestType] == "" {
   287  		return "", fmt.Errorf("cannot find Etherscan action %s", requestType)
   288  	}
   289  
   290  	module := "account"
   291  	tt := "address"
   292  	if requestType == "byHash" {
   293  		module = "proxy"
   294  		tt = "txhash"
   295  	}
   296  
   297  	const str = "[{BASE_URL}]/api?module=[{MODULE}]&sort=asc&action=[{ACTION}]&[{TT}]=[{VALUE}]&page=[{PAGE}]&offset=[{PER_PAGE}]"
   298  	ret := strings.Replace(str, "[{BASE_URL}]", p.baseUrl, -1)
   299  	ret = strings.Replace(ret, "[{MODULE}]", module, -1)
   300  	ret = strings.Replace(ret, "[{TT}]", tt, -1)
   301  	ret = strings.Replace(ret, "[{ACTION}]", actions[requestType], -1)
   302  	ret = strings.Replace(ret, "[{VALUE}]", value, -1)
   303  	ret = strings.Replace(ret, "[{PAGE}]", fmt.Sprintf("%d", paginator.Page()), -1)
   304  	ret = strings.Replace(ret, "[{PER_PAGE}]", fmt.Sprintf("%d", paginator.PerPage()), -1)
   305  	ret = ret + "&apikey=" + p.apiKey
   306  
   307  	return ret, nil
   308  }