users.gno

7.66 Kb ยท 327 lines
  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}