github.com/decred/politeia@v1.4.0/politeiad/plugins/pi/pi.go (about)

     1  // Copyright (c) 2020-2022 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 pi provides a plugin that extends records with functionality for
     6  // decred's proposal system.
     7  package pi
     8  
     9  const (
    10  	// PluginID is the unique identifier for this plugin.
    11  	PluginID = "pi"
    12  
    13  	// CmdSetBillingStatus command sets the billing status.
    14  	CmdSetBillingStatus = "setbillingstatus"
    15  
    16  	// CmdBillingStatusChanges command returns the billing status changes
    17  	// of a proposal.
    18  	CmdBillingStatusChanges = "billingstatuschanges"
    19  
    20  	// CmdSummary command returns a summary for a proposal.
    21  	CmdSummary = "summary"
    22  )
    23  
    24  // Plugin setting keys can be used to specify custom plugin settings. Default
    25  // plugin setting values can be overridden by providing a plugin setting key
    26  // and value to the plugin on startup.
    27  const (
    28  	// SettingKeyTextFileSizeMax is the plugin setting key for the
    29  	// SettingTextFileSizeMax plugin setting.
    30  	SettingKeyTextFileSizeMax = "textfilesizemax"
    31  
    32  	// SettingKeyImageFileCountMax is the plugin setting key for the
    33  	// SettingImageFileCountMax plugin setting.
    34  	SettingKeyImageFileCountMax = "imagefilecountmax"
    35  
    36  	// SettingKeyImageFileSizeMax is the plugin setting key for the
    37  	// SettingImageFileSizeMax plugin setting.
    38  	SettingKeyImageFileSizeMax = "imagefilesizemax"
    39  
    40  	// SettingKeyTitleLengthMin is the plugin setting key for
    41  	// the SettingTitleLengthMin plugin setting.
    42  	SettingKeyTitleLengthMin = "titlelengthmin"
    43  
    44  	// SettingKeyTitleLengthMax is the plugin setting key for
    45  	// the SettingTitleLengthMax plugin setting.
    46  	SettingKeyTitleLengthMax = "titlelengthmax"
    47  
    48  	// SettingKeyTitleSupportedChars is the plugin setting key
    49  	// for the SettingTitleSupportedChars plugin setting.
    50  	SettingKeyTitleSupportedChars = "titlesupportedchars"
    51  
    52  	// SettingKeyProposalAmountMin is the plugin setting key for
    53  	// the SettingProposalAmountMin plugin setting.
    54  	SettingKeyProposalAmountMin = "proposalamountmin"
    55  
    56  	// SettingKeyProposalAmountMax is the plugin setting key for
    57  	// the SettingProposalAmountMax plugin setting.
    58  	SettingKeyProposalAmountMax = "proposalamountmax"
    59  
    60  	// SettingKeyProposalStartDateMin is the plugin setting key for
    61  	// the SettingProposalStartDateMin plugin setting.
    62  	SettingKeyProposalStartDateMin = "proposalstartdatemin"
    63  
    64  	// SettingKeyProposalEndDateMax is the plugin setting key for
    65  	// the SettingProposalEndDateMax plugin setting.
    66  	SettingKeyProposalEndDateMax = "proposalenddatemax"
    67  
    68  	// SettingKeyProposalDomains is the plugin setting key for the
    69  	// SettingProposalDomains plugin setting.
    70  	SettingKeyProposalDomains = "proposaldomains"
    71  
    72  	// SettingKeyBillingStatusChangesMax is the plugin setting
    73  	// key for the SettingBillingStatusChangesMax plugin setting.
    74  	SettingKeyBillingStatusChangesMax = "billingstatuschangesmax"
    75  
    76  	// SettingKeySummariesPageSize is the plugin setting key for the
    77  	// SettingSummariesPageSize plugin setting.
    78  	SettingKeySummariesPageSize = "summariespagesize"
    79  
    80  	// SettingKeyBillingStatusChangesPageSize is the plugin key for
    81  	// the SettingBillingStatusChangesPageSize plugin setting.
    82  	SettingKeyBillingStatusChangesPageSize = "billingstatuschangespagesize"
    83  )
    84  
    85  // Plugin setting default values. These can be overridden by providing a plugin
    86  // setting key and value to the plugin on startup.
    87  const (
    88  	// SettingTextFileSizeMax is the default maximum allowed size of a
    89  	// text file in bytes.
    90  	SettingTextFileSizeMax uint32 = 512 * 1024
    91  
    92  	// SettingImageFileCountMax is the default maximum number of image
    93  	// files that can be included in a proposal.
    94  	SettingImageFileCountMax uint32 = 5
    95  
    96  	// SettingImageFileSizeMax is the default maximum allowed size of
    97  	// an image file in bytes.
    98  	SettingImageFileSizeMax uint32 = 512 * 1024
    99  
   100  	// SettingTitleLengthMin is the default minimum number of
   101  	// characters that a proposal name or a proposal update title can be.
   102  	SettingTitleLengthMin uint32 = 8
   103  
   104  	// SettingTitleLengthMax is the default maximum number of
   105  	// characters that a proposal name or a proposal update title can be.
   106  	SettingTitleLengthMax uint32 = 80
   107  
   108  	// SettingProposalAmountMin is the default minimum funding amount
   109  	// in cents a proposal can have.
   110  	SettingProposalAmountMin uint64 = 100000 // 1k usd in cents.
   111  
   112  	// SettingProposalAmountMax is the default maximum funding amount
   113  	// in cents a proposal can have.
   114  	SettingProposalAmountMax uint64 = 100000000 // 1m usd in cents.
   115  
   116  	// SettingProposalEndDateMax is the default maximum possible proposal
   117  	// end date - seconds from current time.
   118  	SettingProposalEndDateMax int64 = 31557600 // 365.25 days in seconds.
   119  
   120  	// SettingProposalStartDateMin is the default minimum possible proposal
   121  	// start date - seconds from current time.
   122  	SettingProposalStartDateMin int64 = 604800 // One week in seconds.
   123  
   124  	// SettingBillingStatusChangesMax is the default maximum allowed
   125  	// billing status changes.
   126  	SettingBillingStatusChangesMax uint32 = 1
   127  
   128  	// SettingSummariesPageSize is the default maximum number of proposal
   129  	// summaries that can be requested at any one time.
   130  	SettingSummariesPageSize uint32 = 5
   131  
   132  	// SettingBillingStatusChangesPageSize is the default maximum number of
   133  	// billing status changes that can be requested at any one time.
   134  	SettingBillingStatusChangesPageSize uint32 = 5
   135  )
   136  
   137  var (
   138  	// SettingTitleSupportedChars contains the supported
   139  	// characters in a proposal name or a proposal update title.
   140  	SettingTitleSupportedChars = []string{
   141  		"A-z", "0-9", "&", ".", ",", ":", ";", "-", " ", "@", "+", "#",
   142  		"/", "(", ")", "!", "?", "\"", "'",
   143  	}
   144  
   145  	// SettingProposalDomains contains the default proposal domains.
   146  	SettingProposalDomains = []string{
   147  		"development",
   148  		"marketing",
   149  		"research",
   150  		"design",
   151  	}
   152  )
   153  
   154  // ErrorCodeT represents a plugin error that was caused by the user.
   155  type ErrorCodeT uint32
   156  
   157  const (
   158  	// ErrorCodeInvalid represents an invalid error code.
   159  	ErrorCodeInvalid ErrorCodeT = 0
   160  
   161  	// ErrorCodeTextFileNameInvalid is returned when a text file has
   162  	// a file name that is not allowed.
   163  	ErrorCodeTextFileNameInvalid ErrorCodeT = 1
   164  
   165  	// ErrorCodeTextFileSizeInvalid is returned when a text file size
   166  	// exceedes the TextFileSizeMax setting.
   167  	ErrorCodeTextFileSizeInvalid ErrorCodeT = 2
   168  
   169  	// ErrorCodeTextFileMissing is returned when the proposal does not
   170  	// contain one or more of the required text files.
   171  	ErrorCodeTextFileMissing ErrorCodeT = 3
   172  
   173  	// ErrorCodeImageFileCountInvalid is returned when the number of
   174  	// image attachments exceedes the ImageFileCountMax setting.
   175  	ErrorCodeImageFileCountInvalid ErrorCodeT = 4
   176  
   177  	// ErrorCodeImageFileSizeInvalid is returned when an image file
   178  	// size exceedes the ImageFileSizeMax setting.
   179  	ErrorCodeImageFileSizeInvalid ErrorCodeT = 5
   180  
   181  	// ErrorCodeTitleInvalid is returned when a title, proposal title or proposal
   182  	// update title, does not adhere to the title regexp requirements.
   183  	ErrorCodeTitleInvalid ErrorCodeT = 6
   184  
   185  	// ErrorCodeVoteStatusInvalid is returned when a proposal vote
   186  	// status does not allow changes to be made to the proposal.
   187  	ErrorCodeVoteStatusInvalid ErrorCodeT = 7
   188  
   189  	// ErrorCodeProposalStartDateInvalid is returned when a proposal start date
   190  	// does not adhere to the proposal start date settings.
   191  	ErrorCodeProposalStartDateInvalid ErrorCodeT = 8
   192  
   193  	// ErrorCodeProposalEndDateInvalid is returned when a proposal end date
   194  	// does not adhere to the proposal end date settings.
   195  	ErrorCodeProposalEndDateInvalid ErrorCodeT = 9
   196  
   197  	// ErrorCodeProposalAmountInvalid is returned when a proposal amount
   198  	// is not in the range defined by the amount min/max plugin settings.
   199  	ErrorCodeProposalAmountInvalid ErrorCodeT = 10
   200  
   201  	// ErrorCodeProposalDomainInvalid is returned when a proposal domain
   202  	// is not one of the supported domains.
   203  	ErrorCodeProposalDomainInvalid ErrorCodeT = 11
   204  
   205  	// ErrorCodeTokenInvalid is returned when a record token is
   206  	// provided as part of a plugin command payload and is not a valid
   207  	// token or the payload token does not match the token that was
   208  	// used in the API request.
   209  	ErrorCodeTokenInvalid ErrorCodeT = 12
   210  
   211  	// ErrorCodePublicKeyInvalid is returned when a public key is not
   212  	// a valid hex encoded, Ed25519 public key.
   213  	ErrorCodePublicKeyInvalid ErrorCodeT = 13
   214  
   215  	// ErrorCodeSignatureInvalid is returned when a signature is not
   216  	// a valid hex encoded, Ed25519 signature or when the signature is
   217  	// wrong.
   218  	ErrorCodeSignatureInvalid ErrorCodeT = 14
   219  
   220  	// ErrorCodeBillingStatusChangeNotAllowed is returned when a billing status
   221  	// change is not allowed.
   222  	ErrorCodeBillingStatusChangeNotAllowed = 15
   223  
   224  	// ErrorCodeBillingStatusInvalid is returned when an invalid billing status
   225  	// is provided.
   226  	ErrorCodeBillingStatusInvalid = 16
   227  
   228  	// ErrorCodeCommentWriteNotAllowed is returned when a user attempts to submit
   229  	// a new comment or a comment vote, but does not have permission to. This
   230  	// could be because the proposal's vote status does not allow for any
   231  	// additional changes or because the user is trying to write to a thread that
   232  	// is not allowed. Example, once a proposal vote is approved the only comment
   233  	// writes that are allowed are replies and votes to the author's most recent
   234  	// update thread.
   235  	ErrorCodeCommentWriteNotAllowed = 17
   236  
   237  	// ErrorCodeExtraDataHintInvalid is returned when the extra data hint is
   238  	// invalid.
   239  	ErrorCodeExtraDataHintInvalid = 18
   240  
   241  	// ErrorCodeExtraDataInvalid is returned when the extra data payload is
   242  	// invalid.
   243  	ErrorCodeExtraDataInvalid = 19
   244  
   245  	// ErrorCodeLegacyTokenNotAllowed is returned when the legacy token is set
   246  	// during a normal proposal submission.
   247  	ErrorCodeLegacyTokenNotAllowed = 20
   248  
   249  	// ErrorCodeLast is used by unit tests to verify that all error codes have
   250  	// a human readable entry in the ErrorCodes map. This error will never be
   251  	// returned.
   252  	ErrorCodeLast ErrorCodeT = 21
   253  )
   254  
   255  var (
   256  	// ErrorCodes contains the human readable errors.
   257  	ErrorCodes = map[ErrorCodeT]string{
   258  		ErrorCodeInvalid:                       "error code invalid",
   259  		ErrorCodeTextFileNameInvalid:           "text file name invalid",
   260  		ErrorCodeTextFileSizeInvalid:           "text file size invalid",
   261  		ErrorCodeTextFileMissing:               "text file is misisng",
   262  		ErrorCodeImageFileCountInvalid:         "image file count invalid",
   263  		ErrorCodeImageFileSizeInvalid:          "image file size invalid",
   264  		ErrorCodeTitleInvalid:                  "title invalid",
   265  		ErrorCodeVoteStatusInvalid:             "vote status invalid",
   266  		ErrorCodeProposalAmountInvalid:         "proposal amount invalid",
   267  		ErrorCodeProposalStartDateInvalid:      "proposal start date invalid",
   268  		ErrorCodeProposalEndDateInvalid:        "proposal end date invalid",
   269  		ErrorCodeProposalDomainInvalid:         "proposal domain invalid",
   270  		ErrorCodeTokenInvalid:                  "token invalid",
   271  		ErrorCodePublicKeyInvalid:              "public key invalid",
   272  		ErrorCodeSignatureInvalid:              "signature invalid",
   273  		ErrorCodeBillingStatusChangeNotAllowed: "billing status change is not allowed",
   274  		ErrorCodeBillingStatusInvalid:          "billing status invalid",
   275  		ErrorCodeCommentWriteNotAllowed:        "comment write not allowed",
   276  		ErrorCodeExtraDataHintInvalid:          "extra data hint invalid",
   277  		ErrorCodeLegacyTokenNotAllowed:         "setting legacy token is not allowed",
   278  		ErrorCodeExtraDataInvalid:              "extra data payload invalid",
   279  	}
   280  )
   281  
   282  const (
   283  	// FileNameIndexFile is the file name of the proposal markdown
   284  	// file. Every proposal is required to have an index file. The
   285  	// index file should contain the proposal content.
   286  	FileNameIndexFile = "index.md"
   287  
   288  	// FileNameProposalMetadata is the filename of the ProposalMetadata
   289  	// file that is saved to politeiad. ProposalMetadata is saved to
   290  	// politeiad as a file, not as a metadata stream, since it contains
   291  	// user provided metadata and needs to be included in the merkle
   292  	// root that politeiad signs.
   293  	FileNameProposalMetadata = "proposalmetadata.json"
   294  )
   295  
   296  // ProposalMetadata contains metadata that is provided by the user as part of
   297  // the proposal submission bundle. The proposal metadata is included in the
   298  // proposal signature since it is user specified data. The ProposalMetadata
   299  // object is saved to politeiad as a file, not as a metadata stream, since it
   300  // needs to be included in the merkle root that politeiad signs.
   301  type ProposalMetadata struct {
   302  	Name      string `json:"name"`
   303  	Amount    uint64 `json:"amount"`    // Funding amount in cents
   304  	StartDate int64  `json:"startdate"` // Start date, Unix time
   305  	EndDate   int64  `json:"enddate"`   // Estimated end date, Unix time
   306  	Domain    string `json:"domain"`    // Proposal domain
   307  
   308  	// LegacyToken will only be set for legacy proposals that have been imported
   309  	// from the deprecated git backend into the tstore backend. The LegacyToken
   310  	// corresponds to the original token that was assigned to the proposal during
   311  	// submission to the git backed. This token is not used for anything in the
   312  	// current tstore backend, but can be used to lookup the proposal's original
   313  	// timestamps in the legacy proposal git repo. The proposal is assigned a
   314  	// new token by the tstore backend on import. An error is returned if this
   315  	// field is attempted to be set during normal proposal submissions.
   316  	LegacyToken string `json:"legacytoken,omitempty"`
   317  }
   318  
   319  // BillingStatusT represents the billing status of a proposal that has been
   320  // approved by the Decred stakeholders.
   321  type BillingStatusT uint32
   322  
   323  const (
   324  	// BillingStatusInvalid is an invalid billing status.
   325  	BillingStatusInvalid BillingStatusT = 0
   326  
   327  	// BillingStatusActive represents a proposal that was approved by
   328  	// the Decred stakeholders and is being actively billed against.
   329  	BillingStatusActive BillingStatusT = 1
   330  
   331  	// BillingStatusClosed represents a proposal that was approved by
   332  	// the Decred stakeholders, but has been closed by an admin prior
   333  	// to the proposal being completed. The most common reason for this
   334  	// is because a proposal author failed to deliver on the work that
   335  	// was funded in the proposal. A closed proposal can no longer be
   336  	// billed against.
   337  	BillingStatusClosed BillingStatusT = 2
   338  
   339  	// BillingStatusCompleted represents a proposal that was approved
   340  	// by the Decred stakeholders and has been successfully completed.
   341  	// A completed proposal can no longer be billed against. A proposal
   342  	// is marked as completed by an admin.
   343  	BillingStatusCompleted BillingStatusT = 3
   344  
   345  	// BillingStatusLast is used by unit tests to verify that all billing
   346  	// statuses have a human readable entry in the BillingStatuses map. This
   347  	// status will never be returned.
   348  	BillingStatusLast ErrorCodeT = 4
   349  )
   350  
   351  var (
   352  	// BillingStatuses contains the human readable billing statuses.
   353  	BillingStatuses = map[BillingStatusT]string{
   354  		BillingStatusInvalid:   "invalid",
   355  		BillingStatusActive:    "active",
   356  		BillingStatusClosed:    "closed",
   357  		BillingStatusCompleted: "completed",
   358  	}
   359  )
   360  
   361  // BillingStatusChange represents the structure that is saved to disk when
   362  // a proposal has its billing status updated. Some billing status changes
   363  // require a reason to be given. Only admins can update the billing status
   364  // of a proposal.
   365  //
   366  // PublicKey is the admin public key that can be used to verify the signature.
   367  //
   368  // Signature is the admin signature of the Token+Status+Reason.
   369  //
   370  // Receipt is the server signature of the admin signature.
   371  //
   372  // The PublicKey, Signature, and Receipt are all hex encoded and use the
   373  // ed25519 signature scheme.
   374  type BillingStatusChange struct {
   375  	Token     string         `json:"token"`
   376  	Status    BillingStatusT `json:"status"`
   377  	Reason    string         `json:"reason,omitempty"`
   378  	PublicKey string         `json:"publickey"`
   379  	Signature string         `json:"signature"`
   380  	Receipt   string         `json:"receipt"`
   381  	Timestamp int64          `json:"timestamp"` // Unix timestamp
   382  }
   383  
   384  // SetBillingStatus sets the billing status of a proposal. Some billing status
   385  // changes require a reason to be given. Only admins can update the billing
   386  // status of a proposal.
   387  //
   388  // PublicKey is the admin public key that can be used to verify the signature.
   389  //
   390  // Signature is the admin signature of the Token+Status+Reason.
   391  //
   392  // The PublicKey and Signature are hex encoded and use the ed25519 signature
   393  // scheme.
   394  type SetBillingStatus struct {
   395  	Token     string         `json:"token"`
   396  	Status    BillingStatusT `json:"status"`
   397  	Reason    string         `json:"reason,omitempty"`
   398  	PublicKey string         `json:"publickey"`
   399  	Signature string         `json:"signature"`
   400  }
   401  
   402  // SetBillingStatusReply is the reply to the SetBillingStatus command.
   403  //
   404  // Receipt is the server signature of the client signature. It is hex encoded
   405  // and uses the ed25519 signature scheme.
   406  type SetBillingStatusReply struct {
   407  	Receipt   string `json:"receipt"`
   408  	Timestamp int64  `json:"timestamp"` // Unix timestamp
   409  }
   410  
   411  // Summary requests the summary of a proposal.
   412  type Summary struct {
   413  	Token string `json:"token"`
   414  }
   415  
   416  // SummaryReply is the reply to the Summary command.
   417  type SummaryReply struct {
   418  	Summary ProposalSummary `json:"summary"`
   419  }
   420  
   421  // ProposalSummary summarizes proposal information.
   422  type ProposalSummary struct {
   423  	Status PropStatusT `json:"status"`
   424  }
   425  
   426  // PropStatusT represents the status of a proposal. It combines record and
   427  // plugin metadata in order to create a unified map of the various paths a
   428  // proposal can take throughout the proposal process. This serves as the
   429  // source of truth for clients so that they don't need to try and decipher
   430  // what various combinations of plugin metadata mean for the proposal.
   431  //
   432  // The proposal status is determined at runtime by the pi plugin based on the
   433  // various record and plugin metadata that a proposal contains.
   434  type PropStatusT string
   435  
   436  const (
   437  	// PropStatusInvalid represents an invalid proposal status.
   438  	PropStatusInvalid PropStatusT = "invalid"
   439  
   440  	// PropStatusUnvetted represents a proposal that has been submitted but has
   441  	// not yet been made public by the admins.
   442  	PropStatusUnvetted PropStatusT = "unvetted"
   443  
   444  	// PropStatusUnvettedAbandoned represents a proposal that has been
   445  	// submitted, but was abandoned by the author prior to being made public.
   446  	// The proposal can be marked as abandoned by either the proposal author
   447  	// or an admin. Abandoned proposal files are not deleted from the backend
   448  	// and are still retreivable. An abandoned proposal is locked against any
   449  	// additional proposal or plugin changes.
   450  	PropStatusUnvettedAbandoned PropStatusT = "unvetted-abandoned"
   451  
   452  	// PropStatusUnvettedCensored represents a proposal that has been submitted,
   453  	// but was censored by an admin prior to being made public. Censored
   454  	// proposal files are permanently deleted from the backend. No additional
   455  	// changes can be made to the proposal once it has been censored.
   456  	PropStatusUnvettedCensored PropStatusT = "unvetted-censored"
   457  
   458  	// PropStatusUnderReview represents a proposal that has been made public and
   459  	// is being reviewed by the Decred stakeholders, but has not had it's voting
   460  	// period started yet.
   461  	PropStatusUnderReview PropStatusT = "under-review"
   462  
   463  	// PropStatusAbandoned represents a proposal that has been made public, but
   464  	// has been abandoned. A proposal can be marked as abandoned by either the
   465  	// proposal author or by an admin. Abandoned proposals are locked from any
   466  	// additional changes. Abandoned proposal files are not deleted from the
   467  	// backend.
   468  	PropStatusAbandoned PropStatusT = "abandoned"
   469  
   470  	// PropStatusCensored represents a proposal that was censored by an admin
   471  	// after it had already been made public. This can happen if an edit to the
   472  	// proposal adds content that requires censoring.  Censored proposal files
   473  	// are permanently deleted from the backend. No additional changes can be
   474  	// made to the proposal once it has been censored.
   475  	PropStatusCensored PropStatusT = "censored"
   476  
   477  	// PropStatusVoteAuthorized represents a public proposal whose voting period
   478  	// has been authorized by the author. An admin cannot start the voting
   479  	// period of a proposal until the author has authorized it.
   480  	PropStatusVoteAuthorized PropStatusT = "vote-authorized"
   481  
   482  	// PropStatusVoteStarted represents a public proposal that is currently
   483  	// under vote by the Decred stakeholders. The voting period for a proposal
   484  	// is started by an admin. The proposal content cannot change once the
   485  	// voting period has been started, but some plugin data (e.g. comments) can
   486  	// still be added.
   487  	PropStatusVoteStarted PropStatusT = "vote-started"
   488  
   489  	// PropStatusApproved represents a proposal that was voted on by the Decred
   490  	// stakeholders, met the approval criteria, but is not being actively billed
   491  	// against. An example is an RFP proposal. RFP proposals do not request
   492  	// funding and are not billed against once approved.
   493  	PropStatusApproved PropStatusT = "approved"
   494  
   495  	// PropStatusRejected represents a proposal that was voted on by the Decred
   496  	// stakeholders and did not meet the approval criteria. A rejected proposal
   497  	// is locked against any additional proposal or plugin changes.
   498  	PropStatusRejected PropStatusT = "rejected"
   499  
   500  	// PropStatusActive represents a proposal that was voted on by the Decred
   501  	// stakeholders, met the approval criteria, and is now eligible to be billed
   502  	// against. The proposal automatically becomes active once the voting period
   503  	// ends. The proposal content of an active proposal cannot be altered. Some
   504  	// plugin functionality is still allowed. For example, an author is allowed
   505  	// to start a new comment thread in order to give proposal updates that
   506  	// users can reply to.
   507  	PropStatusActive PropStatusT = "active"
   508  
   509  	// PropStatusCompleted represents a proposal that was funded by the Decred
   510  	// stakeholders and has been completed. A completed proposal is marked as
   511  	// completed by an admin and is no longer being billed against. A completed
   512  	// proposal is locked against any additional proposal or plugin changes.
   513  	PropStatusCompleted PropStatusT = "completed"
   514  
   515  	// PropStatusClosed represents a proposal that was funded, but was never
   516  	// completed. A proposal is marked as closed by an admin and cannot be
   517  	// billed against any further. The most common reason a proposal would be
   518  	// closed is because the author failed to deliver on the milestones laid
   519  	// out in the  proposal. A closed proposal is locked against any additional
   520  	// proposal or plugin changes.
   521  	PropStatusClosed PropStatusT = "closed"
   522  )
   523  
   524  const (
   525  	// ProposalUpdateHint is the hint that is included in a comment's
   526  	// ExtraDataHint field to indicate that the comment is an update
   527  	// from the proposal author.
   528  	ProposalUpdateHint = "proposalupdate"
   529  )
   530  
   531  // ProposalUpdateMetadata contains the metadata that is attached to a comment
   532  // in the comment's ExtraData field to indicate that the comment is an update
   533  // from the proposal author.
   534  type ProposalUpdateMetadata struct {
   535  	Title string `json:"title"`
   536  }
   537  
   538  // BillingStatusChanges requests the billing status changes for the provided
   539  // proposal token.
   540  type BillingStatusChanges struct {
   541  	Token string `json:"token"`
   542  }
   543  
   544  // BillingStatusChangesReply is the reply to the BillingStatusChanges command.
   545  type BillingStatusChangesReply struct {
   546  	BillingStatusChanges []BillingStatusChange `json:"billingstatuschanges"`
   547  }