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