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 `