Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

nomaq.gno

14.09 Kb · 646 lines
  1package nomaq
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"chain/runtime/unsafe"
  7	"strconv"
  8	"strings"
  9
 10	"gno.land/p/nt/avl/v0"
 11)
 12
 13const (
 14	pkgPath            = "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/nomaq"
 15	nativeDenom        = "ugnot"
 16	defaultLaunchFee   = int64(1_000_000_000) // 1000 GNOT
 17	maxNameLen         = 64
 18	maxImageLen        = 200
 19	maxSupplyBound     = 1_000_000
 20	maxRoyaltyBps      = 1000
 21	maxSymbolLen       = 11
 22	maxSlugLen         = 11
 23	minSlugLen         = 2
 24	defaultProtocolBps = int64(200) // 2% of the sale price
 25	maxProtocolBps     = int64(500)
 26	// defaultGenesisShareBps is 25% of the protocol fee, not of the sale price.
 27	// 10000 would send the whole protocol fee to GenesisSink.
 28	defaultGenesisShareBps = int64(2500)
 29	maxGenesisShareBps     = int64(10000)
 30)
 31
 32type collection struct {
 33	slug       string
 34	name       string
 35	symbol     string
 36	cover      string
 37	pkg        string
 38	creator    address
 39	maxSupply  int
 40	mintPrice  int64
 41	minted     int
 42	royaltyBps int
 43}
 44
 45type reservation struct {
 46	who  address
 47	paid int64
 48}
 49
 50var (
 51	bySlug          avl.Tree
 52	byPkg           avl.Tree
 53	reserved        avl.Tree
 54	inited          bool
 55	admin           address
 56	self            address
 57	protocolSink    address
 58	launchFee       int64
 59	protocolBps     int64
 60	genesisPkg      string
 61	genesisSink     address
 62	genesisShareBps int64
 63	discountPusher  string
 64	holderDisc      avl.Tree
 65)
 66
 67func Init(cur realm) {
 68	if inited {
 69		panic("factory: already initialized")
 70	}
 71	if !cur.Previous().IsUserCall() {
 72		panic("factory: EOA only")
 73	}
 74	admin = cur.Previous().Address()
 75	protocolSink = admin
 76	self = cur.Address()
 77	bySlug = avl.Tree{}
 78	byPkg = avl.Tree{}
 79	reserved = avl.Tree{}
 80	holderDisc = avl.Tree{}
 81	genesisPkg = ""
 82	genesisShareBps = defaultGenesisShareBps
 83	discountPusher = ""
 84	var z address
 85	genesisSink = z
 86	launchFee = defaultLaunchFee
 87	protocolBps = defaultProtocolBps
 88	inited = true
 89	chain.Emit("Init", "admin", admin.String())
 90}
 91
 92func Address() address {
 93	return chain.PackageAddress(pkgPath)
 94}
 95
 96func LaunchFee() int64 {
 97	requireInit()
 98	return launchFee
 99}
