store.gno
3.75 Kb · 174 lines
1package users
2
3import (
4 "chain"
5 "chain/runtime"
6 "regexp"
7
8 "gno.land/p/nt/avl/v0"
9 "gno.land/p/nt/ufmt/v0"
10)
11
12var (
13 nameStore = avl.NewTree() // name/aliases > *UserData
14 addressStore = avl.NewTree() // address > *UserData
15
16 reAddressLookalike = regexp.MustCompile(`^g1[a-z0-9]{20,38}$`)
17 reAlphanum = regexp.MustCompile(`^[a-zA-Z0-9_]{1,64}$`)
18)
19
20const (
21 RegisterUserEvent = "Registered"
22 UpdateNameEvent = "Updated"
23 DeleteUserEvent = "Deleted"
24)
25
26type UserData struct {
27 addr address
28 username string // contains the latest name of a user
29 deleted bool
30}
31
32func (u UserData) Name() string {
33 return u.username
34}
35
36func (u UserData) Addr() address {
37 return u.addr
38}
39
40func (u UserData) IsDeleted() bool {
41 return u.deleted
42}
43
44// RenderLink provides a render link to the user page on gnoweb
45// `linkText` is optional
46func (u UserData) RenderLink(linkText string) string {
47 if linkText == "" {
48 return ufmt.Sprintf("[@%s](/u/%s)", u.username, u.username)
49 }
50
51 return ufmt.Sprintf("[%s](/u/%s)", linkText, u.username)
52}
53
54// RegisterUser adds a new user to the system.
55func RegisterUser(cur realm, name string, address_XXX address) error {
56 // At genesis (height 0), allow any caller to register users.
57 // After genesis, only whitelisted controllers can register.
58 if runtime.ChainHeight() > 0 && !controllers.Has(runtime.PreviousRealm().Address()) {
59 return NewErrNotWhitelisted()
60 }
61
62 // Validate name
63 if err := validateName(name); err != nil {
64 return err
65 }
66
67 // Validate address
68 if !address_XXX.IsValid() {
69 return ErrInvalidAddress
70 }
71
72 // Check if name is taken
73 if nameStore.Has(name) {
74 return ErrNameTaken
75 }
76
77 raw, ok := addressStore.Get(address_XXX.String())
78 if ok {
79 // Cannot re-register after deletion
80 if raw.(*UserData).IsDeleted() {
81 return ErrDeletedUser
82 }
83
84 // For a second name, use UpdateName
85 return ErrAlreadyHasName
86 }
87
88 // Create UserData
89 data := &UserData{
90 addr: address_XXX,
91 username: name,
92 deleted: false,
93 }
94
95 // Set corresponding stores
96 nameStore.Set(name, data)
97 addressStore.Set(address_XXX.String(), data)
98
99 chain.Emit(RegisterUserEvent,
100 "name", name,
101 "address", address_XXX.String(),
102 )
103 return nil
104}
105
106// UpdateName adds a name that is associated with a specific address
107// All previous names are preserved and resolvable.
108// The new name is the default value returned for address lookups.
109func (u *UserData) UpdateName(newName string) error {
110 if u == nil { // either doesnt exists or was deleted
111 return ErrUserNotExistOrDeleted
112 }
113
114 // Validate caller
115 if !controllers.Has(runtime.CurrentRealm().Address()) {
116 return NewErrNotWhitelisted()
117 }
118
119 // Validate name
120 if err := validateName(newName); err != nil {
121 return err
122 }
123
124 // Check if the requested Alias is already taken
125 if nameStore.Has(newName) {
126 return ErrNameTaken
127 }
128
129 u.username = newName
130 nameStore.Set(newName, u)
131
132 chain.Emit(UpdateNameEvent,
133 "alias", newName,
134 "address", u.addr.String(),
135 )
136 return nil
137}
138
139// Delete marks a user and all their aliases as deleted.
140func (u *UserData) Delete() error {
141 if u == nil {
142 return ErrUserNotExistOrDeleted
143 }
144
145 // Validate caller
146 if !controllers.Has(runtime.CurrentRealm().Address()) {
147 return NewErrNotWhitelisted()
148 }
149
150 u.deleted = true
151
152 chain.Emit(DeleteUserEvent, "address", u.addr.String())
153 return nil
154}
155
156// Validate validates username and address passed in
157// Most of the validation is done in the controllers
158// This provides more flexibility down the line
159func validateName(username string) error {
160 if username == "" {
161 return ErrEmptyUsername
162 }
163
164 if !reAlphanum.MatchString(username) {
165 return ErrInvalidUsername
166 }
167
168 // Check if the username can be decoded or looks like a valid address
169 if address(username).IsValid() || reAddressLookalike.MatchString(username) {
170 return ErrNameLikeAddress
171 }
172
173 return nil
174}