Co-authored-by: Julián Toledano <JulianToledano@users.noreply.github.com> Co-authored-by: Julien Robert <julien@rbrt.fr>
This commit is contained in:
parent
721e83882c
commit
d7e7af42e2
@ -57,6 +57,10 @@ 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
|
||||
|
||||
Every module contains its own CHANGELOG.md. Please refer to the module you are interested in.
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"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")
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
259
client/v2/autocli/prompt/message.go
Normal file
259
client/v2/autocli/prompt/message.go
Normal file
@ -0,0 +1,259 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
|
||||
"cosmossdk.io/client/v2/autocli/flag"
|
||||
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, 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, 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++ {
|
||||
field := fields.Get(i)
|
||||
fieldName := string(field.Name())
|
||||
|
||||
promptUi := promptui.Prompt{
|
||||
Validate: 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.
|
||||
if strings.EqualFold(fieldName, flag.GetSignerFieldName(msg.Descriptor())) {
|
||||
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 = ValidateAddress(addressCodec)
|
||||
case flag.ValidatorAddressStringScalarType:
|
||||
promptUi.Validate = ValidateAddress(validatorAddressCodec)
|
||||
case flag.ConsensusAddressStringScalarType:
|
||||
promptUi.Validate = 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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, 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, consensusAddressCodec addresscodec.Codec,
|
||||
promptPrefix string, stdIn io.ReadCloser, msg protoreflect.Message,
|
||||
) error {
|
||||
fieldName := promptPrefix + "." + string(f.Name())
|
||||
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, 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
|
||||
}
|
||||
59
client/v2/autocli/prompt/message_test.go
Normal file
59
client/v2/autocli/prompt/message_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
|
||||
"cosmossdk.io/client/v2/internal/testpb"
|
||||
|
||||
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
|
||||
}{
|
||||
{
|
||||
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) {
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
134
client/v2/autocli/prompt/struct.go
Normal file
134
client/v2/autocli/prompt/struct.go
Normal file
@ -0,0 +1,134 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
)
|
||||
|
||||
// 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: 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
|
||||
}
|
||||
46
client/v2/autocli/prompt/struct_test.go
Normal file
46
client/v2/autocli/prompt/struct_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
70
client/v2/autocli/prompt/util.go
Normal file
70
client/v2/autocli/prompt/util.go
Normal file
@ -0,0 +1,70 @@
|
||||
package prompt
|
||||
|
||||
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,
|
||||
Items: options,
|
||||
}
|
||||
|
||||
_, selectedProposalType, err := selectUi.Run()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to prompt proposal types: %w", err)
|
||||
}
|
||||
|
||||
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,
|
||||
Validate: validate,
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package client
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"net/url"
|
||||
"unicode"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"cosmossdk.io/core/address"
|
||||
)
|
||||
|
||||
// ValidatePromptNotEmpty validates that the input is not empty.
|
||||
@ -18,6 +18,18 @@ func ValidatePromptNotEmpty(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 address")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ValidatePromptURL validates that the input is a valid URL.
|
||||
func ValidatePromptURL(input string) error {
|
||||
_, err := url.ParseRequestURI(input)
|
||||
@ -28,35 +40,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)
|
||||
55
client/v2/autocli/prompt/validation_test.go
Normal file
55
client/v2/autocli/prompt/validation_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cosmossdk.io/core/address"
|
||||
|
||||
address2 "github.com/cosmos/cosmos-sdk/codec/address"
|
||||
)
|
||||
|
||||
func TestValidatePromptNotEmpty(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
require.NoError(ValidatePromptNotEmpty("foo"))
|
||||
require.ErrorContains(ValidatePromptNotEmpty(""), "input cannot be empty")
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePromptURL(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
require.NoError(ValidatePromptURL("https://example.com"))
|
||||
require.ErrorContains(ValidatePromptURL("foo"), "invalid URL")
|
||||
}
|
||||
@ -119,7 +119,7 @@ require (
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/linxGnu/grocksdb v1.9.3 // indirect
|
||||
github.com/magiconair/properties v1.8.9 // 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
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
package prompt_test
|
||||
|
||||
import (
|
||||
"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")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
@ -4,7 +4,7 @@ go 1.23.3
|
||||
|
||||
require (
|
||||
cosmossdk.io/api v0.8.0 // main
|
||||
cosmossdk.io/client/v2 v2.0.0-20230630094428-02b760776860
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7
|
||||
cosmossdk.io/collections v0.4.1-0.20241209183624-332d0b106d1b // main
|
||||
cosmossdk.io/core v1.0.0-alpha.6 // main
|
||||
cosmossdk.io/core/testing v0.0.0-20241108153815-606544c7be7e // main
|
||||
|
||||
@ -4,7 +4,7 @@ go 1.23.3
|
||||
|
||||
require (
|
||||
cosmossdk.io/api v0.8.0 // main
|
||||
cosmossdk.io/client/v2 v2.0.0-00010101000000-000000000000
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7
|
||||
cosmossdk.io/core v1.0.0 // main
|
||||
cosmossdk.io/core/testing v0.0.0 // indirect; main
|
||||
cosmossdk.io/depinject v1.1.0
|
||||
|
||||
@ -69,7 +69,7 @@ require (
|
||||
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.8 // indirect
|
||||
cloud.google.com/go/storage v1.42.0 // indirect
|
||||
cosmossdk.io/client/v2 v2.0.0-20230630094428-02b760776860 // indirect
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7 // indirect
|
||||
cosmossdk.io/errors v1.0.1 // indirect
|
||||
cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 // indirect
|
||||
cosmossdk.io/indexer/postgres v0.0.0-20241128094659-bd76b47e1d8b // indirect
|
||||
|
||||
@ -12,7 +12,7 @@ require (
|
||||
cosmossdk.io/math v1.4.0
|
||||
cosmossdk.io/store v1.1.1-0.20240909133312-50288938d1b6
|
||||
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.52.0
|
||||
@ -29,6 +29,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7 // indirect
|
||||
github.com/bytedance/sonic v1.12.4 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
|
||||
@ -6,6 +6,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cosmossdk.io/api v0.7.3-0.20240924065902-eb7653cfecdf h1:CttA/mEIxGm4E7vwrjUpju7/Iespns08d9bOza70cIc=
|
||||
cosmossdk.io/api v0.7.3-0.20240924065902-eb7653cfecdf/go.mod h1:YMfx2ATpgITsoydD3hIBa8IkDHtyXp/14rmG0d3sEew=
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7 h1:sV7U1DpnWPAz9Z2Nz8019DIIw1Z+BjekEY1lLzrtL/w=
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7/go.mod h1:8PzpjDx0Wfe5T+r1HkAzaRNQCk/tQzG3ChK8YIq5ObA=
|
||||
cosmossdk.io/collections v0.4.1-0.20241209183624-332d0b106d1b h1:smupoVhpdK+5pztIylyIGkCc+0QaAaGLEvnM7Wnrq18=
|
||||
cosmossdk.io/collections v0.4.1-0.20241209183624-332d0b106d1b/go.mod h1:uf12i1yKvzEIHt2ok7poNqFDQTb71O00RQLitSynmrg=
|
||||
cosmossdk.io/core v1.0.0-alpha.6 h1:5ukC4JcQKmemLQXcAgu/QoOvJI50hpBkIIg4ZT2EN8E=
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -4,14 +4,15 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect" // #nosec
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"cosmossdk.io/x/gov/types"
|
||||
|
||||
@ -19,7 +20,7 @@ import (
|
||||
"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 (
|
||||
@ -60,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 <namePrefix> <field>"
|
||||
// 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
|
||||
@ -163,8 +68,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(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)
|
||||
}
|
||||
@ -176,11 +81,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", ValidatePromptCoins)
|
||||
if err != nil {
|
||||
return nil, metadata, fmt.Errorf("failed to set proposal deposit: %w", err)
|
||||
}
|
||||
@ -190,12 +91,35 @@ 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)
|
||||
}
|
||||
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 := 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)
|
||||
}
|
||||
@ -214,33 +138,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", 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", ValidatePromptNotEmpty)
|
||||
if err != nil {
|
||||
return types.ProposalMetadata{}, fmt.Errorf("failed to set proposal summary: %w", err)
|
||||
}
|
||||
@ -262,17 +175,10 @@ func NewCmdDraftProposal() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
// prompt proposal type
|
||||
proposalTypesPrompt := promptui.Select{
|
||||
Label: "Select proposal type",
|
||||
Items: getProposalSuggestions(),
|
||||
}
|
||||
|
||||
_, selectedProposalType, err := proposalTypesPrompt.Run()
|
||||
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) {
|
||||
@ -283,17 +189,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 +210,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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ go 1.23.3
|
||||
|
||||
require (
|
||||
cosmossdk.io/api v0.8.0 // main
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7
|
||||
cosmossdk.io/collections v0.4.1-0.20241209183624-332d0b106d1b // main
|
||||
cosmossdk.io/core v1.0.0-alpha.6 // main
|
||||
cosmossdk.io/core/testing v0.0.0-20241108153815-606544c7be7e // main
|
||||
@ -15,7 +16,7 @@ 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.52.0
|
||||
@ -23,7 +24,7 @@ require (
|
||||
github.com/golang/mock v1.6.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
|
||||
|
||||
@ -6,6 +6,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cosmossdk.io/api v0.7.3-0.20240924065902-eb7653cfecdf h1:CttA/mEIxGm4E7vwrjUpju7/Iespns08d9bOza70cIc=
|
||||
cosmossdk.io/api v0.7.3-0.20240924065902-eb7653cfecdf/go.mod h1:YMfx2ATpgITsoydD3hIBa8IkDHtyXp/14rmG0d3sEew=
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7 h1:sV7U1DpnWPAz9Z2Nz8019DIIw1Z+BjekEY1lLzrtL/w=
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7/go.mod h1:8PzpjDx0Wfe5T+r1HkAzaRNQCk/tQzG3ChK8YIq5ObA=
|
||||
cosmossdk.io/collections v0.4.1-0.20241209183624-332d0b106d1b h1:smupoVhpdK+5pztIylyIGkCc+0QaAaGLEvnM7Wnrq18=
|
||||
cosmossdk.io/collections v0.4.1-0.20241209183624-332d0b106d1b/go.mod h1:uf12i1yKvzEIHt2ok7poNqFDQTb71O00RQLitSynmrg=
|
||||
cosmossdk.io/core v1.0.0-alpha.6 h1:5ukC4JcQKmemLQXcAgu/QoOvJI50hpBkIIg4ZT2EN8E=
|
||||
|
||||
@ -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,14 +30,15 @@ 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, addressCodec)
|
||||
metadata, err := govcli.PromptMetadata(skipMetadata)
|
||||
if err != nil {
|
||||
return nil, metadata, fmt.Errorf("failed to set proposal metadata: %w", err)
|
||||
}
|
||||
@ -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", 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
|
||||
proposerPrompt := promptui.Prompt{
|
||||
Label: "Enter proposer address",
|
||||
Validate: client.ValidatePromptAddress,
|
||||
}
|
||||
proposerAddress, err := proposerPrompt.Run()
|
||||
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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -14,7 +14,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/v2 v2.0.2
|
||||
@ -24,7 +24,7 @@ require (
|
||||
github.com/golang/mock v1.6.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/stretchr/testify v1.10.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1
|
||||
@ -175,6 +175,8 @@ require (
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
require cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7
|
||||
|
||||
require (
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
|
||||
@ -6,6 +6,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cosmossdk.io/api v0.7.3-0.20240924065902-eb7653cfecdf h1:CttA/mEIxGm4E7vwrjUpju7/Iespns08d9bOza70cIc=
|
||||
cosmossdk.io/api v0.7.3-0.20240924065902-eb7653cfecdf/go.mod h1:YMfx2ATpgITsoydD3hIBa8IkDHtyXp/14rmG0d3sEew=
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7 h1:sV7U1DpnWPAz9Z2Nz8019DIIw1Z+BjekEY1lLzrtL/w=
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7/go.mod h1:8PzpjDx0Wfe5T+r1HkAzaRNQCk/tQzG3ChK8YIq5ObA=
|
||||
cosmossdk.io/collections v0.4.1-0.20241209183624-332d0b106d1b h1:smupoVhpdK+5pztIylyIGkCc+0QaAaGLEvnM7Wnrq18=
|
||||
cosmossdk.io/collections v0.4.1-0.20241209183624-332d0b106d1b/go.mod h1:uf12i1yKvzEIHt2ok7poNqFDQTb71O00RQLitSynmrg=
|
||||
cosmossdk.io/core v1.0.0-alpha.6 h1:5ukC4JcQKmemLQXcAgu/QoOvJI50hpBkIIg4ZT2EN8E=
|
||||
|
||||
@ -10,7 +10,7 @@ require (
|
||||
cosmossdk.io/errors v1.0.1
|
||||
cosmossdk.io/log v1.5.0
|
||||
cosmossdk.io/store v1.1.1-0.20240909133312-50288938d1b6
|
||||
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
|
||||
@ -199,6 +199,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
|
||||
@ -194,6 +194,8 @@ cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1V
|
||||
cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
|
||||
cosmossdk.io/api v0.7.3-0.20240924065902-eb7653cfecdf h1:CttA/mEIxGm4E7vwrjUpju7/Iespns08d9bOza70cIc=
|
||||
cosmossdk.io/api v0.7.3-0.20240924065902-eb7653cfecdf/go.mod h1:YMfx2ATpgITsoydD3hIBa8IkDHtyXp/14rmG0d3sEew=
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7 h1:sV7U1DpnWPAz9Z2Nz8019DIIw1Z+BjekEY1lLzrtL/w=
|
||||
cosmossdk.io/client/v2 v2.0.0-20241211112513-a4c34c41b4c7/go.mod h1:8PzpjDx0Wfe5T+r1HkAzaRNQCk/tQzG3ChK8YIq5ObA=
|
||||
cosmossdk.io/collections v0.4.1-0.20241209183624-332d0b106d1b h1:smupoVhpdK+5pztIylyIGkCc+0QaAaGLEvnM7Wnrq18=
|
||||
cosmossdk.io/collections v0.4.1-0.20241209183624-332d0b106d1b/go.mod h1:uf12i1yKvzEIHt2ok7poNqFDQTb71O00RQLitSynmrg=
|
||||
cosmossdk.io/core v1.0.0-alpha.6 h1:5ukC4JcQKmemLQXcAgu/QoOvJI50hpBkIIg4ZT2EN8E=
|
||||
|
||||
Loading…
Reference in New Issue
Block a user