From 1fe94ec5c93472aed15134e455290513cf471b7b Mon Sep 17 00:00:00 2001 From: a-ok123 Date: Wed, 23 Jul 2025 22:17:43 -0400 Subject: [PATCH 1/3] Fixed command --- supernode/cmd/init.go | 186 ++++++++++++++++++++++++++++++++---------- 1 file changed, 145 insertions(+), 41 deletions(-) diff --git a/supernode/cmd/init.go b/supernode/cmd/init.go index 22c4e9a5..0d7971f2 100644 --- a/supernode/cmd/init.go +++ b/supernode/cmd/init.go @@ -3,8 +3,10 @@ package cmd import ( "fmt" "os" + "os/signal" "path/filepath" "strconv" + "syscall" "github.com/AlecAivazis/survey/v2" "github.com/LumeraProtocol/supernode/pkg/keyring" @@ -18,6 +20,13 @@ var ( forceInit bool skipInteractive bool keyringBackendFlag string + keyNameFlag string + shouldRecoverFlag bool + mnemonicFlag string + supernodeAddrFlag string + supernodePortFlag int + lumeraGrpcFlag string + chainIDFlag string ) // Default configuration values @@ -27,7 +36,7 @@ const ( 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 +69,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 +105,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 +121,7 @@ Example: } // Print success message - printSuccessMessage() + printSuccessMessage(generatedMnemonic) return nil }, } @@ -135,24 +165,61 @@ 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 != "") + + // 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 } + + keyName := DefaultKeyName + if keyNameFlag != "" { + keyName = keyNameFlag + } + + supernodeAddr := DefaultSupernodeAddr + if supernodeAddrFlag != "" { + supernodeAddr = supernodeAddrFlag + } + + supernodePort := DefaultSupernodePort + if supernodePortFlag != 0 { + supernodePort = supernodePortFlag + } + + lumeraGRPC := DefaultLumeraGRPC + if lumeraGrpcFlag != "" { + lumeraGRPC = lumeraGrpcFlag + } + + 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 } @@ -198,27 +265,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 +317,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,9 +358,16 @@ 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") } @@ -300,34 +378,49 @@ func promptKeyringBackend() (string, error) { Message: "Choose keyring backend:", Options: []string{"os", "file", "test"}, Default: "os", - Help: "os: OS keyring (most secure), file: encrypted file, test: unencrypted (dev only)", + 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 - // 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", } 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)", + // 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", } - err = survey.AskOne(mnemonicPrompt, &mnemonic, survey.WithValidator(survey.Required)) + var createOrRecover string + err = survey.AskOne(createOrRecoverPrompt, &createOrRecover) if err != nil { return "", false, "", err } + shouldRecover = createOrRecover == "Recover from mnemonic" + + // If recovering, ask for mnemonic + if shouldRecover { + 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 } @@ -336,6 +429,7 @@ func promptNetworkConfig() (supernodeAddr string, supernodePort int, lumeraGrpcA supernodePrompt := &survey.Input{ Message: "Enter supernode IP address:", Default: DefaultSupernodeAddr, + Help: "IP address for the supernode to listen on, Ctrl-C for exit", } err = survey.AskOne(supernodePrompt, &supernodeAddr) if err != nil { @@ -347,6 +441,7 @@ func promptNetworkConfig() (supernodeAddr string, supernodePort int, lumeraGrpcA supernodePortPrompt := &survey.Input{ Message: "Enter supernode port:", Default: fmt.Sprintf("%d", DefaultSupernodePort), + Help: "Port for the supernode to listen on (1-65535), Ctrl-C for exit", } err = survey.AskOne(supernodePortPrompt, &portStr) if err != nil { @@ -362,6 +457,7 @@ func promptNetworkConfig() (supernodeAddr string, supernodePort int, lumeraGrpcA lumeraPrompt := &survey.Input{ Message: "Enter Lumera GRPC address:", Default: DefaultLumeraGRPC, + Help: "GRPC address of the Lumera node (host:port), Ctrl-C for exit", } err = survey.AskOne(lumeraPrompt, &lumeraGrpcAddr) if err != nil { @@ -372,6 +468,7 @@ func promptNetworkConfig() (supernodeAddr string, supernodePort int, lumeraGrpcA chainPrompt := &survey.Input{ Message: "Enter chain ID:", Default: DefaultChainID, + Help: "Chain ID of the Lumera network, Ctrl-C for exit", } err = survey.AskOne(chainPrompt, &chainID, survey.WithValidator(survey.Required)) if err != nil { @@ -388,4 +485,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") } From 9f1b1474a4bc4012b73566b2246fa4020305ef38 Mon Sep 17 00:00:00 2001 From: a-ok123 Date: Fri, 25 Jul 2025 12:31:23 -0400 Subject: [PATCH 2/3] Imporved - interactive/non-interactive actions for init command --- go.mod | 2 +- go.sum | 3 ++ supernode/cmd/init.go | 95 +++++++++++++++++++++++++++++++------------ 3 files changed, 73 insertions(+), 27 deletions(-) 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 0d7971f2..88493772 100644 --- a/supernode/cmd/init.go +++ b/supernode/cmd/init.go @@ -169,7 +169,17 @@ func setupBaseDirectory() error { func gatherUserInputs() (InitInputs, error) { // Check if all required parameters are provided via flags allFlagsProvided := keyNameFlag != "" && - (supernodeAddrFlag != "" || supernodePortFlag != 0 || lumeraGrpcFlag != "" || chainIDFlag != "") + (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 { @@ -227,17 +237,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) } @@ -372,45 +383,55 @@ func printSuccessMessage(mnemonic string) { } // Interactive prompt functions -func promptKeyringBackend() (string, error) { +func promptKeyringBackend(passedBackend string) (string, error) { var backend string + if passedBackend != "" { + backend = passedBackend + } else { + backend = DefaultKeyringBackend + } prompt := &survey.Select{ Message: "Choose keyring backend:", Options: []string{"os", "file", "test"}, - Default: "os", + 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) { +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, Ctrl-C for exit", + Default: passedKeyName, } err = survey.AskOne(keyNamePrompt, &keyName, survey.WithValidator(survey.Required)) if err != nil { return "", false, "", err } - // 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 = 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" + shouldRecover = createOrRecover == "Recover from mnemonic" + } // If recovering, ask for mnemonic - if shouldRecover { + 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", @@ -424,11 +445,33 @@ func promptKeyManagement() (keyName string, shouldRecover bool, mnemonic string, 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) @@ -440,7 +483,7 @@ func promptNetworkConfig() (supernodeAddr string, supernodePort int, lumeraGrpcA 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) @@ -456,7 +499,7 @@ 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) @@ -467,7 +510,7 @@ func promptNetworkConfig() (supernodeAddr string, supernodePort int, lumeraGrpcA // 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)) From a8dff8fd71f5847ae19e64221821730098a868bf Mon Sep 17 00:00:00 2001 From: a-ok123 Date: Fri, 25 Jul 2025 15:06:19 -0400 Subject: [PATCH 3/3] Imporved - interactive/non-interactive actions for init command --- supernode/cmd/init.go | 142 ++++++++++++++++++++++++++++++++- test_init_validation.sh | 85 ++++++++++++++++++++ test_interactive_validation.sh | 41 ++++++++++ 3 files changed, 267 insertions(+), 1 deletion(-) create mode 100755 test_init_validation.sh create mode 100755 test_interactive_validation.sh diff --git a/supernode/cmd/init.go b/supernode/cmd/init.go index 88493772..3615ad5c 100644 --- a/supernode/cmd/init.go +++ b/supernode/cmd/init.go @@ -2,10 +2,14 @@ package cmd import ( "fmt" + "net" + "net/url" "os" "os/signal" "path/filepath" + "regexp" "strconv" + "strings" "syscall" "github.com/AlecAivazis/survey/v2" @@ -31,7 +35,7 @@ var ( // Default configuration values const ( - DefaultKeyringBackend = "test" + DefaultKeyringBackend = "os" DefaultKeyName = "" DefaultSupernodeAddr = "0.0.0.0" DefaultSupernodePort = 4444 @@ -191,24 +195,49 @@ func gatherUserInputs() (InitInputs, error) { 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 @@ -386,6 +415,9 @@ func printSuccessMessage(mnemonic string) { 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 @@ -411,6 +443,13 @@ func promptKeyManagement(passedKeyName string, recover bool, passedMnemonic stri return "", false, "", err } + // Validate key name format if provided + if keyName != "" { + if err := validateKeyName(keyName); err != nil { + return "", false, "", err + } + } + shouldRecover = recover if !recover { // Ask whether to create a new address or recover from mnemonic @@ -479,6 +518,11 @@ func promptNetworkConfig(passedAddrs string, passedPort int, passedGRPC, passedC return "", 0, "", "", err } + // Validate IP address format + if err := validateIPAddress(supernodeAddr); err != nil { + return "", 0, "", "", err + } + // Supernode port var portStr string supernodePortPrompt := &survey.Input{ @@ -507,6 +551,11 @@ func promptNetworkConfig(passedAddrs string, passedPort int, passedGRPC, passedC 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:", @@ -521,6 +570,97 @@ func promptNetworkConfig(passedAddrs string, passedPort int, passedGRPC, passedC 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) 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