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 `