From a6f207ad1b0d6754d783bdae0b3348d0356f7b78 Mon Sep 17 00:00:00 2001 From: Michael Tsitrin <114929630+mtsitrin@users.noreply.github.com> Date: Sun, 2 Jul 2023 15:26:28 +0300 Subject: [PATCH] feat: add interactive cli for rollapp init (#136) --- cmd/config/init/consts.go | 10 ++-- cmd/config/init/flags.go | 84 +++++++--------------------- cmd/config/init/init.go | 31 ++++++---- cmd/config/init/interactive.go | 60 ++++++++++++++++++++ cmd/config/init/unique_rollapp_id.go | 4 +- cmd/sequencer/start/start.go | 18 +++++- cmd/utils/config.go | 56 ++++++++++++++++++- 7 files changed, 181 insertions(+), 82 deletions(-) create mode 100644 cmd/config/init/interactive.go diff --git a/cmd/config/init/consts.go b/cmd/config/init/consts.go index 974cd871..cb98628f 100644 --- a/cmd/config/init/consts.go +++ b/cmd/config/init/consts.go @@ -6,23 +6,25 @@ var FlagNames = struct { TokenSupply string RollappBinary string HubID string + Interactive string }{ TokenSupply: "token-supply", RollappBinary: "rollapp-binary", HubID: "hub", + Interactive: "interactive", } const ( - StagingHubID = "internal-devnet" + StagingHubID = "devnet" LocalHubID = "local" ) // TODO(#112): The avaialble hub networks should be read from YAML file var Hubs = map[string]utils.HubData{ StagingHubID: { - API_URL: "https://rest-hub-devnet.dymension.xyz", - ID: "devnet_666-1", - RPC_URL: "https://rpc-hub-devnet.dymension.xyz:443", + API_URL: "https://dymension.devnet.api.silknodes.io:443", + ID: "devnet_304-1", + RPC_URL: "https://dymension.devnet.rpc.silknodes.io:443", }, LocalHubID: { API_URL: "http://localhost:1318", diff --git a/cmd/config/init/flags.go b/cmd/config/init/flags.go index 7f1a174e..e6684853 100644 --- a/cmd/config/init/flags.go +++ b/cmd/config/init/flags.go @@ -2,9 +2,6 @@ package initconfig import ( "fmt" - "regexp" - - "math/big" "github.com/dymensionxyz/roller/cmd/consts" "github.com/dymensionxyz/roller/cmd/utils" @@ -17,16 +14,9 @@ const ( func addFlags(cmd *cobra.Command) { cmd.Flags().StringP(FlagNames.HubID, "", StagingHubID, fmt.Sprintf("The ID of the Dymension hub. %s", getAvailableHubsMessage())) - cmd.Flags().StringP(FlagNames.RollappBinary, "", "", "The rollapp binary. Should be passed only if you built a custom rollapp") + cmd.Flags().StringP(FlagNames.RollappBinary, "", consts.Executables.RollappEVM, "The rollapp binary. Should be passed only if you built a custom rollapp") cmd.Flags().StringP(FlagNames.TokenSupply, "", defaultTokenSupply, "The total token supply of the RollApp") -} - -func getRollappBinaryPath(cmd *cobra.Command) string { - rollappBinaryPath := cmd.Flag(FlagNames.RollappBinary).Value.String() - if rollappBinaryPath == "" { - return consts.Executables.RollappEVM - } - return rollappBinaryPath + cmd.Flags().BoolP(FlagNames.Interactive, "", false, "Run roller in interactive mode") } func getTokenSupply(cmd *cobra.Command) string { @@ -34,23 +24,31 @@ func getTokenSupply(cmd *cobra.Command) string { } func GetInitConfig(initCmd *cobra.Command, args []string) (utils.RollappConfig, error) { + cfg := utils.RollappConfig{} + cfg.Home = initCmd.Flag(utils.FlagNames.Home).Value.String() + cfg.RollappBinary = initCmd.Flag(FlagNames.RollappBinary).Value.String() + + interactive, _ := initCmd.Flags().GetBool(FlagNames.Interactive) + if interactive { + RunInteractiveMode(&cfg) + return cfg, nil + } + rollappId := args[0] denom := args[1] - home := initCmd.Flag(utils.FlagNames.Home).Value.String() - rollappBinaryPath := getRollappBinaryPath(initCmd) + hubID := initCmd.Flag(FlagNames.HubID).Value.String() tokenSupply := getTokenSupply(initCmd) - return utils.RollappConfig{ - Home: home, - RollappID: rollappId, - RollappBinary: rollappBinaryPath, - Denom: denom, - HubData: Hubs[hubID], - TokenSupply: tokenSupply, - }, nil + + cfg.RollappID = rollappId + cfg.Denom = denom + cfg.HubData = Hubs[hubID] + cfg.TokenSupply = tokenSupply + + return cfg, nil } func getValidRollappIdMessage() string { - return "A valid RollApp ID should follow the format 'rollapp-name_EIP155-revision', where 'rollapp-name' is made up of" + + return "A valid RollApp ID should follow the format 'name_EIP155-revision', where 'name' is made up of" + " lowercase English letters, 'EIP155-revision' is a 1 to 5 digit number representing the EIP155 rollapp ID, and '" + "revision' is a 1 to 5 digit number representing the revision. For example: 'mars_9721-1'" } @@ -58,43 +56,3 @@ func getValidRollappIdMessage() string { func getAvailableHubsMessage() string { return fmt.Sprintf("Acceptable values are '%s' or '%s'", StagingHubID, LocalHubID) } - -func validateRollAppID(id string) bool { - pattern := `^[a-z]+_[0-9]{1,5}-[0-9]{1,5}$` - r, _ := regexp.Compile(pattern) - return r.MatchString(id) -} - -func verifyHubID(cmd *cobra.Command) error { - hubID, err := cmd.Flags().GetString(FlagNames.HubID) - if err != nil { - return err - } - if _, ok := Hubs[hubID]; !ok { - return fmt.Errorf("invalid hub ID: %s. %s", hubID, getAvailableHubsMessage()) - } - return nil -} - -func verifyTokenSupply(cmd *cobra.Command) error { - tokenSupplyStr, err := cmd.Flags().GetString(FlagNames.TokenSupply) - if err != nil { - return err - } - - tokenSupply := new(big.Int) - _, ok := tokenSupply.SetString(tokenSupplyStr, 10) - if !ok { - return fmt.Errorf("invalid token supply: %s. Must be a valid integer", tokenSupplyStr) - } - - ten := big.NewInt(10) - remainder := new(big.Int) - remainder.Mod(tokenSupply, ten) - - if remainder.Cmp(big.NewInt(0)) != 0 { - return fmt.Errorf("invalid token supply: %s. Must be divisible by 10", tokenSupplyStr) - } - - return nil -} diff --git a/cmd/config/init/init.go b/cmd/config/init/init.go index 30e72121..9fa917d1 100644 --- a/cmd/config/init/init.go +++ b/cmd/config/init/init.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/cosmos/cosmos-sdk/types/errors" "github.com/dymensionxyz/roller/cmd/consts" "github.com/dymensionxyz/roller/cmd/utils" "github.com/spf13/cobra" @@ -14,23 +15,34 @@ func InitCmd() *cobra.Command { Use: "init ", Short: "Initialize a RollApp configuration on your local machine.", PreRunE: func(cmd *cobra.Command, args []string) error { - err := verifyHubID(cmd) - if err != nil { - return err + interactive, _ := cmd.Flags().GetBool(FlagNames.Interactive) + if interactive { + return nil } - err = verifyTokenSupply(cmd) - if err != nil { - return err + + if len(args) == 0 { + fmt.Println("No arguments provided. Running in interactive mode.") + cmd.Flags().Set(FlagNames.Interactive, "true") + return nil } - rollappID := args[0] - if !validateRollAppID(rollappID) { - return fmt.Errorf("invalid RollApp ID '%s'. %s", rollappID, getValidRollappIdMessage()) + + if len(args) < 2 { + return fmt.Errorf("invalid number of arguments. Expected 2, got %d", len(args)) } + + //TODO: parse the config here instead of GetInitConfig in Run command + // cmd.SetContextValue("mydata", data) + return nil }, Run: func(cmd *cobra.Command, args []string) { initConfig, err := GetInitConfig(cmd, args) utils.PrettifyErrorIfExists(err) + + err = initConfig.Validate() + err = errors.Wrap(err, getValidRollappIdMessage()) + utils.PrettifyErrorIfExists(err) + utils.PrettifyErrorIfExists(VerifyUniqueRollappID(initConfig.RollappID, initConfig)) isRootExist, err := dirNotEmpty(initConfig.Home) utils.PrettifyErrorIfExists(err) @@ -81,7 +93,6 @@ func InitCmd() *cobra.Command { /* ------------------------------ Print output ------------------------------ */ printInitOutput(addresses, initConfig.RollappID) }, - Args: cobra.ExactArgs(2), } addFlags(initCmd) diff --git a/cmd/config/init/interactive.go b/cmd/config/init/interactive.go new file mode 100644 index 00000000..663584e7 --- /dev/null +++ b/cmd/config/init/interactive.go @@ -0,0 +1,60 @@ +package initconfig + +import ( + "fmt" + + "github.com/dymensionxyz/roller/cmd/utils" + "github.com/manifoldco/promptui" +) + +// TODO: return error output +func RunInteractiveMode(config *utils.RollappConfig) { + promptNetwork := promptui.Select{ + Label: "Select your network", + Items: []string{"local", "devnet"}, + CursorPos: 1, + } + _, mode, _ := promptNetwork.Run() + config.HubData = Hubs[mode] + + promptChainID := promptui.Prompt{ + Label: "Enter your RollApp ID", + Default: "myrollapp_1234-1", + AllowEdit: true, + Validate: utils.ValidateRollAppID, + } + chainID, _ := promptChainID.Run() + config.RollappID = chainID + + promptDenom := promptui.Prompt{ + Label: "Specify your RollApp denom", + Default: "RAX", + // TODO: add validation for denomination + } + denom, _ := promptDenom.Run() + config.Denom = denom + + promptTokenSupply := promptui.Prompt{ + Label: "How many " + denom + " tokens do you wish to mint for Genesis?", + Default: "1000000000", + Validate: utils.VerifyTokenSupply, + } + supply, _ := promptTokenSupply.Run() + config.TokenSupply = supply + + promptDAType := promptui.Select{ + Label: "Choose your data layer", + Items: []string{"Celestia", "Avail"}, + } + _, _, _ = promptDAType.Run() + fmt.Println("Only Celestia supported for now") + + promptBinaryPath := promptui.Prompt{ + Label: "Set your runtime binary", + Default: config.RollappBinary, + AllowEdit: true, + //TODO: add validate for binary path + } + path, _ := promptBinaryPath.Run() + config.RollappBinary = path +} diff --git a/cmd/config/init/unique_rollapp_id.go b/cmd/config/init/unique_rollapp_id.go index ee8a2f18..95a969ca 100644 --- a/cmd/config/init/unique_rollapp_id.go +++ b/cmd/config/init/unique_rollapp_id.go @@ -2,10 +2,12 @@ package initconfig import ( "fmt" - "github.com/dymensionxyz/roller/cmd/utils" "net/http" + + "github.com/dymensionxyz/roller/cmd/utils" ) +// TODO(#150): roller should use RPC for queries instead of REST func IsRollappIDUnique(rollappID string, initConfig utils.RollappConfig) (bool, error) { url := initConfig.HubData.API_URL + "/dymensionxyz/dymension/rollapp/rollapp/" + rollappID diff --git a/cmd/sequencer/start/start.go b/cmd/sequencer/start/start.go index 0a564834..70e7f6e7 100644 --- a/cmd/sequencer/start/start.go +++ b/cmd/sequencer/start/start.go @@ -16,6 +16,12 @@ import ( // TODO: Test sequencing on 35-C and update the price var OneDaySequencePrice = big.NewInt(1) +var ( + RollappBinary string + RollappDirPath string + LogPath string +) + func StartCmd() *cobra.Command { runCmd := &cobra.Command{ Use: "start", @@ -24,6 +30,11 @@ func StartCmd() *cobra.Command { home := cmd.Flag(utils.FlagNames.Home).Value.String() rollappConfig, err := utils.LoadConfigFromTOML(home) utils.PrettifyErrorIfExists(err) + + LogPath = filepath.Join(rollappConfig.Home, consts.ConfigDirName.Rollapp, "rollapp.log") + RollappBinary = rollappConfig.RollappBinary + RollappDirPath = filepath.Join(rollappConfig.Home, consts.ConfigDirName.Rollapp) + sequencerInsufficientAddrs, err := utils.GetSequencerInsufficientAddrs(rollappConfig, *OneDaySequencePrice) utils.PrettifyErrorIfExists(err) utils.PrintInsufficientBalancesIfAny(sequencerInsufficientAddrs) @@ -47,14 +58,14 @@ var FlagNames = struct { func printOutput() { fmt.Println("💈 The Rollapp sequencer is running on your local machine!") - - //TODO: either mark the ports as default, or read from configuration file + fmt.Println("💈 Default endpoints:") fmt.Println("💈 EVM RPC: http://0.0.0.0:8545") fmt.Println("💈 Node RPC: http://0.0.0.0:26657") fmt.Println("💈 Rest API: http://0.0.0.0:1317") - //TODO: print the log file path + fmt.Println("💈 Log file path: ", LogPath) + fmt.Println("💈 Rollapp root dir: ", RollappDirPath) } func parseError(errMsg string) string { @@ -87,6 +98,7 @@ func GetStartRollappCmd(rollappConfig utils.RollappConfig, lightNodeEndpoint str "--dymint.settlement_config.keyring_home_dir", hubKeysDir, "--dymint.settlement_config.gas_prices", "0udym", "--home", rollappConfigDir, + "--log-file", filepath.Join(rollappConfigDir, "rollapp.log"), "--log_level", "info", "--max-log-size", "2000", ) diff --git a/cmd/utils/config.go b/cmd/utils/config.go index 288fd40f..c1b1e8df 100644 --- a/cmd/utils/config.go +++ b/cmd/utils/config.go @@ -1,9 +1,13 @@ package utils import ( - "github.com/pelletier/go-toml" + "fmt" "io/ioutil" + "math/big" "path/filepath" + "regexp" + + "github.com/pelletier/go-toml" ) func WriteConfigToTOML(InitConfig RollappConfig) error { @@ -42,6 +46,22 @@ type RollappConfig struct { HubData HubData } +func (c RollappConfig) Validate() error { + err := VerifyHubID(c.HubData) + if err != nil { + return err + } + err = VerifyTokenSupply(c.TokenSupply) + if err != nil { + return err + } + err = ValidateRollAppID(c.RollappID) + if err != nil { + return fmt.Errorf("invalid RollApp ID '%s'", c.RollappID) + } + return nil +} + const RollerConfigFileName = "config.toml" type HubData = struct { @@ -49,3 +69,37 @@ type HubData = struct { ID string RPC_URL string } + +func ValidateRollAppID(id string) error { + pattern := `^[a-z]+_[0-9]{1,5}-[0-9]{1,5}$` + r := regexp.MustCompile(pattern) + if !r.MatchString(id) { + return fmt.Errorf("invalid RollApp ID '%s'", id) + } + return nil +} + +func VerifyHubID(data HubData) error { + if data.RPC_URL == "" { + return fmt.Errorf("invalid hub ID: %s. RPC URL cannot be empty", data.ID) + } + return nil +} + +func VerifyTokenSupply(supply string) error { + tokenSupply := new(big.Int) + _, ok := tokenSupply.SetString(supply, 10) + if !ok { + return fmt.Errorf("invalid token supply: %s. Must be a valid integer", supply) + } + + ten := big.NewInt(10) + remainder := new(big.Int) + remainder.Mod(tokenSupply, ten) + + if remainder.Cmp(big.NewInt(0)) != 0 { + return fmt.Errorf("invalid token supply: %s. Must be divisible by 10", supply) + } + + return nil +}