valopers.gno
7.54 Kb ยท 302 lines
1// Package valopers is designed around the permissionless lifecycle of valoper profiles.
2package valopers
3
4import (
5 "crypto/bech32"
6 "errors"
7 "regexp"
8 "std"
9
10 "gno.land/p/demo/avl"
11 "gno.land/p/demo/avl/pager"
12 "gno.land/p/demo/combinederr"
13 "gno.land/p/demo/ownable/exts/authorizable"
14 "gno.land/p/demo/ufmt"
15 "gno.land/p/moul/realmpath"
16)
17
18const (
19 MonikerMaxLength = 32
20 DescriptionMaxLength = 2048
21)
22
23var (
24 ErrValoperExists = errors.New("valoper already exists")
25 ErrValoperMissing = errors.New("valoper does not exist")
26 ErrInvalidAddress = errors.New("invalid address")
27 ErrInvalidMoniker = errors.New("moniker is not valid")
28 ErrInvalidDescription = errors.New("description is not valid")
29)
30
31var (
32 valopers *avl.Tree // valopers keeps track of all the valoper profiles. Address -> Valoper
33 instructions string // markdown instructions for valoper's registration
34 minFee = std.NewCoin("ugnot", 0) // minimum gnot must be paid to register. (0 by default)
35
36 monikerMaxLengthMiddle = ufmt.Sprintf("%d", MonikerMaxLength-2)
37 validateMonikerRe = regexp.MustCompile(`^[a-zA-Z0-9][\w -]{0,` + monikerMaxLengthMiddle + `}[a-zA-Z0-9]$`) // 32 characters, including spaces, hyphens or underscores in the middle
38)
39
40// Valoper represents a validator operator profile
41type Valoper struct {
42 Moniker string // A human-readable name
43 Description string // A description and details about the valoper
44
45 Address std.Address // The bech32 gno address of the validator
46 PubKey string // The bech32 public key of the validator
47 KeepRunning bool // Flag indicating if the owner wants to keep the validator running
48
49 auth *authorizable.Authorizable // The authorizer system for the valoper
50}
51
52func (v Valoper) Auth() *authorizable.Authorizable {
53 return v.auth
54}
55
56func AddToAuthList(address std.Address, member std.Address) {
57 v := GetByAddr(address)
58
59 if err := v.Auth().AddToAuthList(member); err != nil {
60 panic(err)
61 }
62}
63
64func DeleteFromAuthList(address std.Address, member std.Address) {
65 v := GetByAddr(address)
66
67 if err := v.Auth().DeleteFromAuthList(member); err != nil {
68 panic(err)
69 }
70}
71
72// Register registers a new valoper
73func Register(moniker string, description string, address std.Address, pubKey string) {
74 // Check if a fee is enforced
75 if !minFee.IsZero() {
76 sentCoins := std.OriginSend()
77
78 // Coins must be sent and cover the min fee
79 if len(sentCoins) != 1 || sentCoins[0].IsLT(minFee) {
80 panic(ufmt.Sprintf("payment must not be less than %d%s", minFee.Amount, minFee.Denom))
81 }
82 }
83
84 // Check if the valoper is already registered
85 if isValoper(address) {
86 panic(ErrValoperExists)
87 }
88
89 v := Valoper{
90 Moniker: moniker,
91 Description: description,
92 Address: address,
93 PubKey: pubKey,
94 KeepRunning: true,
95 auth: authorizable.NewAuthorizable(),
96 }
97
98 if err := v.Validate(); err != nil {
99 panic(err)
100 }
101
102 // TODO add address derivation from public key
103 // (when the laws of gno make it possible)
104
105 // Save the valoper to the set
106 valopers.Set(v.Address.String(), v)
107}
108
109// UpdateMoniker updates an existing valoper's moniker
110func UpdateMoniker(address std.Address, moniker string) {
111 // Check that the moniker is not empty
112 if err := validateMoniker(moniker); err != nil {
113 panic(err)
114 }
115
116 v := GetByAddr(address)
117
118 // Check that the caller has permissions
119 v.Auth().AssertOnAuthList()
120
121 // Update the moniker
122 v.Moniker = moniker
123
124 // Save the valoper info
125 valopers.Set(address.String(), v)
126}
127
128// UpdateDescription updates an existing valoper's description
129func UpdateDescription(address std.Address, description string) {
130 // Check that the description is not empty
131 if err := validateDescription(description); err != nil {
132 panic(err)
133 }
134
135 v := GetByAddr(address)
136
137 // Check that the caller has permissions
138 v.Auth().AssertOnAuthList()
139
140 // Update the description
141 v.Description = description
142
143 // Save the valoper info
144 valopers.Set(address.String(), v)
145}
146
147// UpdateKeepRunning updates an existing valoper's active status
148func UpdateKeepRunning(address std.Address, keepRunning bool) {
149 v := GetByAddr(address)
150
151 // Check that the caller has permissions
152 v.Auth().AssertOnAuthList()
153
154 // Update status
155 v.KeepRunning = keepRunning
156
157 // Save the valoper info
158 valopers.Set(address.String(), v)
159}
160
161// GetByAddr fetches the valoper using the address, if present
162func GetByAddr(address std.Address) Valoper {
163 valoperRaw, exists := valopers.Get(address.String())
164 if !exists {
165 panic(ErrValoperMissing)
166 }
167
168 return valoperRaw.(Valoper)
169}
170
171// Render renders the current valoper set.
172// "/r/gnoland/valopers" lists all valopers, paginated.
173// "/r/gnoland/valopers:addr" shows the detail for the valoper with the addr.
174func Render(fullPath string) string {
175 req := realmpath.Parse(fullPath)
176 if req.Path == "" {
177 return renderHome(fullPath)
178 } else {
179 addr := req.Path
180 if len(addr) < 2 || addr[:2] != "g1" {
181 return "invalid address " + addr
182 }
183 valoperRaw, exists := valopers.Get(addr)
184 if !exists {
185 return "unknown address " + addr
186 }
187 v := valoperRaw.(Valoper)
188 return "Valoper's details:\n" + v.Render()
189 }
190}
191
192func renderHome(path string) string {
193 // if there are no valopers, display instructions
194 if valopers.Size() == 0 {
195 return ufmt.Sprintf("%s\n\nNo valopers to display.", instructions)
196 }
197
198 page := pager.NewPager(valopers, 50, false).MustGetPageByPath(path)
199
200 output := ""
201
202 // if we are on the first page, display instructions
203 if page.PageNumber == 1 {
204 output += ufmt.Sprintf("%s\n\n", instructions)
205 }
206
207 for _, item := range page.Items {
208 v := item.Value.(Valoper)
209 output += ufmt.Sprintf(" * [%s](/r/gnoland/valopers:%s) - [profile](/r/demo/profile:u/%s)\n",
210 v.Moniker, v.Address, v.Auth().Owner())
211 }
212
213 output += "\n"
214 output += page.Picker(path)
215 return output
216}
217
218// Validate checks if the fields of the Valoper are valid
219func (v *Valoper) Validate() error {
220 errs := &combinederr.CombinedError{}
221
222 errs.Add(validateMoniker(v.Moniker))
223 errs.Add(validateDescription(v.Description))
224 errs.Add(validateBech32(v.Address))
225 errs.Add(validatePubKey(v.PubKey))
226
227 if errs.Size() == 0 {
228 return nil
229 }
230
231 return errs
232}
233
234// Render renders a single valoper with their information
235func (v Valoper) Render() string {
236 output := ufmt.Sprintf("## %s\n", v.Moniker)
237
238 if v.Description != "" {
239 output += ufmt.Sprintf("%s\n\n", v.Description)
240 }
241
242 output += ufmt.Sprintf("- Address: %s\n", v.Address.String())
243 output += ufmt.Sprintf("- PubKey: %s\n\n", v.PubKey)
244 output += ufmt.Sprintf("[Profile link](/r/demo/profile:u/%s)\n", v.Address)
245
246 return output
247}
248
249// isValoper checks if the valoper exists
250func isValoper(address std.Address) bool {
251 _, exists := valopers.Get(address.String())
252
253 return exists
254}
255
256// validateMoniker checks if the moniker is valid
257func validateMoniker(moniker string) error {
258 if moniker == "" {
259 return ErrInvalidMoniker
260 }
261
262 if len(moniker) > MonikerMaxLength {
263 return ErrInvalidMoniker
264 }
265
266 if !validateMonikerRe.MatchString(moniker) {
267 return ErrInvalidMoniker
268 }
269
270 return nil
271}
272
273// validateDescription checks if the description is valid
274func validateDescription(description string) error {
275 if description == "" {
276 return ErrInvalidDescription
277 }
278
279 if len(description) > DescriptionMaxLength {
280 return ErrInvalidDescription
281 }
282
283 return nil
284}
285
286// validateBech32 checks if the value is a valid bech32 address
287func validateBech32(address std.Address) error {
288 if !std.Address.IsValid(address) {
289 return ErrInvalidAddress
290 }
291
292 return nil
293}
294
295// validatePubKey checks if the public key is valid
296func validatePubKey(pubKey string) error {
297 if _, _, err := bech32.DecodeNoLimit(pubKey); err != nil {
298 return err
299 }
300
301 return nil
302}