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

     1  package config
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"net/http"
    10  	"os"
    11  	"os/user"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strconv"
    15  	"strings"
    16  	"sync"
    17  
    18  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/configtypes"
    19  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/logger"
    20  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/usage"
    21  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/utils"
    22  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/version"
    23  )
    24  
    25  const envPrefix = "TB_"
    26  
    27  var trueBlocksConfig configtypes.Config
    28  var cachePath string
    29  var indexPath string
    30  
    31  // init sets up default values for the given configuration
    32  func init() {
    33  	// The location of the per chain caches
    34  	cachePath = filepath.Join(PathToRootConfig(), "cache")
    35  	// The location of the per chain unchained indexes
    36  	indexPath = filepath.Join(PathToRootConfig(), "unchained")
    37  }
    38  
    39  var configMutex sync.Mutex
    40  var configLoaded = false
    41  
    42  func loadFromTomlFile(filePath string, dest *configtypes.Config) error {
    43  	return ReadToml(filePath, dest)
    44  }
    45  
    46  // GetRootConfig reads and the configuration located in trueBlocks.toml file. Note
    47  // that this routine is local to the package
    48  func GetRootConfig() *configtypes.Config {
    49  	if configLoaded {
    50  		return &trueBlocksConfig
    51  	}
    52  	configMutex.Lock()
    53  	defer configMutex.Unlock()
    54  
    55  	configPath := PathToRootConfig()
    56  
    57  	// First load the default config
    58  	trueBlocksConfig = configtypes.NewConfig(cachePath, indexPath, defaultIpfsGateway)
    59  
    60  	// Load TOML file
    61  	tomlConfigFn := filepath.Join(configPath, "trueBlocks.toml")
    62  	if err := loadFromTomlFile(tomlConfigFn, &trueBlocksConfig); err != nil {
    63  		log.Fatal("loading config from .toml file:", err)
    64  	}
    65  
    66  	// Load ENV variables
    67  	if err := loadFromEnv(envPrefix, &trueBlocksConfig); err != nil {
    68  		log.Fatal("loading config from environment variables:", err)
    69  	}
    70  
    71  	user, _ := user.Current()
    72  
    73  	cachePath := trueBlocksConfig.Settings.CachePath
    74  	cachePath = strings.Replace(cachePath, "/", string(os.PathSeparator), -1)
    75  	if len(cachePath) == 0 || cachePath == "<not_set>" {
    76  		cachePath = filepath.Join(configPath, "cache")
    77  	}
    78  	cachePath = strings.Replace(cachePath, "$HOME", user.HomeDir, -1)
    79  	cachePath = strings.Replace(cachePath, "~", user.HomeDir, -1)
    80  	if filepath.Base(cachePath) != "cache" {
    81  		cachePath = filepath.Join(cachePath, "cache")
    82  	}
    83  	trueBlocksConfig.Settings.CachePath = cachePath
    84  
    85  	indexPath := trueBlocksConfig.Settings.IndexPath
    86  	indexPath = strings.Replace(indexPath, "/", string(os.PathSeparator), -1)
    87  	if len(indexPath) == 0 || indexPath == "<not_set>" {
    88  		indexPath = filepath.Join(configPath, "unchained")
    89  	}
    90  	indexPath = strings.Replace(indexPath, "$HOME", user.HomeDir, -1)
    91  	indexPath = strings.Replace(indexPath, "~", user.HomeDir, -1)
    92  	if filepath.Base(indexPath) != "unchained" {
    93  		indexPath = filepath.Join(indexPath, "unchained")
    94  	}
    95  	trueBlocksConfig.Settings.IndexPath = indexPath
    96  
    97  	if len(trueBlocksConfig.Settings.DefaultChain) == 0 {
    98  		trueBlocksConfig.Settings.DefaultChain = "mainnet"
    99  	}
   100  
   101  	// migrate the config file if necessary (note that this does not return if the file is migrated).
   102  	currentVer := version.NewVersion(trueBlocksConfig.Version.Current)
   103  	_ = migrate(currentVer)
   104  
   105  	// clean up the config data
   106  	for chain, ch := range trueBlocksConfig.Chains {
   107  		clean := func(url string) string {
   108  			if !strings.HasPrefix(url, "http") {
   109  				url = "https://" + url
   110  			}
   111  			if !strings.HasSuffix(url, "/") {
   112  				url += "/"
   113  			}
   114  			return url
   115  		}
   116  		ch.Chain = chain
   117  		isDefaulted := len(ch.IpfsGateway) == 0 || strings.Trim(ch.IpfsGateway, "/") == strings.Trim(defaultIpfsGateway, "/")
   118  		if isDefaulted {
   119  			ch.IpfsGateway = trueBlocksConfig.Pinning.GatewayUrl
   120  		}
   121  		ch.IpfsGateway = strings.Replace(ch.IpfsGateway, "[{CHAIN}]", "ipfs", -1)
   122  		ch.LocalExplorer = clean(ch.LocalExplorer)
   123  		ch.RemoteExplorer = clean(ch.RemoteExplorer)
   124  		ch.RpcProvider = strings.Trim(clean(ch.RpcProvider), "/") // Infura, for example, doesn't like the trailing slash
   125  		if err := validateRpcEndpoint(ch.Chain, ch.RpcProvider); err != nil {
   126  			logger.Fatal(err)
   127  		}
   128  		ch.IpfsGateway = clean(ch.IpfsGateway)
   129  		if ch.Scrape.AppsPerChunk == 0 {
   130  			settings := configtypes.ScrapeSettings{
   131  				AppsPerChunk: 2000000,
   132  				SnapToGrid:   250000,
   133  				FirstSnap:    2000000,
   134  				UnripeDist:   28,
   135  				ChannelCount: 20,
   136  				AllowMissing: false,
   137  			}
   138  			if chain == "mainnet" {
   139  				settings.SnapToGrid = 100000
   140  				settings.FirstSnap = 2300000
   141  			}
   142  			ch.Scrape = settings
   143  		}
   144  		trueBlocksConfig.Chains[chain] = ch
   145  	}
   146  	configLoaded = true
   147  	return &trueBlocksConfig
   148  }
   149  
   150  // PathToConfigFile returns the path where to find the configuration file
   151  func PathToConfigFile() string {
   152  	configFolder := PathToRootConfig()
   153  	return filepath.Join(configFolder, "trueBlocks.toml")
   154  }
   155  
   156  // PathToRootConfig returns the path where to find configuration files
   157  func PathToRootConfig() string {
   158  	configPath, err := pathFromXDG("XDG_CONFIG_HOME")
   159  	if err != nil {
   160  		logger.Fatal(err)
   161  	} else if len(configPath) > 0 {
   162  		return configPath
   163  	}
   164  
   165  	// The migration code will have already checked for invalid operating systems
   166  	userOs := runtime.GOOS
   167  	if len(os.Getenv("TEST_OS")) > 0 {
   168  		userOs = os.Getenv("TEST_OS")
   169  	}
   170  
   171  	user, _ := user.Current()
   172  	osPath := ".local/share/trueblocks"
   173  	if userOs == "darwin" {
   174  		osPath = "Library/Application Support/TrueBlocks"
   175  	} else if userOs == "windows" {
   176  		osPath = "AppData/Local/trueblocks"
   177  	}
   178  
   179  	return filepath.Join(user.HomeDir, osPath)
   180  }
   181  
   182  func pathFromXDG(envVar string) (string, error) {
   183  	// If present, we require both an existing path and a fully qualified path
   184  	xdg := os.Getenv(envVar)
   185  	if len(xdg) == 0 {
   186  		return "", nil // it's okay if it's empty
   187  	}
   188  
   189  	if xdg[0] != string(os.PathSeparator)[0] {
   190  		return "", usage.Usage("The {0} value ({1}), must be fully qualified.", envVar, xdg)
   191  	}
   192  
   193  	if _, err := os.Stat(xdg); err != nil {
   194  		return "", usage.Usage("The {0} folder ({1}) must exist.", envVar, xdg)
   195  	}
   196  
   197  	return filepath.Join(xdg, ""), nil
   198  }
   199  
   200  func validateRpcEndpoint(chain, provider string) error {
   201  	if utils.IsPermitted() {
   202  		return nil
   203  	}
   204  
   205  	if provider == "https:" {
   206  		problem := `No rpcProvider found.`
   207  		return usage.Usage(rpcWarning, chain, provider, problem)
   208  	}
   209  
   210  	if !strings.HasPrefix(provider, "http") {
   211  		problem := `Invalid rpcProvider found (must be a url).`
   212  		return usage.Usage(rpcWarning, chain, provider, problem)
   213  	}
   214  
   215  	if chain == "mainnet" {
   216  		// TODO: Eventually this will be parameterized, for example, when we start publishing to Optimism
   217  		deployed := uint64(14957097) // block where the unchained index was deployed to mainnet
   218  		if err := checkUnchainedProvider(chain, deployed); err != nil {
   219  			return err
   220  		}
   221  	}
   222  
   223  	return nil
   224  }
   225  
   226  func checkUnchainedProvider(chain string, deployed uint64) error {
   227  	// TODO: Clean this up
   228  	// TODO: We need to check that the unchained index has been deployed on the chain
   229  	if os.Getenv("TB_NO_PROVIDER_CHECK") == "true" {
   230  		logger.Info("Skipping rpcProvider check")
   231  		return nil
   232  	}
   233  	url := trueBlocksConfig.Chains[chain].RpcProvider
   234  	str := `{ "jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": [ "{0}", true ], "id": 1 }`
   235  	payLoad := []byte(strings.Replace(str, "{0}", fmt.Sprintf("0x%x", deployed), -1))
   236  	req, err := http.NewRequest("POST", url, bytes.NewBuffer(payLoad))
   237  	if err != nil {
   238  		return fmt.Errorf("error creating request to rpcProvider (%s): %v", url, err)
   239  	}
   240  	req.Header.Set("Content-Type", "application/json")
   241  	client := &http.Client{}
   242  	resp, err := client.Do(req)
   243  	if err != nil {
   244  		return fmt.Errorf("error making request to rpcProvider (%s): %v", url, err)
   245  	}
   246  	defer resp.Body.Close()
   247  	if resp.StatusCode != http.StatusOK {
   248  		return fmt.Errorf("unexpected status code from rpcProvider (%s): %d, expected: %d", url, resp.StatusCode, http.StatusOK)
   249  	}
   250  	body, err := io.ReadAll(resp.Body)
   251  	if err != nil {
   252  		return fmt.Errorf("error reading response body: %v", err)
   253  	}
   254  	var result map[string]interface{}
   255  	if err := json.Unmarshal(body, &result); err != nil {
   256  		return fmt.Errorf("error unmarshalling response: %v", err)
   257  	}
   258  	s := result["result"].(map[string]interface{})["number"].(string)
   259  	if bn, _ := strconv.ParseUint(s, 0, 64); bn != deployed {
   260  		msg := `the unchained index was deployed at block %d. Is your node synced that far?`
   261  		return fmt.Errorf(msg, deployed)
   262  	}
   263  	return nil
   264  }
   265  
   266  var rpcWarning string = `
   267  We found a problem with the rpcProvider for the {0} chain.
   268  
   269  	Provider: {1}
   270  	Chain:    {0}
   271  	Problem:  {2}
   272  
   273  Confirm the value for the given provider. You may edit this value with
   274  "chifra config edit".
   275  
   276  Also, try the following curl command. If this command does not work, neither will chifra.
   277  
   278  curl -X POST -H "Content-Type: application/json" --data '{ "jsonrpc": "2.0", "method": "web3_clientVersion", "id": 6 }' {1}
   279  `