valopers.gno
7.75 Kb ยท 300 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(cur realm, address_XXX std.Address, member std.Address) {
57 v := GetByAddr(address_XXX)
58 if err := v.Auth().AddToAuthListByPrevious(member); err != nil {
59 panic(err)
60 }
61}
62
63func DeleteFromAuthList(cur realm, address_XXX std.Address, member std.Address) {
64 v := GetByAddr(address_XXX)
65 if err := v.Auth().DeleteFromAuthListByPrevious(member); err != nil {
66 panic(err)
67 }
68}
69
70// Register registers a new valoper
71func Register(cur realm, moniker string, description string, address_XXX std.Address, pubKey string) {
72 // Check if a fee is enforced
73 if !minFee.IsZero() {
74 sentCoins := std.OriginSend()
75
76 // Coins must be sent and cover the min fee
77 if len(sentCoins) != 1 || sentCoins[0].IsLT(minFee) {
78 panic(ufmt.Sprintf("payment must not be less than %d%s", minFee.Amount, minFee.Denom))
79 }
80 }
81
82 // Check if the valoper is already registered
83 if isValoper(address_XXX) {
84 panic(ErrValoperExists)
85 }
86
87 v := Valoper{
88 Moniker: moniker,
89 Description: description,
90 Address: address_XXX,
91 PubKey: pubKey,
92 KeepRunning: true,
93 auth: authorizable.NewAuthorizableWithOrigin(),
94 }
95
96 if err := v.Validate(); err != nil {
97 panic(err)
98 }
99
100 // TODO add address derivation from public key
101 // (when the laws of gno make it possible)
102
103 // Save the valoper to the set
104 valopers.Set(v.Address.String(), v)
105}
106
107// UpdateMoniker updates an existing valoper's moniker
108func UpdateMoniker(cur realm, address_XXX std.Address, moniker string) {
109 // Check that the moniker is not empty
110 if err := validateMoniker(moniker); err != nil {
111 panic(err)
112 }
113
114 v := GetByAddr(address_XXX)
115
116 // Check that the caller has permissions
117 v.Auth().AssertPreviousOnAuthList()
118
119 // Update the moniker
120 v.Moniker = moniker
121
122 // Save the valoper info
123 valopers.Set(address_XXX.String(), v)
124}
125
126// UpdateDescription updates an existing valoper's description
127func UpdateDescription(cur realm, address_XXX std.Address, description string) {
128 // Check that the description is not empty
129 if err := validateDescription(description); err != nil {
130 panic(err)
131 }
132
133 v := GetByAddr(address_XXX)
134
135 // Check that the caller has permissions
136 v.Auth().AssertPreviousOnAuthList()
137
138 // Update the description
139 v.Description = description
140
141 // Save the valoper info
142 valopers.Set(address_XXX.String(), v)
143}
144
145// UpdateKeepRunning updates an existing valoper's active status
146func UpdateKeepRunning(cur realm, address_XXX std.Address, keepRunning bool) {
147 v := GetByAddr(address_XXX)
148
149 // Check that the caller has permissions
150 v.Auth().AssertPreviousOnAuthList()
151
152 // Update status
153 v.KeepRunning = keepRunning
154
155 // Save the valoper info
156 valopers.Set(address_XXX.String(), v)
157}
158
159// GetByAddr fetches the valoper using the address, if present
160func GetByAddr(address_XXX std.Address) Valoper {
161 valoperRaw, exists := valopers.Get(address_XXX.String())
162 if !exists {
163 panic(ErrValoperMissing)
164 }
165
166 return valoperRaw.(Valoper)
167}
168
169// Render renders the current valoper set.
170// "/r/gnoland/valopers" lists all valopers, paginated.
171// "/r/gnoland/valopers:addr" shows the detail for the valoper with the addr.
172func Render(fullPath string) string {
173 req := realmpath.Parse(fullPath)
174 if req.Path == "" {
175 return renderHome(fullPath)
176 } else {
177 addr := req.Path
178 if len(addr) < 2 || addr[:2] != "g1" {
179 return "invalid address " + addr
180 }
181 valoperRaw, exists := valopers.Get(addr)
182 if !exists {
183 return "unknown address " + addr
184 }
185 v := valoperRaw.(Valoper)
186 return "Valoper's details:\n" + v.Render()
187 }
188}
189
190func renderHome(path string) string {
191 // if there are no valopers, display instructions
192 if valopers.Size() == 0 {
193 return ufmt.Sprintf("%s\n\nNo valopers to display.", instructions)
194 }
195
196 page := pager.NewPager(valopers, 50, false).MustGetPageByPath(path)
197
198 output := ""
199
200 // if we are on the first page, display instructions
201 if page.PageNumber == 1 {
202 output += ufmt.Sprintf("%s\n\n", instructions)
203 }
204
205 for _, item := range page.Items {
206 v := item.Value.(Valoper)
207 output += ufmt.Sprintf(" * [%s](/r/gnoland/valopers:%s) - [profile](/r/demo/profile:u/%s)\n",
208 v.Moniker, v.Address, v.Auth().Owner())
209 }
210
211 output += "\n"
212 output += page.Picker(path)
213 return output
214}
215
216// Validate checks if the fields of the Valoper are valid
217func (v *Valoper) Validate() error {
218 errs := &combinederr.CombinedError{}
219
220 errs.Add(validateMoniker(v.Moniker))
221 errs.Add(validateDescription(v.Description))
222 errs.Add(validateBech32(v.Address))
223 errs.Add(validatePubKey(v.PubKey))
224
225 if errs.Size() == 0 {
226 return nil
227 }
228
229 return errs
230}
231
232// Render renders a single valoper with their information
233func (v Valoper) Render() string {
234 output := ufmt.Sprintf("## %s\n", v.Moniker)
235
236 if v.Description != "" {
237 output += ufmt.Sprintf("%s\n\n", v.Description)
238 }
239
240 output += ufmt.Sprintf("- Address: %s\n", v.Address.String())
241 output += ufmt.Sprintf("- PubKey: %s\n\n", v.PubKey)
242 output += ufmt.Sprintf("[Profile link](/r/demo/profile:u/%s)\n", v.Address)
243
244 return output
245}
246
247// isValoper checks if the valoper exists
248func isValoper(address_XXX std.Address) bool {
249 _, exists := valopers.Get(address_XXX.String())
250
251 return exists
252}
253
254// validateMoniker checks if the moniker is valid
255func validateMoniker(moniker string) error {
256 if moniker == "" {
257 return ErrInvalidMoniker
258 }
259
260 if len(moniker) > MonikerMaxLength {
261 return ErrInvalidMoniker
262 }
263
264 if !validateMonikerRe.MatchString(moniker) {
265 return ErrInvalidMoniker
266 }
267
268 return nil
269}
270
271// validateDescription checks if the description is valid
272func validateDescription(description string) error {
273 if description == "" {
274 return ErrInvalidDescription
275 }
276
277 if len(description) > DescriptionMaxLength {
278 return ErrInvalidDescription
279 }
280
281 return nil
282}
283
284// validateBech32 checks if the value is a valid bech32 address
285func validateBech32(address_XXX std.Address) error {
286 if !std.Address.IsValid(address_XXX) {
287 return ErrInvalidAddress
288 }
289
290 return nil
291}
292
293// validatePubKey checks if the public key is valid
294func validatePubKey(pubKey string) error {
295 if _, _, err := bech32.DecodeNoLimit(pubKey); err != nil {
296 return err
297 }
298
299 return nil
300}