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  }