feat: Add CLI for new gov proposal (#11013)

* feat: Add CLI for new proposal

* Add changelog

* Add comment

* Add metadata

* Add tests for parsing

* Add CLI test

* Use legacy submit proposal in other modules

* Update x/gov/client/cli/tx.go

* Update x/gov/client/cli/tx.go

* Update x/gov/client/cli/tx.go

* Remove deprecated cobra field

* Update x/gov/client/cli/tx.go

Co-authored-by: atheeshp <59333759+atheeshp@users.noreply.github.com>
This commit is contained in:
Amaury 2022-01-31 22:19:20 +01:00 committed by GitHub
parent 24c97d529f
commit 62d97907e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 333 additions and 39 deletions

View File

@ -124,6 +124,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [\#9594](https://github.com/cosmos/cosmos-sdk/pull/9594) Remove legacy REST API. Please see the [REST Endpoints Migration guide](https://docs.cosmos.network/master/migrations/rest.html) to migrate to the new REST endpoints.
* [\#9995](https://github.com/cosmos/cosmos-sdk/pull/9995) Increased gas cost for creating proposals.
* [\#11013](https://github.com/cosmos/cosmos-sdk/pull/) The `tx gov submit-proposal` command has changed syntax to support the new Msg-based gov proposals. To access the old CLI command, please use `tx gov submit-legacy-proposal`.
### CLI Breaking Changes

View File

@ -1503,7 +1503,7 @@ func (s *IntegrationTestSuite) TestAuxSigner() {
for _, tc := range testCases {
tc := tc
s.Run(tc.name, func() {
_, err := govtestutil.MsgSubmitProposal(
_, err := govtestutil.MsgSubmitLegacyProposal(
val.ClientCtx,
val.Address.String(),
"test",
@ -1747,7 +1747,7 @@ func (s *IntegrationTestSuite) TestAuxToFee() {
for _, tc := range testCases {
tc := tc
s.Run(tc.name, func() {
res, err := govtestutil.MsgSubmitProposal(
res, err := govtestutil.MsgSubmitLegacyProposal(
val.ClientCtx,
tipper.String(),
"test",

View File

@ -54,7 +54,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
s.Require().NoError(err)
// create a proposal with deposit
_, err = govtestutil.MsgSubmitProposal(val.ClientCtx, val.Address.String(),
_, err = govtestutil.MsgSubmitLegacyProposal(val.ClientCtx, val.Address.String(),
"Text Proposal 1", "Where is the title!?", govv1beta1.ProposalTypeText,
fmt.Sprintf("--%s=%s", govcli.FlagDeposit, sdk.NewCoin(s.cfg.BondDenom, govv1beta2.DefaultMinDepositTokens).String()))
s.Require().NoError(err)

View File

@ -752,7 +752,7 @@ func (s *IntegrationTestSuite) TestTxWithFeeGrant() {
// granted fee allowance for an account which is not in state and creating
// any tx with it by using --fee-account shouldn't fail
out, err := govtestutil.MsgSubmitProposal(val.ClientCtx, grantee.String(),
out, err := govtestutil.MsgSubmitLegacyProposal(val.ClientCtx, grantee.String(),
"Text Proposal", "No desc", govv1beta1.ProposalTypeText,
fmt.Sprintf("--%s=%s", flags.FlagFeeGranter, granter.String()),
)
@ -892,7 +892,7 @@ func (s *IntegrationTestSuite) TestFilteredFeeAllowance() {
{
"valid proposal tx",
func() (testutil.BufferWriter, error) {
return govtestutil.MsgSubmitProposal(val.ClientCtx, grantee.String(),
return govtestutil.MsgSubmitLegacyProposal(val.ClientCtx, grantee.String(),
"Text Proposal", "No desc", govv1beta1.ProposalTypeText,
fmt.Sprintf("--%s=%s", flags.FlagFeeGranter, granter.String()),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(100))).String()),

View File

@ -7,11 +7,20 @@ import (
"github.com/spf13/pflag"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
govutils "github.com/cosmos/cosmos-sdk/x/gov/client/utils"
)
func parseSubmitProposalFlags(fs *pflag.FlagSet) (*proposal, error) {
proposal := &proposal{}
type legacyProposal struct {
Title string
Description string
Type string
Deposit string
}
func parseSubmitLegacyProposalFlags(fs *pflag.FlagSet) (*legacyProposal, error) {
proposal := &legacyProposal{}
proposalFile, _ := fs.GetString(FlagProposal)
if proposalFile == "" {
@ -42,3 +51,43 @@ func parseSubmitProposalFlags(fs *pflag.FlagSet) (*proposal, error) {
return proposal, nil
}
// proposal defines the new Msg-based proposal.
type proposal struct {
// Msgs defines an array of sdk.Msgs proto-JSON-encoded as Anys.
Messages []json.RawMessage
Metadata []byte
Deposit string
}
func parseSubmitProposal(cdc codec.Codec, path string) ([]sdk.Msg, []byte, sdk.Coins, error) {
var proposal proposal
contents, err := os.ReadFile(path)
if err != nil {
return nil, nil, nil, err
}
err = json.Unmarshal(contents, &proposal)
if err != nil {
return nil, nil, nil, err
}
msgs := make([]sdk.Msg, len(proposal.Messages))
for i, anyJSON := range proposal.Messages {
var msg sdk.Msg
err := cdc.UnmarshalInterfaceJSON(anyJSON, &msg)
if err != nil {
return nil, nil, nil, err
}
msgs[i] = msg
}
deposit, err := sdk.ParseCoinsNormalized(proposal.Deposit)
if err != nil {
return nil, nil, nil, err
}
return msgs, proposal.Metadata, deposit, nil
}

View File

@ -1,14 +1,24 @@
package cli
import (
"encoding/base64"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/testutil"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
"github.com/cosmos/cosmos-sdk/x/gov/types/v1beta2"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)
func TestParseSubmitProposalFlags(t *testing.T) {
func TestParseSubmitLegacyProposalFlags(t *testing.T) {
okJSON := testutil.WriteToNewTempFile(t, `
{
"title": "Test Proposal",
@ -19,21 +29,21 @@ func TestParseSubmitProposalFlags(t *testing.T) {
`)
badJSON := testutil.WriteToNewTempFile(t, "bad json")
fs := NewCmdSubmitProposal().Flags()
fs := NewCmdSubmitLegacyProposal().Flags()
// nonexistent json
fs.Set(FlagProposal, "fileDoesNotExist")
_, err := parseSubmitProposalFlags(fs)
_, err := parseSubmitLegacyProposalFlags(fs)
require.Error(t, err)
// invalid json
fs.Set(FlagProposal, badJSON.Name())
_, err = parseSubmitProposalFlags(fs)
_, err = parseSubmitLegacyProposalFlags(fs)
require.Error(t, err)
// ok json
fs.Set(FlagProposal, okJSON.Name())
proposal1, err := parseSubmitProposalFlags(fs)
proposal1, err := parseSubmitLegacyProposalFlags(fs)
require.Nil(t, err, "unexpected error")
require.Equal(t, "Test Proposal", proposal1.Title)
require.Equal(t, "My awesome proposal", proposal1.Description)
@ -43,7 +53,7 @@ func TestParseSubmitProposalFlags(t *testing.T) {
// flags that can't be used with --proposal
for _, incompatibleFlag := range ProposalFlags {
fs.Set(incompatibleFlag, "some value")
_, err := parseSubmitProposalFlags(fs)
_, err := parseSubmitLegacyProposalFlags(fs)
require.Error(t, err)
fs.Set(incompatibleFlag, "")
}
@ -54,7 +64,7 @@ func TestParseSubmitProposalFlags(t *testing.T) {
fs.Set(FlagDescription, proposal1.Description)
fs.Set(FlagProposalType, proposal1.Type)
fs.Set(FlagDeposit, proposal1.Deposit)
proposal2, err := parseSubmitProposalFlags(fs)
proposal2, err := parseSubmitLegacyProposalFlags(fs)
require.Nil(t, err, "unexpected error")
require.Equal(t, proposal1.Title, proposal2.Title)
@ -67,3 +77,83 @@ func TestParseSubmitProposalFlags(t *testing.T) {
err = badJSON.Close()
require.Nil(t, err, "unexpected error")
}
func TestParseSubmitProposal(t *testing.T) {
_, _, addr := testdata.KeyTestPubAddr()
interfaceRegistry := codectypes.NewInterfaceRegistry()
cdc := codec.NewProtoCodec(interfaceRegistry)
banktypes.RegisterInterfaces(interfaceRegistry)
stakingtypes.RegisterInterfaces(interfaceRegistry)
v1beta1.RegisterInterfaces(interfaceRegistry)
v1beta2.RegisterInterfaces(interfaceRegistry)
expectedMetadata := []byte{42}
okJSON := testutil.WriteToNewTempFile(t, fmt.Sprintf(`
{
"messages": [
{
"@type": "/cosmos.bank.v1beta1.MsgSend",
"from_address": "%s",
"to_address": "%s",
"amount":[{"denom": "stake","amount": "10"}]
},
{
"@type": "/cosmos.staking.v1beta1.MsgDelegate",
"delegator_address": "%s",
"validator_address": "%s",
"amount":{"denom": "stake","amount": "10"}
},
{
"@type": "/cosmos.gov.v1beta2.MsgExecLegacyContent",
"authority": "%s",
"content": {
"@type": "/cosmos.gov.v1beta1.TextProposal",
"title": "My awesome title",
"description": "My awesome description"
}
}
],
"metadata": "%s",
"deposit": "1000test"
}
`, addr, addr, addr, addr, addr, base64.StdEncoding.EncodeToString(expectedMetadata)))
badJSON := testutil.WriteToNewTempFile(t, "bad json")
// nonexistent json
_, _, _, err := parseSubmitProposal(cdc, "fileDoesNotExist")
require.Error(t, err)
// invalid json
_, _, _, err = parseSubmitProposal(cdc, badJSON.Name())
require.Error(t, err)
// ok json
msgs, metadata, deposit, err := parseSubmitProposal(cdc, okJSON.Name())
require.NoError(t, err, "unexpected error")
require.Equal(t, sdk.NewCoins(sdk.NewCoin("test", sdk.NewInt(1000))), deposit)
require.Equal(t, expectedMetadata, metadata)
require.Len(t, msgs, 3)
msg1, ok := msgs[0].(*banktypes.MsgSend)
require.True(t, ok)
require.Equal(t, addr.String(), msg1.FromAddress)
require.Equal(t, addr.String(), msg1.ToAddress)
require.Equal(t, sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(10))), msg1.Amount)
msg2, ok := msgs[1].(*stakingtypes.MsgDelegate)
require.True(t, ok)
require.Equal(t, addr.String(), msg2.DelegatorAddress)
require.Equal(t, addr.String(), msg2.ValidatorAddress)
require.Equal(t, sdk.NewCoin("stake", sdk.NewInt(10)), msg2.Amount)
msg3, ok := msgs[2].(*v1beta2.MsgExecLegacyContent)
require.True(t, ok)
require.Equal(t, addr.String(), msg3.Authority)
textProp, ok := msg3.Content.GetCachedValue().(*v1beta1.TextProposal)
require.True(t, ok)
require.Equal(t, "My awesome title", textProp.Title)
require.Equal(t, "My awesome description", textProp.Description)
err = okJSON.Close()
require.Nil(t, err, "unexpected error")
err = badJSON.Close()
require.Nil(t, err, "unexpected error")
}

View File

@ -20,23 +20,21 @@ import (
// Proposal flags
const (
FlagTitle = "title"
FlagDescription = "description"
// Deprecated: only used for v1beta1 legacy proposals.
FlagTitle = "title"
// Deprecated: only used for v1beta1 legacy proposals.
FlagDescription = "description"
// Deprecated: only used for v1beta1 legacy proposals.
FlagProposalType = "type"
// Deprecated: only used for v1beta1 legacy proposals.
FlagDeposit = "deposit"
flagVoter = "voter"
flagDepositor = "depositor"
flagStatus = "status"
FlagProposal = "proposal"
// Deprecated: only used for v1beta1 legacy proposals.
FlagProposal = "proposal"
)
type proposal struct {
Title string
Description string
Type string
Deposit string
}
// ProposalFlags defines the core required fields of a proposal. It is used to
// verify that these values are not provided in conjunction with a JSON proposal
// file.
@ -61,19 +59,18 @@ func NewTxCmd(propCmds []*cobra.Command) *cobra.Command {
RunE: client.ValidateCmd,
}
// TODO Add CLI for new submit proposal
// https://github.com/cosmos/cosmos-sdk/issues/10952
cmdSubmitProp := NewCmdSubmitProposal()
cmdSubmitLegacyProp := NewCmdSubmitLegacyProposal()
for _, propCmd := range propCmds {
flags.AddTxFlagsToCmd(propCmd)
cmdSubmitProp.AddCommand(propCmd)
cmdSubmitLegacyProp.AddCommand(propCmd)
}
govTxCmd.AddCommand(
NewCmdDeposit(),
NewCmdVote(),
NewCmdWeightedVote(),
cmdSubmitProp,
NewCmdSubmitProposal(),
cmdSubmitLegacyProp,
)
return govTxCmd
@ -83,13 +80,71 @@ func NewTxCmd(propCmds []*cobra.Command) *cobra.Command {
func NewCmdSubmitProposal() *cobra.Command {
cmd := &cobra.Command{
Use: "submit-proposal",
Short: "Submit a proposal along with an initial deposit",
Short: "Submit a proposal along with some messages and metadata",
Args: cobra.ExactArgs(1),
Long: strings.TrimSpace(
fmt.Sprintf(`Submit a proposal along with an initial deposit.
fmt.Sprintf(`Submit a proposal along with some messages and metadata.
Messages, metadata and deposit are defined in a JSON file.
Example:
$ %s tx gov submit-proposal path/to/proposal.json
Where proposal.json contains:
{
// array of proto-JSON-encoded sdk.Msgs
"messages": [
{
"@type": "/cosmos.bank.v1beta1.MsgSend",
"from_address": "cosmos1...",
"to_address": "cosmos1...",
"amount":[{"denom": "stake","amount": "10"}]
}
],
"metadata: "4pIMOgIGx1vZGU=", // base64-encoded metadata
"deposit": "10stake"
}
`,
version.AppName,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
msgs, metadata, deposit, err := parseSubmitProposal(clientCtx.Codec, args[0])
if err != nil {
return err
}
msg, err := v1beta2.NewMsgSubmitProposal(msgs, deposit, clientCtx.GetFromAddress().String(), metadata)
if err != nil {
return fmt.Errorf("invalid message: %w", err)
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}
flags.AddTxFlagsToCmd(cmd)
return cmd
}
// NewCmdSubmitLegacyProposal implements submitting a proposal transaction command.
// Deprecated: please use NewCmdSubmitProposal instead.
func NewCmdSubmitLegacyProposal() *cobra.Command {
cmd := &cobra.Command{
Use: "submit-legacy-proposal",
Short: "Submit a legacy proposal along with an initial deposit",
Long: strings.TrimSpace(
fmt.Sprintf(`Submit a legacy proposal along with an initial deposit.
Proposal title, description, type and deposit can be given directly or through a proposal JSON file.
Example:
$ %s tx gov submit-proposal --proposal="path/to/proposal.json" --from mykey
$ %s tx gov submit-legacy-proposal --proposal="path/to/proposal.json" --from mykey
Where proposal.json contains:
@ -102,7 +157,7 @@ Where proposal.json contains:
Which is equivalent to:
$ %s tx gov submit-proposal --title="Test Proposal" --description="My awesome proposal" --type="Text" --deposit="10test" --from mykey
$ %s tx gov submit-legacy-proposal --title="Test Proposal" --description="My awesome proposal" --type="Text" --deposit="10test" --from mykey
`,
version.AppName, version.AppName,
),
@ -113,7 +168,7 @@ $ %s tx gov submit-proposal --title="Test Proposal" --description="My awesome pr
return err
}
proposal, err := parseSubmitProposalFlags(cmd.Flags())
proposal, err := parseSubmitLegacyProposalFlags(cmd.Flags())
if err != nil {
return fmt.Errorf("failed to parse proposal: %w", err)
}

View File

@ -74,7 +74,7 @@ func (s *DepositTestSuite) createProposal(val *network.Validator, initialDeposit
exactArgs = append(exactArgs, fmt.Sprintf("--%s=%s", cli.FlagDeposit, initialDeposit.String()))
}
_, err := MsgSubmitProposal(
_, err := MsgSubmitLegacyProposal(
val.ClientCtx,
val.Address.String(),
fmt.Sprintf("Text Proposal %d", id),

View File

@ -31,6 +31,20 @@ func MsgSubmitProposal(clientCtx client.Context, from, title, description, propo
return clitestutil.ExecTestCLICmd(clientCtx, govcli.NewCmdSubmitProposal(), args)
}
// MsgSubmitLegacyProposal creates a tx for submit legacy proposal
func MsgSubmitLegacyProposal(clientCtx client.Context, from, title, description, proposalType string, extraArgs ...string) (testutil.BufferWriter, error) {
args := append([]string{
fmt.Sprintf("--%s=%s", govcli.FlagTitle, title),
fmt.Sprintf("--%s=%s", govcli.FlagDescription, description),
fmt.Sprintf("--%s=%s", govcli.FlagProposalType, proposalType),
fmt.Sprintf("--%s=%s", flags.FlagFrom, from),
}, commonArgs...)
args = append(args, extraArgs...)
return clitestutil.ExecTestCLICmd(clientCtx, govcli.NewCmdSubmitLegacyProposal(), args)
}
// MsgVote votes for a proposal
func MsgVote(clientCtx client.Context, from, id, vote string, extraArgs ...string) (testutil.BufferWriter, error) {
args := append([]string{

View File

@ -1,6 +1,7 @@
package testutil
import (
"encoding/base64"
"fmt"
"strings"
@ -15,7 +16,9 @@ import (
clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli"
"github.com/cosmos/cosmos-sdk/testutil/network"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/cosmos/cosmos-sdk/x/gov/client/cli"
"github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
"github.com/cosmos/cosmos-sdk/x/gov/types/v1beta2"
)
@ -44,7 +47,7 @@ func (s *IntegrationTestSuite) SetupSuite() {
val := s.network.Validators[0]
// create a proposal with deposit
_, err = MsgSubmitProposal(val.ClientCtx, val.Address.String(),
_, err = MsgSubmitLegacyProposal(val.ClientCtx, val.Address.String(),
"Text Proposal 1", "Where is the title!?", v1beta1.ProposalTypeText,
fmt.Sprintf("--%s=%s", cli.FlagDeposit, sdk.NewCoin(s.cfg.BondDenom, v1beta2.DefaultMinDepositTokens).String()))
s.Require().NoError(err)
@ -56,14 +59,14 @@ func (s *IntegrationTestSuite) SetupSuite() {
s.Require().NoError(err)
// create a proposal without deposit
_, err = MsgSubmitProposal(val.ClientCtx, val.Address.String(),
_, err = MsgSubmitLegacyProposal(val.ClientCtx, val.Address.String(),
"Text Proposal 2", "Where is the title!?", v1beta1.ProposalTypeText)
s.Require().NoError(err)
_, err = s.network.WaitForHeight(1)
s.Require().NoError(err)
// create a proposal3 with deposit
_, err = MsgSubmitProposal(val.ClientCtx, val.Address.String(),
_, err = MsgSubmitLegacyProposal(val.ClientCtx, val.Address.String(),
"Text Proposal 3", "Where is the title!?", v1beta1.ProposalTypeText,
fmt.Sprintf("--%s=%s", cli.FlagDeposit, sdk.NewCoin(s.cfg.BondDenom, v1beta2.DefaultMinDepositTokens).String()))
s.Require().NoError(err)
@ -278,6 +281,88 @@ func (s *IntegrationTestSuite) TestCmdTally() {
func (s *IntegrationTestSuite) TestNewCmdSubmitProposal() {
val := s.network.Validators[0]
// Create an legacy proposal JSON, make sure it doesn't pass this new CLI
// command.
invalidProp := `{
"title": "",
"description": "Where is the title!?",
"type": "Text",
"deposit": "-324foocoin"
}`
invalidPropFile := testutil.WriteToNewTempFile(s.T(), invalidProp)
// Create a valid new proposal JSON.
propMetadata := []byte{42}
validProp := fmt.Sprintf(`
{
"messages": [
{
"@type": "/cosmos.gov.v1beta2.MsgExecLegacyContent",
"authority": "%s",
"content": {
"@type": "/cosmos.gov.v1beta1.TextProposal",
"title": "My awesome title",
"description": "My awesome description"
}
}
],
"metadata": "%s",
"deposit": "%s"
}`, authtypes.NewModuleAddress(types.ModuleName), base64.StdEncoding.EncodeToString(propMetadata), sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(5431)))
validPropFile := testutil.WriteToNewTempFile(s.T(), validProp)
testCases := []struct {
name string
args []string
expectErr bool
expectedCode uint32
respType proto.Message
}{
{
"invalid proposal",
[]string{
invalidPropFile.Name(),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
},
true, 0, nil,
},
{
"valid proposal",
[]string{
validPropFile.Name(),
fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()),
fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation),
fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock),
fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()),
},
false, 0, &sdk.TxResponse{},
},
}
for _, tc := range testCases {
tc := tc
s.Run(tc.name, func() {
cmd := cli.NewCmdSubmitProposal()
clientCtx := val.ClientCtx
out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)
if tc.expectErr {
s.Require().Error(err)
} else {
s.Require().NoError(err)
s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())
txResp := tc.respType.(*sdk.TxResponse)
s.Require().Equal(tc.expectedCode, txResp.Code, out.String())
}
})
}
}
func (s *IntegrationTestSuite) TestNewCmdSubmitLegacyProposal() {
val := s.network.Validators[0]
invalidProp := `{
"title": "",
"description": "Where is the title!?",
@ -352,7 +437,7 @@ func (s *IntegrationTestSuite) TestNewCmdSubmitProposal() {
tc := tc
s.Run(tc.name, func() {
cmd := cli.NewCmdSubmitProposal()
cmd := cli.NewCmdSubmitLegacyProposal()
clientCtx := val.ClientCtx
out, err := clitestutil.ExecTestCLICmd(clientCtx, cmd, tc.args)