proposal_data.gno
8.45 Kb · 288 lines
1package governance
2
3import (
4 "strings"
5
6 "gno.land/p/gnoswap/utils/v1"
7 ufmt "gno.land/p/nt/ufmt/v0"
8
9 "gno.land/r/gnoswap/gov/governance"
10)
11
12type ProposalMetadataResolver struct {
13 *governance.ProposalMetadata
14}
15
16// NewProposalMetadataResolver wraps proposal metadata for validation.
17//
18// Parameters:
19// - metadata: proposal title and description to validate.
20//
21// Returns:
22// - *ProposalMetadataResolver: resolver backed by metadata.
23func NewProposalMetadataResolver(metadata *governance.ProposalMetadata) *ProposalMetadataResolver {
24 return &ProposalMetadataResolver{
25 ProposalMetadata: metadata,
26 }
27}
28
29// Validate performs comprehensive validation of the proposal metadata.
30// Checks title and description length and content requirements.
31//
32// Returns:
33// - error: validation error if metadata is invalid
34func (r *ProposalMetadataResolver) Validate() error {
35 // Validate title meets requirements
36 if err := r.validateTitle(r.Title()); err != nil {
37 return err
38 }
39
40 // Validate description meets requirements
41 if err := r.validateDescription(r.Description()); err != nil {
42 return err
43 }
44
45 return nil
46}
47
48// validateTitle checks if the proposal title meets length and content requirements.
49//
50// Parameters:
51// - title: title string to validate
52//
53// Returns:
54// - error: validation error if title is invalid
55func (r *ProposalMetadataResolver) validateTitle(title string) error {
56 // Title cannot be empty
57 if title == "" {
58 return makeErrorWithDetails(errInvalidInput, "title is empty")
59 }
60
61 // Title cannot exceed maximum length
62 if len(title) > maxTitleLength {
63 return makeErrorWithDetails(
64 errInvalidInput,
65 "title is too long, max length is 255 characters",
66 )
67 }
68
69 return nil
70}
71
72// validateDescription checks if the proposal description meets length and content requirements.
73//
74// Parameters:
75// - description: description string to validate
76//
77// Returns:
78// - error: validation error if description is invalid
79func (r *ProposalMetadataResolver) validateDescription(description string) error {
80 // Description cannot be empty
81 if description == "" {
82 return makeErrorWithDetails(
83 errInvalidInput,
84 "description is empty",
85 )
86 }
87
88 // Description cannot exceed maximum length
89 if len(description) > maxDescriptionLength {
90 return makeErrorWithDetails(
91 errInvalidInput,
92 "description is too long, max length is 10,000 characters",
93 )
94 }
95
96 return nil
97}
98
99// ProposalDataResolver handles business logic for proposal data.
100type ProposalDataResolver struct {
101 *governance.ProposalData
102}
103
104// NewProposalDataResolver wraps proposal data for type-specific validation and
105// execution-message parsing.
106//
107// Parameters:
108// - proposalData: proposal data to resolve and validate.
109//
110// Returns:
111// - *ProposalDataResolver: resolver backed by proposalData.
112func NewProposalDataResolver(proposalData *governance.ProposalData) *ProposalDataResolver {
113 return &ProposalDataResolver{
114 ProposalData: proposalData,
115 }
116}
117
118// Validate performs type-specific validation of the proposal data.
119// Different proposal types have different validation requirements.
120//
121// Returns:
122// - error: validation error if data is invalid
123func (r *ProposalDataResolver) Validate() error {
124 switch r.ProposalType() {
125 case governance.Text:
126 return r.validateText()
127 case governance.CommunityPoolSpend:
128 return r.validateCommunityPoolSpend()
129 case governance.ParameterChange:
130 return r.validateParameterChange()
131 }
132 return nil
133}
134
135// validateText validates text proposal data.
136// Text proposals have no additional validation requirements.
137//
138// Returns:
139// - error: always nil for text proposals
140func (r *ProposalDataResolver) validateText() error {
141 return nil
142}
143
144// validateCommunityPoolSpend validates community pool spend proposal data.
145// Checks recipient address, token path, and amount validity.
146//
147// Returns:
148// - error: validation error if community pool spend data is invalid
149func (r *ProposalDataResolver) validateCommunityPoolSpend() error {
150 // Validate recipient address
151 communityPoolSpend := r.ProposalData.CommunityPoolSpend()
152 if communityPoolSpend == nil {
153 return makeErrorWithDetails(
154 errInvalidInput, "community pool spend info is missing")
155 }
156
157 if !communityPoolSpend.To().IsValid() {
158 return makeErrorWithDetails(
159 errInvalidInput, "to is invalid address")
160 }
161
162 // Validate amount is greater than 0
163 if communityPoolSpend.Amount() <= 0 {
164 return makeErrorWithDetails(
165 errInvalidInput, "amount is not positive")
166 }
167
168 return nil
169}
170
171// validateParameterChange validates parameter change proposal data.
172// Delegates to validateExecutions for full validation including count checks,
173// message format, handler existence, and parameter type validation.
174//
175// Returns:
176// - error: validation error if parameter change data is invalid
177func (r *ProposalDataResolver) validateParameterChange() error {
178 execution := r.Execution()
179 if execution == nil {
180 return makeErrorWithDetails(
181 errInvalidInput,
182 "execution info is missing",
183 )
184 }
185 return validateExecutions(execution.Num(), execution.Msgs())
186}
187
188// ParameterChangesInfos parses the execution messages and returns structured parameter change information.
189// Each message is expected to be in format: pkgPath*EXE*function*EXE*params
190//
191// Returns:
192// - []ParameterChangeInfo: slice of parsed parameter change information
193// - error: validation error if any execution message is malformed
194func (r *ProposalDataResolver) ParameterChangesInfos() ([]governance.ParameterChangeInfo, error) {
195 infos := make([]governance.ParameterChangeInfo, 0)
196
197 // Return empty slice if no executions
198 execution := r.Execution()
199 if execution == nil || execution.Num() <= 0 {
200 return infos, nil
201 }
202
203 // Parse each execution message
204 for _, msg := range execution.Msgs() {
205 pkgPath, function, params, partCount := parseExecutionMessage(msg)
206 if partCount != 3 {
207 return nil, makeErrorWithDetails(
208 errInvalidMessageFormat,
209 ufmt.Sprintf("malformed execution message: expected 3 parts (pkgPath, function, params), got %d", partCount),
210 )
211 }
212
213 // Create parameter change info structure
214 info := governance.NewParameterChangeInfo(pkgPath, function, params)
215 infos = append(infos, info)
216 }
217
218 return infos, nil
219}
220
221// NewProposalTextData creates proposal data for a text proposal.
222// Text proposals have no additional data requirements.
223//
224// Returns:
225// - *ProposalData: proposal data configured for text proposal
226func NewProposalTextData() *governance.ProposalData {
227 return governance.NewProposalData(governance.Text, nil, nil)
228}
229
230// NewProposalCommunityPoolSpendData creates proposal data for a community pool spend proposal.
231// Automatically generates the execution message for the token transfer.
232//
233// Parameters:
234// - tokenPath: path of the token to transfer
235// - to: recipient address for the transfer
236// - amount: amount of tokens to transfer
237// - communityPoolPackagePath: package path of the community pool contract
238//
239// Returns:
240// - *ProposalData: proposal data configured for community pool spending
241func NewProposalCommunityPoolSpendData(
242 tokenPath string,
243 to address,
244 amount int64,
245 communityPoolPackagePath string,
246) *governance.ProposalData {
247 // Create execution message for the token transfer
248 executionInfoMessage := makeExecuteMessage(
249 communityPoolPackagePath,
250 "TransferToken",
251 []string{tokenPath, to.String(), utils.FormatInt(amount)},
252 )
253
254 return governance.NewProposalData(
255 governance.CommunityPoolSpend,
256 governance.NewCommunityPoolSpendInfo(to, tokenPath, amount),
257 governance.NewExecutionInfo(1, []string{executionInfoMessage}),
258 )
259}
260
261// NewProposalExecutionData creates proposal data for a parameter change proposal.
262// Each message in executions should be formatted as <pkgPath>*EXE*<function>*EXE*<params>,
263// separated by *GOV* when there are multiple messages.
264//
265// Parameters:
266// - numToExecute: number of parameter changes to execute
267// - executions: raw encoded execution string with parameter changes
268//
269// Returns:
270// - *ProposalData: proposal data configured for parameter changes
271func NewProposalExecutionData(numToExecute int64, executions string) *governance.ProposalData {
272 return governance.NewProposalData(
273 governance.ParameterChange,
274 nil,
275 governance.NewExecutionInfo(numToExecute, splitExecutionsRaw(executions)),
276 )
277}
278
279// makeExecuteMessage creates a message to execute a function.
280// Message format: <pkgPath>*EXE*<function>*EXE*<params>.
281func makeExecuteMessage(pkgPath, function string, params []string) string {
282 messageParams := []string{
283 pkgPath,
284 function,
285 strings.Join(params, ","),
286 }
287 return strings.Join(messageParams, parameterSeparator)
288}