279 lines
7.2 KiB
Go
279 lines
7.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"reflect" // #nosec
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/manifoldco/promptui"
|
|
"github.com/spf13/cobra"
|
|
|
|
"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"
|
|
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
|
"github.com/cosmos/cosmos-sdk/x/gov/types"
|
|
)
|
|
|
|
const (
|
|
proposalText = "text"
|
|
proposalOther = "other"
|
|
draftProposalFileName = "draft_proposal.json"
|
|
draftMetadataFileName = "draft_metadata.json"
|
|
)
|
|
|
|
// Prompt prompts the user for all values of the given type.
|
|
// data is the struct to be filled
|
|
// namePrefix is the name to be display as "Enter <namePrefix> <field>"
|
|
func Prompt[T any](data T, namePrefix string) (T, error) {
|
|
v := reflect.ValueOf(&data).Elem()
|
|
if v.Kind() == reflect.Interface {
|
|
v = reflect.ValueOf(data)
|
|
if v.Kind() == reflect.Ptr {
|
|
v = v.Elem()
|
|
}
|
|
}
|
|
|
|
for i := 0; i < v.NumField(); i++ {
|
|
if v.Field(i).Kind() == reflect.Struct || v.Field(i).Kind() == reflect.Slice {
|
|
// if the field is a struct skip
|
|
// in a future we can add a recursive call to Prompt
|
|
continue
|
|
}
|
|
|
|
// create prompts
|
|
prompt := promptui.Prompt{
|
|
Label: fmt.Sprintf("Enter %s %s", namePrefix, strings.ToLower(client.CamelCaseToString(v.Type().Field(i).Name))),
|
|
Validate: client.ValidatePromptNotEmpty,
|
|
}
|
|
|
|
fieldName := strings.ToLower(v.Type().Field(i).Name)
|
|
// validation per field name
|
|
if strings.Contains(fieldName, "url") {
|
|
prompt.Validate = client.ValidatePromptURL
|
|
}
|
|
|
|
if strings.EqualFold(fieldName, "authority") {
|
|
// pre-fill with gov address
|
|
prompt.Default = authtypes.NewModuleAddress(types.ModuleName).String()
|
|
prompt.Validate = client.ValidatePromptAddress
|
|
}
|
|
|
|
if strings.Contains(fieldName, "addr") ||
|
|
strings.Contains(fieldName, "sender") ||
|
|
strings.Contains(fieldName, "voter") ||
|
|
strings.Contains(fieldName, "depositor") ||
|
|
strings.Contains(fieldName, "granter") ||
|
|
strings.Contains(fieldName, "grantee") ||
|
|
strings.Contains(fieldName, "recipient") {
|
|
prompt.Validate = client.ValidatePromptAddress
|
|
}
|
|
|
|
result, err := prompt.Run()
|
|
if err != nil {
|
|
return data, fmt.Errorf("failed to prompt for %s: %w", fieldName, err)
|
|
}
|
|
|
|
switch v.Field(i).Kind() {
|
|
case reflect.String:
|
|
v.Field(i).SetString(result)
|
|
case reflect.Int:
|
|
resultInt, err := strconv.ParseInt(result, 10, 0)
|
|
if err != nil {
|
|
return data, fmt.Errorf("invalid value for int: %w", err)
|
|
}
|
|
// If a value was successfully parsed the ranges of:
|
|
// [minInt, maxInt]
|
|
// are within the ranges of:
|
|
// [minInt64, maxInt64]
|
|
// of which on 64-bit machines, which are most common,
|
|
// int==int64
|
|
v.Field(i).SetInt(resultInt)
|
|
default:
|
|
// skip other types
|
|
// possibly in the future we can add more types (like slices)
|
|
continue
|
|
}
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
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) (*proposal, types.ProposalMetadata, error) {
|
|
proposal := &proposal{}
|
|
|
|
// set metadata
|
|
metadata, err := Prompt(types.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 deposit
|
|
depositPrompt := promptui.Prompt{
|
|
Label: "Enter proposal deposit",
|
|
Validate: client.ValidatePromptCoins,
|
|
}
|
|
proposal.Deposit, err = depositPrompt.Run()
|
|
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
|
|
result, err := 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
|
|
}
|
|
|
|
var suggestedProposalTypes = []proposalType{
|
|
{
|
|
Name: proposalText,
|
|
MsgType: "", // no message for text proposal
|
|
},
|
|
{
|
|
Name: "community-pool-spend",
|
|
MsgType: "/cosmos.distribution.v1beta1.MsgCommunityPoolSpend",
|
|
},
|
|
{
|
|
Name: "software-upgrade",
|
|
MsgType: "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade",
|
|
},
|
|
{
|
|
Name: "cancel-software-upgrade",
|
|
MsgType: "/cosmos.upgrade.v1beta1.MsgCancelUpgrade",
|
|
},
|
|
{
|
|
Name: proposalOther,
|
|
MsgType: "", // user will input the message type
|
|
},
|
|
}
|
|
|
|
func getProposalSuggestions() []string {
|
|
types := make([]string, len(suggestedProposalTypes))
|
|
for i, p := range suggestedProposalTypes {
|
|
types[i] = p.Name
|
|
}
|
|
return types
|
|
}
|
|
|
|
// 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: getProposalSuggestions(),
|
|
}
|
|
|
|
_, selectedProposalType, err := proposalTypesPrompt.Run()
|
|
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 {
|
|
// prompt proposal type
|
|
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.MsgType = result
|
|
}
|
|
|
|
if proposal.MsgType != "" {
|
|
proposal.Msg, err = sdk.GetMsgFromTypeURL(clientCtx.Codec, proposal.MsgType)
|
|
if err != nil {
|
|
// should never happen
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|