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}