From a7de4650e37b09d7dc61b35d00c7a12c45db54b1 Mon Sep 17 00:00:00 2001 From: Julian Toledano Date: Tue, 3 Dec 2024 13:20:46 +0100 Subject: [PATCH 01/12] init --- client/v2/autocli/prompt/promt.go | 32 ++++++++++++++++++++++++++ client/v2/autocli/prompt/promt_test.go | 32 ++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 client/v2/autocli/prompt/promt.go create mode 100644 client/v2/autocli/prompt/promt_test.go diff --git a/client/v2/autocli/prompt/promt.go b/client/v2/autocli/prompt/promt.go new file mode 100644 index 000000000000..a8fd4ea47833 --- /dev/null +++ b/client/v2/autocli/prompt/promt.go @@ -0,0 +1,32 @@ +package prompt + +import ( + "cosmossdk.io/client/v2/autocli/flag" + "google.golang.org/protobuf/reflect/protoreflect" + "strings" + + addresscodec "cosmossdk.io/core/address" +) + +func Prompt( + addressCodec addresscodec.Codec, + validatorAddressCodec addresscodec.Codec, + consensusAddressCodec addresscodec.Codec, + promptPrefix string, + msg protoreflect.Message, +) (protoreflect.Message, error) { + fields := msg.Descriptor().Fields() + for i := 0; i < fields.Len(); i++ { + field := fields.Get(i) + fieldName := string(field.Name()) + + // create prompt with promptui + + // signer field + if strings.EqualFold(fieldName, flag.GetSignerFieldName(msg.Descriptor())) { + // here signer must be set in some cases. For example gov module address but this prompt should work for any + // kind of message... + } + } + return nil, nil +} diff --git a/client/v2/autocli/prompt/promt_test.go b/client/v2/autocli/prompt/promt_test.go new file mode 100644 index 000000000000..3c0625bc1755 --- /dev/null +++ b/client/v2/autocli/prompt/promt_test.go @@ -0,0 +1,32 @@ +package prompt + +import ( + govtypes "cosmossdk.io/api/cosmos/gov/v1beta1" + + address2 "github.com/cosmos/cosmos-sdk/codec/address" + "google.golang.org/protobuf/reflect/protoreflect" + "reflect" + "testing" +) + +func TestPrompt(t *testing.T) { + tests := []struct { + name string + want protoreflect.Message + wantErr bool + }{ + {}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Prompt(address2.NewBech32Codec("cosmos"), address2.NewBech32Codec("cosmosval"), address2.NewBech32Codec("cosmos"), "prefix", (&govtypes.MsgSubmitProposal{}).ProtoReflect()) + if (err != nil) != tt.wantErr { + t.Errorf("Prompt() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Prompt() got = %v, want %v", got, tt.want) + } + }) + } +} From 4d1179ac4fc4117d923c0102f7a6dc7962530bdd Mon Sep 17 00:00:00 2001 From: Julian Toledano Date: Wed, 4 Dec 2024 17:57:40 +0100 Subject: [PATCH 02/12] add: message input prompt --- client/v2/autocli/prompt/promt.go | 262 ++++++++++++++++++++++-- client/v2/autocli/prompt/promt_test.go | 52 +++-- client/v2/internal/prompt/validation.go | 14 ++ 3 files changed, 300 insertions(+), 28 deletions(-) diff --git a/client/v2/autocli/prompt/promt.go b/client/v2/autocli/prompt/promt.go index a8fd4ea47833..dbfb6f1084b3 100644 --- a/client/v2/autocli/prompt/promt.go +++ b/client/v2/autocli/prompt/promt.go @@ -1,32 +1,266 @@ package prompt import ( - "cosmossdk.io/client/v2/autocli/flag" - "google.golang.org/protobuf/reflect/protoreflect" + "fmt" + "io" + "strconv" "strings" + "github.com/manifoldco/promptui" + "google.golang.org/protobuf/reflect/protoreflect" + + "cosmossdk.io/client/v2/autocli/flag" + "cosmossdk.io/client/v2/internal/prompt" addresscodec "cosmossdk.io/core/address" ) -func Prompt( - addressCodec addresscodec.Codec, - validatorAddressCodec addresscodec.Codec, - consensusAddressCodec addresscodec.Codec, - promptPrefix string, - msg protoreflect.Message, +// PromptMessage prompts the user for values to populate a protobuf message interactively. +// It returns the populated message and any error encountered during prompting. +func PromptMessage( + addressCodec addresscodec.Codec, validatorAddressCodec addresscodec.Codec, + consensusAddressCodec addresscodec.Codec, promptPrefix string, msg protoreflect.Message, +) (protoreflect.Message, error) { + return promptMessage(addressCodec, validatorAddressCodec, consensusAddressCodec, promptPrefix, nil, msg) +} + +// promptMessage prompts the user for values to populate a protobuf message interactively. +// stdIn is provided to make the function easier to unit test by allowing injection of predefined inputs. +func promptMessage( + addressCodec addresscodec.Codec, validatorAddressCodec addresscodec.Codec, + consensusAddressCodec addresscodec.Codec, promptPrefix string, + stdIn io.ReadCloser, msg protoreflect.Message, ) (protoreflect.Message, error) { + promptUi := promptui.Prompt{ + Validate: prompt.ValidatePromptNotEmpty, + Stdin: stdIn, + } + fields := msg.Descriptor().Fields() for i := 0; i < fields.Len(); i++ { field := fields.Get(i) fieldName := string(field.Name()) - // create prompt with promptui - - // signer field + // If this signer field has already a valid default value set, + // use that value as the default prompt value. This is useful for + // commands that have an authority such as gov. if strings.EqualFold(fieldName, flag.GetSignerFieldName(msg.Descriptor())) { - // here signer must be set in some cases. For example gov module address but this prompt should work for any - // kind of message... + if defaultValue := msg.Get(field); defaultValue.IsValid() { + promptUi.Default = defaultValue.String() + } + } + + // validate address fields + scalarField, ok := flag.GetScalarType(field) + if ok { + switch scalarField { + case flag.AddressStringScalarType: + promptUi.Validate = prompt.ValidateAddress(addressCodec) + case flag.ValidatorAddressStringScalarType: + promptUi.Validate = prompt.ValidateAddress(validatorAddressCodec) + case flag.ConsensusAddressStringScalarType: + promptUi.Validate = prompt.ValidateAddress(consensusAddressCodec) + default: + // prompt.Validate = ValidatePromptNotEmpty (we possibly don't want to force all fields to be non-empty) + promptUi.Validate = nil + } + } + + // handle nested message fields recursively + if field.Kind() == protoreflect.MessageKind { + err := promptInnerMessageKind(field, addressCodec, validatorAddressCodec, consensusAddressCodec, promptPrefix, stdIn, msg) + if err != nil { + return nil, err + } + continue + } + + // handle repeated fields by prompting for a comma-separated list of values + if field.IsList() { + list, err := promptList(field, msg, promptUi, promptPrefix) + if err != nil { + return nil, err + } + + msg.Set(field, protoreflect.ValueOfList(list)) + continue + } + + promptUi.Label = fmt.Sprintf("Enter %s %s", promptPrefix, fieldName) + result, err := promptUi.Run() + if err != nil { + return msg, fmt.Errorf("failed to prompt for %s: %w", fieldName, err) + } + + v, err := valueOf(field, result) + if err != nil { + return msg, err + } + msg.Set(field, v) + } + + return msg, nil +} + +// valueOf converts a string input value to a protoreflect.Value based on the field's type. +// It handles string, numeric, bool, bytes and enum field types. +// Returns the converted value and any error that occurred during conversion. +func valueOf(field protoreflect.FieldDescriptor, result string) (protoreflect.Value, error) { + switch field.Kind() { + case protoreflect.StringKind: + return protoreflect.ValueOfString(result), nil + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind, protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + resultUint, err := strconv.ParseUint(result, 10, 0) + if err != nil { + return protoreflect.Value{}, fmt.Errorf("invalid value for int: %w", err) + } + + return protoreflect.ValueOfUint64(resultUint), nil + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind, protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + resultInt, err := strconv.ParseInt(result, 10, 0) + if err != nil { + return protoreflect.Value{}, fmt.Errorf("invalid value for int: %w", err) } + // If a value was successfully parsed the ranges of: + // [minInt, maxInt] + // are within the ranges of: + // [minInt64, maxInt64] + // of which on 64-bit machines, which are most common, + // int==int64 + return protoreflect.ValueOfInt64(resultInt), nil + case protoreflect.BoolKind: + resultBool, err := strconv.ParseBool(result) + if err != nil { + return protoreflect.Value{}, fmt.Errorf("invalid value for bool: %w", err) + } + + return protoreflect.ValueOfBool(resultBool), nil + case protoreflect.BytesKind: + resultBytes := []byte(result) + return protoreflect.ValueOfBytes(resultBytes), nil + case protoreflect.EnumKind: + enumValue := field.Enum().Values().ByName(protoreflect.Name(result)) + if enumValue == nil { + return protoreflect.Value{}, fmt.Errorf("invalid enum value %q", result) + } + return protoreflect.ValueOfEnum(enumValue.Number()), nil + default: + // TODO: add more kinds + // skip any other types + return protoreflect.Value{}, nil + } +} + +// valueOf prompts the user for a comma-separated list of values for a repeated field. +// The user will be prompted to enter values separated by commas which will be parsed +// according to the field's type using valueOf. +func promptList(field protoreflect.FieldDescriptor, msg protoreflect.Message, promptUi promptui.Prompt, promptPrefix string) (protoreflect.List, error) { + promptUi.Label = fmt.Sprintf("Enter %s %s list (separate values with ',')", promptPrefix, string(field.Name())) + result, err := promptUi.Run() + if err != nil { + return nil, fmt.Errorf("failed to prompt for %s: %w", string(field.Name()), err) } - return nil, nil + + list := msg.Mutable(field).List() + for _, item := range strings.Split(result, ",") { + v, err := valueOf(field, item) + if err != nil { + return nil, err + } + list.Append(v) + } + + return list, nil +} + +// promptInnerMessageKind handles prompting for fields that are of message kind. +// It handles both single messages and repeated message fields by delegating to +// promptInnerMessage and promptMessageList respectively. +func promptInnerMessageKind( + f protoreflect.FieldDescriptor, addressCodec addresscodec.Codec, + validatorAddressCodec addresscodec.Codec, consensusAddressCodec addresscodec.Codec, + promptPrefix string, stdIn io.ReadCloser, msg protoreflect.Message, +) error { + if f.IsList() { + return promptMessageList(f, addressCodec, validatorAddressCodec, consensusAddressCodec, promptPrefix, stdIn, msg) + } + return promptInnerMessage(f, addressCodec, validatorAddressCodec, consensusAddressCodec, promptPrefix, stdIn, msg) +} + +// promptInnerMessage prompts for a single nested message field. It creates a new message instance, +// recursively prompts for its fields, and sets the populated message on the parent message. +func promptInnerMessage( + f protoreflect.FieldDescriptor, addressCodec addresscodec.Codec, + validatorAddressCodec addresscodec.Codec, consensusAddressCodec addresscodec.Codec, + promptPrefix string, stdIn io.ReadCloser, msg protoreflect.Message, +) error { + fieldName := promptPrefix + "." + string(f.Name()) + nestedMsg := msg.Get(f).Message() + //if nestedMsg.IsValid() { + // nestedMsg = nestedMsg.New() + //} else { + // nestedMsg = msg.Get(f).Message() + //} + nestedMsg = nestedMsg.New() + // Recursively prompt for nested message fields + updatedMsg, err := promptMessage( + addressCodec, + validatorAddressCodec, + consensusAddressCodec, + fieldName, + stdIn, + nestedMsg, + ) + if err != nil { + return fmt.Errorf("failed to prompt for nested message %s: %w", fieldName, err) + } + + msg.Set(f, protoreflect.ValueOfMessage(updatedMsg)) + return nil +} + +// promptMessageList prompts for a repeated message field by repeatedly creating new message instances, +// prompting for their fields, and appending them to the list until the user chooses to stop. +func promptMessageList( + f protoreflect.FieldDescriptor, addressCodec addresscodec.Codec, + validatorAddressCodec addresscodec.Codec, consensusAddressCodec addresscodec.Codec, + promptPrefix string, stdIn io.ReadCloser, msg protoreflect.Message, +) error { + list := msg.Mutable(f).List() + for { + fieldName := promptPrefix + "." + string(f.Name()) + // Create and populate a new message for the list + nestedMsg := list.NewElement().Message() + updatedMsg, err := promptMessage( + addressCodec, + validatorAddressCodec, + consensusAddressCodec, + fieldName, + stdIn, + nestedMsg, + ) + if err != nil { + return fmt.Errorf("failed to prompt for list item in %s: %w", fieldName, err) + } + + list.Append(protoreflect.ValueOfMessage(updatedMsg)) + + // Prompt whether to continue + // TODO: may be better yes/no rather than interactive? + continuePrompt := promptui.Select{ + Label: "Add another item?", + Items: []string{"No", "Yes"}, + Stdin: stdIn, + } + + _, result, err := continuePrompt.Run() + if err != nil { + return fmt.Errorf("failed to prompt for continuation: %w", err) + } + + if result == "No" { + break + } + } + + return nil } diff --git a/client/v2/autocli/prompt/promt_test.go b/client/v2/autocli/prompt/promt_test.go index 3c0625bc1755..a83bef911875 100644 --- a/client/v2/autocli/prompt/promt_test.go +++ b/client/v2/autocli/prompt/promt_test.go @@ -1,32 +1,56 @@ package prompt import ( - govtypes "cosmossdk.io/api/cosmos/gov/v1beta1" + "io" + "strings" + "testing" - address2 "github.com/cosmos/cosmos-sdk/codec/address" + protocolpool "cosmossdk.io/api/cosmos/protocolpool/v1" + "github.com/stretchr/testify/require" + + "cosmossdk.io/client/v2/internal/testpb" "google.golang.org/protobuf/reflect/protoreflect" - "reflect" - "testing" + + address2 "github.com/cosmos/cosmos-sdk/codec/address" ) -func TestPrompt(t *testing.T) { +func TestPromptMessage(t *testing.T) { tests := []struct { name string - want protoreflect.Message + msg protoreflect.Message + inputs []string wantErr bool }{ - {}, + { + name: "community pool spend", + inputs: []string{"cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn", "cosmos129lxcu2n3hx54fdxlwsahqkjr3sp32cxm00zlm", "10000", "stake", "No"}, + msg: (&protocolpool.MsgCommunityPoolSpend{ + Authority: "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn", + }).ProtoReflect(), + }, + { + name: "testPb", + inputs: []string{"1", "2", "string", "bytes", "10101010", "0", "234234", "3", "4", "5", "true", "ENUM_ONE", + "bar", "6", "10000", "stake", "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn", + "bytes", "6", "7", "false", "false", "true,false,true", "1,2,3", "hello,hola,ciao", "ENUM_ONE,ENUM_TWO", + "10239", "0", "No", "bar", "343", "No", "134", "positional2", "23455", "stake", "No", "deprecate", + "shorthand", "false", "cosmosvaloper1tnh2q55v8wyygtt9srz5safamzdengsn9dsd7z"}, + msg: (&testpb.MsgRequest{}).ProtoReflect(), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Prompt(address2.NewBech32Codec("cosmos"), address2.NewBech32Codec("cosmosval"), address2.NewBech32Codec("cosmos"), "prefix", (&govtypes.MsgSubmitProposal{}).ProtoReflect()) - if (err != nil) != tt.wantErr { - t.Errorf("Prompt() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Prompt() got = %v, want %v", got, tt.want) + // https://github.com/manifoldco/promptui/issues/63#issuecomment-621118463 + var paddedInputs []string + for _, input := range tt.inputs { + padding := strings.Repeat("a", 4096-1-len(input)%4096) + paddedInputs = append(paddedInputs, input+"\n"+padding) } + reader := io.NopCloser(strings.NewReader(strings.Join(paddedInputs, ""))) + + got, err := promptMessage(address2.NewBech32Codec("cosmos"), address2.NewBech32Codec("cosmosvaloper"), address2.NewBech32Codec("cosmosvalcons"), "prefix", reader, tt.msg) + require.NoError(t, err) + require.NotNil(t, got) }) } } diff --git a/client/v2/internal/prompt/validation.go b/client/v2/internal/prompt/validation.go index d914999f214d..9e2c97ae6ec0 100644 --- a/client/v2/internal/prompt/validation.go +++ b/client/v2/internal/prompt/validation.go @@ -5,6 +5,8 @@ import ( "fmt" "net/url" + "cosmossdk.io/core/address" + sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -35,3 +37,15 @@ func ValidatePromptCoins(input string) error { return nil } + +// ValidateAddress returns a validation function that checks if a string is a valid address +// for the given address codec. +func ValidateAddress(ac address.Codec) func(string) error { + return func(i string) error { + if _, err := ac.StringToBytes(i); err != nil { + return fmt.Errorf("invalid consensus address") + } + + return nil + } +} From 5701160984b9196d5d2b511d35d4d42d5e5d3c00 Mon Sep 17 00:00:00 2001 From: Julian Toledano Date: Wed, 4 Dec 2024 19:42:17 +0100 Subject: [PATCH 03/12] add: promptStruct --- client/v2/autocli/prompt/promt.go | 124 +++++++++++++++++++++++++ client/v2/autocli/prompt/promt_test.go | 56 ++++++++++- 2 files changed, 176 insertions(+), 4 deletions(-) diff --git a/client/v2/autocli/prompt/promt.go b/client/v2/autocli/prompt/promt.go index dbfb6f1084b3..0bfc26f16ae4 100644 --- a/client/v2/autocli/prompt/promt.go +++ b/client/v2/autocli/prompt/promt.go @@ -3,6 +3,7 @@ package prompt import ( "fmt" "io" + "reflect" "strconv" "strings" @@ -264,3 +265,126 @@ func promptMessageList( return nil } + +// PromptStruct prompts for values of a struct's fields interactively. +// It returns the populated struct and any error encountered. +func PromptStruct[T any](promptPrefix string, data T) (T, error) { + return promptStruct(promptPrefix, data, nil) +} + +// promptStruct prompts for values of a struct's fields interactively. +// +// For each field in the struct: +// - Pointer fields are initialized if nil and handled recursively if they contain structs +// - Struct fields are handled recursively +// - String and int slices are supported +// - String and int fields are prompted for and populated +// - Only String and int pointers are supported +// - Other types are skipped +func promptStruct[T any](promptPrefix string, data T, stdIn io.ReadCloser) (T, error) { + v := reflect.ValueOf(&data).Elem() + if v.Kind() == reflect.Interface { + v = reflect.ValueOf(data) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + } + + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + fieldName := strings.ToLower(v.Type().Field(i).Name) + + // Handle pointer types + if field.Kind() == reflect.Ptr { + if field.IsNil() { + field.Set(reflect.New(field.Type().Elem())) + } + if field.Elem().Kind() == reflect.Struct { + result, err := promptStruct(promptPrefix+"."+fieldName, field.Interface(), stdIn) + if err != nil { + return data, err + } + field.Set(reflect.ValueOf(result)) + continue + } + } + + switch field.Kind() { + case reflect.Struct: + // For struct fields, create a new pointer to handle them + structPtr := reflect.New(field.Type()).Interface() + reflect.ValueOf(structPtr).Elem().Set(field) + + result, err := promptStruct(promptPrefix+"."+fieldName, structPtr, stdIn) + if err != nil { + return data, err + } + + // Get the actual struct value from the result + resultValue := reflect.ValueOf(result) + if resultValue.Kind() == reflect.Ptr { + resultValue = resultValue.Elem() + } + field.Set(resultValue) + continue + case reflect.Slice: + if v.Field(i).Type().Elem().Kind() != reflect.String && v.Field(i).Type().Elem().Kind() != reflect.Int { + continue + } + } + + // create prompts + prompt := promptui.Prompt{ + Label: fmt.Sprintf("Enter %s %s", promptPrefix, strings.Title(fieldName)), // nolint:staticcheck // strings.Title has a better API + Validate: prompt.ValidatePromptNotEmpty, + Stdin: stdIn, + } + + result, err := prompt.Run() + if err != nil { + return data, fmt.Errorf("failed to prompt for %s: %w", fieldName, err) + } + + switch field.Kind() { + case reflect.String: + v.Field(i).SetString(result) + case reflect.Int: + resultInt, err := strconv.ParseInt(result, 10, 0) + if err != nil { + return data, fmt.Errorf("invalid value for int: %w", err) + } + v.Field(i).SetInt(resultInt) + case reflect.Slice: + switch v.Field(i).Type().Elem().Kind() { + case reflect.String: + v.Field(i).Set(reflect.ValueOf([]string{result})) + case reflect.Int: + resultInt, err := strconv.ParseInt(result, 10, 0) + if err != nil { + return data, fmt.Errorf("invalid value for int: %w", err) + } + + v.Field(i).Set(reflect.ValueOf([]int{int(resultInt)})) + } + case reflect.Ptr: + // Handle pointer fields by creating a new value and setting it + ptrValue := reflect.New(field.Type().Elem()) + if ptrValue.Elem().Kind() == reflect.String { + ptrValue.Elem().SetString(result) + v.Field(i).Set(ptrValue) + } else if ptrValue.Elem().Kind() == reflect.Int { + resultInt, err := strconv.ParseInt(result, 10, 0) + if err != nil { + return data, fmt.Errorf("invalid value for int: %w", err) + } + ptrValue.Elem().SetInt(resultInt) + v.Field(i).Set(ptrValue) + } + default: + // skip any other types + continue + } + } + + return data, nil +} diff --git a/client/v2/autocli/prompt/promt_test.go b/client/v2/autocli/prompt/promt_test.go index a83bef911875..62081cd32715 100644 --- a/client/v2/autocli/prompt/promt_test.go +++ b/client/v2/autocli/prompt/promt_test.go @@ -14,12 +14,21 @@ import ( address2 "github.com/cosmos/cosmos-sdk/codec/address" ) +func getReader(inputs []string) io.ReadCloser { + // https://github.com/manifoldco/promptui/issues/63#issuecomment-621118463 + var paddedInputs []string + for _, input := range inputs { + padding := strings.Repeat("a", 4096-1-len(input)%4096) + paddedInputs = append(paddedInputs, input+"\n"+padding) + } + return io.NopCloser(strings.NewReader(strings.Join(paddedInputs, ""))) +} + func TestPromptMessage(t *testing.T) { tests := []struct { - name string - msg protoreflect.Message - inputs []string - wantErr bool + name string + msg protoreflect.Message + inputs []string }{ { name: "community pool spend", @@ -54,3 +63,42 @@ func TestPromptMessage(t *testing.T) { }) } } + +type innerStruct struct { + A string + B int +} + +type testStruct struct { + A string + B int + C *innerStruct + D innerStruct + E *string + F []string +} + +func TestPromptStruct(t *testing.T) { + type testCase[T any] struct { + name string + data T + inputs []string + } + tests := []testCase[testStruct]{ + { + name: "test struct", + data: testStruct{}, + inputs: []string{ + "a", "1", "b", "2", "c", "3", "pointerStr", "list", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + inputs := getReader(tt.inputs) + got, err := promptStruct("testStruct", tt.data, inputs) + require.NoError(t, err) + require.NotNil(t, got) + }) + } +} From 4bd6be8c7e5ec9a0c74ae8587426eda4f20eec15 Mon Sep 17 00:00:00 2001 From: Julian Toledano Date: Thu, 5 Dec 2024 10:23:04 +0100 Subject: [PATCH 04/12] update gov --- .../autocli/prompt/{promt.go => message.go} | 124 ---------------- .../prompt/{promt_test.go => message_test.go} | 39 ----- client/v2/autocli/prompt/struct.go | 136 ++++++++++++++++++ client/v2/autocli/prompt/struct_test.go | 46 ++++++ client/v2/autocli/prompt/util.go | 30 ++++ x/gov/client/cli/prompt.go | 75 ++++------ x/gov/go.mod | 2 + x/group/client/cli/prompt.go | 2 +- x/group/go.mod | 1 + 9 files changed, 240 insertions(+), 215 deletions(-) rename client/v2/autocli/prompt/{promt.go => message.go} (71%) rename client/v2/autocli/prompt/{promt_test.go => message_test.go} (77%) create mode 100644 client/v2/autocli/prompt/struct.go create mode 100644 client/v2/autocli/prompt/struct_test.go create mode 100644 client/v2/autocli/prompt/util.go diff --git a/client/v2/autocli/prompt/promt.go b/client/v2/autocli/prompt/message.go similarity index 71% rename from client/v2/autocli/prompt/promt.go rename to client/v2/autocli/prompt/message.go index 0bfc26f16ae4..dbfb6f1084b3 100644 --- a/client/v2/autocli/prompt/promt.go +++ b/client/v2/autocli/prompt/message.go @@ -3,7 +3,6 @@ package prompt import ( "fmt" "io" - "reflect" "strconv" "strings" @@ -265,126 +264,3 @@ func promptMessageList( return nil } - -// PromptStruct prompts for values of a struct's fields interactively. -// It returns the populated struct and any error encountered. -func PromptStruct[T any](promptPrefix string, data T) (T, error) { - return promptStruct(promptPrefix, data, nil) -} - -// promptStruct prompts for values of a struct's fields interactively. -// -// For each field in the struct: -// - Pointer fields are initialized if nil and handled recursively if they contain structs -// - Struct fields are handled recursively -// - String and int slices are supported -// - String and int fields are prompted for and populated -// - Only String and int pointers are supported -// - Other types are skipped -func promptStruct[T any](promptPrefix string, data T, stdIn io.ReadCloser) (T, error) { - v := reflect.ValueOf(&data).Elem() - if v.Kind() == reflect.Interface { - v = reflect.ValueOf(data) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - } - - for i := 0; i < v.NumField(); i++ { - field := v.Field(i) - fieldName := strings.ToLower(v.Type().Field(i).Name) - - // Handle pointer types - if field.Kind() == reflect.Ptr { - if field.IsNil() { - field.Set(reflect.New(field.Type().Elem())) - } - if field.Elem().Kind() == reflect.Struct { - result, err := promptStruct(promptPrefix+"."+fieldName, field.Interface(), stdIn) - if err != nil { - return data, err - } - field.Set(reflect.ValueOf(result)) - continue - } - } - - switch field.Kind() { - case reflect.Struct: - // For struct fields, create a new pointer to handle them - structPtr := reflect.New(field.Type()).Interface() - reflect.ValueOf(structPtr).Elem().Set(field) - - result, err := promptStruct(promptPrefix+"."+fieldName, structPtr, stdIn) - if err != nil { - return data, err - } - - // Get the actual struct value from the result - resultValue := reflect.ValueOf(result) - if resultValue.Kind() == reflect.Ptr { - resultValue = resultValue.Elem() - } - field.Set(resultValue) - continue - case reflect.Slice: - if v.Field(i).Type().Elem().Kind() != reflect.String && v.Field(i).Type().Elem().Kind() != reflect.Int { - continue - } - } - - // create prompts - prompt := promptui.Prompt{ - Label: fmt.Sprintf("Enter %s %s", promptPrefix, strings.Title(fieldName)), // nolint:staticcheck // strings.Title has a better API - Validate: prompt.ValidatePromptNotEmpty, - Stdin: stdIn, - } - - result, err := prompt.Run() - if err != nil { - return data, fmt.Errorf("failed to prompt for %s: %w", fieldName, err) - } - - switch field.Kind() { - case reflect.String: - v.Field(i).SetString(result) - case reflect.Int: - resultInt, err := strconv.ParseInt(result, 10, 0) - if err != nil { - return data, fmt.Errorf("invalid value for int: %w", err) - } - v.Field(i).SetInt(resultInt) - case reflect.Slice: - switch v.Field(i).Type().Elem().Kind() { - case reflect.String: - v.Field(i).Set(reflect.ValueOf([]string{result})) - case reflect.Int: - resultInt, err := strconv.ParseInt(result, 10, 0) - if err != nil { - return data, fmt.Errorf("invalid value for int: %w", err) - } - - v.Field(i).Set(reflect.ValueOf([]int{int(resultInt)})) - } - case reflect.Ptr: - // Handle pointer fields by creating a new value and setting it - ptrValue := reflect.New(field.Type().Elem()) - if ptrValue.Elem().Kind() == reflect.String { - ptrValue.Elem().SetString(result) - v.Field(i).Set(ptrValue) - } else if ptrValue.Elem().Kind() == reflect.Int { - resultInt, err := strconv.ParseInt(result, 10, 0) - if err != nil { - return data, fmt.Errorf("invalid value for int: %w", err) - } - ptrValue.Elem().SetInt(resultInt) - v.Field(i).Set(ptrValue) - } - default: - // skip any other types - continue - } - } - - return data, nil -} diff --git a/client/v2/autocli/prompt/promt_test.go b/client/v2/autocli/prompt/message_test.go similarity index 77% rename from client/v2/autocli/prompt/promt_test.go rename to client/v2/autocli/prompt/message_test.go index 62081cd32715..510d6c859b20 100644 --- a/client/v2/autocli/prompt/promt_test.go +++ b/client/v2/autocli/prompt/message_test.go @@ -63,42 +63,3 @@ func TestPromptMessage(t *testing.T) { }) } } - -type innerStruct struct { - A string - B int -} - -type testStruct struct { - A string - B int - C *innerStruct - D innerStruct - E *string - F []string -} - -func TestPromptStruct(t *testing.T) { - type testCase[T any] struct { - name string - data T - inputs []string - } - tests := []testCase[testStruct]{ - { - name: "test struct", - data: testStruct{}, - inputs: []string{ - "a", "1", "b", "2", "c", "3", "pointerStr", "list", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - inputs := getReader(tt.inputs) - got, err := promptStruct("testStruct", tt.data, inputs) - require.NoError(t, err) - require.NotNil(t, got) - }) - } -} diff --git a/client/v2/autocli/prompt/struct.go b/client/v2/autocli/prompt/struct.go new file mode 100644 index 000000000000..16c8ee796f9d --- /dev/null +++ b/client/v2/autocli/prompt/struct.go @@ -0,0 +1,136 @@ +package prompt + +import ( + "fmt" + "io" + "reflect" + "strconv" + "strings" + + "github.com/manifoldco/promptui" + + "cosmossdk.io/client/v2/internal/prompt" +) + +// PromptStruct prompts for values of a struct's fields interactively. +// It returns the populated struct and any error encountered. +func PromptStruct[T any](promptPrefix string, data T) (T, error) { + return promptStruct(promptPrefix, data, nil) +} + +// promptStruct prompts for values of a struct's fields interactively. +// +// For each field in the struct: +// - Pointer fields are initialized if nil and handled recursively if they contain structs +// - Struct fields are handled recursively +// - String and int slices are supported +// - String and int fields are prompted for and populated +// - Only String and int pointers are supported +// - Other types are skipped +func promptStruct[T any](promptPrefix string, data T, stdIn io.ReadCloser) (T, error) { + v := reflect.ValueOf(&data).Elem() + if v.Kind() == reflect.Interface { + v = reflect.ValueOf(data) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + } + + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + fieldName := strings.ToLower(v.Type().Field(i).Name) + + // Handle pointer types + if field.Kind() == reflect.Ptr { + if field.IsNil() { + field.Set(reflect.New(field.Type().Elem())) + } + if field.Elem().Kind() == reflect.Struct { + result, err := promptStruct(promptPrefix+"."+fieldName, field.Interface(), stdIn) + if err != nil { + return data, err + } + field.Set(reflect.ValueOf(result)) + continue + } + } + + switch field.Kind() { + case reflect.Struct: + // For struct fields, create a new pointer to handle them + structPtr := reflect.New(field.Type()).Interface() + reflect.ValueOf(structPtr).Elem().Set(field) + + result, err := promptStruct(promptPrefix+"."+fieldName, structPtr, stdIn) + if err != nil { + return data, err + } + + // Get the actual struct value from the result + resultValue := reflect.ValueOf(result) + if resultValue.Kind() == reflect.Ptr { + resultValue = resultValue.Elem() + } + field.Set(resultValue) + continue + case reflect.Slice: + if v.Field(i).Type().Elem().Kind() != reflect.String && v.Field(i).Type().Elem().Kind() != reflect.Int { + continue + } + } + + // create prompts + prompt := promptui.Prompt{ + Label: fmt.Sprintf("Enter %s %s", promptPrefix, strings.Title(fieldName)), // nolint:staticcheck // strings.Title has a better API + Validate: prompt.ValidatePromptNotEmpty, + Stdin: stdIn, + } + + result, err := prompt.Run() + if err != nil { + return data, fmt.Errorf("failed to prompt for %s: %w", fieldName, err) + } + + switch field.Kind() { + case reflect.String: + v.Field(i).SetString(result) + case reflect.Int: + resultInt, err := strconv.ParseInt(result, 10, 0) + if err != nil { + return data, fmt.Errorf("invalid value for int: %w", err) + } + v.Field(i).SetInt(resultInt) + case reflect.Slice: + switch v.Field(i).Type().Elem().Kind() { + case reflect.String: + v.Field(i).Set(reflect.ValueOf([]string{result})) + case reflect.Int: + resultInt, err := strconv.ParseInt(result, 10, 0) + if err != nil { + return data, fmt.Errorf("invalid value for int: %w", err) + } + + v.Field(i).Set(reflect.ValueOf([]int{int(resultInt)})) + } + case reflect.Ptr: + // Handle pointer fields by creating a new value and setting it + ptrValue := reflect.New(field.Type().Elem()) + if ptrValue.Elem().Kind() == reflect.String { + ptrValue.Elem().SetString(result) + v.Field(i).Set(ptrValue) + } else if ptrValue.Elem().Kind() == reflect.Int { + resultInt, err := strconv.ParseInt(result, 10, 0) + if err != nil { + return data, fmt.Errorf("invalid value for int: %w", err) + } + ptrValue.Elem().SetInt(resultInt) + v.Field(i).Set(ptrValue) + } + default: + // skip any other types + continue + } + } + + return data, nil +} diff --git a/client/v2/autocli/prompt/struct_test.go b/client/v2/autocli/prompt/struct_test.go new file mode 100644 index 000000000000..1b712d81a0e7 --- /dev/null +++ b/client/v2/autocli/prompt/struct_test.go @@ -0,0 +1,46 @@ +package prompt + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +type innerStruct struct { + A string + B int +} + +type testStruct struct { + A string + B int + C *innerStruct + D innerStruct + E *string + F []string +} + +func TestPromptStruct(t *testing.T) { + type testCase[T any] struct { + name string + data T + inputs []string + } + tests := []testCase[testStruct]{ + { + name: "test struct", + data: testStruct{}, + inputs: []string{ + "a", "1", "b", "2", "c", "3", "pointerStr", "list", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + inputs := getReader(tt.inputs) + got, err := promptStruct("testStruct", tt.data, inputs) + require.NoError(t, err) + require.NotNil(t, got) + }) + } +} diff --git a/client/v2/autocli/prompt/util.go b/client/v2/autocli/prompt/util.go new file mode 100644 index 000000000000..e8da04ed96f3 --- /dev/null +++ b/client/v2/autocli/prompt/util.go @@ -0,0 +1,30 @@ +package prompt + +import ( + "fmt" + + "github.com/manifoldco/promptui" +) + +func Select(label string, options []string) (string, error) { + selectUi := promptui.Select{ + Label: label, + Items: options, + } + + _, selectedProposalType, err := selectUi.Run() + if err != nil { + return "", fmt.Errorf("failed to prompt proposal types: %w", err) + } + + return selectedProposalType, nil +} + +func PromptString(label string, validate func(string) error) (string, error) { + promptUi := promptui.Prompt{ + Label: label, + Validate: validate, + } + + return promptUi.Run() +} diff --git a/x/gov/client/cli/prompt.go b/x/gov/client/cli/prompt.go index ec5f49287711..caeaacf920b3 100644 --- a/x/gov/client/cli/prompt.go +++ b/x/gov/client/cli/prompt.go @@ -3,21 +3,22 @@ package cli import ( "encoding/json" "fmt" + "google.golang.org/protobuf/encoding/protojson" "os" "reflect" // #nosec "sort" "strconv" "strings" - "github.com/manifoldco/promptui" - "github.com/spf13/cobra" - + "cosmossdk.io/client/v2/autocli/prompt" "cosmossdk.io/core/address" "cosmossdk.io/x/gov/types" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "google.golang.org/protobuf/reflect/protoregistry" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) @@ -163,8 +164,8 @@ type proposalType struct { } // Prompt the proposal type values and return the proposal and its metadata -func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec address.Codec) (*proposal, types.ProposalMetadata, error) { - metadata, err := PromptMetadata(skipMetadata, addressCodec) +func (p *proposalType) Prompt(skipMetadata bool, addressCodec, validatorAddressCodec, consensusAddressCodec address.Codec) (*proposal, types.ProposalMetadata, error) { + metadata, err := PromptMetadata(skipMetadata) if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal metadata: %w", err) } @@ -176,11 +177,7 @@ func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec a } // set deposit - depositPrompt := promptui.Prompt{ - Label: "Enter proposal deposit", - Validate: client.ValidatePromptCoins, - } - proposal.Deposit, err = depositPrompt.Run() + proposal.Deposit, err = prompt.PromptString("Enter proposal deposit", client.ValidatePromptCoins) if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal deposit: %w", err) } @@ -190,12 +187,16 @@ func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec a } // set messages field - result, err := Prompt(p.Msg, "msg", addressCodec) + msg, err := protoregistry.GlobalTypes.FindMessageByURL(p.MsgType) + if err != nil { + return nil, metadata, fmt.Errorf("failed to find proposal msg: %w", err) + } + result, err := prompt.PromptMessage(addressCodec, validatorAddressCodec, consensusAddressCodec, "msg", msg.New()) if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal message: %w", err) } - message, err := cdc.MarshalInterfaceJSON(result) + message, err := protojson.Marshal(result.Interface()) if err != nil { return nil, metadata, fmt.Errorf("failed to marshal proposal message: %w", err) } @@ -214,33 +215,22 @@ func getProposalSuggestions() []string { } // PromptMetadata prompts for proposal metadata or only title and summary if skip is true -func PromptMetadata(skip bool, addressCodec address.Codec) (types.ProposalMetadata, error) { +func PromptMetadata(skip bool) (types.ProposalMetadata, error) { if !skip { - metadata, err := Prompt(types.ProposalMetadata{}, "proposal", addressCodec) + metadata, err := prompt.PromptStruct("proposal", types.ProposalMetadata{}) if err != nil { - return metadata, fmt.Errorf("failed to set proposal metadata: %w", err) + return types.ProposalMetadata{}, err } return metadata, nil } - // prompt for title and summary - titlePrompt := promptui.Prompt{ - Label: "Enter proposal title", - Validate: client.ValidatePromptNotEmpty, - } - - title, err := titlePrompt.Run() + title, err := prompt.PromptString("Enter proposal title", client.ValidatePromptNotEmpty) if err != nil { return types.ProposalMetadata{}, fmt.Errorf("failed to set proposal title: %w", err) } - summaryPrompt := promptui.Prompt{ - Label: "Enter proposal summary", - Validate: client.ValidatePromptNotEmpty, - } - - summary, err := summaryPrompt.Run() + summary, err := prompt.PromptString("Enter proposal summary", client.ValidatePromptNotEmpty) if err != nil { return types.ProposalMetadata{}, fmt.Errorf("failed to set proposal summary: %w", err) } @@ -262,17 +252,7 @@ func NewCmdDraftProposal() *cobra.Command { return err } - // prompt proposal type - proposalTypesPrompt := promptui.Select{ - Label: "Select proposal type", - Items: getProposalSuggestions(), - } - - _, selectedProposalType, err := proposalTypesPrompt.Run() - if err != nil { - return fmt.Errorf("failed to prompt proposal types: %w", err) - } - + selectedProposalType, err := prompt.Select("Select proposal type", getProposalSuggestions()) var proposal proposalType for _, p := range suggestedProposalTypes { if strings.EqualFold(p.Name, selectedProposalType) { @@ -283,17 +263,10 @@ func NewCmdDraftProposal() *cobra.Command { // create any proposal type if proposal.Name == proposalOther { - // prompt proposal type - msgPrompt := promptui.Select{ - Label: "Select proposal message type:", - Items: func() []string { - msgs := clientCtx.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName) - sort.Strings(msgs) - return msgs - }(), - } + msgs := clientCtx.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName) + sort.Strings(msgs) - _, result, err := msgPrompt.Run() + result, err := prompt.Select("Select proposal message type:", msgs) if err != nil { return fmt.Errorf("failed to prompt proposal types: %w", err) } @@ -311,7 +284,7 @@ func NewCmdDraftProposal() *cobra.Command { skipMetadataPrompt, _ := cmd.Flags().GetBool(flagSkipMetadata) - result, metadata, err := proposal.Prompt(clientCtx.Codec, skipMetadataPrompt, clientCtx.AddressCodec) + result, metadata, err := proposal.Prompt(skipMetadataPrompt, clientCtx.AddressCodec, clientCtx.ValidatorAddressCodec, clientCtx.ConsensusAddressCodec) if err != nil { return err } diff --git a/x/gov/go.mod b/x/gov/go.mod index 691b6a00a3a3..b90468c51852 100644 --- a/x/gov/go.mod +++ b/x/gov/go.mod @@ -4,6 +4,7 @@ go 1.23.3 require ( cosmossdk.io/api v0.7.6 + cosmossdk.io/client/v2 v2.0.0-00010101000000-000000000000 cosmossdk.io/collections v0.4.1-0.20241128094659-bd76b47e1d8b cosmossdk.io/core v1.0.0-alpha.6 cosmossdk.io/core/testing v0.0.0-20241108153815-606544c7be7e @@ -181,6 +182,7 @@ replace github.com/cosmos/cosmos-sdk => ../../. // TODO remove post spinning out all modules replace ( cosmossdk.io/api => ../../api + cosmossdk.io/client/v2 => ../../client/v2 cosmossdk.io/store => ../../store cosmossdk.io/x/bank => ../bank cosmossdk.io/x/protocolpool => ../protocolpool diff --git a/x/group/client/cli/prompt.go b/x/group/client/cli/prompt.go index 3cb018e02548..0a972e6ce33b 100644 --- a/x/group/client/cli/prompt.go +++ b/x/group/client/cli/prompt.go @@ -34,7 +34,7 @@ type proposalType struct { // Prompt the proposal type values and return the proposal and its metadata. func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec address.Codec) (*Proposal, govtypes.ProposalMetadata, error) { // set metadata - metadata, err := govcli.PromptMetadata(skipMetadata, addressCodec) + metadata, err := govcli.PromptMetadata(skipMetadata) if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal metadata: %w", err) } diff --git a/x/group/go.mod b/x/group/go.mod index c6e96fac33b2..f7f6a099aa78 100644 --- a/x/group/go.mod +++ b/x/group/go.mod @@ -184,6 +184,7 @@ replace github.com/cosmos/cosmos-sdk => ../../ // TODO remove post spinning out all modules replace ( cosmossdk.io/api => ../../api + cosmossdk.io/client/v2 => ../../client/v2 cosmossdk.io/store => ../../store cosmossdk.io/x/accounts => ../accounts cosmossdk.io/x/accounts/defaults/base => ../accounts/defaults/base From 9ae0b9fee55b33e3dd0b478d46575600ed4cdc5d Mon Sep 17 00:00:00 2001 From: Julian Toledano Date: Thu, 5 Dec 2024 12:17:04 +0100 Subject: [PATCH 05/12] del: gov prompt --- client/v2/autocli/prompt/message.go | 9 +- client/v2/autocli/prompt/message_test.go | 8 -- client/v2/autocli/prompt/util.go | 40 +++++++ x/gov/client/cli/prompt.go | 137 +++++------------------ x/gov/client/cli/prompt_test.go | 90 --------------- x/group/client/cli/prompt.go | 69 ++++++------ 6 files changed, 111 insertions(+), 242 deletions(-) delete mode 100644 x/gov/client/cli/prompt_test.go diff --git a/client/v2/autocli/prompt/message.go b/client/v2/autocli/prompt/message.go index dbfb6f1084b3..25f41af6b7e8 100644 --- a/client/v2/autocli/prompt/message.go +++ b/client/v2/autocli/prompt/message.go @@ -30,16 +30,17 @@ func promptMessage( consensusAddressCodec addresscodec.Codec, promptPrefix string, stdIn io.ReadCloser, msg protoreflect.Message, ) (protoreflect.Message, error) { - promptUi := promptui.Prompt{ - Validate: prompt.ValidatePromptNotEmpty, - Stdin: stdIn, - } fields := msg.Descriptor().Fields() for i := 0; i < fields.Len(); i++ { field := fields.Get(i) fieldName := string(field.Name()) + promptUi := promptui.Prompt{ + Validate: prompt.ValidatePromptNotEmpty, + Stdin: stdIn, + } + // If this signer field has already a valid default value set, // use that value as the default prompt value. This is useful for // commands that have an authority such as gov. diff --git a/client/v2/autocli/prompt/message_test.go b/client/v2/autocli/prompt/message_test.go index 510d6c859b20..cc37c91d10b5 100644 --- a/client/v2/autocli/prompt/message_test.go +++ b/client/v2/autocli/prompt/message_test.go @@ -5,7 +5,6 @@ import ( "strings" "testing" - protocolpool "cosmossdk.io/api/cosmos/protocolpool/v1" "github.com/stretchr/testify/require" "cosmossdk.io/client/v2/internal/testpb" @@ -30,13 +29,6 @@ func TestPromptMessage(t *testing.T) { msg protoreflect.Message inputs []string }{ - { - name: "community pool spend", - inputs: []string{"cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn", "cosmos129lxcu2n3hx54fdxlwsahqkjr3sp32cxm00zlm", "10000", "stake", "No"}, - msg: (&protocolpool.MsgCommunityPoolSpend{ - Authority: "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn", - }).ProtoReflect(), - }, { name: "testPb", inputs: []string{"1", "2", "string", "bytes", "10101010", "0", "234234", "3", "4", "5", "true", "ENUM_ONE", diff --git a/client/v2/autocli/prompt/util.go b/client/v2/autocli/prompt/util.go index e8da04ed96f3..4b3bdec56baf 100644 --- a/client/v2/autocli/prompt/util.go +++ b/client/v2/autocli/prompt/util.go @@ -4,8 +4,11 @@ import ( "fmt" "github.com/manifoldco/promptui" + "google.golang.org/protobuf/reflect/protoreflect" ) +// Select prompts the user to select an option from a list of choices. +// It takes a label string to display above the selection prompt and a slice of string options to choose from. func Select(label string, options []string) (string, error) { selectUi := promptui.Select{ Label: label, @@ -20,6 +23,8 @@ func Select(label string, options []string) (string, error) { return selectedProposalType, nil } +// PromptString prompts the user for a string input with the given label. +// It validates the input using the provided validate function. func PromptString(label string, validate func(string) error) (string, error) { promptUi := promptui.Prompt{ Label: label, @@ -28,3 +33,38 @@ func PromptString(label string, validate func(string) error) (string, error) { return promptUi.Run() } + +// SetDefaults sets default values on a protobuf message based on a map of field names to values. +// It iterates through the message fields and sets values from the defaults map if the field name +// and type match. +func SetDefaults(msg protoreflect.Message, defaults map[string]interface{}) { + fields := msg.Descriptor().Fields() + for i := 0; i < fields.Len(); i++ { + field := fields.Get(i) + fieldName := string(field.Name()) + + if v, ok := defaults[fieldName]; ok { + // Get the field's kind + fieldKind := field.Kind() + + switch v.(type) { + case string: + if fieldKind == protoreflect.StringKind { + msg.Set(field, protoreflect.ValueOf(v)) + } + case int64: + if fieldKind == protoreflect.Int64Kind { + msg.Set(field, protoreflect.ValueOf(v)) + } + case int32: + if fieldKind == protoreflect.Int32Kind { + msg.Set(field, protoreflect.ValueOf(v)) + } + case bool: + if fieldKind == protoreflect.BoolKind { + msg.Set(field, protoreflect.ValueOf(v)) + } + } + } + } +} diff --git a/x/gov/client/cli/prompt.go b/x/gov/client/cli/prompt.go index caeaacf920b3..f6833f72776d 100644 --- a/x/gov/client/cli/prompt.go +++ b/x/gov/client/cli/prompt.go @@ -3,24 +3,24 @@ package cli import ( "encoding/json" "fmt" - "google.golang.org/protobuf/encoding/protojson" "os" - "reflect" // #nosec "sort" - "strconv" "strings" + gogoproto "github.com/cosmos/gogoproto/proto" + "github.com/spf13/cobra" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoregistry" + "cosmossdk.io/client/v2/autocli/prompt" "cosmossdk.io/core/address" "cosmossdk.io/x/gov/types" - "github.com/manifoldco/promptui" - "github.com/spf13/cobra" - "google.golang.org/protobuf/reflect/protoregistry" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + sdkaddress "github.com/cosmos/cosmos-sdk/types/address" ) const ( @@ -61,102 +61,6 @@ var suggestedProposalTypes = []proposalType{ }, } -// Prompt prompts the user for all values of the given type. -// data is the struct to be filled -// namePrefix is the name to be displayed as "Enter " -// TODO: when bringing this in autocli, use proto message instead -// this will simplify the get address logic -func Prompt[T any](data T, namePrefix string, addressCodec address.Codec) (T, error) { - v := reflect.ValueOf(&data).Elem() - if v.Kind() == reflect.Interface { - v = reflect.ValueOf(data) - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - } - - for i := 0; i < v.NumField(); i++ { - // if the field is a struct skip or not slice of string or int then skip - switch v.Field(i).Kind() { - case reflect.Struct: - // TODO(@julienrbrt) in the future we can add a recursive call to Prompt - continue - case reflect.Slice: - if v.Field(i).Type().Elem().Kind() != reflect.String && v.Field(i).Type().Elem().Kind() != reflect.Int { - continue - } - } - - // create prompts - prompt := promptui.Prompt{ - Label: fmt.Sprintf("Enter %s %s", namePrefix, strings.ToLower(client.CamelCaseToString(v.Type().Field(i).Name))), - Validate: client.ValidatePromptNotEmpty, - } - - fieldName := strings.ToLower(v.Type().Field(i).Name) - - if strings.EqualFold(fieldName, "authority") { - // pre-fill with gov address - defaultAddr, err := addressCodec.BytesToString(authtypes.NewModuleAddress(types.ModuleName)) - if err != nil { - return data, err - } - prompt.Default = defaultAddr - prompt.Validate = client.ValidatePromptAddress - } - - // TODO(@julienrbrt) use scalar annotation instead of dumb string name matching - if strings.Contains(fieldName, "addr") || - strings.Contains(fieldName, "sender") || - strings.Contains(fieldName, "voter") || - strings.Contains(fieldName, "depositor") || - strings.Contains(fieldName, "granter") || - strings.Contains(fieldName, "grantee") || - strings.Contains(fieldName, "recipient") { - prompt.Validate = client.ValidatePromptAddress - } - - result, err := prompt.Run() - if err != nil { - return data, fmt.Errorf("failed to prompt for %s: %w", fieldName, err) - } - - switch v.Field(i).Kind() { - case reflect.String: - v.Field(i).SetString(result) - case reflect.Int: - resultInt, err := strconv.ParseInt(result, 10, 0) - if err != nil { - return data, fmt.Errorf("invalid value for int: %w", err) - } - // If a value was successfully parsed the ranges of: - // [minInt, maxInt] - // are within the ranges of: - // [minInt64, maxInt64] - // of which on 64-bit machines, which are most common, - // int==int64 - v.Field(i).SetInt(resultInt) - case reflect.Slice: - switch v.Field(i).Type().Elem().Kind() { - case reflect.String: - v.Field(i).Set(reflect.ValueOf([]string{result})) - case reflect.Int: - resultInt, err := strconv.ParseInt(result, 10, 0) - if err != nil { - return data, fmt.Errorf("invalid value for int: %w", err) - } - - v.Field(i).Set(reflect.ValueOf([]int{int(resultInt)})) - } - default: - // skip any other types - continue - } - } - - return data, nil -} - type proposalType struct { Name string MsgType string @@ -164,7 +68,7 @@ type proposalType struct { } // Prompt the proposal type values and return the proposal and its metadata -func (p *proposalType) Prompt(skipMetadata bool, addressCodec, validatorAddressCodec, consensusAddressCodec address.Codec) (*proposal, types.ProposalMetadata, error) { +func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec, validatorAddressCodec, consensusAddressCodec address.Codec) (*proposal, types.ProposalMetadata, error) { metadata, err := PromptMetadata(skipMetadata) if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal metadata: %w", err) @@ -191,12 +95,31 @@ func (p *proposalType) Prompt(skipMetadata bool, addressCodec, validatorAddressC if err != nil { return nil, metadata, fmt.Errorf("failed to find proposal msg: %w", err) } - result, err := prompt.PromptMessage(addressCodec, validatorAddressCodec, consensusAddressCodec, "msg", msg.New()) + newMsg := msg.New() + govAddr := sdkaddress.Module(types.ModuleName) + govAddrStr, err := addressCodec.BytesToString(govAddr) + if err != nil { + return nil, metadata, fmt.Errorf("failed to convert gov address to string: %w", err) + } + + prompt.SetDefaults(newMsg, map[string]interface{}{"authority": govAddrStr}) + result, err := prompt.PromptMessage(addressCodec, validatorAddressCodec, consensusAddressCodec, "msg", newMsg) if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal message: %w", err) } - message, err := protojson.Marshal(result.Interface()) + // message must be converted to gogoproto so @type is not lost + resultBytes, err := proto.Marshal(result.Interface()) + if err != nil { + return nil, metadata, fmt.Errorf("failed to marshal proposal message: %w", err) + } + + err = gogoproto.Unmarshal(resultBytes, p.Msg) + if err != nil { + return nil, metadata, fmt.Errorf("failed to unmarshal proposal message: %w", err) + } + + message, err := cdc.MarshalInterfaceJSON(p.Msg) if err != nil { return nil, metadata, fmt.Errorf("failed to marshal proposal message: %w", err) } @@ -284,7 +207,7 @@ func NewCmdDraftProposal() *cobra.Command { skipMetadataPrompt, _ := cmd.Flags().GetBool(flagSkipMetadata) - result, metadata, err := proposal.Prompt(skipMetadataPrompt, clientCtx.AddressCodec, clientCtx.ValidatorAddressCodec, clientCtx.ConsensusAddressCodec) + result, metadata, err := proposal.Prompt(clientCtx.Codec, skipMetadataPrompt, clientCtx.AddressCodec, clientCtx.ValidatorAddressCodec, clientCtx.ConsensusAddressCodec) if err != nil { return err } diff --git a/x/gov/client/cli/prompt_test.go b/x/gov/client/cli/prompt_test.go deleted file mode 100644 index 359c9dea5b53..000000000000 --- a/x/gov/client/cli/prompt_test.go +++ /dev/null @@ -1,90 +0,0 @@ -//go:build !race -// +build !race - -// Disabled -race because the package github.com/manifoldco/promptui@v0.9.0 -// has a data race and this code exposes it, but fixing it would require -// holding up the associated change to this. - -package cli_test - -import ( - "fmt" - "math" - "os" - "testing" - - "github.com/chzyer/readline" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "cosmossdk.io/x/gov/client/cli" - - codectestutil "github.com/cosmos/cosmos-sdk/codec/testutil" -) - -type st struct { - I int -} - -// Tests that we successfully report overflows in parsing ints -// See https://github.com/cosmos/cosmos-sdk/issues/13346 -func TestPromptIntegerOverflow(t *testing.T) { - // Intentionally sending values out of the range of int. - intOverflowers := []string{ - "-9223372036854775809", - "9223372036854775808", - "9923372036854775809", - "-9923372036854775809", - "18446744073709551616", - "-18446744073709551616", - } - - for _, intOverflower := range intOverflowers { - overflowStr := intOverflower - t.Run(overflowStr, func(t *testing.T) { - origStdin := readline.Stdin - defer func() { - readline.Stdin = origStdin - }() - - fin, fw := readline.NewFillableStdin(os.Stdin) - readline.Stdin = fin - _, err := fw.Write([]byte(overflowStr + "\n")) - assert.NoError(t, err) - - v, err := cli.Prompt(st{}, "", codectestutil.CodecOptions{}.GetAddressCodec()) - assert.Equal(t, st{}, v, "expected a value of zero") - require.NotNil(t, err, "expected a report of an overflow") - require.Contains(t, err.Error(), "range") - }) - } -} - -func TestPromptParseInteger(t *testing.T) { - // Intentionally sending a value out of the range of - values := []struct { - in string - want int - }{ - {fmt.Sprintf("%d", math.MinInt), math.MinInt}, - {"19991", 19991}, - {"991000000199", 991000000199}, - } - - for _, tc := range values { - t.Run(tc.in, func(t *testing.T) { - origStdin := readline.Stdin - defer func() { - readline.Stdin = origStdin - }() - - fin, fw := readline.NewFillableStdin(os.Stdin) - readline.Stdin = fin - _, err := fw.Write([]byte(tc.in + "\n")) - assert.NoError(t, err) - v, err := cli.Prompt(st{}, "", codectestutil.CodecOptions{}.GetAddressCodec()) - assert.Nil(t, err, "expected a nil error") - assert.Equal(t, tc.want, v.I, "expected %d = %d", tc.want, v.I) - }) - } -} diff --git a/x/group/client/cli/prompt.go b/x/group/client/cli/prompt.go index 0a972e6ce33b..480e937a14f8 100644 --- a/x/group/client/cli/prompt.go +++ b/x/group/client/cli/prompt.go @@ -6,9 +6,12 @@ import ( "os" "sort" - "github.com/manifoldco/promptui" + gogoproto "github.com/cosmos/gogoproto/proto" "github.com/spf13/cobra" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoregistry" + "cosmossdk.io/client/v2/autocli/prompt" "cosmossdk.io/core/address" govcli "cosmossdk.io/x/gov/client/cli" govtypes "cosmossdk.io/x/gov/types" @@ -27,12 +30,13 @@ const ( ) type proposalType struct { - Name string - Msg sdk.Msg + Name string + MsgType string + Msg sdk.Msg } // Prompt the proposal type values and return the proposal and its metadata. -func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec address.Codec) (*Proposal, govtypes.ProposalMetadata, error) { +func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec, validatorAddressCodec, consensusAddressCodec address.Codec) (*Proposal, govtypes.ProposalMetadata, error) { // set metadata metadata, err := govcli.PromptMetadata(skipMetadata) if err != nil { @@ -46,22 +50,14 @@ func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec a } // set group policy address - policyAddressPrompt := promptui.Prompt{ - Label: "Enter group policy address", - Validate: client.ValidatePromptAddress, - } - groupPolicyAddress, err := policyAddressPrompt.Run() + groupPolicyAddress, err := prompt.PromptString("Enter group policy address", client.ValidatePromptAddress) if err != nil { return nil, metadata, fmt.Errorf("failed to set group policy address: %w", err) } proposal.GroupPolicyAddress = groupPolicyAddress // set proposer address - proposerPrompt := promptui.Prompt{ - Label: "Enter proposer address", - Validate: client.ValidatePromptAddress, - } - proposerAddress, err := proposerPrompt.Run() + proposerAddress, err := prompt.PromptString("Enter proposer address", client.ValidatePromptAddress) if err != nil { return nil, metadata, fmt.Errorf("failed to set proposer address: %w", err) } @@ -72,12 +68,29 @@ func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec a } // set messages field - result, err := govcli.Prompt(p.Msg, "msg", addressCodec) + msg, err := protoregistry.GlobalTypes.FindMessageByURL(p.MsgType) + if err != nil { + return nil, metadata, fmt.Errorf("failed to find proposal msg: %w", err) + } + newMsg := msg.New() + + result, err := prompt.PromptMessage(addressCodec, validatorAddressCodec, consensusAddressCodec, "msg", newMsg) if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal message: %w", err) } - message, err := cdc.MarshalInterfaceJSON(result) + // message must be converted to gogoproto so @type is not lost + resultBytes, err := proto.Marshal(result.Interface()) + if err != nil { + return nil, metadata, fmt.Errorf("failed to marshal proposal message: %w", err) + } + + err = gogoproto.Unmarshal(resultBytes, p.Msg) + if err != nil { + return nil, metadata, fmt.Errorf("failed to unmarshal proposal message: %w", err) + } + + message, err := cdc.MarshalInterfaceJSON(p.Msg) if err != nil { return nil, metadata, fmt.Errorf("failed to marshal proposal message: %w", err) } @@ -101,12 +114,7 @@ func NewCmdDraftProposal() *cobra.Command { } // prompt proposal type - proposalTypesPrompt := promptui.Select{ - Label: "Select proposal type", - Items: []string{proposalText, proposalOther}, - } - - _, selectedProposalType, err := proposalTypesPrompt.Run() + selectedProposalType, err := prompt.Select("Select proposal type", []string{proposalText, proposalOther}) if err != nil { return fmt.Errorf("failed to prompt proposal types: %w", err) } @@ -118,20 +126,15 @@ func NewCmdDraftProposal() *cobra.Command { case proposalOther: // prompt proposal type proposal = &proposalType{Name: proposalOther} - msgPrompt := promptui.Select{ - Label: "Select proposal message type:", - Items: func() []string { - msgs := clientCtx.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName) - sort.Strings(msgs) - return msgs - }(), - } - _, result, err := msgPrompt.Run() + msgs := clientCtx.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName) + sort.Strings(msgs) + + result, err := prompt.Select("Select proposal message type:", msgs) if err != nil { return fmt.Errorf("failed to prompt proposal types: %w", err) } - + proposal.MsgType = result proposal.Msg, err = sdk.GetMsgFromTypeURL(clientCtx.Codec, result) if err != nil { // should never happen @@ -143,7 +146,7 @@ func NewCmdDraftProposal() *cobra.Command { skipMetadataPrompt, _ := cmd.Flags().GetBool(flagSkipMetadata) - result, metadata, err := proposal.Prompt(clientCtx.Codec, skipMetadataPrompt, clientCtx.AddressCodec) + result, metadata, err := proposal.Prompt(clientCtx.Codec, skipMetadataPrompt, clientCtx.AddressCodec, clientCtx.ValidatorAddressCodec, clientCtx.ConsensusAddressCodec) if err != nil { return err } From c053d1a90adbfdaeed608ed1150c0c6d6d4671de Mon Sep 17 00:00:00 2001 From: Julian Toledano Date: Thu, 5 Dec 2024 12:26:28 +0100 Subject: [PATCH 06/12] lint --- client/v2/autocli/prompt/message.go | 13 ++++++------- client/v2/autocli/prompt/message_test.go | 8 +++++--- x/gov/client/cli/prompt.go | 3 +++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/client/v2/autocli/prompt/message.go b/client/v2/autocli/prompt/message.go index 25f41af6b7e8..1fabd03a1044 100644 --- a/client/v2/autocli/prompt/message.go +++ b/client/v2/autocli/prompt/message.go @@ -17,7 +17,7 @@ import ( // PromptMessage prompts the user for values to populate a protobuf message interactively. // It returns the populated message and any error encountered during prompting. func PromptMessage( - addressCodec addresscodec.Codec, validatorAddressCodec addresscodec.Codec, + addressCodec, validatorAddressCodec addresscodec.Codec, consensusAddressCodec addresscodec.Codec, promptPrefix string, msg protoreflect.Message, ) (protoreflect.Message, error) { return promptMessage(addressCodec, validatorAddressCodec, consensusAddressCodec, promptPrefix, nil, msg) @@ -26,11 +26,10 @@ func PromptMessage( // promptMessage prompts the user for values to populate a protobuf message interactively. // stdIn is provided to make the function easier to unit test by allowing injection of predefined inputs. func promptMessage( - addressCodec addresscodec.Codec, validatorAddressCodec addresscodec.Codec, + addressCodec, validatorAddressCodec addresscodec.Codec, consensusAddressCodec addresscodec.Codec, promptPrefix string, stdIn io.ReadCloser, msg protoreflect.Message, ) (protoreflect.Message, error) { - fields := msg.Descriptor().Fields() for i := 0; i < fields.Len(); i++ { field := fields.Get(i) @@ -178,7 +177,7 @@ func promptList(field protoreflect.FieldDescriptor, msg protoreflect.Message, pr // promptInnerMessage and promptMessageList respectively. func promptInnerMessageKind( f protoreflect.FieldDescriptor, addressCodec addresscodec.Codec, - validatorAddressCodec addresscodec.Codec, consensusAddressCodec addresscodec.Codec, + validatorAddressCodec, consensusAddressCodec addresscodec.Codec, promptPrefix string, stdIn io.ReadCloser, msg protoreflect.Message, ) error { if f.IsList() { @@ -191,12 +190,12 @@ func promptInnerMessageKind( // recursively prompts for its fields, and sets the populated message on the parent message. func promptInnerMessage( f protoreflect.FieldDescriptor, addressCodec addresscodec.Codec, - validatorAddressCodec addresscodec.Codec, consensusAddressCodec addresscodec.Codec, + validatorAddressCodec, consensusAddressCodec addresscodec.Codec, promptPrefix string, stdIn io.ReadCloser, msg protoreflect.Message, ) error { fieldName := promptPrefix + "." + string(f.Name()) nestedMsg := msg.Get(f).Message() - //if nestedMsg.IsValid() { + // if nestedMsg.IsValid() { // nestedMsg = nestedMsg.New() //} else { // nestedMsg = msg.Get(f).Message() @@ -223,7 +222,7 @@ func promptInnerMessage( // prompting for their fields, and appending them to the list until the user chooses to stop. func promptMessageList( f protoreflect.FieldDescriptor, addressCodec addresscodec.Codec, - validatorAddressCodec addresscodec.Codec, consensusAddressCodec addresscodec.Codec, + validatorAddressCodec, consensusAddressCodec addresscodec.Codec, promptPrefix string, stdIn io.ReadCloser, msg protoreflect.Message, ) error { list := msg.Mutable(f).List() diff --git a/client/v2/autocli/prompt/message_test.go b/client/v2/autocli/prompt/message_test.go index cc37c91d10b5..353cbf6cd725 100644 --- a/client/v2/autocli/prompt/message_test.go +++ b/client/v2/autocli/prompt/message_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/reflect/protoreflect" "cosmossdk.io/client/v2/internal/testpb" - "google.golang.org/protobuf/reflect/protoreflect" address2 "github.com/cosmos/cosmos-sdk/codec/address" ) @@ -31,11 +31,13 @@ func TestPromptMessage(t *testing.T) { }{ { name: "testPb", - inputs: []string{"1", "2", "string", "bytes", "10101010", "0", "234234", "3", "4", "5", "true", "ENUM_ONE", + inputs: []string{ + "1", "2", "string", "bytes", "10101010", "0", "234234", "3", "4", "5", "true", "ENUM_ONE", "bar", "6", "10000", "stake", "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn", "bytes", "6", "7", "false", "false", "true,false,true", "1,2,3", "hello,hola,ciao", "ENUM_ONE,ENUM_TWO", "10239", "0", "No", "bar", "343", "No", "134", "positional2", "23455", "stake", "No", "deprecate", - "shorthand", "false", "cosmosvaloper1tnh2q55v8wyygtt9srz5safamzdengsn9dsd7z"}, + "shorthand", "false", "cosmosvaloper1tnh2q55v8wyygtt9srz5safamzdengsn9dsd7z", + }, msg: (&testpb.MsgRequest{}).ProtoReflect(), }, } diff --git a/x/gov/client/cli/prompt.go b/x/gov/client/cli/prompt.go index f6833f72776d..ef014d46e21f 100644 --- a/x/gov/client/cli/prompt.go +++ b/x/gov/client/cli/prompt.go @@ -176,6 +176,9 @@ func NewCmdDraftProposal() *cobra.Command { } selectedProposalType, err := prompt.Select("Select proposal type", getProposalSuggestions()) + if err != nil { + return fmt.Errorf("failed to prompt proposal types: %w", err) + } var proposal proposalType for _, p := range suggestedProposalTypes { if strings.EqualFold(p.Name, selectedProposalType) { From 9823ff80c4d921ab618c1eacfb8e6b9340f731cd Mon Sep 17 00:00:00 2001 From: Julian Toledano Date: Thu, 5 Dec 2024 17:39:21 +0100 Subject: [PATCH 07/12] del: client/v1 prompt validation --- client/v2/{internal => autocli}/prompt/validation.go | 0 client/v2/{internal => autocli}/prompt/validation_test.go | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename client/v2/{internal => autocli}/prompt/validation.go (100%) rename client/v2/{internal => autocli}/prompt/validation_test.go (100%) diff --git a/client/v2/internal/prompt/validation.go b/client/v2/autocli/prompt/validation.go similarity index 100% rename from client/v2/internal/prompt/validation.go rename to client/v2/autocli/prompt/validation.go diff --git a/client/v2/internal/prompt/validation_test.go b/client/v2/autocli/prompt/validation_test.go similarity index 100% rename from client/v2/internal/prompt/validation_test.go rename to client/v2/autocli/prompt/validation_test.go From 04540dc57b688903af1659e02f15457419c78ad3 Mon Sep 17 00:00:00 2001 From: Julian Toledano Date: Thu, 5 Dec 2024 17:40:33 +0100 Subject: [PATCH 08/12] del: v1 prompt validation --- CHANGELOG.md | 2 + client/prompt_validation.go | 41 ----------------- client/prompt_validation_test.go | 23 ---------- client/v2/CHANGELOG.md | 1 + client/v2/autocli/prompt/message.go | 23 ++++------ client/v2/autocli/prompt/struct.go | 4 +- client/v2/autocli/prompt/validation.go | 24 +--------- client/v2/autocli/prompt/validation_test.go | 50 ++++++++++++++------- x/gov/CHANGELOG.md | 1 + x/gov/client/cli/prompt.go | 6 +-- x/gov/client/cli/util.go | 18 ++++++++ x/gov/client/cli/util_test.go | 14 ++++++ x/group/client/cli/prompt.go | 4 +- 13 files changed, 84 insertions(+), 127 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a3515a5b7a9..76e63abba6cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,8 @@ Every module contains its own CHANGELOG.md. Please refer to the module you are i ### API Breaking Changes +* (client) [#22775](https://github.com/cosmos/cosmos-sdk/pull/22775) Removed client prompt validations. + ### Deprecated ## [v0.52.0](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.52.0) - 2024-XX-XX diff --git a/client/prompt_validation.go b/client/prompt_validation.go index 9d3af0d58f67..3f2500548c31 100644 --- a/client/prompt_validation.go +++ b/client/prompt_validation.go @@ -1,23 +1,11 @@ package client import ( - "errors" "fmt" "net/url" "unicode" - - sdk "github.com/cosmos/cosmos-sdk/types" ) -// ValidatePromptNotEmpty validates that the input is not empty. -func ValidatePromptNotEmpty(input string) error { - if input == "" { - return errors.New("input cannot be empty") - } - - return nil -} - // ValidatePromptURL validates that the input is a valid URL. func ValidatePromptURL(input string) error { _, err := url.ParseRequestURI(input) @@ -28,35 +16,6 @@ func ValidatePromptURL(input string) error { return nil } -// ValidatePromptAddress validates that the input is a valid Bech32 address. -func ValidatePromptAddress(input string) error { // TODO(@julienrbrt) remove and add prompts in AutoCLI - _, err := sdk.AccAddressFromBech32(input) - if err == nil { - return nil - } - - _, err = sdk.ValAddressFromBech32(input) - if err == nil { - return nil - } - - _, err = sdk.ConsAddressFromBech32(input) - if err == nil { - return nil - } - - return fmt.Errorf("invalid address: %w", err) -} - -// ValidatePromptCoins validates that the input contains valid sdk.Coins -func ValidatePromptCoins(input string) error { - if _, err := sdk.ParseCoinsNormalized(input); err != nil { - return fmt.Errorf("invalid coins: %w", err) - } - - return nil -} - // CamelCaseToString converts a camel case string to a string with spaces. func CamelCaseToString(str string) string { w := []rune(str) diff --git a/client/prompt_validation_test.go b/client/prompt_validation_test.go index 488aa03e5414..540a5c2e6529 100644 --- a/client/prompt_validation_test.go +++ b/client/prompt_validation_test.go @@ -8,32 +8,9 @@ import ( "github.com/cosmos/cosmos-sdk/client" ) -func TestValidatePromptNotEmpty(t *testing.T) { - require := require.New(t) - - require.NoError(client.ValidatePromptNotEmpty("foo")) - require.ErrorContains(client.ValidatePromptNotEmpty(""), "input cannot be empty") -} - func TestValidatePromptURL(t *testing.T) { require := require.New(t) require.NoError(client.ValidatePromptURL("https://example.com")) require.ErrorContains(client.ValidatePromptURL("foo"), "invalid URL") } - -func TestValidatePromptAddress(t *testing.T) { - require := require.New(t) - - require.NoError(client.ValidatePromptAddress("cosmos1huydeevpz37sd9snkgul6070mstupukw00xkw9")) - require.NoError(client.ValidatePromptAddress("cosmosvaloper1sjllsnramtg3ewxqwwrwjxfgc4n4ef9u2lcnj0")) - require.NoError(client.ValidatePromptAddress("cosmosvalcons1ntk8eualewuprz0gamh8hnvcem2nrcdsgz563h")) - require.ErrorContains(client.ValidatePromptAddress("foo"), "invalid address") -} - -func TestValidatePromptCoins(t *testing.T) { - require := require.New(t) - - require.NoError(client.ValidatePromptCoins("100stake")) - require.ErrorContains(client.ValidatePromptCoins("foo"), "invalid coins") -} diff --git a/client/v2/CHANGELOG.md b/client/v2/CHANGELOG.md index 5cff1928e437..7f29c0c675dd 100644 --- a/client/v2/CHANGELOG.md +++ b/client/v2/CHANGELOG.md @@ -45,6 +45,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * [#20623](https://github.com/cosmos/cosmos-sdk/pull/20623) Introduce client/v2 tx factory. * [#20623](https://github.com/cosmos/cosmos-sdk/pull/20623) Extend client/v2 keyring interface with `KeyType` and `KeyInfo`. * [#22282](https://github.com/cosmos/cosmos-sdk/pull/22282) Added custom broadcast logic. +* [#22775](https://github.com/cosmos/cosmos-sdk/pull/22775) Added interactive autocli prompt functionality, including message field prompting, validation helpers, and default value support. ### Improvements diff --git a/client/v2/autocli/prompt/message.go b/client/v2/autocli/prompt/message.go index 1fabd03a1044..a1e080a50e5d 100644 --- a/client/v2/autocli/prompt/message.go +++ b/client/v2/autocli/prompt/message.go @@ -10,15 +10,14 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" "cosmossdk.io/client/v2/autocli/flag" - "cosmossdk.io/client/v2/internal/prompt" addresscodec "cosmossdk.io/core/address" ) // PromptMessage prompts the user for values to populate a protobuf message interactively. // It returns the populated message and any error encountered during prompting. func PromptMessage( - addressCodec, validatorAddressCodec addresscodec.Codec, - consensusAddressCodec addresscodec.Codec, promptPrefix string, msg protoreflect.Message, + addressCodec, validatorAddressCodec, consensusAddressCodec addresscodec.Codec, + promptPrefix string, msg protoreflect.Message, ) (protoreflect.Message, error) { return promptMessage(addressCodec, validatorAddressCodec, consensusAddressCodec, promptPrefix, nil, msg) } @@ -26,9 +25,8 @@ func PromptMessage( // promptMessage prompts the user for values to populate a protobuf message interactively. // stdIn is provided to make the function easier to unit test by allowing injection of predefined inputs. func promptMessage( - addressCodec, validatorAddressCodec addresscodec.Codec, - consensusAddressCodec addresscodec.Codec, promptPrefix string, - stdIn io.ReadCloser, msg protoreflect.Message, + addressCodec, validatorAddressCodec, consensusAddressCodec addresscodec.Codec, + promptPrefix string, stdIn io.ReadCloser, msg protoreflect.Message, ) (protoreflect.Message, error) { fields := msg.Descriptor().Fields() for i := 0; i < fields.Len(); i++ { @@ -36,7 +34,7 @@ func promptMessage( fieldName := string(field.Name()) promptUi := promptui.Prompt{ - Validate: prompt.ValidatePromptNotEmpty, + Validate: ValidatePromptNotEmpty, Stdin: stdIn, } @@ -54,11 +52,11 @@ func promptMessage( if ok { switch scalarField { case flag.AddressStringScalarType: - promptUi.Validate = prompt.ValidateAddress(addressCodec) + promptUi.Validate = ValidateAddress(addressCodec) case flag.ValidatorAddressStringScalarType: - promptUi.Validate = prompt.ValidateAddress(validatorAddressCodec) + promptUi.Validate = ValidateAddress(validatorAddressCodec) case flag.ConsensusAddressStringScalarType: - promptUi.Validate = prompt.ValidateAddress(consensusAddressCodec) + promptUi.Validate = ValidateAddress(consensusAddressCodec) default: // prompt.Validate = ValidatePromptNotEmpty (we possibly don't want to force all fields to be non-empty) promptUi.Validate = nil @@ -195,11 +193,6 @@ func promptInnerMessage( ) error { fieldName := promptPrefix + "." + string(f.Name()) nestedMsg := msg.Get(f).Message() - // if nestedMsg.IsValid() { - // nestedMsg = nestedMsg.New() - //} else { - // nestedMsg = msg.Get(f).Message() - //} nestedMsg = nestedMsg.New() // Recursively prompt for nested message fields updatedMsg, err := promptMessage( diff --git a/client/v2/autocli/prompt/struct.go b/client/v2/autocli/prompt/struct.go index 16c8ee796f9d..a450b5a60e22 100644 --- a/client/v2/autocli/prompt/struct.go +++ b/client/v2/autocli/prompt/struct.go @@ -8,8 +8,6 @@ import ( "strings" "github.com/manifoldco/promptui" - - "cosmossdk.io/client/v2/internal/prompt" ) // PromptStruct prompts for values of a struct's fields interactively. @@ -82,7 +80,7 @@ func promptStruct[T any](promptPrefix string, data T, stdIn io.ReadCloser) (T, e // create prompts prompt := promptui.Prompt{ Label: fmt.Sprintf("Enter %s %s", promptPrefix, strings.Title(fieldName)), // nolint:staticcheck // strings.Title has a better API - Validate: prompt.ValidatePromptNotEmpty, + Validate: ValidatePromptNotEmpty, Stdin: stdIn, } diff --git a/client/v2/autocli/prompt/validation.go b/client/v2/autocli/prompt/validation.go index 9e2c97ae6ec0..f796035326c4 100644 --- a/client/v2/autocli/prompt/validation.go +++ b/client/v2/autocli/prompt/validation.go @@ -3,11 +3,8 @@ package prompt import ( "errors" "fmt" - "net/url" "cosmossdk.io/core/address" - - sdk "github.com/cosmos/cosmos-sdk/types" ) // ValidatePromptNotEmpty validates that the input is not empty. @@ -19,31 +16,12 @@ func ValidatePromptNotEmpty(input string) error { return nil } -// ValidatePromptURL validates that the input is a valid URL. -func ValidatePromptURL(input string) error { - _, err := url.ParseRequestURI(input) - if err != nil { - return fmt.Errorf("invalid URL: %w", err) - } - - return nil -} - -// ValidatePromptCoins validates that the input contains valid sdk.Coins -func ValidatePromptCoins(input string) error { - if _, err := sdk.ParseCoinsNormalized(input); err != nil { - return fmt.Errorf("invalid coins: %w", err) - } - - return nil -} - // ValidateAddress returns a validation function that checks if a string is a valid address // for the given address codec. func ValidateAddress(ac address.Codec) func(string) error { return func(i string) error { if _, err := ac.StringToBytes(i); err != nil { - return fmt.Errorf("invalid consensus address") + return fmt.Errorf("invalid address") } return nil diff --git a/client/v2/autocli/prompt/validation_test.go b/client/v2/autocli/prompt/validation_test.go index 86e4ba4ab475..a14faabfbdbc 100644 --- a/client/v2/autocli/prompt/validation_test.go +++ b/client/v2/autocli/prompt/validation_test.go @@ -1,30 +1,46 @@ -package prompt_test +package prompt import ( + "cosmossdk.io/core/address" + address2 "github.com/cosmos/cosmos-sdk/codec/address" "testing" "github.com/stretchr/testify/require" - - "cosmossdk.io/client/v2/internal/prompt" ) func TestValidatePromptNotEmpty(t *testing.T) { require := require.New(t) - require.NoError(prompt.ValidatePromptNotEmpty("foo")) - require.ErrorContains(prompt.ValidatePromptNotEmpty(""), "input cannot be empty") + require.NoError(ValidatePromptNotEmpty("foo")) + require.ErrorContains(ValidatePromptNotEmpty(""), "input cannot be empty") } -func TestValidatePromptURL(t *testing.T) { - require := require.New(t) - - require.NoError(prompt.ValidatePromptURL("https://example.com")) - require.ErrorContains(prompt.ValidatePromptURL("foo"), "invalid URL") -} - -func TestValidatePromptCoins(t *testing.T) { - require := require.New(t) - - require.NoError(prompt.ValidatePromptCoins("100stake")) - require.ErrorContains(prompt.ValidatePromptCoins("foo"), "invalid coins") +func TestValidateAddress(t *testing.T) { + tests := []struct { + name string + ac address.Codec + addr string + }{ + { + name: "address", + ac: address2.NewBech32Codec("cosmos"), + addr: "cosmos129lxcu2n3hx54fdxlwsahqkjr3sp32cxm00zlm", + }, + { + name: "validator address", + ac: address2.NewBech32Codec("cosmosvaloper"), + addr: "cosmosvaloper1tnh2q55v8wyygtt9srz5safamzdengsn9dsd7z", + }, + { + name: "consensus address", + ac: address2.NewBech32Codec("cosmosvalcons"), + addr: "cosmosvalcons136uu5rj23kdr3jjcmjt7aw5qpugjjat2klgrus", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateAddress(tt.ac)(tt.addr) + require.NoError(t, err) + }) + } } diff --git a/x/gov/CHANGELOG.md b/x/gov/CHANGELOG.md index 231a520171a9..4743c45d6571 100644 --- a/x/gov/CHANGELOG.md +++ b/x/gov/CHANGELOG.md @@ -61,6 +61,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Client Breaking Changes * [#19101](https://github.com/cosmos/cosmos-sdk/pull/19101) Querying specific params types was deprecated in gov/v1 and has been removed. gov/v1beta1 rest unchanged. +* [#22775](https://github.com/cosmos/cosmos-sdk/pull/22775) Refactored interactive proposal prompts to use `client/v2/autocli/prompt` package. ### API Breaking Changes diff --git a/x/gov/client/cli/prompt.go b/x/gov/client/cli/prompt.go index ef014d46e21f..08a4c1d05d77 100644 --- a/x/gov/client/cli/prompt.go +++ b/x/gov/client/cli/prompt.go @@ -81,7 +81,7 @@ func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec, } // set deposit - proposal.Deposit, err = prompt.PromptString("Enter proposal deposit", client.ValidatePromptCoins) + proposal.Deposit, err = prompt.PromptString("Enter proposal deposit", ValidatePromptCoins) if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal deposit: %w", err) } @@ -148,12 +148,12 @@ func PromptMetadata(skip bool) (types.ProposalMetadata, error) { return metadata, nil } - title, err := prompt.PromptString("Enter proposal title", client.ValidatePromptNotEmpty) + title, err := prompt.PromptString("Enter proposal title", ValidatePromptNotEmpty) if err != nil { return types.ProposalMetadata{}, fmt.Errorf("failed to set proposal title: %w", err) } - summary, err := prompt.PromptString("Enter proposal summary", client.ValidatePromptNotEmpty) + summary, err := prompt.PromptString("Enter proposal summary", ValidatePromptNotEmpty) if err != nil { return types.ProposalMetadata{}, fmt.Errorf("failed to set proposal summary: %w", err) } diff --git a/x/gov/client/cli/util.go b/x/gov/client/cli/util.go index f8628f9cffed..cdf76268b1da 100644 --- a/x/gov/client/cli/util.go +++ b/x/gov/client/cli/util.go @@ -209,3 +209,21 @@ func ReadGovPropFlags(clientCtx client.Context, flagSet *pflag.FlagSet) (*govv1. return ReadGovPropCmdFlags(addr, flagSet) } + +// ValidatePromptCoins validates that the input contains valid sdk.Coins +func ValidatePromptCoins(input string) error { + if _, err := sdk.ParseCoinsNormalized(input); err != nil { + return fmt.Errorf("invalid coins: %w", err) + } + + return nil +} + +// ValidatePromptNotEmpty validates that the input is not empty. +func ValidatePromptNotEmpty(input string) error { + if input == "" { + return errors.New("input cannot be empty") + } + + return nil +} diff --git a/x/gov/client/cli/util_test.go b/x/gov/client/cli/util_test.go index 2601a526fe1b..e5ad5efcc29b 100644 --- a/x/gov/client/cli/util_test.go +++ b/x/gov/client/cli/util_test.go @@ -714,3 +714,17 @@ func TestReadGovPropFlags(t *testing.T) { }) } } + +func TestValidatePromptNotEmpty(t *testing.T) { + require := require.New(t) + + require.NoError(ValidatePromptNotEmpty("foo")) + require.ErrorContains(ValidatePromptNotEmpty(""), "input cannot be empty") +} + +func TestValidatePromptCoins(t *testing.T) { + require := require.New(t) + + require.NoError(ValidatePromptCoins("100stake")) + require.ErrorContains(ValidatePromptCoins("foo"), "invalid coins") +} diff --git a/x/group/client/cli/prompt.go b/x/group/client/cli/prompt.go index 480e937a14f8..f3677c485059 100644 --- a/x/group/client/cli/prompt.go +++ b/x/group/client/cli/prompt.go @@ -50,14 +50,14 @@ func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec, } // set group policy address - groupPolicyAddress, err := prompt.PromptString("Enter group policy address", client.ValidatePromptAddress) + groupPolicyAddress, err := prompt.PromptString("Enter group policy address", prompt.ValidateAddress(addressCodec)) if err != nil { return nil, metadata, fmt.Errorf("failed to set group policy address: %w", err) } proposal.GroupPolicyAddress = groupPolicyAddress // set proposer address - proposerAddress, err := prompt.PromptString("Enter proposer address", client.ValidatePromptAddress) + proposerAddress, err := prompt.PromptString("Enter proposer address", prompt.ValidateAddress(addressCodec)) if err != nil { return nil, metadata, fmt.Errorf("failed to set proposer address: %w", err) } From ad05f766650a2b575d0f9d0800fdbb51dbdf4574 Mon Sep 17 00:00:00 2001 From: Julian Toledano Date: Thu, 5 Dec 2024 17:45:52 +0100 Subject: [PATCH 09/12] go-mod-tidy-all --- client/v2/go.mod | 2 +- simapp/go.mod | 2 +- simapp/v2/go.mod | 2 +- tests/go.mod | 2 +- x/feegrant/go.mod | 4 +++- x/gov/go.mod | 6 +++--- x/group/go.mod | 5 +++-- x/upgrade/go.mod | 4 +++- 8 files changed, 16 insertions(+), 11 deletions(-) diff --git a/client/v2/go.mod b/client/v2/go.mod index b39ea3a3eaf8..8e5105381fb4 100644 --- a/client/v2/go.mod +++ b/client/v2/go.mod @@ -118,7 +118,7 @@ require ( github.com/lib/pq v1.10.9 // indirect github.com/linxGnu/grocksdb v1.9.3 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/manifoldco/promptui v0.9.0 // indirect + github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/highwayhash v1.0.3 // indirect diff --git a/simapp/go.mod b/simapp/go.mod index 1c55eae3a989..20a95f6545c0 100644 --- a/simapp/go.mod +++ b/simapp/go.mod @@ -4,7 +4,7 @@ go 1.23.3 require ( cosmossdk.io/api v0.7.6 - cosmossdk.io/client/v2 v2.0.0-20230630094428-02b760776860 + cosmossdk.io/client/v2 v2.0.0-beta.6 cosmossdk.io/collections v0.4.1-0.20241128094659-bd76b47e1d8b cosmossdk.io/core v1.0.0-alpha.6 cosmossdk.io/core/testing v0.0.0-20241108153815-606544c7be7e diff --git a/simapp/v2/go.mod b/simapp/v2/go.mod index a42a48e4410f..b72f18ce26ab 100644 --- a/simapp/v2/go.mod +++ b/simapp/v2/go.mod @@ -4,7 +4,7 @@ go 1.23.3 require ( cosmossdk.io/api v0.7.6 - cosmossdk.io/client/v2 v2.0.0-00010101000000-000000000000 + cosmossdk.io/client/v2 v2.0.0-beta.6 cosmossdk.io/core v1.0.0-alpha.6 cosmossdk.io/depinject v1.1.0 cosmossdk.io/log v1.5.0 diff --git a/tests/go.mod b/tests/go.mod index ef77ab0e0775..434d832ed6cd 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -68,7 +68,7 @@ require ( cloud.google.com/go/compute/metadata v0.5.0 // indirect cloud.google.com/go/iam v1.1.13 // indirect cloud.google.com/go/storage v1.43.0 // indirect - cosmossdk.io/client/v2 v2.0.0-20230630094428-02b760776860 // indirect + cosmossdk.io/client/v2 v2.0.0-beta.6 // indirect cosmossdk.io/errors v1.0.1 // indirect cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 // indirect cosmossdk.io/indexer/postgres v0.1.0 // indirect diff --git a/x/feegrant/go.mod b/x/feegrant/go.mod index 6e890901726d..cde12519213a 100644 --- a/x/feegrant/go.mod +++ b/x/feegrant/go.mod @@ -12,7 +12,7 @@ require ( cosmossdk.io/math v1.4.0 cosmossdk.io/store v1.1.1-0.20240418092142-896cdf1971bc cosmossdk.io/x/bank v0.0.0-20240226161501-23359a0b6d91 - cosmossdk.io/x/gov v0.0.0-20230925135524-a1bc045b3190 + cosmossdk.io/x/gov v0.0.0-20231113122742-912390d5fc4a github.com/cometbft/cometbft v1.0.0-rc2.0.20241127125717-4ce33b646ac9 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.5 github.com/cosmos/cosmos-sdk v0.53.0 @@ -36,6 +36,7 @@ require ( require ( buf.build/gen/go/cometbft/cometbft/protocolbuffers/go v1.35.2-20241120201313-68e42a58b301.1 // indirect buf.build/gen/go/cosmos/gogo-proto/protocolbuffers/go v1.35.2-20240130113600-88ef6483f90f.1 // indirect + cosmossdk.io/client/v2 v2.0.0-beta.6 // indirect cosmossdk.io/log v1.5.0 // indirect cosmossdk.io/schema v0.3.1-0.20241128094659-bd76b47e1d8b // indirect cosmossdk.io/x/protocolpool v0.0.0-20230925135524-a1bc045b3190 // indirect @@ -182,6 +183,7 @@ replace github.com/cosmos/cosmos-sdk => ../../. // TODO remove post spinning out all modules replace ( cosmossdk.io/api => ../../api + cosmossdk.io/client/v2 => ../../client/v2 cosmossdk.io/store => ../../store cosmossdk.io/x/bank => ../bank cosmossdk.io/x/gov => ../gov diff --git a/x/gov/go.mod b/x/gov/go.mod index b90468c51852..f4b5d150cba1 100644 --- a/x/gov/go.mod +++ b/x/gov/go.mod @@ -4,7 +4,7 @@ go 1.23.3 require ( cosmossdk.io/api v0.7.6 - cosmossdk.io/client/v2 v2.0.0-00010101000000-000000000000 + cosmossdk.io/client/v2 v2.0.0-beta.6 cosmossdk.io/collections v0.4.1-0.20241128094659-bd76b47e1d8b cosmossdk.io/core v1.0.0-alpha.6 cosmossdk.io/core/testing v0.0.0-20241108153815-606544c7be7e @@ -16,14 +16,14 @@ require ( cosmossdk.io/x/bank v0.0.0-20240226161501-23359a0b6d91 cosmossdk.io/x/protocolpool v0.0.0-20230925135524-a1bc045b3190 cosmossdk.io/x/staking v0.0.0-00010101000000-000000000000 - github.com/chzyer/readline v1.5.1 + github.com/chzyer/readline v1.5.1 // indirect github.com/cometbft/cometbft v1.0.0-rc2.0.20241127125717-4ce33b646ac9 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.5 github.com/cosmos/cosmos-sdk v0.53.0 github.com/cosmos/gogoproto v1.7.0 github.com/golang/protobuf v1.5.4 github.com/grpc-ecosystem/grpc-gateway v1.16.0 - github.com/manifoldco/promptui v0.9.0 + github.com/manifoldco/promptui v0.9.0 // indirect github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.10.0 diff --git a/x/group/go.mod b/x/group/go.mod index f7f6a099aa78..3f732bfae857 100644 --- a/x/group/go.mod +++ b/x/group/go.mod @@ -4,6 +4,7 @@ go 1.23.3 require ( cosmossdk.io/api v0.7.6 + cosmossdk.io/client/v2 v2.0.0-beta.6 cosmossdk.io/core v1.0.0-alpha.6 cosmossdk.io/core/testing v0.0.0-20241108153815-606544c7be7e cosmossdk.io/depinject v1.1.0 @@ -15,7 +16,7 @@ require ( cosmossdk.io/x/authz v0.0.0-00010101000000-000000000000 cosmossdk.io/x/bank v0.0.0-20240226161501-23359a0b6d91 cosmossdk.io/x/consensus v0.0.0-00010101000000-000000000000 - cosmossdk.io/x/gov v0.0.0-20230925135524-a1bc045b3190 + cosmossdk.io/x/gov v0.0.0-20231113122742-912390d5fc4a cosmossdk.io/x/mint v0.0.0-00010101000000-000000000000 cosmossdk.io/x/staking v0.0.0-00010101000000-000000000000 github.com/cockroachdb/apd/v3 v3.2.1 @@ -24,7 +25,6 @@ require ( github.com/cosmos/gogoproto v1.7.0 github.com/golang/protobuf v1.5.4 github.com/grpc-ecosystem/grpc-gateway v1.16.0 - github.com/manifoldco/promptui v0.9.0 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.10.0 go.uber.org/mock v0.5.0 @@ -120,6 +120,7 @@ require ( github.com/lib/pq v1.10.9 // indirect github.com/linxGnu/grocksdb v1.9.3 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/highwayhash v1.0.3 // indirect diff --git a/x/upgrade/go.mod b/x/upgrade/go.mod index fce45690b32e..215f62d2c4ec 100644 --- a/x/upgrade/go.mod +++ b/x/upgrade/go.mod @@ -10,7 +10,7 @@ require ( cosmossdk.io/errors v1.0.1 cosmossdk.io/log v1.5.0 cosmossdk.io/store v1.1.1-0.20240418092142-896cdf1971bc - cosmossdk.io/x/gov v0.0.0-20230925135524-a1bc045b3190 + cosmossdk.io/x/gov v0.0.0-20231113122742-912390d5fc4a github.com/cometbft/cometbft v1.0.0-rc2.0.20241127125717-4ce33b646ac9 github.com/cometbft/cometbft/api v1.0.0-rc2 github.com/cosmos/cosmos-proto v1.0.0-beta.5 @@ -40,6 +40,7 @@ require ( cloud.google.com/go/compute/metadata v0.5.0 // indirect cloud.google.com/go/iam v1.1.13 // indirect cloud.google.com/go/storage v1.43.0 // indirect + cosmossdk.io/client/v2 v2.0.0-beta.6 // indirect cosmossdk.io/collections v0.4.1-0.20241128094659-bd76b47e1d8b // indirect cosmossdk.io/math v1.4.0 // indirect cosmossdk.io/schema v0.3.1-0.20241128094659-bd76b47e1d8b // indirect @@ -206,6 +207,7 @@ replace github.com/cosmos/cosmos-sdk => ../../. replace ( cosmossdk.io/api => ../../api + cosmossdk.io/client/v2 => ../../client/v2 cosmossdk.io/store => ../../store cosmossdk.io/x/bank => ../bank cosmossdk.io/x/gov => ../gov From ab16339972fdb066658ef94d041cf96ec0583113 Mon Sep 17 00:00:00 2001 From: Julian Toledano Date: Thu, 5 Dec 2024 17:52:57 +0100 Subject: [PATCH 10/12] lint --- client/v2/autocli/prompt/validation_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/v2/autocli/prompt/validation_test.go b/client/v2/autocli/prompt/validation_test.go index a14faabfbdbc..04587c35bb9c 100644 --- a/client/v2/autocli/prompt/validation_test.go +++ b/client/v2/autocli/prompt/validation_test.go @@ -1,11 +1,14 @@ package prompt import ( - "cosmossdk.io/core/address" - address2 "github.com/cosmos/cosmos-sdk/codec/address" "testing" + "github.com/stretchr/testify/require" + + "cosmossdk.io/core/address" + + address2 "github.com/cosmos/cosmos-sdk/codec/address" ) func TestValidatePromptNotEmpty(t *testing.T) { From f4c80a60fdf326d509a27d0b413d0f38e4683345 Mon Sep 17 00:00:00 2001 From: Julian Toledano Date: Fri, 6 Dec 2024 11:54:15 +0100 Subject: [PATCH 11/12] move client validation --- client/prompt_validation.go | 28 --------------------- client/prompt_validation_test.go | 16 ------------ client/v2/autocli/prompt/message.go | 2 +- client/v2/autocli/prompt/validation.go | 23 +++++++++++++++++ client/v2/autocli/prompt/validation_test.go | 8 +++++- 5 files changed, 31 insertions(+), 46 deletions(-) delete mode 100644 client/prompt_validation.go delete mode 100644 client/prompt_validation_test.go diff --git a/client/prompt_validation.go b/client/prompt_validation.go deleted file mode 100644 index 3f2500548c31..000000000000 --- a/client/prompt_validation.go +++ /dev/null @@ -1,28 +0,0 @@ -package client - -import ( - "fmt" - "net/url" - "unicode" -) - -// ValidatePromptURL validates that the input is a valid URL. -func ValidatePromptURL(input string) error { - _, err := url.ParseRequestURI(input) - if err != nil { - return fmt.Errorf("invalid URL: %w", err) - } - - return nil -} - -// CamelCaseToString converts a camel case string to a string with spaces. -func CamelCaseToString(str string) string { - w := []rune(str) - for i := len(w) - 1; i > 1; i-- { - if unicode.IsUpper(w[i]) { - w = append(w[:i], append([]rune{' '}, w[i:]...)...) - } - } - return string(w) -} diff --git a/client/prompt_validation_test.go b/client/prompt_validation_test.go deleted file mode 100644 index 540a5c2e6529..000000000000 --- a/client/prompt_validation_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package client_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/cosmos/cosmos-sdk/client" -) - -func TestValidatePromptURL(t *testing.T) { - require := require.New(t) - - require.NoError(client.ValidatePromptURL("https://example.com")) - require.ErrorContains(client.ValidatePromptURL("foo"), "invalid URL") -} diff --git a/client/v2/autocli/prompt/message.go b/client/v2/autocli/prompt/message.go index a1e080a50e5d..c6e3738b8668 100644 --- a/client/v2/autocli/prompt/message.go +++ b/client/v2/autocli/prompt/message.go @@ -148,7 +148,7 @@ func valueOf(field protoreflect.FieldDescriptor, result string) (protoreflect.Va } } -// valueOf prompts the user for a comma-separated list of values for a repeated field. +// promptList prompts the user for a comma-separated list of values for a repeated field. // The user will be prompted to enter values separated by commas which will be parsed // according to the field's type using valueOf. func promptList(field protoreflect.FieldDescriptor, msg protoreflect.Message, promptUi promptui.Prompt, promptPrefix string) (protoreflect.List, error) { diff --git a/client/v2/autocli/prompt/validation.go b/client/v2/autocli/prompt/validation.go index f796035326c4..c189918434ab 100644 --- a/client/v2/autocli/prompt/validation.go +++ b/client/v2/autocli/prompt/validation.go @@ -3,6 +3,8 @@ package prompt import ( "errors" "fmt" + "net/url" + "unicode" "cosmossdk.io/core/address" ) @@ -27,3 +29,24 @@ func ValidateAddress(ac address.Codec) func(string) error { return nil } } + +// ValidatePromptURL validates that the input is a valid URL. +func ValidatePromptURL(input string) error { + _, err := url.ParseRequestURI(input) + if err != nil { + return fmt.Errorf("invalid URL: %w", err) + } + + return nil +} + +// CamelCaseToString converts a camel case string to a string with spaces. +func CamelCaseToString(str string) string { + w := []rune(str) + for i := len(w) - 1; i > 1; i-- { + if unicode.IsUpper(w[i]) { + w = append(w[:i], append([]rune{' '}, w[i:]...)...) + } + } + return string(w) +} diff --git a/client/v2/autocli/prompt/validation_test.go b/client/v2/autocli/prompt/validation_test.go index 04587c35bb9c..32b65b2c5b26 100644 --- a/client/v2/autocli/prompt/validation_test.go +++ b/client/v2/autocli/prompt/validation_test.go @@ -3,7 +3,6 @@ package prompt import ( "testing" - "github.com/stretchr/testify/require" "cosmossdk.io/core/address" @@ -47,3 +46,10 @@ func TestValidateAddress(t *testing.T) { }) } } + +func TestValidatePromptURL(t *testing.T) { + require := require.New(t) + + require.NoError(ValidatePromptURL("https://example.com")) + require.ErrorContains(ValidatePromptURL("foo"), "invalid URL") +} From 0b189db1932017e3b3af87ab4a015c83cf33d426 Mon Sep 17 00:00:00 2001 From: Julian Toledano Date: Wed, 11 Dec 2024 10:35:29 +0100 Subject: [PATCH 12/12] tidy --- client/v2/go.mod | 2 +- x/group/go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/v2/go.mod b/client/v2/go.mod index e35f80d6a186..825387886272 100644 --- a/client/v2/go.mod +++ b/client/v2/go.mod @@ -117,7 +117,7 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/linxGnu/grocksdb v1.9.3 // indirect - github.com/magiconair/properties v1.8.7 // indirect + github.com/magiconair/properties v1.8.9 // indirect github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/x/group/go.mod b/x/group/go.mod index e1a3c9140dbc..059b237bc831 100644 --- a/x/group/go.mod +++ b/x/group/go.mod @@ -119,8 +119,8 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/linxGnu/grocksdb v1.9.3 // indirect - github.com/manifoldco/promptui v0.9.0 // indirect github.com/magiconair/properties v1.8.9 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/highwayhash v1.0.3 // indirect