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}