100
101func Admin() string {
102	requireInit()
103	return admin.String()
104}
105
106func SetLaunchFee(cur realm, ugnot int64) {
107	requireAdmin(cur)
108	if ugnot < 0 {
109		panic("factory: launch fee")
110	}
111	launchFee = ugnot
112	chain.Emit("SetLaunchFee", "ugnot", strconv.FormatInt(ugnot, 10))
113}
114
115func ProtocolBps() int64 {
116	requireInit()
117	if protocolBps <= 0 {
118		return defaultProtocolBps
119	}
120	return protocolBps
121}
122
123func SetProtocolBps(cur realm, bps int64) {
124	requireAdmin(cur)
125	if bps < 0 || bps > maxProtocolBps {
126		panic("factory: protocol bps")
127	}
128	protocolBps = bps
129	chain.Emit("SetProtocolBps", "bps", strconv.FormatInt(bps, 10))
130}
131
132func ProtocolSink() address {
133	requireInit()
134	return protocolSink
135}
136
137func SetProtocolSink(cur realm, next address) {
138	requireAdmin(cur)
139	if !next.IsValid() {
140		panic("factory: invalid address")
141	}
142	if next == protocolSink {
143		panic("factory: already sink")
144	}
145	protocolSink = next
146	chain.Emit("SetProtocolSink", "addr", next.String())
147}
148
149func GenesisPkg() string {
150	requireInit()
151	return genesisPkg
152}
153
154func GenesisSink() address {
155	requireInit()
156	return genesisSink
157}
158
159// GenesisShareBps is the holder share of the protocol fee, in bps of 10000.
160// It is 0 until a genesis package is attached. After that it returns the
161// stored value, including 0. The Init default is 2500 (25%).
162func GenesisShareBps() int64 {
163	requireInit()
164	if genesisPkg == "" {
165		return 0
166	}
167	return genesisShareBps
168}
169
170// SetGenesisShareBps changes the holder share for later sales.
171// bps is 0 through 10000. Only the admin EOA may call it.
172// Collection books must read GenesisShareBps at Buy time.
173func SetGenesisShareBps(cur realm, bps int64) {
174	requireAdmin(cur)
175	if bps < 0 || bps > maxGenesisShareBps {
176		panic("factory: genesis share")
177	}
178	genesisShareBps = bps
179	chain.Emit("SetGenesisShareBps", "bps", strconv.FormatInt(bps, 10))
180}
181
182func HolderDiscount(owner string) int64 {
183	requireInit()
184	if genesisPkg == "" {
185		return 0
186	}
187	v := holderDisc.Get(strings.ToLower(strings.TrimSpace(owner)))
188	if v == nil {
189		return 0
190	}
191	return v.(int64)
192}
193
194func AttachGenesis(cur realm) {
195	requireInit()
196	prev := cur.Previous()
197	if !prev.IsCode() {
198		panic("factory: genesis realm")
199	}
200	eoa := prev.Previous()
201	if !eoa.IsUserCall() || eoa.Address() != admin {
202		panic("factory: not admin")
203	}
204	pkg := prev.PkgPath()
205	if pkg == "" {
206		panic("factory: genesis pkg")
207	}
208	if genesisPkg != "" && genesisPkg != pkg {
209		panic("factory: genesis set")
210	}
211	genesisPkg = pkg
212	genesisSink = prev.Address()
213	chain.Emit("AttachGenesis", "pkg", pkg)
214}
215
216func SetGenesis(cur realm, pkg string) {
217	requireAdmin(cur)
218	pkg = strings.TrimSpace(pkg)
219	if pkg == "" || !strings.HasPrefix(pkg, "gno.land/r/") {
220		panic("factory: genesis pkg")
221	}
222	genesisPkg = pkg
223	genesisSink = chain.PackageAddress(pkg)
224	chain.Emit("SetGenesis", "pkg", pkg)
225}
226
227func SetDiscountPusher(cur realm, pkg string) {
228	requireAdmin(cur)
229	pkg = strings.TrimSpace(pkg)
230	if pkg == "" || !strings.HasPrefix(pkg, "gno.land/r/") {
231		panic("factory: pusher pkg")
232	}
233	discountPusher = pkg
234	chain.Emit("SetDiscountPusher", "pkg", pkg)
235}
236
237func DiscountPusher() string {
238	requireInit()
239	return discountPusher
240}
241
242func NoteHolderDiscount(cur realm, owner string, bps int64) {
243	requireInit()
244	prev := cur.Previous()
245	if !prev.IsCode() {
246		panic("factory: not genesis")
247	}
248	from := prev.PkgPath()
249	ok := (genesisPkg != "" && from == genesisPkg) || (discountPusher != "" && from == discountPusher)
250	if !ok {
251		panic("factory: not genesis")
252	}
253	if bps < 0 {
254		panic("factory: discount")
255	}
256	who := strings.ToLower(strings.TrimSpace(owner))
257	if who == "" {
258		panic("factory: owner")
259	}
260	if bps == 0 {
261		holderDisc.Remove(who)
262		return
263	}
264	holderDisc.Set(who, bps)
265}
266
267func Register(cur realm, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) {
268	requireInit()
269	prev := cur.Previous()
270	if !prev.IsCode() {
271		panic("factory: collection realm")
272	}
273	creatorRlm := prev.Previous()
274	if !creatorRlm.IsUserCall() {
275		panic("factory: EOA creator")
276	}
277	pkg := prev.PkgPath()
278	slug := lastElem(pkg)
279	res := mustReserve(slug)
280	if creatorRlm.Address() != res.who {
281		panic("factory: reserved")
282	}
283	reserved.Remove(slug)
284	register(pkg, creatorRlm.Address(), name, symbol, cover, maxSupply, mintPrice, royaltyBps)
285}
286
287func Reserve(cur realm, slug string) {
288	requireInit()
289	if !cur.Previous().IsUserCall() {
290		panic("factory: EOA only")
291	}
292	slug = strings.ToLower(strings.TrimSpace(slug))
293	if !validSlug(slug) {
294		panic("factory: slug")
295	}
296	if bySlug.Has(slug) || reserved.Has(slug) {
297		panic("factory: slug taken")
298	}
299	caller := cur.Previous().Address()
300	paid := reserveDue(caller)
301	takeFactorySend(paid)
302	reserved.Set(slug, &reservation{who: caller, paid: paid})
303	chain.Emit("Reserve", "slug", slug, "paid", strconv.FormatInt(paid, 10))
304}
305
306// ReserveDue is the ugnot Reserve will require from addr. The factory admin
307// (the wallet that called Init) pays 0. Everyone else pays LaunchFee.
308func ReserveDue(addr string) int64 {
309	requireInit()
310	who, ok := parseAddr(addr)
311	if !ok {
312		return launchFee
313	}
314	return reserveDue(who)
315}
316
317func reserveDue(caller address) int64 {
318	if caller == admin {
319		return 0
320	}
321	return launchFee
322}
323
324func parseAddr(raw string) (address, bool) {
325	s := strings.TrimSpace(raw)
326	if s == "" {
327		var zero address
328		return zero, false
329	}
330	who := address(s)
331	if !who.IsValid() {
332		var zero address
333		return zero, false
334	}
335	return who, true
336}
337
338func CancelReserve(cur realm, slug string) {
339	requireInit()
340	if !cur.Previous().IsUserCall() {
341		panic("factory: EOA only")
342	}
343	slug = strings.ToLower(strings.TrimSpace(slug))
344	res := mustReserve(slug)
345	if res.who != cur.Previous().Address() {
346		panic("factory: not reserver")
347	}
348	if bySlug.Has(slug) {
349		panic("factory: already registered")
350	}
351	reserved.Remove(slug)
352	if res.paid > 0 {
353		bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
354		bk.SendCoins(self, res.who, chain.Coins{{Denom: nativeDenom, Amount: res.paid}})
355	}
356	chain.Emit("CancelReserve", "slug", slug, "paid", strconv.FormatInt(res.paid, 10))
357}
358
359func ReservedCreator(slug string) string {
360	requireInit()
361	slug = strings.ToLower(strings.TrimSpace(slug))
362	if !reserved.Has(slug) {
363		return ""
364	}
365	return reserved.Get(slug).(*reservation).who.String()
366}
367
368func takeFactorySend(want int64) {
369	sent := unsafe.OriginSend()
370	if want == 0 {
371		if len(sent) != 0 {
372			panic("factory: extra denoms")
373		}
374		return
375	}
376	if len(sent) != 1 || sent[0].Denom != nativeDenom || sent[0].Amount != want {
377		panic("factory: wrong payment")
378	}
379}
380
381func NoteMinted(cur realm, n int) {
382	requireInit()
383	if n < 0 {
384		panic("factory: minted")
385	}
386	prev := cur.Previous()
387	if !prev.IsCode() {
388		panic("factory: collection realm")
389	}
390	c := mustByPkg(prev.PkgPath())
391	c.minted = n
392	byPkg.Set(c.pkg, c)
393	bySlug.Set(c.slug, c)
394}
395
396func NoteRoyalty(cur realm, bps int) {
397	requireInit()
398	if bps < 0 || bps > maxRoyaltyBps {
399		panic("factory: royalty")
400	}
401	prev := cur.Previous()
402	if !prev.IsCode() {
403		panic("factory: collection realm")
404	}
405	c := mustByPkg(prev.PkgPath())
406	c.royaltyBps = bps
407	byPkg.Set(c.pkg, c)
408	bySlug.Set(c.slug, c)
409}
410
411func CollectionOf(slug string) string {
412	requireInit()
413	c := mustBySlug(slug)
414	return c.pkg
415}
416
417func CollectionAddr(slug string) string {
418	requireInit()
419	c := mustBySlug(slug)
420	return chain.PackageAddress(c.pkg).String()
421}
422
423func RoyaltyOf(slug string) int {
424	requireInit()
425	c := mustBySlug(slug)
426	return c.royaltyBps
427}
428
429func ListCollections() string {
430	requireInit()
431	out := ""
432	bySlug.Iterate("", "", func(_ string, value any) bool {
433		c := value.(*collection)
434		if out != "" {
435			out += "\n"
436		}
437		out += c.slug + "|" + c.name + "|" + c.cover + "|" + c.pkg + "|" + strconv.FormatInt(c.mintPrice, 10) + "|" + strconv.Itoa(c.maxSupply) + "|" + strconv.Itoa(c.minted) + "|" + c.creator.String() + "|" + chain.PackageAddress(c.pkg).String() + "|" + strconv.Itoa(c.royaltyBps)
438		return false
439	})
440	return out
441}
442
443func Withdraw(cur realm) {
444	requireAdmin(cur)
445	bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
446	ugnot := bk.GetCoins(self).AmountOf(nativeDenom)
447	locked := reservedPaid()
448	avail := ugnot - locked
449	if avail <= 0 {
450		panic("factory: no funds")
451	}
452	bk.SendCoins(self, admin, chain.Coins{{Denom: nativeDenom, Amount: avail}})
453	chain.Emit("Withdraw", "amount", strconv.FormatInt(avail, 10))
454}
455
456func TransferAdmin(cur realm, next address) {
457	requireAdmin(cur)
458	if !next.IsValid() {
459		panic("factory: invalid address")
460	}
461	if next == admin {
462		panic("factory: already admin")
463	}
464	admin = next
465	chain.Emit("TransferAdmin", "addr", next.String())
466}
467
468func Render(_ string) string {
469	if !inited {
470		return "# Nomaq\n\nCall Init first.\n"
471	}
472	out := "# Nomaq\n\n"
473	out += "- launch fee: " + strconv.FormatInt(launchFee, 10) + " ugnot\n"
474	out += "- admin reserve: 0 ugnot\n"
475	out += "- holder share: " + strconv.FormatInt(GenesisShareBps(), 10) + " bps of the protocol fee\n"
476	out += "- collections: " + strconv.Itoa(bySlug.Size()) + "\n\n"
477	lines := ListCollections()
478	if lines == "" {
479		out += "_No collections._\n"
480	} else {
481		out += "```\n" + lines + "\n```\n"
482	}
483	return out
484}
485
486func register(pkg string, creator address, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) {
487	if !strings.HasPrefix(pkg, "gno.land/r/") {
488		panic("factory: pkg")
489	}
490	slug := lastElem(pkg)
491	if !validSlug(slug) {
492		panic("factory: slug")
493	}
494	if !validName(name) {
495		panic("factory: name")
496	}
497	if !validSymbol(symbol) {
498		panic("factory: symbol")
499	}
500	if !validImage(cover) {
501		panic("factory: cover")
502	}
503	if maxSupply < 0 || maxSupply > maxSupplyBound {
504		panic("factory: maxSupply")
505	}
506	if mintPrice < 0 {
507		panic("factory: mintPrice")
508	}
509	if royaltyBps < 0 || royaltyBps > maxRoyaltyBps {
510		panic("factory: royalty")
511	}
512	if bySlug.Has(slug) {
513		panic("factory: slug taken")
514	}
515	if byPkg.Has(pkg) {
516		panic("factory: pkg already registered")
517	}
518	c := &collection{
519		slug:       slug,
520		name:       name,
521		symbol:     symbol,
522		cover:      cover,
523		pkg:        pkg,
524		creator:    creator,
525		maxSupply:  maxSupply,
526		mintPrice:  mintPrice,
527		royaltyBps: int(royaltyBps),
528	}
529	bySlug.Set(slug, c)
530	byPkg.Set(pkg, c)
531	chain.Emit("Register", "slug", slug, "pkg", pkg, "creator", creator.String())
532}
533
534func requireInit() {
535	if !inited {
536		panic("factory: call Init first")
537	}
538}
539
540func requireAdmin(cur realm) {
541	requireInit()
542	if !cur.Previous().IsUserCall() {
543		panic("factory: EOA only")
544	}
545	if cur.Previous().Address() != admin {
546		panic("factory: not admin")
547	}
548}
549
550func mustReserve(slug string) *reservation {
551	v := reserved.Get(slug)
552	if v == nil {
553		panic("factory: not reserved")
554	}
555	return v.(*reservation)
556}
557
558func reservedPaid() int64 {
559	var n int64
560	reserved.Iterate("", "", func(_ string, value any) bool {
561		n += value.(*reservation).paid
562		return false
563	})
564	return n
565}
566
567func mustBySlug(slug string) *collection {
568	v := bySlug.Get(slug)
569	if v == nil {
570		panic("factory: unknown collection")
571	}
572	return v.(*collection)
573}
574
575func mustByPkg(pkg string) *collection {
576	v := byPkg.Get(pkg)
577	if v == nil {
578		panic("factory: unknown collection")
579	}
580	return v.(*collection)
581}
582
583func lastElem(pkg string) string {
584	i := strings.LastIndex(pkg, "/")
585	if i < 0 || i+1 >= len(pkg) {
586		panic("factory: pkg")
587	}
588	return pkg[i+1:]
589}
590
591func validSlug(s string) bool {
592	if len(s) < minSlugLen || len(s) > maxSlugLen {
593		return false
594	}
595	if s[0] < 'a' || s[0] > 'z' {
596		return false
597	}
598	return asciiSet(s, "abcdefghijklmnopqrstuvwxyz0123456789")
599}
600
601func validSymbol(s string) bool {
602	if len(s) < 1 || len(s) > maxSymbolLen {
603		return false
604	}
605	return asciiSet(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
606}
607
608func validName(name string) bool {
609	if len(name) < 1 || len(name) > maxNameLen {
610		return false
611	}
612	return !strings.ContainsAny(name, "\n\r|")
613}
614
615func validImage(u string) bool {
616	if u == "" {
617		return true
618	}
619	if len(u) > maxImageLen {
620		return false
621	}
622	if strings.ContainsAny(u, " \t\n\r|<>\"'`") {
623		return false
624	}
625	if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {
626		return true
627	}
628	const prefix = "/samples/"
629	if !strings.HasPrefix(u, prefix) {
630		return false
631	}
632	rest := u[len(prefix):]
633	if rest == "" {
634		return false
635	}
636	return asciiSet(rest, "abcdefghijklmnopqrstuvwxyz0123456789._-")
637}
638
639func asciiSet(s, allowed string) bool {
640	for i := 0; i < len(s); i++ {
641		if !strings.ContainsRune(allowed, rune(s[i])) {
642			return false
643		}
644	}
645	return true
646}