feat: add draft-proposal for x/group (#13353)
* feat: add `draft-proposal` for x/group * add changelog * extract useful function * add `GetMsgFromTypeURL` tests
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -45,6 +45,7 @@ func TxCmd(name string) *cobra.Command {
|
||||
MsgVoteCmd(),
|
||||
MsgExecCmd(),
|
||||
MsgLeaveGroupCmd(),
|
||||
NewCmdDraftProposal(),
|
||||
)
|
||||
|
||||
return txCmd
|
||||
|
||||
@@ -61,9 +61,9 @@ func execFromString(execStr string) group.Exec {
|
||||
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) (Proposal, error) {
|
||||
|
||||
Reference in New Issue
Block a user