Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}