diff --git a/go.mod b/go.mod index 17511556..b653fc76 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ replace github.com/bytedance/sonic/loader => github.com/bytedance/sonic/loader v require ( cosmossdk.io/math v1.5.3 + github.com/AlecAivazis/survey/v2 v2.3.7 github.com/LumeraProtocol/lumera v1.6.0 github.com/LumeraProtocol/rq-go v0.2.1 github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce @@ -59,7 +60,6 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.2 // indirect - github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/DataDog/datadog-go v3.2.0+incompatible // indirect github.com/DataDog/zstd v1.5.5 // indirect github.com/benbjohnson/clock v1.3.0 // indirect diff --git a/go.sum b/go.sum index 6a5f2370..4918ed01 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,7 @@ github.com/LumeraProtocol/lumera v1.6.0 h1:5I172U/f1Migt7tRxnywhz5aRKCpBOx/IMgOz github.com/LumeraProtocol/lumera v1.6.0/go.mod h1:c1M+sjewuCvxw+pznwlspUzenDJI8Y+suKB3RFKS2Wo= github.com/LumeraProtocol/rq-go v0.2.1 h1:8B3UzRChLsGMmvZ+UVbJsJj6JZzL9P9iYxbdUwGsQI4= github.com/LumeraProtocol/rq-go v0.2.1/go.mod h1:APnKCZRh1Es2Vtrd2w4kCLgAyaL5Bqrkz/BURoRJ+O8= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= @@ -208,6 +209,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= @@ -464,6 +466,7 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7HsjsjeEZ6auqU= github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= diff --git a/supernode/cmd/init.go b/supernode/cmd/init.go index 22c4e9a5..3615ad5c 100644 --- a/supernode/cmd/init.go +++ b/supernode/cmd/init.go @@ -2,9 +2,15 @@ package cmd import ( "fmt" + "net" + "net/url" "os" + "os/signal" "path/filepath" + "regexp" "strconv" + "strings" + "syscall" "github.com/AlecAivazis/survey/v2" "github.com/LumeraProtocol/supernode/pkg/keyring" @@ -18,16 +24,23 @@ var ( forceInit bool skipInteractive bool keyringBackendFlag string + keyNameFlag string + shouldRecoverFlag bool + mnemonicFlag string + supernodeAddrFlag string + supernodePortFlag int + lumeraGrpcFlag string + chainIDFlag string ) // Default configuration values const ( - DefaultKeyringBackend = "test" + DefaultKeyringBackend = "os" DefaultKeyName = "" DefaultSupernodeAddr = "0.0.0.0" DefaultSupernodePort = 4444 DefaultLumeraGRPC = "localhost:9090" - DefaultChainID = "lumera" + DefaultChainID = "lumera-mainnet-1" ) // InitInputs holds all user inputs for initialization @@ -60,14 +73,34 @@ Example: supernode init -y # Use default values, skip interactive prompts`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + // Set up signal handling for graceful exit + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + // Create a channel to communicate when the command is done + done := make(chan struct{}) + + // Handle signals in a separate goroutine + go func() { + select { + case <-sigCh: + fmt.Println("\nInterrupted. Exiting...") + os.Exit(0) + case <-done: + return + } + }() + // Setup base directory if err := setupBaseDirectory(); err != nil { + close(done) return err } // Get user inputs through interactive prompts or use defaults inputs, err := gatherUserInputs() if err != nil { + close(done) return err } @@ -76,13 +109,14 @@ Example: return err } - // Setup keyring and handle key creation/recovery (skip if using -y flag) + // Setup keyring and handle key creation/recovery var address string - if !skipInteractive { - address, err = setupKeyring(inputs.KeyName, inputs.ShouldRecover, inputs.Mnemonic) - if err != nil { - return err - } + var generatedMnemonic string + + // Always setup keyring, even in non-interactive mode + address, generatedMnemonic, err = setupKeyring(inputs.KeyName, inputs.ShouldRecover, inputs.Mnemonic) + if err != nil { + return err } // Update config with gathered settings and save @@ -91,7 +125,7 @@ Example: } // Print success message - printSuccessMessage() + printSuccessMessage(generatedMnemonic) return nil }, } @@ -135,24 +169,96 @@ func setupBaseDirectory() error { return nil } -// gatherUserInputs collects all user inputs through interactive prompts or uses defaults +// gatherUserInputs collects all user inputs through interactive prompts or uses defaults/flags func gatherUserInputs() (InitInputs, error) { - if skipInteractive { - // Use default values when -y flag is set - fmt.Println("Using default configuration values...") + // Check if all required parameters are provided via flags + allFlagsProvided := keyNameFlag != "" && + (supernodeAddrFlag != "" && supernodePortFlag != 0 && lumeraGrpcFlag != "" && chainIDFlag != "") && + (!shouldRecoverFlag && mnemonicFlag == "" || shouldRecoverFlag && mnemonicFlag != "") && + keyringBackendFlag != "" + + if skipInteractive && shouldRecoverFlag && mnemonicFlag == "" { + return InitInputs{}, fmt.Errorf("--mnemonic flag is required when --recover flag is set for non-interactive mode") + } + + if !shouldRecoverFlag && mnemonicFlag != "" { + return InitInputs{}, fmt.Errorf("--mnemonic flag should not be set when not recovering a key") + } + + // If -y flag is set or all flags are provided, use flags or defaults + if skipInteractive || allFlagsProvided { + fmt.Println("Using provided flags or default configuration values...") + + // Use flags if provided, otherwise use defaults backend := DefaultKeyringBackend if keyringBackendFlag != "" { backend = keyringBackendFlag } + + // Validate keyring backend + if err := validateKeyringBackend(backend); err != nil { + return InitInputs{}, err + } + + keyName := DefaultKeyName + if keyNameFlag != "" { + keyName = keyNameFlag + + // Validate key name only if provided (default empty string is allowed) + if err := validateKeyName(keyName); err != nil { + return InitInputs{}, err + } + } + + supernodeAddr := DefaultSupernodeAddr + if supernodeAddrFlag != "" { + supernodeAddr = supernodeAddrFlag + + // Validate supernode IP address + if err := validateIPAddress(supernodeAddr); err != nil { + return InitInputs{}, err + } + } + + supernodePort := DefaultSupernodePort + if supernodePortFlag != 0 { + supernodePort = supernodePortFlag + + // Port validation is handled by the flag type (int) + if supernodePort < 1 || supernodePort > 65535 { + return InitInputs{}, fmt.Errorf("invalid supernode port: %d, must be between 1 and 65535", supernodePort) + } + } + + lumeraGRPC := DefaultLumeraGRPC + if lumeraGrpcFlag != "" { + lumeraGRPC = lumeraGrpcFlag + + // Validate GRPC address + if err := validateGRPCAddress(lumeraGRPC); err != nil { + return InitInputs{}, err + } + } + + chainID := DefaultChainID + if chainIDFlag != "" { + chainID = chainIDFlag + } + + // Check if mnemonic is provided when recover flag is set + if shouldRecoverFlag && mnemonicFlag == "" { + return InitInputs{}, fmt.Errorf("--mnemonic flag is required when --recover flag is set") + } + return InitInputs{ KeyringBackend: backend, - KeyName: "", - ShouldRecover: false, - Mnemonic: "", - SupernodeAddr: DefaultSupernodeAddr, - SupernodePort: DefaultSupernodePort, - LumeraGRPC: DefaultLumeraGRPC, - ChainID: DefaultChainID, + KeyName: keyName, + ShouldRecover: shouldRecoverFlag, + Mnemonic: mnemonicFlag, + SupernodeAddr: supernodeAddr, + SupernodePort: supernodePort, + LumeraGRPC: lumeraGRPC, + ChainID: chainID, }, nil } @@ -160,17 +266,18 @@ func gatherUserInputs() (InitInputs, error) { var err error // Interactive setup - inputs.KeyringBackend, err = promptKeyringBackend() + inputs.KeyringBackend, err = promptKeyringBackend(keyringBackendFlag) if err != nil { return InitInputs{}, fmt.Errorf("failed to select keyring backend: %w", err) } - inputs.KeyName, inputs.ShouldRecover, inputs.Mnemonic, err = promptKeyManagement() + inputs.KeyName, inputs.ShouldRecover, inputs.Mnemonic, err = promptKeyManagement(keyNameFlag, shouldRecoverFlag, mnemonicFlag) if err != nil { return InitInputs{}, fmt.Errorf("failed to configure key management: %w", err) } - inputs.SupernodeAddr, inputs.SupernodePort, inputs.LumeraGRPC, inputs.ChainID, err = promptNetworkConfig() + inputs.SupernodeAddr, inputs.SupernodePort, inputs.LumeraGRPC, inputs.ChainID, err = + promptNetworkConfig(supernodeAddrFlag, supernodePortFlag, lumeraGrpcFlag, chainIDFlag) if err != nil { return InitInputs{}, fmt.Errorf("failed to configure network settings: %w", err) } @@ -198,27 +305,30 @@ func createAndSetupConfig(keyName, chainID, keyringBackend string) error { } // setupKeyring initializes keyring and handles key creation or recovery -func setupKeyring(keyName string, shouldRecover bool, mnemonic string) (string, error) { +// Returns address and mnemonic (if a new key was created) +func setupKeyring(keyName string, shouldRecover bool, mnemonic string) (string, string, error) { kr, err := initKeyringFromConfig(appConfig) if err != nil { - return "", fmt.Errorf("failed to initialize keyring: %w", err) + return "", "", fmt.Errorf("failed to initialize keyring: %w", err) } var address string + var generatedMnemonic string if shouldRecover { address, err = recoverExistingKey(kr, keyName, mnemonic) if err != nil { - return "", err + return "", "", err } } else { - address, err = createNewKey(kr, keyName) + address, generatedMnemonic, err = createNewKey(kr, keyName) if err != nil { - return "", err + return "", "", err + } } - return address, nil + return address, generatedMnemonic, nil } // recoverExistingKey handles the recovery of an existing key from mnemonic @@ -247,24 +357,25 @@ func recoverExistingKey(kr consmoskeyring.Keyring, keyName, mnemonic string) (st } // createNewKey handles the creation of a new key -func createNewKey(kr consmoskeyring.Keyring, keyName string) (string, error) { +func createNewKey(kr consmoskeyring.Keyring, keyName string) (string, string, error) { // Generate mnemonic and create new account keyMnemonic, info, err := keyring.CreateNewAccount(kr, keyName) if err != nil { - return "", fmt.Errorf("failed to create new account: %w", err) + return "", "", fmt.Errorf("failed to create new account: %w", err) } addr, err := getAddressFromKeyName(kr, keyName) if err != nil { - return "", fmt.Errorf("failed to get address: %w", err) + return "", "", fmt.Errorf("failed to get address: %w", err) } address := addr.String() - fmt.Printf("Key generated successfully! Name: %s, Address: %s, Mnemonic: %s\n", info.Name, address, keyMnemonic) + fmt.Printf("Key generated successfully! Name: %s, Address: %s\n", info.Name, address) fmt.Println("\nIMPORTANT: Write down the mnemonic and keep it in a safe place.") fmt.Println("The mnemonic is the only way to recover your account if you forget your password.") + fmt.Printf("Mnemonic: %s\n", keyMnemonic) - return address, nil + return address, keyMnemonic, nil } // updateAndSaveConfig updates the configuration with network settings and saves it @@ -287,66 +398,137 @@ func updateAndSaveConfig(address, supernodeAddr string, supernodePort int, lumer } // printSuccessMessage displays the final success message -func printSuccessMessage() { +func printSuccessMessage(mnemonic string) { fmt.Println("\nYour supernode has been initialized successfully!") - fmt.Println("You can now start your supernode with:") + + // If a mnemonic was generated, display it again + if mnemonic != "" { + fmt.Println("\nIMPORTANT: Make sure you have saved your mnemonic:") + fmt.Printf("Mnemonic: %s\n", mnemonic) + } + + fmt.Println("\nYou can now start your supernode with:") fmt.Println(" supernode start") } // Interactive prompt functions -func promptKeyringBackend() (string, error) { +func promptKeyringBackend(passedBackend string) (string, error) { var backend string + if passedBackend != "" { + if passedBackend != "os" && passedBackend != "file" && passedBackend != "test" { + return "", fmt.Errorf("invalid keyring backend: %s, must be one of 'os', 'file', or 'test'", passedBackend) + } + backend = passedBackend + } else { + backend = DefaultKeyringBackend + } prompt := &survey.Select{ Message: "Choose keyring backend:", Options: []string{"os", "file", "test"}, - Default: "os", - Help: "os: OS keyring (most secure), file: encrypted file, test: unencrypted (dev only)", + Default: backend, + Help: "os: OS keyring (most secure), file: encrypted file, test: unencrypted (dev only), Ctrl-C for exit", } return backend, survey.AskOne(prompt, &backend) } -func promptKeyManagement() (keyName string, shouldRecover bool, mnemonic string, err error) { - shouldRecover = true - +func promptKeyManagement(passedKeyName string, recover bool, passedMnemonic string) (keyName string, shouldRecover bool, mnemonic string, err error) { // Key name input with validation keyNamePrompt := &survey.Input{ Message: "Enter key name:", - Help: "Alphanumeric characters and underscores only", + Help: "Alphanumeric characters and underscores only, Ctrl-C for exit", + Default: passedKeyName, } err = survey.AskOne(keyNamePrompt, &keyName, survey.WithValidator(survey.Required)) if err != nil { return "", false, "", err } - // Mnemonic input for recovery - mnemonicPrompt := &survey.Password{ - Message: "Enter your mnemonic phrase:", - Help: "Space-separated words (typically 12 or 24 words)", + // Validate key name format if provided + if keyName != "" { + if err := validateKeyName(keyName); err != nil { + return "", false, "", err + } } - err = survey.AskOne(mnemonicPrompt, &mnemonic, survey.WithValidator(survey.Required)) - if err != nil { - return "", false, "", err + + shouldRecover = recover + if !recover { + // Ask whether to create a new address or recover from mnemonic + createOrRecoverPrompt := &survey.Select{ + Message: "Would you like to create a new address or recover from mnemonic?", + Options: []string{"Create new address", "Recover from mnemonic"}, + Default: "Create new address", + Help: "Create a new address or recover an existing one from mnemonic, Ctrl-C for exit", + } + var createOrRecover string + err = survey.AskOne(createOrRecoverPrompt, &createOrRecover) + if err != nil { + return "", false, "", err + } + + shouldRecover = createOrRecover == "Recover from mnemonic" + } + + // If recovering, ask for mnemonic + mnemonic = passedMnemonic + if shouldRecover && passedMnemonic == "" { + mnemonicPrompt := &survey.Password{ + Message: "Enter your mnemonic phrase:", + Help: "Space-separated words (typically 12 or 24 words), Ctrl-C for exit", + } + err = survey.AskOne(mnemonicPrompt, &mnemonic, survey.WithValidator(survey.Required)) + if err != nil { + return "", false, "", err + } } return keyName, shouldRecover, mnemonic, nil } -func promptNetworkConfig() (supernodeAddr string, supernodePort int, lumeraGrpcAddr string, chainID string, err error) { +func promptNetworkConfig(passedAddrs string, passedPort int, passedGRPC, passedChainID string) (supernodeAddr string, supernodePort int, lumeraGrpcAddr string, chainID string, err error) { + if passedAddrs != "" { + supernodeAddr = passedAddrs + } else { + supernodeAddr = DefaultSupernodeAddr + } + var port string + if passedPort != 0 { + port = fmt.Sprintf("%d", passedPort) + } else { + port = fmt.Sprintf("%d", DefaultSupernodePort) + } + if passedGRPC != "" { + lumeraGrpcAddr = passedGRPC + } else { + lumeraGrpcAddr = DefaultLumeraGRPC + } + if passedChainID != "" { + chainID = passedChainID + } else { + chainID = DefaultChainID + } + // Supernode IP address supernodePrompt := &survey.Input{ Message: "Enter supernode IP address:", - Default: DefaultSupernodeAddr, + Default: supernodeAddr, + Help: "IP address for the supernode to listen on, Ctrl-C for exit", } err = survey.AskOne(supernodePrompt, &supernodeAddr) if err != nil { return "", 0, "", "", err } + // Validate IP address format + if err := validateIPAddress(supernodeAddr); err != nil { + return "", 0, "", "", err + } + // Supernode port var portStr string supernodePortPrompt := &survey.Input{ Message: "Enter supernode port:", - Default: fmt.Sprintf("%d", DefaultSupernodePort), + Default: port, + Help: "Port for the supernode to listen on (1-65535), Ctrl-C for exit", } err = survey.AskOne(supernodePortPrompt, &portStr) if err != nil { @@ -361,17 +543,24 @@ func promptNetworkConfig() (supernodeAddr string, supernodePort int, lumeraGrpcA // Lumera GRPC address (full address with port) lumeraPrompt := &survey.Input{ Message: "Enter Lumera GRPC address:", - Default: DefaultLumeraGRPC, + Default: lumeraGrpcAddr, + Help: "GRPC address of the Lumera node (host:port), Ctrl-C for exit", } err = survey.AskOne(lumeraPrompt, &lumeraGrpcAddr) if err != nil { return "", 0, "", "", err } + // Validate GRPC address format + if err := validateGRPCAddress(lumeraGrpcAddr); err != nil { + return "", 0, "", "", err + } + // Chain ID chainPrompt := &survey.Input{ Message: "Enter chain ID:", - Default: DefaultChainID, + Default: chainID, + Help: "Chain ID of the Lumera network, Ctrl-C for exit", } err = survey.AskOne(chainPrompt, &chainID, survey.WithValidator(survey.Required)) if err != nil { @@ -381,6 +570,97 @@ func promptNetworkConfig() (supernodeAddr string, supernodePort int, lumeraGrpcA return supernodeAddr, supernodePort, lumeraGrpcAddr, chainID, nil } +// validateKeyringBackend checks if the provided keyring backend is valid +func validateKeyringBackend(backend string) error { + if backend != "os" && backend != "file" && backend != "test" { + return fmt.Errorf("invalid keyring backend: %s, must be one of 'os', 'file', or 'test'", backend) + } + return nil +} + +// validateKeyName checks if the provided key name contains only alphanumeric characters and underscores +func validateKeyName(keyName string) error { + if keyName == "" { + return fmt.Errorf("key name cannot be empty") + } + + matched, err := regexp.MatchString("^[a-zA-Z0-9_]+$", keyName) + if err != nil { + return fmt.Errorf("error validating key name: %w", err) + } + + if !matched { + return fmt.Errorf("invalid key name: %s, must contain only alphanumeric characters and underscores", keyName) + } + + return nil +} + +// validateIPAddress checks if the provided string is a valid IP address +func validateIPAddress(ipAddress string) error { + if ipAddress == "" { + return fmt.Errorf("IP address cannot be empty") + } + + ip := net.ParseIP(ipAddress) + if ip == nil { + return fmt.Errorf("invalid IP address format: %s", ipAddress) + } + + return nil +} + +// validateGRPCAddress checks if the provided string follows valid GRPC address formats: +// - host:port +// - schema://hostname-or-ip +// - schema://hostname-or-ip:port +func validateGRPCAddress(grpcAddress string) error { + if grpcAddress == "" { + return fmt.Errorf("GRPC address cannot be empty") + } + + // Check if the address has a schema (starts with schema://) + if strings.Contains(grpcAddress, "://") { + // Parse as URL + parsedURL, err := url.Parse(grpcAddress) + if err != nil { + return fmt.Errorf("invalid GRPC address format: %s, error: %v", grpcAddress, err) + } + + // Validate host part + if parsedURL.Host == "" { + return fmt.Errorf("host part of GRPC address cannot be empty") + } + + // If port is specified, validate it + if parsedURL.Port() != "" { + portNum, err := strconv.Atoi(parsedURL.Port()) + if err != nil || portNum < 1 || portNum > 65535 { + return fmt.Errorf("invalid port in GRPC address: %s, must be a number between 1 and 65535", parsedURL.Port()) + } + } + } else { + // No schema, should be in host:port format + host, port, err := net.SplitHostPort(grpcAddress) + if err != nil { + return fmt.Errorf("invalid GRPC address format: %s, must be in host:port format or schema://host[:port] format", grpcAddress) + } + + // Validate host part + if host == "" { + return fmt.Errorf("host part of GRPC address cannot be empty") + } + + // Validate port part + portNum, err := strconv.Atoi(port) + if err != nil || portNum < 1 || portNum > 65535 { + return fmt.Errorf("invalid port in GRPC address: %s, must be a number between 1 and 65535", port) + } + } + + return nil +} + func init() { rootCmd.AddCommand(initCmd) @@ -388,4 +668,11 @@ func init() { initCmd.Flags().BoolVar(&forceInit, "force", false, "Force initialization, overwriting existing directory") initCmd.Flags().BoolVarP(&skipInteractive, "yes", "y", false, "Skip interactive prompts and use default values") initCmd.Flags().StringVar(&keyringBackendFlag, "keyring-backend", "", "Keyring backend to use with -y flag (test, file, os)") + initCmd.Flags().StringVar(&keyNameFlag, "key-name", "", "Name of the key to create or recover") + initCmd.Flags().BoolVar(&shouldRecoverFlag, "recover", false, "Recover an existing key from mnemonic") + initCmd.Flags().StringVar(&mnemonicFlag, "mnemonic", "", "Mnemonic phrase for key recovery (only used with --recover)") + initCmd.Flags().StringVar(&supernodeAddrFlag, "supernode-addr", "", "IP address for the supernode to listen on") + initCmd.Flags().IntVar(&supernodePortFlag, "supernode-port", 0, "Port for the supernode to listen on") + initCmd.Flags().StringVar(&lumeraGrpcFlag, "lumera-grpc", "", "GRPC address of the Lumera node (host:port)") + initCmd.Flags().StringVar(&chainIDFlag, "chain-id", "", "Chain ID of the Lumera network") } diff --git a/test_init_validation.sh b/test_init_validation.sh new file mode 100755 index 00000000..3a95987b --- /dev/null +++ b/test_init_validation.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# Test script for init command validation + +# Define colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' # No Color + +# Build the supernode binary +echo -e "${CYAN}Building supernode binary...${NC}" +make build + +# Test cases for keyring backend validation +echo -e "\n\n${BOLD}${BLUE}=== Testing keyring backend validation ===${NC}" +echo -e "${YELLOW}Test 1: Invalid keyring backend${NC}" +./release/supernode-linux-amd64 init -y --keyring-backend=invalid +echo -e "${RED}Expected: Error about invalid keyring backend${NC}" + +echo -e "\n${YELLOW}Test 2: Valid keyring backend (test)${NC}" +./release/supernode-linux-amd64 init -y --keyring-backend=test --force +echo -e "${GREEN}Expected: No error about keyring backend${NC}" + +# Test cases for key name validation +echo -e "\n\n${BOLD}${BLUE}=== Testing key name validation ===${NC}" +echo -e "${YELLOW}Test 3: Invalid key name with special characters${NC}" +./release/supernode-linux-amd64 init -y --key-name="invalid@name" --force +echo -e "${RED}Expected: Error about invalid key name${NC}" + +echo -e "\n${YELLOW}Test 4: Valid key name${NC}" +./release/supernode-linux-amd64 init -y --key-name="valid_name_123" --force +echo -e "${GREEN}Expected: No error about key name${NC}" + +# Test cases for IP address validation +echo -e "\n\n${BOLD}${BLUE}=== Testing IP address validation ===${NC}" +echo -e "${YELLOW}Test 5: Invalid IP address${NC}" +./release/supernode-linux-amd64 init -y --supernode-addr="not.an.ip.address" --force +echo -e "${RED}Expected: Error about invalid IP address${NC}" + +echo -e "\n${YELLOW}Test 6: Valid IP address${NC}" +./release/supernode-linux-amd64 init -y --supernode-addr="192.168.1.1" --force +echo -e "${GREEN}Expected: No error about IP address${NC}" + +# Test cases for GRPC address validation +echo -e "\n\n${BOLD}${BLUE}=== Testing GRPC address validation ===${NC}" +echo -e "${YELLOW}Test 7: Invalid GRPC address (no port)${NC}" +./release/supernode-linux-amd64 init -y --lumera-grpc="localhost" --force +echo -e "${RED}Expected: Error about invalid GRPC address format${NC}" + +echo -e "\n${YELLOW}Test 8: Invalid GRPC address (invalid port)${NC}" +./release/supernode-linux-amd64 init -y --lumera-grpc="localhost:invalid" --force +echo -e "${RED}Expected: Error about invalid port in GRPC address${NC}" + +echo -e "\n${YELLOW}Test 9: Valid GRPC address (host:port format)${NC}" +./release/supernode-linux-amd64 init -y --lumera-grpc="localhost:9090" --force +echo -e "${GREEN}Expected: No error about GRPC address${NC}" + +echo -e "\n${YELLOW}Test 12: Valid GRPC address with schema (schema://host)${NC}" +./release/supernode-linux-amd64 init -y --lumera-grpc="http://example.com" --force +echo -e "${GREEN}Expected: No error about GRPC address${NC}" + +echo -e "\n${YELLOW}Test 13: Valid GRPC address with schema and port (schema://host:port)${NC}" +./release/supernode-linux-amd64 init -y --lumera-grpc="https://example.com:8080" --force +echo -e "${GREEN}Expected: No error about GRPC address${NC}" + +echo -e "\n${YELLOW}Test 14: Invalid GRPC address with schema (invalid port)${NC}" +./release/supernode-linux-amd64 init -y --lumera-grpc="http://example.com:invalid" --force +echo -e "${RED}Expected: Error about invalid port in GRPC address${NC}" + +# Test cases for port validation +echo -e "\n\n${BOLD}${BLUE}=== Testing port validation ===${NC}" +echo -e "${YELLOW}Test 10: Invalid port (too high)${NC}" +./release/supernode-linux-amd64 init -y --supernode-port=70000 --force +echo -e "${RED}Expected: Error about invalid port${NC}" + +echo -e "\n${YELLOW}Test 11: Valid port${NC}" +./release/supernode-linux-amd64 init -y --supernode-port=4444 --force +echo -e "${GREEN}Expected: No error about port${NC}" + +echo -e "\n\n${BOLD}${GREEN}All tests completed.${NC}" \ No newline at end of file diff --git a/test_interactive_validation.sh b/test_interactive_validation.sh new file mode 100755 index 00000000..e5faf8f4 --- /dev/null +++ b/test_interactive_validation.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Define colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' # No Color + +echo -e "${BOLD}${BLUE}=== Testing Interactive Mode Validation ===${NC}" +echo -e "${CYAN}This script will help you manually test the validation in interactive mode.${NC}" +echo -e "${CYAN}Follow the prompts and enter invalid values to test validation.${NC}" +echo "" + +echo -e "${BOLD}${YELLOW}1. Testing Key Name Validation${NC}" +echo -e "${YELLOW} - Enter an invalid key name (e.g., 'invalid@name')${NC}" +echo -e "${MAGENTA} - Validation should fail with an error message${NC}" +echo "" + +echo -e "${BOLD}${YELLOW}2. Testing IP Address Validation${NC}" +echo -e "${YELLOW} - Enter an invalid IP address (e.g., 'not.an.ip')${NC}" +echo -e "${MAGENTA} - Validation should fail with an error message${NC}" +echo "" + +echo -e "${BOLD}${YELLOW}3. Testing GRPC Address Validation${NC}" +echo -e "${YELLOW} - Enter an invalid GRPC address (e.g., 'localhost' without port)${NC}" +echo -e "${MAGENTA} - Validation should fail with an error message${NC}" +echo "" + +echo -e "${CYAN}Press Enter to start the test...${NC}" +read + +# Force cleanup of previous config +echo -e "${BLUE}Running initialization with force flag...${NC}" +./release/supernode-linux-amd64 init --force + +echo "" +echo -e "${BOLD}${GREEN}Test completed. Check if validation errors were displayed correctly.${NC}" \ No newline at end of file