github.com/cosmos/cosmos-sdk@v0.50.10/x/group/client/cli/prompt.go (about) 1 package cli 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "sort" 8 9 "github.com/manifoldco/promptui" 10 "github.com/spf13/cobra" 11 12 "github.com/cosmos/cosmos-sdk/client" 13 "github.com/cosmos/cosmos-sdk/client/flags" 14 "github.com/cosmos/cosmos-sdk/codec" 15 sdk "github.com/cosmos/cosmos-sdk/types" 16 govcli "github.com/cosmos/cosmos-sdk/x/gov/client/cli" 17 govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" 18 ) 19 20 const ( 21 proposalText = "text" 22 proposalOther = "other" 23 draftProposalFileName = "draft_group_proposal.json" 24 draftMetadataFileName = "draft_group_metadata.json" 25 ) 26 27 type proposalType struct { 28 Name string 29 Msg sdk.Msg 30 } 31 32 // Prompt the proposal type values and return the proposal and its metadata. 33 func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool) (*Proposal, govtypes.ProposalMetadata, error) { 34 // set metadata 35 metadata, err := govcli.PromptMetadata(skipMetadata) 36 if err != nil { 37 return nil, metadata, fmt.Errorf("failed to set proposal metadata: %w", err) 38 } 39 40 proposal := &Proposal{ 41 Metadata: "ipfs://CID", // the metadata must be saved on IPFS, set placeholder 42 Title: metadata.Title, 43 Summary: metadata.Summary, 44 } 45 46 // set group policy address 47 policyAddressPrompt := promptui.Prompt{ 48 Label: "Enter group policy address", 49 Validate: client.ValidatePromptAddress, 50 } 51 groupPolicyAddress, err := policyAddressPrompt.Run() 52 if err != nil { 53 return nil, metadata, fmt.Errorf("failed to set group policy address: %w", err) 54 } 55 proposal.GroupPolicyAddress = groupPolicyAddress 56 57 // set proposer address 58 proposerPrompt := promptui.Prompt{ 59 Label: "Enter proposer address", 60 Validate: client.ValidatePromptAddress, 61 } 62 proposerAddress, err := proposerPrompt.Run() 63 if err != nil { 64 return nil, metadata, fmt.Errorf("failed to set proposer address: %w", err) 65 } 66 proposal.Proposers = []string{proposerAddress} 67 68 if p.Msg == nil { 69 return proposal, metadata, nil 70 } 71 72 // set messages field 73 result, err := govcli.Prompt(p.Msg, "msg") 74 if err != nil { 75 return nil, metadata, fmt.Errorf("failed to set proposal message: %w", err) 76 } 77 78 message, err := cdc.MarshalInterfaceJSON(result) 79 if err != nil { 80 return nil, metadata, fmt.Errorf("failed to marshal proposal message: %w", err) 81 } 82 proposal.Messages = append(proposal.Messages, message) 83 84 return proposal, metadata, nil 85 } 86 87 // NewCmdDraftProposal let a user generate a draft proposal. 88 func NewCmdDraftProposal() *cobra.Command { 89 flagSkipMetadata := "skip-metadata" 90 91 cmd := &cobra.Command{ 92 Use: "draft-proposal", 93 Short: "Generate a draft proposal json file. The generated proposal json contains only one message (skeleton).", 94 SilenceUsage: true, 95 RunE: func(cmd *cobra.Command, _ []string) error { 96 clientCtx, err := client.GetClientTxContext(cmd) 97 if err != nil { 98 return err 99 } 100 101 // prompt proposal type 102 proposalTypesPrompt := promptui.Select{ 103 Label: "Select proposal type", 104 Items: []string{proposalText, proposalOther}, 105 } 106 107 _, selectedProposalType, err := proposalTypesPrompt.Run() 108 if err != nil { 109 return fmt.Errorf("failed to prompt proposal types: %w", err) 110 } 111 112 var proposal *proposalType 113 switch selectedProposalType { 114 case proposalText: 115 proposal = &proposalType{Name: proposalText} 116 case proposalOther: 117 // prompt proposal type 118 proposal = &proposalType{Name: proposalOther} 119 msgPrompt := promptui.Select{ 120 Label: "Select proposal message type:", 121 Items: func() []string { 122 msgs := clientCtx.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName) 123 sort.Strings(msgs) 124 return msgs 125 }(), 126 } 127 128 _, result, err := msgPrompt.Run() 129 if err != nil { 130 return fmt.Errorf("failed to prompt proposal types: %w", err) 131 } 132 133 proposal.Msg, err = sdk.GetMsgFromTypeURL(clientCtx.Codec, result) 134 if err != nil { 135 // should never happen 136 panic(err) 137 } 138 default: 139 panic("unexpected proposal type") 140 } 141 142 skipMetadataPrompt, _ := cmd.Flags().GetBool(flagSkipMetadata) 143 144 result, metadata, err := proposal.Prompt(clientCtx.Codec, skipMetadataPrompt) 145 if err != nil { 146 return err 147 } 148 149 if err := writeFile(draftProposalFileName, result); err != nil { 150 return err 151 } 152 153 if !skipMetadataPrompt { 154 if err := writeFile(draftMetadataFileName, metadata); err != nil { 155 return err 156 } 157 } 158 159 cmd.Println("The 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.") 160 161 return nil 162 }, 163 } 164 165 flags.AddTxFlagsToCmd(cmd) 166 cmd.Flags().Bool(flagSkipMetadata, false, "skip metadata prompt") 167 168 return cmd 169 } 170 171 // writeFile writes the input to the file. 172 func writeFile(fileName string, input any) error { 173 raw, err := json.MarshalIndent(input, "", " ") 174 if err != nil { 175 return fmt.Errorf("failed to marshal proposal: %w", err) 176 } 177 178 if err := os.WriteFile(fileName, raw, 0o600); err != nil { 179 return err 180 } 181 182 return nil 183 }