store.gno
3.61 Kb ยท 173 lines
1package users
2
3import (
4 "chain"
5 "chain/runtime"
6 "regexp"
7
8 "gno.land/p/nt/avl"
9 "gno.land/p/nt/ufmt"
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 // Validate caller
57 if !controllers.Has(runtime.PreviousRealm().Address()) {
58 return NewErrNotWhitelisted()
59 }
60
61 // Validate name
62 if err := validateName(name); err != nil {
63 return err
64 }
65
66 // Validate address
67 if !address_XXX.IsValid() {
68 return ErrInvalidAddress
69 }
70
71 // Check if name is taken
72 if nameStore.Has(name) {
73 return ErrNameTaken
74 }
75
76 raw, ok := addressStore.Get(address_XXX.String())
77 if ok {
78 // Cannot re-register after deletion
79 if raw.(*UserData).IsDeleted() {
80 return ErrDeletedUser
81 }
82
83 // For a second name, use UpdateName
84 return ErrAlreadyHasName
85 }
86
87 // Create UserData
88 data := &UserData{
89 addr: address_XXX,
90 username: name,
91 deleted: false,
92 }
93
94 // Set corresponding stores
95 nameStore.Set(name, data)
96 addressStore.Set(address_XXX.String(), data)
97
98 chain.Emit(RegisterUserEvent,
99 "name", name,
100 "address", address_XXX.String(),
101 )
102 return nil
103}
104
105// UpdateName adds a name that is associated with a specific address
106// All previous names are preserved and resolvable.
107// The new name is the default value returned for address lookups.
108func (u *UserData) UpdateName(newName string) error {
109 if u == nil { // either doesnt exists or was deleted
110 return ErrUserNotExistOrDeleted
111 }
112
113 // Validate caller
114 if !controllers.Has(runtime.CurrentRealm().Address()) {
115 return NewErrNotWhitelisted()
116 }
117
118 // Validate name
119 if err := validateName(newName); err != nil {
120 return err
121 }
122
123 // Check if the requested Alias is already taken
124 if nameStore.Has(newName) {
125 return ErrNameTaken
126 }
127
128 u.username = newName
129 nameStore.Set(newName, u)
130
131 chain.Emit(UpdateNameEvent,
132 "alias", newName,
133 "address", u.addr.String(),
134 )
135 return nil
136}
137
138// Delete marks a user and all their aliases as deleted.
139func (u *UserData) Delete() error {
140 if u == nil {
141 return ErrUserNotExistOrDeleted
142 }
143
144 // Validate caller
145 if !controllers.Has(runtime.CurrentRealm().Address()) {
146 return NewErrNotWhitelisted()
147 }
148
149 u.deleted = true
150
151 chain.Emit(DeleteUserEvent, "address", u.addr.String())
152 return nil
153}
154
155// Validate validates username and address passed in
156// Most of the validation is done in the controllers
157// This provides more flexibility down the line
158func validateName(username string) error {
159 if username == "" {
160 return ErrEmptyUsername
161 }
162
163 if !reAlphanum.MatchString(username) {
164 return ErrInvalidUsername
165 }
166
167 // Check if the username can be decoded or looks like a valid address
168 if address(username).IsValid() || reAddressLookalike.MatchString(username) {
169 return ErrNameLikeAddress
170 }
171
172 return nil
173}