1package users
2
3import (
4 "regexp"
5 "std"
6 "strconv"
7 "strings"
8
9 "gno.land/p/demo/avl"
10 "gno.land/p/demo/avl/pager"
11 "gno.land/p/demo/avlhelpers"
12 "gno.land/p/demo/users"
13)
14
15//----------------------------------------
16// State
17
18var (
19 admin std.Address = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" // @moul
20
21 restricted avl.Tree // Name -> true - restricted name
22 name2User avl.Tree // Name -> *users.User
23 addr2User avl.Tree // std.Address -> *users.User
24 invites avl.Tree // string(inviter+":"+invited) -> true
25 counter int // user id counter
26 minFee int64 = 20 * 1_000_000 // minimum gnot must be paid to register.
27 maxFeeMult int64 = 10 // maximum multiples of minFee accepted.
28)
29
30//----------------------------------------
31// Top-level functions
32
33func Register(inviter std.Address, name string, profile string) {
34 // assert CallTx call.
35 std.AssertOriginCall()
36 // assert invited or paid.
37 caller := std.GetCallerAt(2)
38 if caller != std.GetOrigCaller() {
39 panic("should not happen") // because std.AssertOrigCall().
40 }
41
42 sentCoins := std.GetOrigSend()
43 minCoin := std.NewCoin("ugnot", minFee)
44
45 if inviter == "" {
46 // banker := std.GetBanker(std.BankerTypeOrigSend)
47 if len(sentCoins) == 1 && sentCoins[0].IsGTE(minCoin) {
48 if sentCoins[0].Amount > minFee*maxFeeMult {
49 panic("payment must not be greater than " + strconv.Itoa(int(minFee*maxFeeMult)))
50 } else {
51 // ok
52 }
53 } else {
54 panic("payment must not be less than " + strconv.Itoa(int(minFee)))
55 }
56 } else {
57 invitekey := inviter.String() + ":" + caller.String()
58 _, ok := invites.Get(invitekey)
59 if !ok {
60 panic("invalid invitation")
61 }
62 invites.Remove(invitekey)
63 }
64
65 // assert not already registered.
66 _, ok := name2User.Get(name)
67 if ok {
68 panic("name already registered: " + name)
69 }
70 _, ok = addr2User.Get(caller.String())
71 if ok {
72 panic("address already registered: " + caller.String())
73 }
74
75 isInviterAdmin := inviter == admin
76
77 // check for restricted name
78 if _, isRestricted := restricted.Get(name); isRestricted {
79 // only address invite by the admin can register restricted name
80 if !isInviterAdmin {
81 panic("restricted name: " + name)
82 }
83
84 restricted.Remove(name)
85 }
86
87 // assert name is valid.
88 // admin inviter can bypass name restriction
89 if !isInviterAdmin && !reName.MatchString(name) {
90 panic("invalid name: " + name + " (must be at least 6 characters, lowercase alphanumeric with underscore)")
91 }
92
93 // remainder of fees go toward invites.
94 invites := int(0)
95 if len(sentCoins) == 1 {
96 if sentCoins[0].Denom == "ugnot" && sentCoins[0].Amount >= minFee {
97 invites = int(sentCoins[0].Amount / minFee)
98 if inviter == "" && invites > 0 {
99 invites -= 1
100 }
101 }
102 }
103 // register.
104 counter++
105 user := &users.User{
106 Address: caller,
107 Name: name,
108 Profile: profile,
109 Number: counter,
110 Invites: invites,
111 Inviter: inviter,
112 }
113 name2User.Set(name, user)
114 addr2User.Set(caller.String(), user)
115}
116
117func Invite(invitee string) {
118 // assert CallTx call.
119 std.AssertOriginCall()
120 // get caller/inviter.
121 caller := std.GetCallerAt(2)
122 if caller != std.GetOrigCaller() {
123 panic("should not happen") // because std.AssertOrigCall().
124 }
125 lines := strings.Split(invitee, "\n")
126 if caller == admin {
127 // nothing to do, all good
128 } else {
129 // ensure has invites.
130 userI, ok := addr2User.Get(caller.String())
131 if !ok {
132 panic("user unknown")
133 }
134 user := userI.(*users.User)
135 if user.Invites <= 0 {
136 panic("user has no invite tokens")
137 }
138 user.Invites -= len(lines)
139 if user.Invites < 0 {
140 panic("user has insufficient invite tokens")
141 }
142 }
143 // for each line...
144 for _, line := range lines {
145 if line == "" {
146 continue // file bodies have a trailing newline.
147 } else if strings.HasPrefix(line, `//`) {
148 continue // comment
149 }
150 // record invite.
151 invitekey := string(caller) + ":" + string(line)
152 invites.Set(invitekey, true)
153 }
154}
155
156func GrantInvites(invites string) {
157 // assert CallTx call.
158 std.AssertOriginCall()
159 // assert admin.
160 caller := std.GetCallerAt(2)
161 if caller != std.GetOrigCaller() {
162 panic("should not happen") // because std.AssertOrigCall().
163 }
164 if caller != admin {
165 panic("unauthorized")
166 }
167 // for each line...
168 lines := strings.Split(invites, "\n")
169 for _, line := range lines {
170 if line == "" {
171 continue // file bodies have a trailing newline.
172 } else if strings.HasPrefix(line, `//`) {
173 continue // comment
174 }
175 // parse name and invites.
176 var name string
177 var invites int
178 parts := strings.Split(line, ":")
179 if len(parts) == 1 { // short for :1.
180 name = parts[0]
181 invites = 1
182 } else if len(parts) == 2 {
183 name = parts[0]
184 invites_, err := strconv.Atoi(parts[1])
185 if err != nil {
186 panic(err)
187 }
188 invites = int(invites_)
189 } else {
190 panic("should not happen")
191 }
192 // give invites.
193 userI, ok := name2User.Get(name)
194 if !ok {
195 // maybe address.
196 userI, ok = addr2User.Get(name)
197 if !ok {
198 panic("invalid user " + name)
199 }
200 }
201 user := userI.(*users.User)
202 user.Invites += invites
203 }
204}
205
206// Any leftover fees go toward invitations.
207func SetMinFee(newMinFee int64) {
208 // assert CallTx call.
209 std.AssertOriginCall()
210 // assert admin caller.
211 caller := std.GetCallerAt(2)
212 if caller != admin {
213 panic("unauthorized")
214 }
215 // update global variables.
216 minFee = newMinFee
217}
218
219// This helps prevent fat finger accidents.
220func SetMaxFeeMultiple(newMaxFeeMult int64) {
221 // assert CallTx call.
222 std.AssertOriginCall()
223 // assert admin caller.
224 caller := std.GetCallerAt(2)
225 if caller != admin {
226 panic("unauthorized")
227 }
228 // update global variables.
229 maxFeeMult = newMaxFeeMult
230}
231
232//----------------------------------------
233// Exposed public functions
234
235func GetUserByName(name string) *users.User {
236 userI, ok := name2User.Get(name)
237 if !ok {
238 return nil
239 }
240 return userI.(*users.User)
241}
242
243func GetUserByAddress(addr std.Address) *users.User {
244 userI, ok := addr2User.Get(addr.String())
245 if !ok {
246 return nil
247 }
248 return userI.(*users.User)
249}
250
251// unlike GetUserByName, input must be "@" prefixed for names.
252func GetUserByAddressOrName(input users.AddressOrName) *users.User {
253 name, isName := input.GetName()
254 if isName {
255 return GetUserByName(name)
256 }
257 return GetUserByAddress(std.Address(input))
258}
259
260// Get a list of user names starting from the given prefix. Limit the
261// number of results to maxResults. (This can be used for a name search tool.)
262func ListUsersByPrefix(prefix string, maxResults int) []string {
263 return avlhelpers.ListByteStringKeysByPrefix(name2User, prefix, maxResults)
264}
265
266func Resolve(input users.AddressOrName) std.Address {
267 name, isName := input.GetName()
268 if !isName {
269 return std.Address(input) // TODO check validity
270 }
271
272 user := GetUserByName(name)
273 return user.Address
274}
275
276// Add restricted name to the list
277func AdminAddRestrictedName(name string) {
278 // assert CallTx call.
279 std.AssertOriginCall()
280 // get caller
281 caller := std.GetOrigCaller()
282 // assert admin
283 if caller != admin {
284 panic("unauthorized")
285 }
286
287 if user := GetUserByName(name); user != nil {
288 panic("already registered name")
289 }
290
291 // register restricted name
292
293 restricted.Set(name, true)
294}
295
296//----------------------------------------
297// Constants
298
299// NOTE: name length must be clearly distinguishable from a bech32 address.
300var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{5,16}$`)
301
302//----------------------------------------
303// Render main page
304
305func Render(fullPath string) string {
306 path, _ := splitPathAndQuery(fullPath)
307 if path == "" {
308 return renderHome(fullPath)
309 } else if len(path) >= 38 { // 39? 40?
310 if path[:2] != "g1" {
311 return "invalid address " + path
312 }
313 user := GetUserByAddress(std.Address(path))
314 if user == nil {
315 // TODO: display basic information about account.
316 return "unknown address " + path
317 }
318 return user.Render()
319 } else {
320 user := GetUserByName(path)
321 if user == nil {
322 return "unknown username " + path
323 }
324 return user.Render()
325 }
326}
327
328func renderHome(path string) string {
329 doc := ""
330
331 page := pager.NewPager(&name2User, 50).MustGetPageByPath(path)
332
333 for _, item := range page.Items {
334 user := item.Value.(*users.User)
335 doc += " * [" + user.Name + "](/r/demo/users:" + user.Name + ")\n"
336 }
337 doc += "\n"
338 doc += page.Selector()
339 return doc
340}
341
342func splitPathAndQuery(fullPath string) (string, string) {
343 parts := strings.SplitN(fullPath, "?", 2)
344 path := parts[0]
345 queryString := ""
346 if len(parts) > 1 {
347 queryString = "?" + parts[1]
348 }
349 return path, queryString
350}
users.gno
8.33 Kb ยท 350 lines