github.com/decred/politeia@v1.4.0/politeiawww/cmd/pictl/cmdproposalnew.go (about)

     1  // Copyright (c) 2020-2021 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"encoding/base64"
     9  	"encoding/hex"
    10  	"encoding/json"
    11  	"fmt"
    12  	"math/rand"
    13  	"time"
    14  
    15  	"github.com/decred/politeia/politeiad/api/v1/mime"
    16  	piv1 "github.com/decred/politeia/politeiawww/api/pi/v1"
    17  	rcv1 "github.com/decred/politeia/politeiawww/api/records/v1"
    18  	pclient "github.com/decred/politeia/politeiawww/client"
    19  	"github.com/decred/politeia/politeiawww/cmd/shared"
    20  	"github.com/decred/politeia/util"
    21  )
    22  
    23  // cmdProposalNew submits a new proposal.
    24  type cmdProposalNew struct {
    25  	Args struct {
    26  		IndexFile   string   `positional-arg-name:"indexfile"`
    27  		Attachments []string `positional-arg-name:"attachments"`
    28  	} `positional-args:"true" optional:"true"`
    29  
    30  	// Metadata fields that can be set by the user
    31  	Name      string `long:"name" optional:"true"`
    32  	LinkTo    string `long:"linkto" optional:"true"`
    33  	LinkBy    string `long:"linkby" optional:"true"`
    34  	Amount    uint64 `long:"amount" optional:"true"`
    35  	StartDate string `long:"startdate" optional:"true"`
    36  	EndDate   string `long:"enddate" optional:"true"`
    37  	Domain    string `long:"domain" optional:"true"`
    38  
    39  	// RFP is a flag that is intended to make submitting an RFP easier
    40  	// by calculating and inserting a linkby timestamp automatically
    41  	// instead of having to pass in a timestamp using the --linkby
    42  	// flag.
    43  	RFP bool `long:"rfp" optional:"true"`
    44  
    45  	// Random generate random proposal data. The IndexFile argument is
    46  	// not allowed when using this flag.
    47  	Random bool `long:"random" optional:"true"`
    48  
    49  	// RandomImages generates random image attachments. The Attachments
    50  	// argument is not allowed when using this flag.
    51  	RandomImages bool `long:"randomimages" optional:"true"`
    52  }
    53  
    54  // Execute executes the cmdProposalNew command.
    55  //
    56  // This function satisfies the go-flags Commander interface.
    57  func (c *cmdProposalNew) Execute(args []string) error {
    58  	_, err := proposalNew(c)
    59  	if err != nil {
    60  		return err
    61  	}
    62  	return nil
    63  }
    64  
    65  // proposalNew creates a new proposal. This function has been pulled out of
    66  // the Execute method so that it can be used in the test commands.
    67  func proposalNew(c *cmdProposalNew) (*rcv1.Record, error) {
    68  	// Unpack args
    69  	indexFile := c.Args.IndexFile
    70  	attachments := c.Args.Attachments
    71  
    72  	// Verify args and flags
    73  	switch {
    74  	case !c.Random && indexFile == "":
    75  		return nil, fmt.Errorf("index file not found; you must either " +
    76  			"provide an index.md file or use --random")
    77  
    78  	case c.RandomImages && len(attachments) > 0:
    79  		return nil, fmt.Errorf("you cannot provide attachment files and " +
    80  			"use the --randomimages flag at the same time")
    81  
    82  	case c.RFP && c.LinkBy != "":
    83  		return nil, fmt.Errorf("you cannot use both the --rfp and --linkby " +
    84  			"flags at the same time")
    85  	}
    86  
    87  	// Check for user identity. A user identity is required to sign
    88  	// the proposal files.
    89  	if cfg.Identity == nil {
    90  		return nil, shared.ErrUserIdentityNotFound
    91  	}
    92  
    93  	// Setup client
    94  	opts := pclient.Opts{
    95  		HTTPSCert:  cfg.HTTPSCert,
    96  		Cookies:    cfg.Cookies,
    97  		HeaderCSRF: cfg.CSRF,
    98  		Verbose:    cfg.Verbose,
    99  		RawJSON:    cfg.RawJSON,
   100  	}
   101  	pc, err := pclient.New(cfg.Host, opts)
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	// Get the pi policy. It contains the proposal requirements.
   107  	pr, err := pc.PiPolicy()
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	// Setup proposal files
   113  	indexFileSize := 10000 // In bytes
   114  	var files []rcv1.File
   115  	switch {
   116  	case c.Random && c.RandomImages:
   117  		// Create a index file and random attachments
   118  		files, err = proposalFilesRandom(indexFileSize,
   119  			int(pr.ImageFileCountMax))
   120  		if err != nil {
   121  			return nil, err
   122  		}
   123  	case c.Random:
   124  		// Create a index file
   125  		files, err = proposalFilesRandom(indexFileSize, 0)
   126  		if err != nil {
   127  			return nil, err
   128  		}
   129  	default:
   130  		// Read files from disk
   131  		files, err = proposalFilesFromDisk(indexFile, attachments)
   132  		if err != nil {
   133  			return nil, err
   134  		}
   135  	}
   136  
   137  	// Setup proposal metadata
   138  	if c.Random {
   139  		if c.Name == "" {
   140  			r, err := util.Random(int(pr.NameLengthMin))
   141  			if err != nil {
   142  				return nil, err
   143  			}
   144  			c.Name = fmt.Sprintf("A Proposal Name %x", r)
   145  		}
   146  		// Set proposal domain if not provided
   147  		if c.Domain == "" {
   148  			// Pick random domain from the pi policy domains.
   149  			randomIndex := rand.Intn(len(pr.Domains))
   150  			c.Domain = pr.Domains[randomIndex]
   151  		}
   152  		// In case of RFP no need to populate startdate, enddate &
   153  		// amount metadata fields.
   154  		if !c.RFP && c.LinkBy == "" {
   155  			// Set start date one month from now if not provided
   156  			if c.StartDate == "" {
   157  				c.StartDate = dateFromUnix(defaultStartDate)
   158  			}
   159  			// Set end date 4 months from now if not provided
   160  			if c.EndDate == "" {
   161  				c.EndDate = dateFromUnix(defaultEndDate)
   162  			}
   163  			if c.Amount == 0 {
   164  				c.Amount = defaultAmount
   165  			}
   166  		}
   167  	}
   168  
   169  	pm := piv1.ProposalMetadata{
   170  		Name:   c.Name,
   171  		Amount: c.Amount,
   172  		Domain: c.Domain,
   173  	}
   174  	// Parse start & end dates string timestamps.
   175  	if c.StartDate != "" {
   176  		pm.StartDate, err = unixFromDate(c.StartDate)
   177  		if err != nil {
   178  			return nil, err
   179  		}
   180  	}
   181  	if c.EndDate != "" {
   182  		pm.EndDate, err = unixFromDate(c.EndDate)
   183  		if err != nil {
   184  			return nil, err
   185  		}
   186  	}
   187  
   188  	pmb, err := json.Marshal(pm)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  	files = append(files, rcv1.File{
   193  		Name:    piv1.FileNameProposalMetadata,
   194  		MIME:    mime.DetectMimeType(pmb),
   195  		Digest:  hex.EncodeToString(util.Digest(pmb)),
   196  		Payload: base64.StdEncoding.EncodeToString(pmb),
   197  	})
   198  
   199  	// Setup vote metadata
   200  	var linkBy int64
   201  	switch {
   202  	case c.RFP:
   203  		// Set linkby to a month from now
   204  		linkBy = time.Now().Add(time.Hour * 24 * 30).Unix()
   205  	case c.LinkBy != "":
   206  		// Parse the provided linkby
   207  		d, err := time.ParseDuration(c.LinkBy)
   208  		if err != nil {
   209  			return nil, fmt.Errorf("unable to parse linkby: %v", err)
   210  		}
   211  		linkBy = time.Now().Add(d).Unix()
   212  	}
   213  	if linkBy != 0 || c.LinkTo != "" {
   214  		vm := piv1.VoteMetadata{
   215  			LinkTo: c.LinkTo,
   216  			LinkBy: linkBy,
   217  		}
   218  		vmb, err := json.Marshal(vm)
   219  		if err != nil {
   220  			return nil, err
   221  		}
   222  		files = append(files, rcv1.File{
   223  			Name:    piv1.FileNameVoteMetadata,
   224  			MIME:    mime.DetectMimeType(vmb),
   225  			Digest:  hex.EncodeToString(util.Digest(vmb)),
   226  			Payload: base64.StdEncoding.EncodeToString(vmb),
   227  		})
   228  	}
   229  
   230  	// Print proposal to stdout
   231  	printf("Files\n")
   232  	err = printProposalFiles(files)
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  
   237  	// Submit proposal
   238  	sig, err := signedMerkleRoot(files, cfg.Identity)
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  	n := rcv1.New{
   243  		Files:     files,
   244  		PublicKey: cfg.Identity.Public.String(),
   245  		Signature: sig,
   246  	}
   247  	nr, err := pc.RecordNew(n)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  
   252  	// Verify record
   253  	vr, err := client.Version()
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	err = pclient.RecordVerify(nr.Record, vr.PubKey)
   258  	if err != nil {
   259  		return nil, fmt.Errorf("unable to verify record: %v", err)
   260  	}
   261  
   262  	// Print censorship record
   263  	printf("Token  : %v\n", nr.Record.CensorshipRecord.Token)
   264  	printf("Merkle : %v\n", nr.Record.CensorshipRecord.Merkle)
   265  	printf("Receipt: %v\n", nr.Record.CensorshipRecord.Signature)
   266  
   267  	return &nr.Record, nil
   268  }
   269  
   270  // proposalNewHelpMsg is the printed to stdout by the help command.
   271  const proposalNewHelpMsg = `proposalnew [flags] "indexfile" "attachments" 
   272  
   273  Submit a new proposal to Politeia.
   274  
   275  A proposal can be submitted as an RFP (Request for Proposals) by using either
   276  the --rfp flag or by manually specifying a link by deadline using the --linkby
   277  flag. Only one of these flags can be used at a time.
   278  
   279  A proposal can be submitted as an RFP submission by using the --linkto flag
   280  to link to and an existing RFP proposal.
   281  
   282  Arguments:
   283  1. indexfile   (string, optional) Index file.
   284  2. attachments (string, optional) Attachment files.
   285  
   286  Flags:
   287   --name         (string) Name of the proposal.
   288   
   289   --amount       (int)    Funding amount in cents.
   290  
   291   --startdate    (string) Start Date, Format: "01/02/2006"
   292  
   293   --enddate      (string) End Date, Format: "01/02/2006"
   294  
   295   --domain       (string) Default supported domains: ["development", 
   296                           "research", "design", "marketing"]
   297  
   298   --linkto       (string) Token of an existing public proposal to link to.
   299  
   300   --linkby       (string) Make the proposal and RFP by setting the linkby
   301                           deadline. Other proposals must be entered as RFP
   302                           submissions by this linkby deadline. The provided
   303                           string should be a duration that will be added onto
   304                           the current time. Valid duration units are:
   305                           s (seconds), m (minutes), h (hours).
   306  
   307   --rfp          (bool)   Make the proposal an RFP by setting the linkby to one
   308                           month from the current time. This is intended to be
   309                           used in place of --linkby.
   310  
   311   --random       (bool)   Generate random proposal data, not including
   312                           attachments. The indexFile argument is not allowed
   313                           when using this flag.
   314  
   315   --randomimages (bool)   Generate random attachments. The attachments argument
   316                           is not allowed when using this flag.
   317  
   318  Examples:
   319  
   320  # Set linkby 24 hours from current time
   321  $ pictl proposalnew --random --linkby=24h
   322  
   323  # Use --rfp to set the linky 1 month from current time
   324  $ pictl proposalnew --rfp index.md proposalmetadata.json
   325  `