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 }