diff --git a/CHANGELOG.md b/CHANGELOG.md index f0bd8958e37e..b90d49eeb347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,7 +43,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features -* (cli) [#13304](https://github.com/cosmos/cosmos-sdk/pull/13304) Add `tx gov draft-proposal` command for generating proposal JSONs. +* (cli) [#13353](https://github.com/cosmos/cosmos-sdk/pull/13353) Add `tx group draft-proposal` command for generating group proposal JSONs (skeleton). +* (cli) [#13304](https://github.com/cosmos/cosmos-sdk/pull/13304) Add `tx gov draft-proposal` command for generating proposal JSONs (skeleton). * (x/authz) [#13047](https://github.com/cosmos/cosmos-sdk/pull/13047) Add a GetAuthorization function to the keeper. * (cli) [#12742](https://github.com/cosmos/cosmos-sdk/pull/12742) Add the `prune` CLI cmd to manually prune app store history versions based on the pruning options. @@ -389,8 +390,96 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Deprecated -* (x/upgrade) [\#9906](https://github.com/cosmos/cosmos-sdk/pull/9906) Deprecate `UpgradeConsensusState` gRPC query since this functionality is only used for IBC, which now has its own [IBC replacement](https://github.com/cosmos/ibc-go/blob/2c880a22e9f9cc75f62b527ca94aa75ce1106001/proto/ibc/core/client/v1/query.proto#L54) -* (types) [\#10948](https://github.com/cosmos/cosmos-sdk/issues/10948) Deprecate the types.DBBackend variable and types.NewLevelDB function. They are replaced by a new entry in `app.toml`: `app-db-backend` and `tendermint/tm-db`s `NewDB` function. If `app-db-backend` is defined, then it is used. Otherwise, if `types.DBBackend` is defined, it is used (until removed: [\#11241](https://github.com/cosmos/cosmos-sdk/issues/11241)). Otherwise, Tendermint config's `db-backend` is used. +* (x/upgrade) [#9906](https://github.com/cosmos/cosmos-sdk/pull/9906) Deprecate `UpgradeConsensusState` gRPC query since this functionality is only used for IBC, which now has its own [IBC replacement](https://github.com/cosmos/ibc-go/blob/2c880a22e9f9cc75f62b527ca94aa75ce1106001/proto/ibc/core/client/v1/query.proto#L54) +* (types) [#10948](https://github.com/cosmos/cosmos-sdk/issues/10948) Deprecate the types.DBBackend variable and types.NewLevelDB function. They are replaced by a new entry in `app.toml`: `app-db-backend` and `tendermint/tm-db`s `NewDB` function. If `app-db-backend` is defined, then it is used. Otherwise, if `types.DBBackend` is defined, it is used (until removed: [#11241](https://github.com/cosmos/cosmos-sdk/issues/11241)). Otherwise, Tendermint config's `db-backend` is used. + +## v0.45.8 - 2022-08-25 + +### Improvements + +* [#12981](https://github.com/cosmos/cosmos-sdk/pull/12981) Return proper error when parsing telemetry configuration. +* [#12885](https://github.com/cosmos/cosmos-sdk/pull/12885) Amortize cost of processing cache KV store. +* [#12970](https://github.com/cosmos/cosmos-sdk/pull/12970) Bump Tendermint to `v0.34.21` and IAVL to `v0.19.1`. +* [#12693](https://github.com/cosmos/cosmos-sdk/pull/12693) Make sure the order of each node is consistent when emitting proto events. + +### Bug Fixes + +* [#13046](https://github.com/cosmos/cosmos-sdk/pull/13046) Fix missing return statement in BaseApp.Query. + +## v0.45.7 - 2022-08-04 + +### Features + +* (upgrade) [#12603](https://github.com/cosmos/cosmos-sdk/pull/12603) feat: Move AppModule.BeginBlock and AppModule.EndBlock to extension interfaces + +### Improvements + +* (events) [#12850](https://github.com/cosmos/cosmos-sdk/pull/12850) Add a new `fee_payer` attribute to the `tx` event that is emitted from the `DeductFeeDecorator` AnteHandler decorator. +* (x/params) [#12724](https://github.com/cosmos/cosmos-sdk/pull/12724) Add `GetParamSetIfExists` function to params `Subspace` to prevent panics on breaking changes. +* [#12668](https://github.com/cosmos/cosmos-sdk/pull/12668) Add `authz_msg_index` event attribute to message events emitted when executing via `MsgExec` through `x/authz`. +* [#12697](https://github.com/cosmos/cosmos-sdk/pull/12697) Upgrade IAVL to v0.19.0 with fast index and error propagation. NOTE: first start will take a while to propagate into new model. + * Note: after upgrading to this version it may take up to 15 minutes to migrate from 0.17 to 0.19. This time is used to create the fast cache introduced into IAVL for performance +* [#12784](https://github.com/cosmos/cosmos-sdk/pull/12784) Upgrade Tendermint to 0.34.20. +* (x/bank) [#12674](https://github.com/cosmos/cosmos-sdk/pull/12674) Add convenience function `CreatePrefixedAccountStoreKey()` to construct key to access account's balance for a given denom. + +### Bug Fixes + +* (x/mint) [#12384](https://github.com/cosmos/cosmos-sdk/pull/12384) Ensure `GoalBonded` must be positive when performing `x/mint` parameter validation. +* (simapp) [#12437](https://github.com/cosmos/cosmos-sdk/pull/12437) fix the non-determinstic behavior in simulations caused by `GenTx` and check +empty coins slice before it is used to create `banktype.MsgSend`. +* (x/capability) [12818](https://github.com/cosmos/cosmos-sdk/pull/12818) Use fixed length hex for pointer at FwdCapabilityKey. + +## [v0.45.6](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.45.6) - 2022-06-28 + +### Improvements + +* (simapp) [#12314](https://github.com/cosmos/cosmos-sdk/pull/12314) Increase `DefaultGenTxGas` from `1000000` to `10000000` +* [#12371](https://github.com/cosmos/cosmos-sdk/pull/12371) Update min required Golang version to 1.18. + +### Bug Fixes + +* [#12317](https://github.com/cosmos/cosmos-sdk/pull/12317) Rename `edit-validator` command's `--moniker` flag to `--new-moniker` +* (x/upgrade) [#12264](https://github.com/cosmos/cosmos-sdk/pull/12264) Fix `GetLastCompleteUpgrade` to properly return the latest upgrade. +* (x/crisis) [#12208](https://github.com/cosmos/cosmos-sdk/pull/12208) Fix progress index of crisis invariant assertion logs. + +### Features + +* (query) [#12253](https://github.com/cosmos/cosmos-sdk/pull/12253) Add `GenericFilteredPaginate` to the `query` package to improve UX. + +## [v0.45.5](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.45.5) - 2022-06-09 + +### Improvements + +* (x/feegrant) [#11813](https://github.com/cosmos/cosmos-sdk/pull/11813) Fix pagination total count in `AllowancesByGranter` query. +* (errors) [#12002](https://github.com/cosmos/cosmos-sdk/pull/12002) Removed 'redacted' error message from defaultErrEncoder. +* (ante) [#12017](https://github.com/cosmos/cosmos-sdk/pull/12017) Index ante events for failed tx (backport #12013). +* [#12153](https://github.com/cosmos/cosmos-sdk/pull/12153) Add a new `NewSimulationManagerFromAppModules` constructor, to simplify simulation wiring. + +### Bug Fixes + +* [#11796](https://github.com/cosmos/cosmos-sdk/pull/11796) Handle EOF error case in `readLineFromBuf`, which allows successful reading of passphrases from STDIN. +* [#11772](https://github.com/cosmos/cosmos-sdk/pull/11772) Limit types.Dec length to avoid overflow. +* [#10947](https://github.com/cosmos/cosmos-sdk/pull/10947) Add `AllowancesByGranter` query to the feegrant module +* [#9639](https://github.com/cosmos/cosmos-sdk/pull/9639) Check store keys length before accessing them by making sure that `key` is of length `m+1` (for `key[n:m]`) +* [#11983](https://github.com/cosmos/cosmos-sdk/pull/11983) (x/feegrant, x/authz) rename grants query commands to `grants-by-grantee`, `grants-by-granter` cmds. + +## Improvements + +* [#11886](https://github.com/cosmos/cosmos-sdk/pull/11886) Improve error messages + +## [v0.45.4](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.45.4) - 2022-04-25 + +### Bug Fixes + +* [#11624](https://github.com/cosmos/cosmos-sdk/pull/11624) Handle the error returned from `NewNode` in the `server` package. +* [#11724](https://github.com/cosmos/cosmos-sdk/pull/11724) Fix data race issues with `api.Server`. + +### Improvements + +* (types) [#12201](https://github.com/cosmos/cosmos-sdk/pull/12201) Add `MustAccAddressFromBech32` util function +* [#11693](https://github.com/cosmos/cosmos-sdk/pull/11693) Add validation for gentx cmd. +* [#11686](https://github.com/cosmos/cosmos-sdk/pull/11686) Update the min required Golang version to `1.17`. +* (x/auth/vesting) [#11652](https://github.com/cosmos/cosmos-sdk/pull/11652) Add util functions for `Period(s)` ## [v0.45.3](https://github.com/cosmos/cosmos-sdk/releases/tag/v0.45.3) - 2022-04-12 diff --git a/types/tx_msg.go b/types/tx_msg.go index 42d01fdc55ee..61244be897ef 100644 --- a/types/tx_msg.go +++ b/types/tx_msg.go @@ -1,8 +1,12 @@ package types import ( + "encoding/json" + fmt "fmt" + "github.com/gogo/protobuf/proto" + "github.com/cosmos/cosmos-sdk/codec" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" ) @@ -79,3 +83,22 @@ type TxEncoder func(tx Tx) ([]byte, error) func MsgTypeURL(msg Msg) string { return "/" + proto.MessageName(msg) } + +// GetMsgFromTypeURL returns a `sdk.Msg` message type from a type URL +func GetMsgFromTypeURL(cdc codec.Codec, input string) (Msg, error) { + var msg Msg + bz, err := json.Marshal(struct { + Type string `json:"@type"` + }{ + Type: input, + }) + if err != nil { + return nil, err + } + + if err := cdc.UnmarshalInterfaceJSON(bz, &msg); err != nil { + return nil, fmt.Errorf("failed to determine sdk.Msg for %s URL : %w", input, err) + } + + return msg, nil +} diff --git a/types/tx_msg_test.go b/types/tx_msg_test.go index 7e72035d5175..0366d4fb14bc 100644 --- a/types/tx_msg_test.go +++ b/types/tx_msg_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/suite" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -33,3 +34,12 @@ func (s *testMsgSuite) TestMsg() { func (s *testMsgSuite) TestMsgTypeURL() { s.Require().Equal("/testdata.TestMsg", sdk.MsgTypeURL(new(testdata.TestMsg))) } + +func (s *testMsgSuite) TestGetMsgFromTypeURL() { + msg := new(testdata.TestMsg) + cdc := codec.NewProtoCodec(testdata.NewTestInterfaceRegistry()) + + result, err := sdk.GetMsgFromTypeURL(cdc, "/testdata.TestMsg") + s.Require().NoError(err) + s.Require().Equal(msg, result) +} diff --git a/x/gov/client/cli/prompt.go b/x/gov/client/cli/prompt.go index 3d3dd366d0ac..22201ebe8036 100644 --- a/x/gov/client/cli/prompt.go +++ b/x/gov/client/cli/prompt.go @@ -27,17 +27,6 @@ const ( draftMetadataFileName = "draft_metadata.json" ) -// ProposalMetadata is the metadata of a proposal -// This metadata is supposed to live off-chain when submitted in a proposal -type ProposalMetadata struct { - Title string `json:"title"` - Authors string `json:"authors"` - Summary string `json:"summary"` - Details string `json:"details"` - ProposalForumUrl string `json:"proposal_forum_url"` // named 'Url' instead of 'URL' for avoiding the camel case split - VoteOptionContext string `json:"vote_option_context"` -} - // Prompt prompts the user for all values of the given type. // data is the struct to be filled // namePrefix is the name to be display as "Enter " @@ -94,8 +83,17 @@ func Prompt[T any](data T, namePrefix string) (T, error) { case reflect.String: v.Field(i).SetString(result) case reflect.Int: - resultInt, _ := strconv.Atoi(result) - v.Field(i).SetInt(int64(resultInt)) + 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) default: // skip other types // possibly in the future we can add more types (like slices) @@ -106,21 +104,22 @@ func Prompt[T any](data T, namePrefix string) (T, error) { return data, nil } -type proposalTypes struct { - Type string +type proposalType struct { + Name string MsgType string Msg sdk.Msg } // Prompt the proposal type values and return the proposal and its metadata -func (p *proposalTypes) Prompt(cdc codec.Codec) (*proposal, ProposalMetadata, error) { +func (p *proposalType) Prompt(cdc codec.Codec) (*proposal, types.ProposalMetadata, error) { proposal := &proposal{} // set metadata - metadata, err := Prompt(ProposalMetadata{}, "proposal") + metadata, err := Prompt(types.ProposalMetadata{}, "proposal") if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal metadata: %w", err) } + // the metadata must be saved on IPFS, set placeholder proposal.Metadata = "ipfs://CID" // set deposit @@ -151,56 +150,38 @@ func (p *proposalTypes) Prompt(cdc codec.Codec) (*proposal, ProposalMetadata, er return proposal, metadata, nil } -var supportedProposalTypes = []proposalTypes{ +var suggestedProposalTypes = []proposalType{ { - Type: proposalText, + Name: proposalText, MsgType: "", // no message for text proposal }, { - Type: "software-upgrade", + Name: "software-upgrade", MsgType: "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", }, { - Type: "cancel-software-upgrade", + Name: "cancel-software-upgrade", MsgType: "/cosmos.upgrade.v1beta1.MsgCancelUpgrade", }, { - Type: proposalOther, + Name: proposalOther, MsgType: "", // user will input the message type }, } -func getProposalTypes() []string { - types := make([]string, len(supportedProposalTypes)) - for i, p := range supportedProposalTypes { - types[i] = p.Type +func getProposalSuggestions() []string { + types := make([]string, len(suggestedProposalTypes)) + for i, p := range suggestedProposalTypes { + types[i] = p.Name } return types } -func getProposalMsg(cdc codec.Codec, input string) (sdk.Msg, error) { - var msg sdk.Msg - bz, err := json.Marshal(struct { - Type string `json:"@type"` - }{ - Type: input, - }) - if err != nil { - return nil, err - } - - if err := cdc.UnmarshalInterfaceJSON(bz, &msg); err != nil { - return nil, fmt.Errorf("failed to determined sdk.Msg from %s proposal type : %w", input, err) - } - - return msg, nil -} - // NewCmdDraftProposal let a user generate a draft proposal. func NewCmdDraftProposal() *cobra.Command { cmd := &cobra.Command{ Use: "draft-proposal", - Short: "Generate a draft proposal json file. The generated proposal json contains only one message.", + Short: "Generate a draft proposal json file. The generated proposal json contains only one message (skeleton).", SilenceUsage: true, RunE: func(cmd *cobra.Command, _ []string) error { clientCtx, err := client.GetClientTxContext(cmd) @@ -211,24 +192,24 @@ func NewCmdDraftProposal() *cobra.Command { // prompt proposal type proposalTypesPrompt := promptui.Select{ Label: "Select proposal type", - Items: getProposalTypes(), + Items: getProposalSuggestions(), } - _, proposalType, err := proposalTypesPrompt.Run() + _, selectedProposalType, err := proposalTypesPrompt.Run() if err != nil { return fmt.Errorf("failed to prompt proposal types: %w", err) } - var proposal proposalTypes - for _, p := range supportedProposalTypes { - if strings.EqualFold(p.Type, proposalType) { + var proposal proposalType + for _, p := range suggestedProposalTypes { + if strings.EqualFold(p.Name, selectedProposalType) { proposal = p break } } // create any proposal type - if proposal.Type == proposalOther { + if proposal.Name == proposalOther { // prompt proposal type msgPrompt := promptui.Select{ Label: "Select proposal message type:", @@ -248,23 +229,23 @@ func NewCmdDraftProposal() *cobra.Command { } if proposal.MsgType != "" { - proposal.Msg, err = getProposalMsg(clientCtx.Codec, proposal.MsgType) + proposal.Msg, err = sdk.GetMsgFromTypeURL(clientCtx.Codec, proposal.MsgType) if err != nil { // should never happen panic(err) } } - prop, metadata, err := proposal.Prompt(clientCtx.Codec) + result, metadata, err := proposal.Prompt(clientCtx.Codec) if err != nil { return err } - if err := writeFile(draftMetadataFileName, metadata); err != nil { + if err := writeFile(draftProposalFileName, result); err != nil { return err } - if err := writeFile(draftProposalFileName, prop); err != nil { + if err := writeFile(draftMetadataFileName, metadata); err != nil { return err } diff --git a/x/gov/client/cli/prompt_test.go b/x/gov/client/cli/prompt_test.go new file mode 100644 index 000000000000..e5b01d7da37e --- /dev/null +++ b/x/gov/client/cli/prompt_test.go @@ -0,0 +1,88 @@ +//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" + + "github.com/cosmos/cosmos-sdk/x/gov/client/cli" +) + +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 + fw.Write([]byte(overflowStr + "\n")) + + v, err := cli.Prompt(st{}, "") + 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 { + tc := tc + 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 + fw.Write([]byte(tc.in + "\n")) + + 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) + }) + } +} diff --git a/x/gov/types/metadata.go b/x/gov/types/metadata.go new file mode 100644 index 000000000000..8b7b961f2229 --- /dev/null +++ b/x/gov/types/metadata.go @@ -0,0 +1,12 @@ +package types + +// ProposalMetadata is the metadata of a proposal +// This metadata is supposed to live off-chain when submitted in a proposal +type ProposalMetadata struct { + Title string `json:"title"` + Authors string `json:"authors"` + Summary string `json:"summary"` + Details string `json:"details"` + ProposalForumUrl string `json:"proposal_forum_url"` // named 'Url' instead of 'URL' for avoiding the camel case split + VoteOptionContext string `json:"vote_option_context"` +} diff --git a/x/group/client/cli/prompt.go b/x/group/client/cli/prompt.go new file mode 100644 index 000000000000..b8e446df56a9 --- /dev/null +++ b/x/group/client/cli/prompt.go @@ -0,0 +1,160 @@ +package cli + +import ( + "encoding/json" + "fmt" + "os" + "sort" + + "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" + govcli "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" +) + +const ( + proposalText = "text" + proposalOther = "other" + draftProposalFileName = "draft_group_proposal.json" + draftMetadataFileName = "draft_group_metadata.json" +) + +type proposalType struct { + Name string + Msg sdk.Msg +} + +// Prompt the proposal type values and return the proposal and its metadata +func (p *proposalType) Prompt(cdc codec.Codec) (*Proposal, govtypes.ProposalMetadata, error) { + proposal := &Proposal{} + + // set metadata + metadata, err := govcli.Prompt(govtypes.ProposalMetadata{}, "proposal") + if err != nil { + return nil, metadata, fmt.Errorf("failed to set proposal metadata: %w", err) + } + // the metadata must be saved on IPFS, set placeholder + proposal.Metadata = "ipfs://CID" + + // set group policy address + policyAddressPrompt := promptui.Prompt{ + Label: "Enter group policy address", + Validate: client.ValidatePromptAddress, + } + groupPolicyAddress, err := policyAddressPrompt.Run() + if err != nil { + return nil, metadata, fmt.Errorf("failed to set group policy address: %w", err) + } + proposal.GroupPolicyAddress = groupPolicyAddress + + if p.Msg == nil { + return proposal, metadata, nil + } + + // set messages field + result, err := govcli.Prompt(p.Msg, "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) + } + proposal.Messages = append(proposal.Messages, message) + return proposal, metadata, nil +} + +// NewCmdDraftProposal let a user generate a draft proposal. +func NewCmdDraftProposal() *cobra.Command { + cmd := &cobra.Command{ + Use: "draft-proposal", + Short: "Generate a draft proposal json file. The generated proposal json contains only one message (skeleton).", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, _ []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + // prompt proposal type + proposalTypesPrompt := promptui.Select{ + Label: "Select proposal type", + Items: []string{proposalText, proposalOther}, + } + + _, selectedProposalType, err := proposalTypesPrompt.Run() + if err != nil { + return fmt.Errorf("failed to prompt proposal types: %w", err) + } + + var proposal *proposalType + switch selectedProposalType { + case proposalText: + proposal = &proposalType{Name: proposalText} + 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() + if err != nil { + return fmt.Errorf("failed to prompt proposal types: %w", err) + } + + proposal.Msg, err = sdk.GetMsgFromTypeURL(clientCtx.Codec, result) + if err != nil { + // should never happen + panic(err) + } + default: + panic("unexpected proposal type") + } + + result, metadata, err := proposal.Prompt(clientCtx.Codec) + if err != nil { + return err + } + + if err := writeFile(draftProposalFileName, result); err != nil { + return err + } + + if err := writeFile(draftMetadataFileName, metadata); err != nil { + return err + } + + fmt.Printf("Your draft proposal has successfully been generated.\nProposals should contain off-chain metadata, please upload the metadata JSON to IPFS.\nThen, replace the generated metadata field with the IPFS CID.\n") + + return nil + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +func writeFile(fileName string, input any) error { + raw, err := json.MarshalIndent(input, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal proposal: %w", err) + } + + if err := os.WriteFile(fileName, raw, 0o600); err != nil { + return err + } + + return nil +} diff --git a/x/group/client/cli/tx.go b/x/group/client/cli/tx.go index 4d8c46628df8..54a7efb7c2a6 100644 --- a/x/group/client/cli/tx.go +++ b/x/group/client/cli/tx.go @@ -45,6 +45,7 @@ func TxCmd(name string) *cobra.Command { MsgVoteCmd(), MsgExecCmd(), MsgLeaveGroupCmd(), + NewCmdDraftProposal(), ) return txCmd diff --git a/x/group/client/cli/util.go b/x/group/client/cli/util.go index 4d9d8fef70f8..14592d988844 100644 --- a/x/group/client/cli/util.go +++ b/x/group/client/cli/util.go @@ -57,34 +57,34 @@ func execFromString(execStr string) group.Exec { return exec } -// CLIProposal defines a Msg-based group proposal for CLI purposes. -type CLIProposal struct { +// Proposal defines a Msg-based group proposal for CLI purposes. +type Proposal struct { GroupPolicyAddress string `json:"group_policy_address"` // Messages defines an array of sdk.Msgs proto-JSON-encoded as Anys. - Messages []json.RawMessage `json:"messages"` + Messages []json.RawMessage `json:"messages,omitempty"` Metadata string `json:"metadata"` - Proposers []string `json:"proposers"` + Proposers []string `json:"proposers,omitempty"` } -func getCLIProposal(path string) (CLIProposal, error) { +func getCLIProposal(path string) (Proposal, error) { contents, err := os.ReadFile(path) if err != nil { - return CLIProposal{}, err + return Proposal{}, err } return parseCLIProposal(contents) } -func parseCLIProposal(contents []byte) (CLIProposal, error) { - var p CLIProposal +func parseCLIProposal(contents []byte) (Proposal, error) { + var p Proposal if err := json.Unmarshal(contents, &p); err != nil { - return CLIProposal{}, err + return Proposal{}, err } return p, nil } -func parseMsgs(cdc codec.Codec, p CLIProposal) ([]sdk.Msg, error) { +func parseMsgs(cdc codec.Codec, p Proposal) ([]sdk.Msg, error) { msgs := make([]sdk.Msg, len(p.Messages)) for i, anyJSON := range p.Messages { var msg sdk.Msg diff --git a/x/group/client/testutil/tx.go b/x/group/client/testutil/tx.go index c345f21a759b..b4d5e433c1cc 100644 --- a/x/group/client/testutil/tx.go +++ b/x/group/client/testutil/tx.go @@ -2450,7 +2450,7 @@ func (s *IntegrationTestSuite) createCLIProposal(groupPolicyAddress, proposer, s msgJSON, err := s.cfg.Codec.MarshalInterfaceJSON(&msg) s.Require().NoError(err) - p := client.CLIProposal{ + p := client.Proposal{ GroupPolicyAddress: groupPolicyAddress, Messages: []json.RawMessage{msgJSON}, Metadata: metadata,