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