253 lines
7.1 KiB
Go
253 lines
7.1 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
gogoproto "github.com/cosmos/gogoproto/proto"
|
|
"github.com/spf13/cobra"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/reflect/protoregistry"
|
|
|
|
"cosmossdk.io/client/v2/autocli/prompt"
|
|
"cosmossdk.io/core/address"
|
|
"cosmossdk.io/x/gov/types"
|
|
|
|
"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"
|
|
sdkaddress "github.com/cosmos/cosmos-sdk/types/address"
|
|
)
|
|
|
|
const (
|
|
proposalText = "text"
|
|
proposalOther = "other"
|
|
draftProposalFileName = "draft_proposal.json"
|
|
draftMetadataFileName = "draft_metadata.json"
|
|
)
|
|
|
|
var suggestedProposalTypes = []proposalType{
|
|
{
|
|
Name: proposalText,
|
|
MsgType: "", // no message for text proposal
|
|
},
|
|
{
|
|
Name: "community-pool-spend",
|
|
MsgType: "/cosmos.protocolpool.v1.MsgCommunityPoolSpend",
|
|
},
|
|
{
|
|
Name: "software-upgrade",
|
|
MsgType: "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade",
|
|
},
|
|
{
|
|
Name: "cancel-software-upgrade",
|
|
MsgType: "/cosmos.upgrade.v1beta1.MsgCancelUpgrade",
|
|
},
|
|
{
|
|
Name: "submit-budget-proposal",
|
|
MsgType: "/cosmos.protocolpool.v1.MsgSubmitBudgetProposal",
|
|
},
|
|
{
|
|
Name: "create-continuous-fund",
|
|
MsgType: "/cosmos.protocolpool.v1.MsgCreateContinuousFund",
|
|
},
|
|
{
|
|
Name: proposalOther,
|
|
MsgType: "", // user will input the message type
|
|
},
|
|
}
|
|
|
|
type proposalType struct {
|
|
Name string
|
|
MsgType string
|
|
Msg sdk.Msg
|
|
}
|
|
|
|
// Prompt the proposal type values and return the proposal and its metadata
|
|
func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec, validatorAddressCodec, consensusAddressCodec address.Codec) (*proposal, types.ProposalMetadata, error) {
|
|
metadata, err := PromptMetadata(skipMetadata)
|
|
if err != nil {
|
|
return nil, metadata, fmt.Errorf("failed to set proposal metadata: %w", err)
|
|
}
|
|
|
|
proposal := &proposal{
|
|
Metadata: "ipfs://CID", // the metadata must be saved on IPFS, set placeholder
|
|
Title: metadata.Title,
|
|
Summary: metadata.Summary,
|
|
}
|
|
|
|
// set deposit
|
|
proposal.Deposit, err = prompt.PromptString("Enter proposal deposit", ValidatePromptCoins)
|
|
if err != nil {
|
|
return nil, metadata, fmt.Errorf("failed to set proposal deposit: %w", err)
|
|
}
|
|
|
|
if p.Msg == nil {
|
|
return proposal, metadata, nil
|
|
}
|
|
|
|
// set messages field
|
|
msg, err := protoregistry.GlobalTypes.FindMessageByURL(p.MsgType)
|
|
if err != nil {
|
|
return nil, metadata, fmt.Errorf("failed to find proposal msg: %w", err)
|
|
}
|
|
newMsg := msg.New()
|
|
govAddr := sdkaddress.Module(types.ModuleName)
|
|
govAddrStr, err := addressCodec.BytesToString(govAddr)
|
|
if err != nil {
|
|
return nil, metadata, fmt.Errorf("failed to convert gov address to string: %w", err)
|
|
}
|
|
|
|
prompt.SetDefaults(newMsg, map[string]interface{}{"authority": govAddrStr})
|
|
result, err := prompt.PromptMessage(addressCodec, validatorAddressCodec, consensusAddressCodec, "msg", newMsg)
|
|
if err != nil {
|
|
return nil, metadata, fmt.Errorf("failed to set proposal message: %w", err)
|
|
}
|
|
|
|
// message must be converted to gogoproto so @type is not lost
|
|
resultBytes, err := proto.Marshal(result.Interface())
|
|
if err != nil {
|
|
return nil, metadata, fmt.Errorf("failed to marshal proposal message: %w", err)
|
|
}
|
|
|
|
err = gogoproto.Unmarshal(resultBytes, p.Msg)
|
|
if err != nil {
|
|
return nil, metadata, fmt.Errorf("failed to unmarshal proposal message: %w", err)
|
|
}
|
|
|
|
message, err := cdc.MarshalInterfaceJSON(p.Msg)
|
|
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
|
|
}
|
|
|
|
// getProposalSuggestions suggests a list of proposal types
|
|
func getProposalSuggestions() []string {
|
|
types := make([]string, len(suggestedProposalTypes))
|
|
for i, p := range suggestedProposalTypes {
|
|
types[i] = p.Name
|
|
}
|
|
return types
|
|
}
|
|
|
|
// PromptMetadata prompts for proposal metadata or only title and summary if skip is true
|
|
func PromptMetadata(skip bool) (types.ProposalMetadata, error) {
|
|
if !skip {
|
|
metadata, err := prompt.PromptStruct("proposal", types.ProposalMetadata{})
|
|
if err != nil {
|
|
return types.ProposalMetadata{}, err
|
|
}
|
|
|
|
return metadata, nil
|
|
}
|
|
|
|
title, err := prompt.PromptString("Enter proposal title", ValidatePromptNotEmpty)
|
|
if err != nil {
|
|
return types.ProposalMetadata{}, fmt.Errorf("failed to set proposal title: %w", err)
|
|
}
|
|
|
|
summary, err := prompt.PromptString("Enter proposal summary", ValidatePromptNotEmpty)
|
|
if err != nil {
|
|
return types.ProposalMetadata{}, fmt.Errorf("failed to set proposal summary: %w", err)
|
|
}
|
|
|
|
return types.ProposalMetadata{Title: title, Summary: summary}, nil
|
|
}
|
|
|
|
// NewCmdDraftProposal let a user generate a draft proposal.
|
|
func NewCmdDraftProposal() *cobra.Command {
|
|
flagSkipMetadata := "skip-metadata"
|
|
|
|
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
|
|
}
|
|
|
|
selectedProposalType, err := prompt.Select("Select proposal type", getProposalSuggestions())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to prompt proposal types: %w", err)
|
|
}
|
|
var proposal proposalType
|
|
for _, p := range suggestedProposalTypes {
|
|
if strings.EqualFold(p.Name, selectedProposalType) {
|
|
proposal = p
|
|
break
|
|
}
|
|
}
|
|
|
|
// create any proposal type
|
|
if proposal.Name == proposalOther {
|
|
msgs := clientCtx.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName)
|
|
sort.Strings(msgs)
|
|
|
|
result, err := prompt.Select("Select proposal message type:", msgs)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to prompt proposal types: %w", err)
|
|
}
|
|
|
|
proposal.MsgType = result
|
|
}
|
|
|
|
if proposal.MsgType != "" {
|
|
proposal.Msg, err = sdk.GetMsgFromTypeURL(clientCtx.Codec, proposal.MsgType)
|
|
if err != nil {
|
|
// should never happen
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
skipMetadataPrompt, _ := cmd.Flags().GetBool(flagSkipMetadata)
|
|
|
|
result, metadata, err := proposal.Prompt(clientCtx.Codec, skipMetadataPrompt, clientCtx.AddressCodec, clientCtx.ValidatorAddressCodec, clientCtx.ConsensusAddressCodec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := writeFile(draftProposalFileName, result); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !skipMetadataPrompt {
|
|
if err := writeFile(draftMetadataFileName, metadata); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
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.")
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
flags.AddTxFlagsToCmd(cmd)
|
|
cmd.Flags().Bool(flagSkipMetadata, false, "skip metadata prompt")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// writeFile writes the input to the file
|
|
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
|
|
}
|