github.com/iotexproject/iotex-core@v1.14.1-rc1/ioctl/newcmd/config/config.go (about) 1 // Copyright (c) 2022 IoTeX Foundation 2 // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability 3 // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. 4 // This source code is governed by Apache License 2.0 that can be found in the LICENSE file. 5 6 package config 7 8 import ( 9 "encoding/json" 10 "fmt" 11 "os" 12 "path/filepath" 13 "regexp" 14 "strconv" 15 "strings" 16 17 "github.com/pkg/errors" 18 "github.com/spf13/cobra" 19 "golang.org/x/text/cases" 20 "golang.org/x/text/language" 21 "gopkg.in/yaml.v2" 22 23 serverCfg "github.com/iotexproject/iotex-core/config" 24 "github.com/iotexproject/iotex-core/ioctl" 25 "github.com/iotexproject/iotex-core/ioctl/config" 26 "github.com/iotexproject/iotex-core/ioctl/validator" 27 ) 28 29 // Regexp patterns 30 const ( 31 _ipPattern = `((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)` 32 _domainPattern = `[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}(\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,62})*(\.[a-zA-Z][a-zA-Z0-9]{0,10}){1}` 33 _localPattern = "localhost" 34 _endpointPattern = "(" + _ipPattern + "|(" + _domainPattern + ")" + "|(" + _localPattern + "))" + `(:\d{1,5})?` 35 _defaultAnalyserEndpoint = "https://iotex-analyser-api-mainnet.chainanalytics.org" 36 _defaultConfigFileName = "config.default" 37 // _defaultWsEndpoint default w3bstream endpoint 38 _defaultWsEndpoint = "sprout-staging.w3bstream.com:9000" 39 // _defaultIPFSEndpoint default IPFS endpoint for uploading 40 _defaultIPFSEndpoint = "ipfs.mainnet.iotex.io" 41 // _defaultIPFSGateway default IPFS gateway for resource fetching 42 _defaultIPFSGateway = "https://ipfs.io" 43 // _defaultWsRegisterContract default w3bstream project register contract address 44 _defaultWsRegisterContract = "0x184C72E39a642058CCBc369485c7fd614B40a03d" 45 ) 46 47 var ( 48 _supportedLanguage = []string{"English", "中文"} 49 _validArgs = []string{"endpoint", "wallet", "explorer", "defaultacc", "language", "nsv2height", "wsEndpoint", "ipfsEndpoint", "ipfsGateway", "wsRegisterContract"} 50 _validGetArgs = []string{"endpoint", "wallet", "explorer", "defaultacc", "language", "nsv2height", "wsEndpoint", "ipfsEndpoint", "ipfsGateway", "analyserEndpoint", "wsRegisterContract", "all"} 51 _validExpl = []string{"iotexscan", "iotxplorer"} 52 _endpointCompile = regexp.MustCompile("^" + _endpointPattern + "$") 53 _configDir = os.Getenv("HOME") + "/.config/ioctl/default" 54 ) 55 56 // Multi-language support 57 var ( 58 _configCmdShorts = map[config.Language]string{ 59 config.English: "Manage the configuration of ioctl", 60 config.Chinese: "ioctl配置管理", 61 } 62 ) 63 64 // NewConfigCmd represents the new node command. 65 func NewConfigCmd(client ioctl.Client) *cobra.Command { 66 configShorts, _ := client.SelectTranslation(_configCmdShorts) 67 68 cmd := &cobra.Command{ 69 Use: "config", 70 Short: configShorts, 71 } 72 cmd.AddCommand(NewConfigSetCmd(client)) 73 cmd.AddCommand(NewConfigGetCmd(client)) 74 cmd.AddCommand(NewConfigResetCmd(client)) 75 76 return cmd 77 } 78 79 // info contains the information of config file 80 type info struct { 81 readConfig config.Config 82 defaultConfigFile string // Path to config file 83 } 84 85 // InitConfig load config data from default config file 86 func InitConfig() (config.Config, string, error) { 87 info := &info{ 88 readConfig: config.Config{ 89 Aliases: make(map[string]string), 90 }, 91 } 92 93 // Create path to config directory 94 err := os.MkdirAll(_configDir, 0700) 95 if err != nil { 96 return info.readConfig, info.defaultConfigFile, err 97 } 98 info.defaultConfigFile = filepath.Join(_configDir, _defaultConfigFileName) 99 100 // Load or reset config file 101 err = info.loadConfig() 102 if os.IsNotExist(err) { 103 err = info.reset() 104 } 105 if err != nil { 106 return info.readConfig, info.defaultConfigFile, err 107 } 108 109 // Check completeness of config file 110 completeness := true 111 if info.readConfig.Wallet == "" { 112 info.readConfig.Wallet = _configDir 113 completeness = false 114 } 115 if info.readConfig.Language == "" { 116 info.readConfig.Language = _supportedLanguage[0] 117 completeness = false 118 } 119 if info.readConfig.Nsv2height == 0 { 120 info.readConfig.Nsv2height = serverCfg.Default.Genesis.FairbankBlockHeight 121 } 122 if info.readConfig.AnalyserEndpoint == "" { 123 info.readConfig.AnalyserEndpoint = _defaultAnalyserEndpoint 124 completeness = false 125 } 126 if info.readConfig.WsEndpoint == "" { 127 info.readConfig.WsEndpoint = _defaultWsEndpoint 128 completeness = false 129 } 130 if info.readConfig.IPFSEndpoint == "" { 131 info.readConfig.IPFSEndpoint = _defaultIPFSEndpoint 132 } 133 if info.readConfig.IPFSGateway == "" { 134 info.readConfig.IPFSGateway = _defaultIPFSGateway 135 } 136 if info.readConfig.WsRegisterContract == "" { 137 info.readConfig.WsRegisterContract = _defaultWsRegisterContract 138 } 139 if !completeness { 140 if err = info.writeConfig(); err != nil { 141 return info.readConfig, info.defaultConfigFile, err 142 } 143 } 144 // Set language for ioctl 145 if isSupportedLanguage(info.readConfig.Language) == -1 { 146 fmt.Printf("Warn: Language %s is not supported, English instead.\n", info.readConfig.Language) 147 } 148 return info.readConfig, info.defaultConfigFile, nil 149 } 150 151 // newInfo create config info 152 func newInfo(readConfig config.Config, defaultConfigFile string) *info { 153 return &info{ 154 readConfig: readConfig, 155 defaultConfigFile: defaultConfigFile, 156 } 157 } 158 159 // reset resets all values of config 160 func (c *info) reset() error { 161 c.readConfig.Wallet = filepath.Dir(c.defaultConfigFile) 162 c.readConfig.Endpoint = "" 163 c.readConfig.SecureConnect = true 164 c.readConfig.DefaultAccount = *new(config.Context) 165 c.readConfig.Explorer = _validExpl[0] 166 c.readConfig.Language = _supportedLanguage[0] 167 c.readConfig.AnalyserEndpoint = _defaultAnalyserEndpoint 168 c.readConfig.WsEndpoint = _defaultWsEndpoint 169 c.readConfig.IPFSEndpoint = _defaultIPFSEndpoint 170 c.readConfig.IPFSGateway = _defaultIPFSGateway 171 c.readConfig.WsRegisterContract = _defaultWsRegisterContract 172 173 err := c.writeConfig() 174 if err != nil { 175 return err 176 } 177 178 fmt.Println("Config set to default values") 179 return nil 180 } 181 182 // set sets config variable 183 func (c *info) set(args []string, insecure bool, client ioctl.Client) (string, error) { 184 switch args[0] { 185 case "endpoint": 186 if !isValidEndpoint(args[1]) { 187 return "", errors.Errorf("endpoint %s is not valid", args[1]) 188 } 189 c.readConfig.Endpoint = args[1] 190 c.readConfig.SecureConnect = !insecure 191 case "analyserEndpoint": 192 c.readConfig.AnalyserEndpoint = args[1] 193 case "wallet": 194 c.readConfig.Wallet = args[1] 195 case "explorer": 196 lowArg := strings.ToLower(args[1]) 197 switch { 198 case isValidExplorer(lowArg): 199 c.readConfig.Explorer = lowArg 200 case args[1] == "custom": 201 link, err := client.ReadCustomLink() 202 if err != nil { 203 return "", errors.Wrapf(err, "invalid link %s", link) 204 } 205 c.readConfig.Explorer = link 206 default: 207 return "", errors.Errorf("explorer %s is not valid\nValid explorers: %s", 208 args[1], append(_validExpl, "custom")) 209 } 210 case "defaultacc": 211 if err := validator.ValidateAlias(args[1]); err == nil { 212 } else if err = validator.ValidateAddress(args[1]); err == nil { 213 } else { 214 return "", errors.Errorf("failed to validate alias or address %s", args[1]) 215 } 216 c.readConfig.DefaultAccount.AddressOrAlias = args[1] 217 case "language": 218 lang := isSupportedLanguage(args[1]) 219 if lang == -1 { 220 return "", errors.Errorf("language %s is not supported\nSupported languages: %s", 221 args[1], _supportedLanguage) 222 } 223 c.readConfig.Language = _supportedLanguage[lang] 224 case "nsv2height": 225 height, err := strconv.ParseUint(args[1], 10, 64) 226 if err != nil { 227 return "", errors.Wrapf(err, "invalid height %d", height) 228 } 229 c.readConfig.Nsv2height = height 230 case "wsEndpoint": 231 c.readConfig.WsEndpoint = args[1] 232 case "ipfsEndpoint": 233 c.readConfig.IPFSEndpoint = args[1] 234 case "ipfsGateway": 235 c.readConfig.IPFSGateway = args[1] 236 case "wsRegisterContract": 237 c.readConfig.WsRegisterContract = args[1] 238 default: 239 return "", config.ErrConfigNotMatch 240 } 241 242 err := c.writeConfig() 243 if err != nil { 244 return "", err 245 } 246 247 return cases.Title(language.Und).String(args[0]) + " is set to " + args[1], nil 248 } 249 250 // get retrieves a config item from its key. 251 func (c *info) get(arg string) (string, error) { 252 switch arg { 253 case "endpoint": 254 if c.readConfig.Endpoint == "" { 255 return "", config.ErrEmptyEndpoint 256 } 257 return fmt.Sprintf("%s secure connect(TLS): %t", c.readConfig.Endpoint, c.readConfig.SecureConnect), nil 258 case "wallet": 259 return c.readConfig.Wallet, nil 260 case "defaultacc": 261 if c.readConfig.DefaultAccount.AddressOrAlias == "" { 262 return "", config.ErrConfigDefaultAccountNotSet 263 } 264 return jsonString(c.readConfig.DefaultAccount) 265 case "explorer": 266 return c.readConfig.Explorer, nil 267 case "language": 268 return c.readConfig.Language, nil 269 case "nsv2height": 270 return strconv.FormatUint(c.readConfig.Nsv2height, 10), nil 271 case "analyserEndpoint": 272 return c.readConfig.AnalyserEndpoint, nil 273 case "wsEndpoint": 274 return c.readConfig.WsEndpoint, nil 275 case "ipfsEndpoint": 276 return c.readConfig.IPFSEndpoint, nil 277 case "ipfsGateway": 278 return c.readConfig.IPFSGateway, nil 279 case "wsRegisterContract": 280 return c.readConfig.WsRegisterContract, nil 281 case "all": 282 return jsonString(c.readConfig) 283 default: 284 return "", config.ErrConfigNotMatch 285 } 286 } 287 288 // isValidEndpoint makes sure the endpoint matches the endpoint match pattern 289 func isValidEndpoint(endpoint string) bool { 290 return _endpointCompile.MatchString(endpoint) 291 } 292 293 // isValidExplorer checks if the explorer is a valid option 294 func isValidExplorer(arg string) bool { 295 for _, exp := range _validExpl { 296 if arg == exp { 297 return true 298 } 299 } 300 return false 301 } 302 303 // writeConfig writes to config file 304 func (c *info) writeConfig() error { 305 out, err := yaml.Marshal(&c.readConfig) 306 if err != nil { 307 return errors.Wrap(err, "failed to marshal config") 308 } 309 if err := os.WriteFile(c.defaultConfigFile, out, 0600); err != nil { 310 return errors.Wrap(err, fmt.Sprintf("failed to write to config file %s", c.defaultConfigFile)) 311 } 312 return nil 313 } 314 315 // loadConfig loads config file in yaml format 316 func (c *info) loadConfig() error { 317 in, err := os.ReadFile(c.defaultConfigFile) 318 if err != nil { 319 return err 320 } 321 if err = yaml.Unmarshal(in, &c.readConfig); err != nil { 322 return errors.Wrap(err, "failed to unmarshal config") 323 } 324 return nil 325 } 326 327 // isSupportedLanguage checks if the language is a supported option and returns index when supported 328 func isSupportedLanguage(arg string) config.Language { 329 if index, err := strconv.Atoi(arg); err == nil && index >= 0 && index < len(_supportedLanguage) { 330 return config.Language(index) 331 } 332 for i, lang := range _supportedLanguage { 333 if strings.EqualFold(arg, lang) { 334 return config.Language(i) 335 } 336 } 337 return config.Language(-1) 338 } 339 340 // jsonString returns json string for message 341 func jsonString(input interface{}) (string, error) { 342 byteAsJSON, err := json.MarshalIndent(input, "", " ") 343 if err != nil { 344 return "", errors.Wrap(err, "failed to JSON marshal config field") 345 } 346 return fmt.Sprint(string(byteAsJSON)), nil 347 }