mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
464 lines
12 KiB
Go
464 lines
12 KiB
Go
// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package prompts
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"net/http"
|
|
"net/mail"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/luxfi/address"
|
|
"github.com/luxfi/cli/pkg/ux"
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/sdk/models"
|
|
)
|
|
|
|
func validateEmail(input string) error {
|
|
_, err := mail.ParseAddress(input)
|
|
return err
|
|
}
|
|
|
|
func ValidateURLFormat(input string) error {
|
|
if input == "" {
|
|
return errors.New("empty url")
|
|
}
|
|
parsedURL, err := url.Parse(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if parsedURL.Scheme == "" {
|
|
return errors.New("invalid URI for request")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePositiveBigInt(input string) error {
|
|
n := new(big.Int)
|
|
n, ok := n.SetString(input, 10)
|
|
if !ok {
|
|
return errors.New("invalid number")
|
|
}
|
|
if n.Cmp(big.NewInt(0)) == -1 {
|
|
return errors.New("invalid number")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateTime(input string) error {
|
|
t, err := time.Parse(constants.TimeParseLayout, input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if t.Before(time.Now().Add(constants.StakingStartLeadTime)) {
|
|
return fmt.Errorf("time should be at least start from now + %s", constants.StakingStartLeadTime)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateNodeID(input string) error {
|
|
_, err := ids.NodeIDFromString(input)
|
|
return err
|
|
}
|
|
|
|
func validateAddress(input string) error {
|
|
if !common.IsHexAddress(input) {
|
|
return errors.New("invalid address")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateExistingFilepath(input string) error {
|
|
if fileInfo, err := os.Stat(input); err == nil && !fileInfo.IsDir() {
|
|
return nil
|
|
}
|
|
return errors.New("file doesn't exist")
|
|
}
|
|
|
|
func validateWeight(input string) error {
|
|
val, err := strconv.ParseUint(input, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if val < constants.MinStakeWeight {
|
|
return errors.New("the weight must be an integer between 1 and 100")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateBiggerThanZero(input string) error {
|
|
val, err := strconv.ParseUint(input, 0, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if val == 0 {
|
|
return errors.New("the value must be bigger than zero")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateURL(input string) error {
|
|
_, err := url.ParseRequestURI(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePChainAddress(input string) (string, error) {
|
|
chainID, hrp, _, err := address.Parse(input)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if chainID != "P" {
|
|
return "", errors.New("this is not a PChain address")
|
|
}
|
|
return hrp, nil
|
|
}
|
|
|
|
func validatePChainTestnetAddress(input string) error {
|
|
hrp, err := validatePChainAddress(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if hrp != constants.TestnetHRP {
|
|
return errors.New("this is not a testnet address")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePChainMainAddress(input string) error {
|
|
hrp, err := validatePChainAddress(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if hrp != constants.MainnetHRP {
|
|
return errors.New("this is not a mainnet address")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePChainLocalAddress(input string) error {
|
|
hrp, err := validatePChainAddress(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if hrp != constants.CustomHRP {
|
|
return errors.New("this is not a custom address")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getPChainValidationFunc(network models.Network) func(string) error {
|
|
switch network.Kind() {
|
|
case models.Testnet:
|
|
return validatePChainTestnetAddress
|
|
case models.Mainnet:
|
|
return validatePChainMainAddress
|
|
case models.Local, models.Devnet:
|
|
return validatePChainLocalAddress
|
|
default:
|
|
return func(string) error {
|
|
return errors.New("unsupported network")
|
|
}
|
|
}
|
|
}
|
|
|
|
// validateXChainAddress validates an X-Chain address format
|
|
func validateXChainAddress(input string) (string, error) {
|
|
chainID, hrp, _, err := address.Parse(input)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if chainID != "X" {
|
|
return "", errors.New("not a XChain address")
|
|
}
|
|
return hrp, nil
|
|
}
|
|
|
|
// validateXChainTestnetAddress validates an X-Chain testnet address
|
|
func validateXChainTestnetAddress(input string) error {
|
|
hrp, err := validateXChainAddress(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if hrp != constants.TestnetHRP {
|
|
return errors.New("this is not a testnet address")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateXChainMainAddress validates an X-Chain mainnet address
|
|
func validateXChainMainAddress(input string) error {
|
|
hrp, err := validateXChainAddress(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if hrp != constants.MainnetHRP {
|
|
return errors.New("this is not a mainnet address")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateXChainLocalAddress validates an X-Chain local address
|
|
func validateXChainLocalAddress(input string) error {
|
|
hrp, err := validateXChainAddress(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if hrp != constants.CustomHRP {
|
|
return errors.New("this is not a custom address")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getXChainValidationFunc returns the appropriate X-Chain validation function for a network
|
|
func getXChainValidationFunc(network models.Network) func(string) error {
|
|
switch network.Kind() {
|
|
case models.Testnet:
|
|
return validateXChainTestnetAddress
|
|
case models.Mainnet:
|
|
return validateXChainMainAddress
|
|
case models.Local, models.Devnet:
|
|
return validateXChainLocalAddress
|
|
default:
|
|
return func(string) error {
|
|
return errors.New("unsupported network")
|
|
}
|
|
}
|
|
}
|
|
|
|
// ValidateHexa validates a hexadecimal string
|
|
func ValidateHexa(s string) error {
|
|
if !strings.HasPrefix(s, "0x") {
|
|
return errors.New("hexadecimal string must start with 0x")
|
|
}
|
|
if len(s) <= 2 {
|
|
return errors.New("hexadecimal string must have at least one character after 0x")
|
|
}
|
|
for _, c := range s[2:] {
|
|
if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
|
|
return errors.New("invalid hexadecimal character")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateID(input string) error {
|
|
_, err := ids.FromString(input)
|
|
return err
|
|
}
|
|
|
|
func validateNewFilepath(input string) error {
|
|
if _, err := os.Stat(input); err != nil && os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return errors.New("file already exists")
|
|
}
|
|
|
|
// validateNonEmpty validates that a string is not empty
|
|
func validateNonEmpty(input string) error {
|
|
if input == "" {
|
|
return errors.New("string cannot be empty")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateMainnetStakingDuration validates staking duration for mainnet
|
|
func validateMainnetStakingDuration(input string) error {
|
|
duration, err := time.ParseDuration(input)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid duration format: %w", err)
|
|
}
|
|
// Mainnet min staking duration is 2 weeks
|
|
if duration < 14*24*time.Hour {
|
|
return fmt.Errorf("below the minimum staking duration of %s", ux.FormatDuration(14*24*time.Hour))
|
|
}
|
|
// Mainnet max staking duration is 1 year
|
|
if duration > 365*24*time.Hour {
|
|
return fmt.Errorf("exceeds maximum staking duration of %s", ux.FormatDuration(365*24*time.Hour))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateMainnetL1StakingDuration validates L1 staking duration for mainnet
|
|
func validateMainnetL1StakingDuration(input string) error {
|
|
duration, err := time.ParseDuration(input)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid duration format: %w", err)
|
|
}
|
|
// L1 min staking duration is 24 hours
|
|
if duration < 24*time.Hour {
|
|
return fmt.Errorf("below the minimum staking duration of %s", ux.FormatDuration(24*time.Hour))
|
|
}
|
|
// L1 max staking duration is 1 year
|
|
if duration > 365*24*time.Hour {
|
|
return fmt.Errorf("exceeds maximum staking duration of %s", ux.FormatDuration(365*24*time.Hour))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateTestnetStakingDuration validates staking duration for testnet
|
|
func validateTestnetStakingDuration(input string) error {
|
|
duration, err := time.ParseDuration(input)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid duration format: %w", err)
|
|
}
|
|
// Testnet min staking duration is 24 hours
|
|
if duration < 24*time.Hour {
|
|
return fmt.Errorf("below the minimum staking duration of %s", ux.FormatDuration(24*time.Hour))
|
|
}
|
|
// Testnet max staking duration is 365 days
|
|
if duration > 365*24*time.Hour {
|
|
return fmt.Errorf("exceeds maximum staking duration of %s", ux.FormatDuration(365*24*time.Hour))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateDuration validates a general duration string
|
|
func validateDuration(input string) error {
|
|
_, err := time.ParseDuration(input)
|
|
return err
|
|
}
|
|
|
|
// ValidateNodeID validates a node ID string (exported for external use)
|
|
func ValidateNodeID(input string) error {
|
|
return validateNodeID(input)
|
|
}
|
|
|
|
// validateAddresses validates comma-separated addresses
|
|
func validateAddresses(input string) error {
|
|
parts := strings.Split(input, ",")
|
|
for _, part := range parts {
|
|
addr := strings.TrimSpace(part)
|
|
if !common.IsHexAddress(addr) {
|
|
return fmt.Errorf("invalid address: %s", addr)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateValidatorBalanceFunc returns a validator function for balance
|
|
func validateValidatorBalanceFunc(availableBalance float64, minBalance float64) func(string) error {
|
|
return func(input string) error {
|
|
val, err := strconv.ParseFloat(input, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if val <= 0 {
|
|
return fmt.Errorf("entered value has to be greater than 0 LUX")
|
|
}
|
|
if val < minBalance {
|
|
return fmt.Errorf("validator balance must be at least %.2f LUX", minBalance)
|
|
}
|
|
if val > availableBalance {
|
|
return fmt.Errorf("current balance of %.2f is not sufficient", availableBalance)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RequestURL makes a GET request to validate URL connectivity
|
|
func RequestURL(url string) error {
|
|
// First validate the format
|
|
if err := ValidateURLFormat(url); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Make HTTP HEAD request to check if URL is reachable
|
|
client := &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
}
|
|
resp, err := client.Head(url)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to reach URL: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// Check for successful status codes (2xx or 3xx)
|
|
if resp.StatusCode >= 400 {
|
|
return fmt.Errorf("URL returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateURL validates URL format and optionally checks connectivity
|
|
func ValidateURL(input string, checkConnection bool) error {
|
|
if err := ValidateURLFormat(input); err != nil {
|
|
return err
|
|
}
|
|
if checkConnection {
|
|
return RequestURL(input)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateRepoBranch validates a git branch name
|
|
func ValidateRepoBranch(branch string) error {
|
|
// First check if non-empty using generic validator
|
|
if err := validateNonEmpty(branch); err != nil {
|
|
return err
|
|
}
|
|
// Basic validation for branch names
|
|
if strings.Contains(branch, " ") {
|
|
return errors.New("branch name cannot contain spaces")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateRepoFile validates a repository file path
|
|
func ValidateRepoFile(filepath string) error {
|
|
// First check if non-empty using generic validator
|
|
if err := validateNonEmpty(filepath); err != nil {
|
|
return err
|
|
}
|
|
// Basic validation for file paths
|
|
if strings.HasPrefix(filepath, "/") {
|
|
return errors.New("file path should be relative, not absolute")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateWeightFunc returns a validator function for weight values
|
|
//
|
|
//nolint:unparam // minWeight is configurable even if tests only use 1
|
|
func validateWeightFunc(minWeight, maxWeight uint64) func(string) error {
|
|
return func(input string) error {
|
|
val, err := strconv.ParseUint(input, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if val < minWeight {
|
|
return fmt.Errorf("weight must be at least %d", minWeight)
|
|
}
|
|
if val > maxWeight {
|
|
return fmt.Errorf("weight cannot exceed %d", maxWeight)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ValidatePositiveInt validates that a string can be parsed as a positive integer
|
|
func ValidatePositiveInt(input string) error {
|
|
val, err := strconv.Atoi(input)
|
|
if err != nil {
|
|
return errors.New("invalid integer format")
|
|
}
|
|
if val <= 0 {
|
|
return errors.New("value must be positive")
|
|
}
|
|
return nil
|
|
}
|