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

     1  package abi
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"os"
    10  	"strings"
    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/utils"
    17  )
    18  
    19  var perfTiming bool
    20  
    21  func init() {
    22  	perfTiming = os.Getenv("TB_TIMER_ON") == "true"
    23  }
    24  
    25  // DO NOT CHANGE THIS VALUE. It is used to cache the error response from Etherscan.
    26  // It's length is 42 bytes and is used to test for this case
    27  
    28  var AbiNotFound = `[{"name":"AbiNotFound","type":"function"}]`
    29  
    30  // downloadAbi downloads the ABI for the given address and saves it to the cache.
    31  // TODO: This function should be easy to replace with "ABI providers" (different services like
    32  // Sourcify or custom ones configured by the user)
    33  func (abiMap *SelectorSyncMap) downloadAbi(chain string, address base.Address) error {
    34  	if address.IsZero() {
    35  		return errors.New("address is 0x0 in downloadAbi")
    36  	}
    37  
    38  	// C++ code used do check if the address is contract in 2 places: here and in handle_addresses. We
    39  	// check only in handle_addresses.
    40  
    41  	key := config.GetKey("etherscan").ApiKey
    42  	if key == "" {
    43  		return errors.New("cannot read Etherscan API key")
    44  	}
    45  	url := fmt.Sprintf(
    46  		"https://api.etherscan.io/api?module=contract&action=getabi&address=%s&apikey=%s",
    47  		address.Hex(),
    48  		key,
    49  	)
    50  
    51  	debug.DebugCurlStr(url)
    52  	resp, err := http.Get(url)
    53  	if err != nil {
    54  		return err
    55  	}
    56  	defer resp.Body.Close()
    57  
    58  	// Check server response
    59  	if resp.StatusCode != http.StatusOK {
    60  		return fmt.Errorf("etherscan API error: %s", resp.Status)
    61  	}
    62  
    63  	data := map[string]string{}
    64  	decoder := json.NewDecoder(resp.Body)
    65  	if err = decoder.Decode(&data); err != nil {
    66  		return err
    67  	}
    68  	resp.Body.Close()
    69  
    70  	if data["message"] == "NOTOK" {
    71  		// Etherscan sends 200 OK responses even if there's an error. We want to cache the error
    72  		// response so we don't keep asking Etherscan for the same address. The user may later
    73  		// remove empty ABIs with chifra abis --clean.
    74  		if !perfTiming && os.Getenv("TEST_MODE") != "true" && !utils.IsFuzzing() {
    75  			logger.Warn("provider responded with:", address.Hex(), data["message"], ss)
    76  		}
    77  
    78  		reader := strings.NewReader(AbiNotFound)
    79  		_ = fromJson(reader, abiMap)
    80  		if _, err = reader.Seek(0, io.SeekStart); err != nil {
    81  			return err
    82  		}
    83  		if err = insertAbi(chain, address, reader); err != nil {
    84  			return err
    85  		}
    86  		return nil
    87  	}
    88  
    89  	reader := strings.NewReader(data["result"])
    90  	_ = fromJson(reader, abiMap)
    91  	if _, err = reader.Seek(0, io.SeekStart); err != nil {
    92  		return err
    93  	}
    94  
    95  	// Write the body to file
    96  	if err = insertAbi(chain, address, reader); err != nil {
    97  		return err
    98  	}
    99  
   100  	return nil
   101  }
   102  
   103  var ss = strings.Repeat(" ", 40)