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