diff --git a/client/prompt_validation.go b/client/prompt_validation.go deleted file mode 100644 index 288a1c95ef72..000000000000 --- a/client/prompt_validation.go +++ /dev/null @@ -1,68 +0,0 @@ -package client - -import ( - "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 fmt.Errorf("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 -} - -// 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) - 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 488aa03e5414..000000000000 --- a/client/prompt_validation_test.go +++ /dev/null @@ -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") -} diff --git a/client/v2/autocli/flag/builder.go b/client/v2/autocli/flag/builder.go index 2fe5eb72424f..9d64d699bbd3 100644 --- a/client/v2/autocli/flag/builder.go +++ b/client/v2/autocli/flag/builder.go @@ -28,7 +28,9 @@ const ( AddressStringScalarType = "cosmos.AddressString" ValidatorAddressStringScalarType = "cosmos.ValidatorAddressString" ConsensusAddressStringScalarType = "cosmos.ConsensusAddressString" - PubkeyScalarType = "cosmos.Pubkey" + + CoinScalarType = "cosmos.base.v1beta1.Coin" + PubkeyScalarType = "cosmos.Pubkey" ) // Builder manages options for building pflag flags for protobuf messages. @@ -64,7 +66,7 @@ func (b *Builder) init() { b.messageFlagTypes = map[protoreflect.FullName]Type{} b.messageFlagTypes["google.protobuf.Timestamp"] = timestampType{} b.messageFlagTypes["google.protobuf.Duration"] = durationType{} - b.messageFlagTypes["cosmos.base.v1beta1.Coin"] = coinType{} + b.messageFlagTypes[CoinScalarType] = coinType{} } if b.scalarFlagTypes == nil { diff --git a/client/v2/autocli/flag/coin.go b/client/v2/autocli/flag/coin.go index 8496a2b0f656..060eb6e99cfc 100644 --- a/client/v2/autocli/flag/coin.go +++ b/client/v2/autocli/flag/coin.go @@ -50,5 +50,5 @@ func (c *coinValue) Set(stringValue string) error { } func (c *coinValue) Type() string { - return "cosmos.base.v1beta1.Coin" + return CoinScalarType } diff --git a/client/v2/autocli/prompt/prompt.go b/client/v2/autocli/prompt/prompt.go new file mode 100644 index 000000000000..53f64c5e3cfc --- /dev/null +++ b/client/v2/autocli/prompt/prompt.go @@ -0,0 +1,193 @@ +package prompt + +import ( + "fmt" + "reflect" + "strconv" + "strings" + + "cosmossdk.io/client/v2/autocli/flag" + addresscodec "cosmossdk.io/core/address" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/cosmos/cosmos-sdk/types/address" + "github.com/manifoldco/promptui" +) + +const GovModuleName = "gov" + +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 prompts + prompt := promptui.Prompt{ + Label: fmt.Sprintf("Enter %s %s", promptPrefix, fieldName), + Validate: ValidatePromptNotEmpty, + } + + // signer field + if strings.EqualFold(fieldName, flag.GetSignerFieldName(msg.Descriptor())) { + // pre-fill with gov address + govAddr := address.Module(GovModuleName) + govAddrStr, err := addressCodec.BytesToString(govAddr) + if err != nil { + return msg, fmt.Errorf("failed to convert gov address to string: %w", err) + } + + // note, we don't set prompt.Validate here because we need to get the scalar annotation + prompt.Default = govAddrStr + } + + // validate address fields + scalarField, ok := flag.GetScalarType(field) + if ok { + switch scalarField { + case flag.AddressStringScalarType: + prompt.Validate = func(input string) error { + if _, err := addressCodec.StringToBytes(input); err != nil { + return fmt.Errorf("invalid address") + } + + return nil + } + case flag.ValidatorAddressStringScalarType: + prompt.Validate = func(input string) error { + if _, err := validatorAddressCodec.StringToBytes(input); err != nil { + return fmt.Errorf("invalid validator address") + } + + return nil + } + case flag.ConsensusAddressStringScalarType: + prompt.Validate = func(input string) error { + if _, err := consensusAddressCodec.StringToBytes(input); err != nil { + return fmt.Errorf("invalid consensus address") + } + + return nil + } + case flag.CoinScalarType: + prompt.Validate = ValidatePromptCoins + default: + // prompt.Validate = ValidatePromptNotEmpty (we possibly don't want to force all fields to be non-empty) + prompt.Validate = nil + } + } + + result, err := prompt.Run() + if err != nil { + return msg, fmt.Errorf("failed to prompt for %s: %w", fieldName, err) + } + + switch field.Kind() { + case protoreflect.StringKind: + msg.Set(field, protoreflect.ValueOfString(result)) + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind, protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + resultUint, err := strconv.ParseUint(result, 10, 0) + if err != nil { + return msg, fmt.Errorf("invalid value for int: %w", err) + } + + msg.Set(field, protoreflect.ValueOfUint64(resultUint)) + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind, protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + resultInt, err := strconv.ParseInt(result, 10, 0) + if err != nil { + return msg, 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 + msg.Set(field, protoreflect.ValueOfInt64(resultInt)) + case protoreflect.BoolKind: + resultBool, err := strconv.ParseBool(result) + if err != nil { + return msg, fmt.Errorf("invalid value for bool: %w", err) + } + + msg.Set(field, protoreflect.ValueOfBool(resultBool)) + case protoreflect.MessageKind: + // TODO + default: + // skip any other types + continue // TODO(@julienrbrt) add support for other types + } + } + + return msg, nil +} + +func PromptStruct[T any](promptPrefix string, data T) (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", promptPrefix, strings.Title(v.Type().Field(i).Name)), // nolint:staticcheck // strings.Title has a better API + Validate: ValidatePromptNotEmpty, + } + + fieldName := strings.ToLower(v.Type().Field(i).Name) + + 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) + } + 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 +} diff --git a/x/gov/client/cli/prompt_test.go b/client/v2/autocli/prompt/prompt_test.go similarity index 66% rename from x/gov/client/cli/prompt_test.go rename to client/v2/autocli/prompt/prompt_test.go index cfb23272a184..f23c1c796a9f 100644 --- a/x/gov/client/cli/prompt_test.go +++ b/client/v2/autocli/prompt/prompt_test.go @@ -5,7 +5,7 @@ // has a data race and this code exposes it, but fixing it would require // holding up the associated change to this. -package cli_test +package prompt_test import ( "fmt" @@ -14,16 +14,12 @@ import ( "testing" "github.com/chzyer/readline" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "cosmossdk.io/x/gov/client/cli" + "cosmossdk.io/client/v2/autocli/prompt" + "cosmossdk.io/client/v2/internal/testpb" ) -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) { @@ -48,10 +44,10 @@ func TestPromptIntegerOverflow(t *testing.T) { fin, fw := readline.NewFillableStdin(os.Stdin) readline.Stdin = fin _, err := fw.Write([]byte(overflowStr + "\n")) - assert.NoError(t, err) + require.NoError(t, err) - v, err := cli.Prompt(st{}, "") - assert.Equal(t, st{}, v, "expected a value of zero") + v, err := prompt.Prompt(mockAddressCodec{}, mockAddressCodec{}, mockAddressCodec{}, "", (&testpb.MsgRequest{}).ProtoReflect()) + require.Equal(t, (&testpb.MsgRequest{}).ProtoReflect(), v, "expected a value of zero") require.NotNil(t, err, "expected a report of an overflow") require.Contains(t, err.Error(), "range") }) @@ -80,10 +76,21 @@ func TestPromptParseInteger(t *testing.T) { 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{}, "") - assert.Nil(t, err, "expected a nil error") - assert.Equal(t, tc.want, v.I, "expected %d = %d", tc.want, v.I) + require.NoError(t, err) + v, err := prompt.Prompt(mockAddressCodec{}, mockAddressCodec{}, mockAddressCodec{}, "", (&testpb.MsgRequest{}).ProtoReflect()) + require.Nil(t, err, "expected a nil error") + require.NotNil(t, v) + // require.Equal(t, tc.want, v.I, "expected %d = %d", tc.want, v.I) }) } } + +type mockAddressCodec struct{} + +func (mockAddressCodec) BytesToString([]byte) (string, error) { + return "cosmos1y74p8wyy4enfhfn342njve6cjmj5c8dtl6emdk", nil +} + +func (mockAddressCodec) StringToBytes(string) ([]byte, error) { + return nil, nil +} 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 94% rename from client/v2/internal/prompt/validation_test.go rename to client/v2/autocli/prompt/validation_test.go index 86e4ba4ab475..1cb41c3429ff 100644 --- a/client/v2/internal/prompt/validation_test.go +++ b/client/v2/autocli/prompt/validation_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "cosmossdk.io/client/v2/internal/prompt" + "cosmossdk.io/client/v2/autocli/prompt" ) func TestValidatePromptNotEmpty(t *testing.T) { diff --git a/client/v2/go.mod b/client/v2/go.mod index be9bd4b32bee..83ccafab901b 100644 --- a/client/v2/go.mod +++ b/client/v2/go.mod @@ -9,11 +9,11 @@ require ( cosmossdk.io/x/bank v0.0.0-00010101000000-000000000000 cosmossdk.io/x/gov v0.0.0-20231113122742-912390d5fc4a cosmossdk.io/x/tx v0.13.0 - github.com/chzyer/readline v1.5.1 // indirect + github.com/chzyer/readline v1.5.1 github.com/cockroachdb/errors v1.11.1 github.com/cosmos/cosmos-proto v1.0.0-beta.3 github.com/cosmos/cosmos-sdk v0.51.0 - github.com/manifoldco/promptui v0.9.0 // indirect + github.com/manifoldco/promptui v0.9.0 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 google.golang.org/grpc v1.61.0 diff --git a/x/gov/client/cli/prompt.go b/x/gov/client/cli/prompt.go index 8ab8a823cdcb..19148de55f93 100644 --- a/x/gov/client/cli/prompt.go +++ b/x/gov/client/cli/prompt.go @@ -3,17 +3,19 @@ package cli import ( "encoding/json" "fmt" - "os" - "reflect" // #nosec + "os" // #nosec "sort" - "strconv" "strings" + gogoproto "github.com/cosmos/gogoproto/proto" "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "google.golang.org/protobuf/reflect/protoregistry" - authtypes "cosmossdk.io/x/auth/types" + "cosmossdk.io/client/v2/autocli/prompt" // TODO to delete this dependency + "cosmossdk.io/core/address" "cosmossdk.io/x/gov/types" + "cosmossdk.io/x/tx/signing/aminojson" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" @@ -59,107 +61,15 @@ 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) (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 - prompt.Default = authtypes.NewModuleAddress(types.ModuleName).String() - 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 - Msg sdk.Msg + Msg gogoproto.Message } // Prompt the proposal type values and return the proposal and its metadata -func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool) (*proposal, types.ProposalMetadata, error) { - metadata, err := PromptMetadata(skipMetadata) +func (p *proposalType) Prompt(addressCodec address.Codec, validatorAddressCodec address.Codec, consensusAddressCodec address.Codec, cdc codec.Codec, skipMetadata bool) (*proposal, types.ProposalMetadata, error) { + metadata, err := PromptMetadata(addressCodec, skipMetadata) if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal metadata: %w", err) } @@ -173,7 +83,7 @@ func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool) (*proposal, ty // set deposit depositPrompt := promptui.Prompt{ Label: "Enter proposal deposit", - Validate: client.ValidatePromptCoins, + Validate: prompt.ValidatePromptCoins, } proposal.Deposit, err = depositPrompt.Run() if err != nil { @@ -185,16 +95,22 @@ func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool) (*proposal, ty } // set messages field - result, err := Prompt(p.Msg, "msg") + msg, err := prompt.Prompt(addressCodec, validatorAddressCodec, consensusAddressCodec, "msg", p.Msg) if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal message: %w", err) } - message, err := cdc.MarshalInterfaceJSON(result) - if err != nil { - return nil, metadata, fmt.Errorf("failed to marshal proposal message: %w", err) + // setup encoder + encoderOptions := aminojson.EncoderOptions{ + Indent: " ", + DoNotSortFields: true, + TypeResolver: protoregistry.GlobalTypes, + FileResolver: cdc.InterfaceRegistry(), } - proposal.Messages = append(proposal.Messages, message) + enc := aminojson.NewEncoder(encoderOptions) + bz, err := enc.Marshal(msg.Interface()) + + proposal.Messages = append(proposal.Messages, bz) return proposal, metadata, nil } @@ -209,9 +125,9 @@ func getProposalSuggestions() []string { } // PromptMetadata prompts for proposal metadata or only title and summary if skip is true -func PromptMetadata(skip bool) (types.ProposalMetadata, error) { +func PromptMetadata(addressCodec address.Codec, skip bool) (types.ProposalMetadata, error) { if !skip { - metadata, err := Prompt(types.ProposalMetadata{}, "proposal") + metadata, err := prompt.PromptStruct("proposal", types.ProposalMetadata{}) if err != nil { return metadata, fmt.Errorf("failed to set proposal metadata: %w", err) } @@ -222,7 +138,7 @@ func PromptMetadata(skip bool) (types.ProposalMetadata, error) { // prompt for title and summary titlePrompt := promptui.Prompt{ Label: "Enter proposal title", - Validate: client.ValidatePromptNotEmpty, + Validate: prompt.ValidatePromptNotEmpty, } title, err := titlePrompt.Run() @@ -232,7 +148,7 @@ func PromptMetadata(skip bool) (types.ProposalMetadata, error) { summaryPrompt := promptui.Prompt{ Label: "Enter proposal summary", - Validate: client.ValidatePromptNotEmpty, + Validate: prompt.ValidatePromptNotEmpty, } summary, err := summaryPrompt.Run() @@ -306,7 +222,7 @@ func NewCmdDraftProposal() *cobra.Command { skipMetadataPrompt, _ := cmd.Flags().GetBool(flagSkipMetadata) - result, metadata, err := proposal.Prompt(clientCtx.Codec, skipMetadataPrompt) + result, metadata, err := proposal.Prompt(clientCtx.AddressCodec, clientCtx.ValidatorAddressCodec, clientCtx.ConsensusAddressCodec, clientCtx.Codec, skipMetadataPrompt) if err != nil { return err } diff --git a/x/gov/go.mod b/x/gov/go.mod index d79672ad4b50..97ac29a8a40f 100644 --- a/x/gov/go.mod +++ b/x/gov/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( cosmossdk.io/api v0.7.3-0.20231113122742-912390d5fc4a + cosmossdk.io/client/v2 v2.0.0-00010101000000-000000000000 cosmossdk.io/collections v0.4.0 cosmossdk.io/core v0.12.1-0.20231114100755-569e3ff6a0d7 cosmossdk.io/depinject v1.0.0-alpha.4 @@ -168,6 +169,7 @@ replace github.com/cosmos/cosmos-sdk => ../../. replace ( cosmossdk.io/api => ../../api + cosmossdk.io/client/v2 => ../../client/v2 cosmossdk.io/depinject => ../../depinject cosmossdk.io/x/auth => ../auth cosmossdk.io/x/bank => ../bank