Realm
gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/bazaarv4
Overview
Realm Path
gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/bazaarv4
Exported Functions
31
State Entries
28
Source Files
2
Total Package Entries
73
Exported Functions
31 exported functions
State
28 state entries
Source Code
FILES
bazaarv4.gno
go
1package bazaarv4
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/bazaarv4"
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 paid := launchFee
290 takeFactorySend(paid)
291 reserved.Set(slug, &reservation{who: cur.Previous().Address(), paid: paid})
292 chain.Emit("Reserve", "slug", slug, "paid", strconv.FormatInt(paid, 10))
293}
294
295func CancelReserve(cur realm, slug string) {
296 requireInit()
297 if !cur.Previous().IsUserCall() {
298 panic("factory: EOA only")
299 }
300 slug = strings.ToLower(strings.TrimSpace(slug))
301 res := mustReserve(slug)
302 if res.who != cur.Previous().Address() {
303 panic("factory: not reserver")
304 }
305 if bySlug.Has(slug) {
306 panic("factory: already registered")
307 }
308 reserved.Remove(slug)
309 if res.paid > 0 {
310 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
311 bk.SendCoins(self, res.who, chain.Coins{{Denom: nativeDenom, Amount: res.paid}})
312 }
313 chain.Emit("CancelReserve", "slug", slug, "paid", strconv.FormatInt(res.paid, 10))
314}
315
316func ReservedCreator(slug string) string {
317 requireInit()
318 slug = strings.ToLower(strings.TrimSpace(slug))
319 if !reserved.Has(slug) {
320 return ""
321 }
322 return reserved.Get(slug).(*reservation).who.String()
323}
324
325func takeFactorySend(want int64) {
326 sent := unsafe.OriginSend()
327 if want == 0 {
328 if len(sent) != 0 {
329 panic("factory: extra denoms")
330 }
331 return
332 }
333 if len(sent) != 1 || sent[0].Denom != nativeDenom || sent[0].Amount != want {
334 panic("factory: wrong payment")
335 }
336}
337
338func NoteMinted(cur realm, n int) {
339 requireInit()
340 if n < 0 {
341 panic("factory: minted")
342 }
343 prev := cur.Previous()
344 if !prev.IsCode() {
345 panic("factory: collection realm")
346 }
347 c := mustByPkg(prev.PkgPath())
348 c.minted = n
349 byPkg.Set(c.pkg, c)
350 bySlug.Set(c.slug, c)
351}
352
353func NoteRoyalty(cur realm, bps int) {
354 requireInit()
355 if bps < 0 || bps > maxRoyaltyBps {
356 panic("factory: royalty")
357 }
358 prev := cur.Previous()
359 if !prev.IsCode() {
360 panic("factory: collection realm")
361 }
362 c := mustByPkg(prev.PkgPath())
363 c.royaltyBps = bps
364 byPkg.Set(c.pkg, c)
365 bySlug.Set(c.slug, c)
366}
367
368func CollectionOf(slug string) string {
369 requireInit()
370 c := mustBySlug(slug)
371 return c.pkg
372}
373
374func CollectionAddr(slug string) string {
375 requireInit()
376 c := mustBySlug(slug)
377 return chain.PackageAddress(c.pkg).String()
378}
379
380func RoyaltyOf(slug string) int {
381 requireInit()
382 c := mustBySlug(slug)
383 return c.royaltyBps
384}
385
386func ListCollections() string {
387 requireInit()
388 out := ""
389 bySlug.Iterate("", "", func(_ string, value any) bool {
390 c := value.(*collection)
391 if out != "" {
392 out += "\n"
393 }
394 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)
395 return false
396 })
397 return out
398}
399
400func Withdraw(cur realm) {
401 requireAdmin(cur)
402 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
403 ugnot := bk.GetCoins(self).AmountOf(nativeDenom)
404 locked := reservedPaid()
405 avail := ugnot - locked
406 if avail <= 0 {
407 panic("factory: no funds")
408 }
409 bk.SendCoins(self, admin, chain.Coins{{Denom: nativeDenom, Amount: avail}})
410 chain.Emit("Withdraw", "amount", strconv.FormatInt(avail, 10))
411}
412
413func TransferAdmin(cur realm, next address) {
414 requireAdmin(cur)
415 if !next.IsValid() {
416 panic("factory: invalid address")
417 }
418 if next == admin {
419 panic("factory: already admin")
420 }
421 admin = next
422 chain.Emit("TransferAdmin", "addr", next.String())
423}
424
425func Render(_ string) string {
426 if !inited {
427 return "# Bazaar factory\n\nCall Init first.\n"
428 }
429 out := "# Bazaar factory\n\n"
430 out += "- launch fee: " + strconv.FormatInt(launchFee, 10) + " ugnot\n"
431 out += "- collections: " + strconv.Itoa(bySlug.Size()) + "\n\n"
432 lines := ListCollections()
433 if lines == "" {
434 out += "_No collections._\n"
435 } else {
436 out += "```\n" + lines + "\n```\n"
437 }
438 return out
439}
440
441func register(pkg string, creator address, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) {
442 if !strings.HasPrefix(pkg, "gno.land/r/") {
443 panic("factory: pkg")
444 }
445 slug := lastElem(pkg)
446 if !validSlug(slug) {
447 panic("factory: slug")
448 }
449 if !validName(name) {
450 panic("factory: name")
451 }
452 if !validSymbol(symbol) {
453 panic("factory: symbol")
454 }
455 if !validImage(cover) {
456 panic("factory: cover")
457 }
458 if maxSupply < 0 || maxSupply > maxSupplyBound {
459 panic("factory: maxSupply")
460 }
461 if mintPrice < 0 {
462 panic("factory: mintPrice")
463 }
464 if royaltyBps < 0 || royaltyBps > maxRoyaltyBps {
465 panic("factory: royalty")
466 }
467 if bySlug.Has(slug) {
468 panic("factory: slug taken")
469 }
470 if byPkg.Has(pkg) {
471 panic("factory: pkg already registered")
472 }
473 c := &collection{
474 slug: slug,
475 name: name,
476 symbol: symbol,
477 cover: cover,
478 pkg: pkg,
479 creator: creator,
480 maxSupply: maxSupply,
481 mintPrice: mintPrice,
482 royaltyBps: int(royaltyBps),
483 }
484 bySlug.Set(slug, c)
485 byPkg.Set(pkg, c)
486 chain.Emit("Register", "slug", slug, "pkg", pkg, "creator", creator.String())
487}
488
489func requireInit() {
490 if !inited {
491 panic("factory: call Init first")
492 }
493}
494
495func requireAdmin(cur realm) {
496 requireInit()
497 if !cur.Previous().IsUserCall() {
498 panic("factory: EOA only")
499 }
500 if cur.Previous().Address() != admin {
501 panic("factory: not admin")
502 }
503}
504
505func mustReserve(slug string) *reservation {
506 v := reserved.Get(slug)
507 if v == nil {
508 panic("factory: not reserved")
509 }
510 return v.(*reservation)
511}
512
513func reservedPaid() int64 {
514 var n int64
515 reserved.Iterate("", "", func(_ string, value any) bool {
516 n += value.(*reservation).paid
517 return false
518 })
519 return n
520}
521
522func mustBySlug(slug string) *collection {
523 v := bySlug.Get(slug)
524 if v == nil {
525 panic("factory: unknown collection")
526 }
527 return v.(*collection)
528}
529
530func mustByPkg(pkg string) *collection {
531 v := byPkg.Get(pkg)
532 if v == nil {
533 panic("factory: unknown collection")
534 }
535 return v.(*collection)
536}
537
538func lastElem(pkg string) string {
539 i := strings.LastIndex(pkg, "/")
540 if i < 0 || i+1 >= len(pkg) {
541 panic("factory: pkg")
542 }
543 return pkg[i+1:]
544}
545
546func validSlug(s string) bool {
547 if len(s) < minSlugLen || len(s) > maxSlugLen {
548 return false
549 }
550 if s[0] < 'a' || s[0] > 'z' {
551 return false
552 }
553 return asciiSet(s, "abcdefghijklmnopqrstuvwxyz0123456789")
554}
555
556func validSymbol(s string) bool {
557 if len(s) < 1 || len(s) > maxSymbolLen {
558 return false
559 }
560 return asciiSet(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
561}
562
563func validName(name string) bool {
564 if len(name) < 1 || len(name) > maxNameLen {
565 return false
566 }
567 return !strings.ContainsAny(name, "\n\r|")
568}
569
570func validImage(u string) bool {
571 if u == "" {
572 return true
573 }
574 if len(u) > maxImageLen {
575 return false
576 }
577 if strings.ContainsAny(u, " \t\n\r|<>\"'`") {
578 return false
579 }
580 if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {
581 return true
582 }
583 const prefix = "/samples/"
584 if !strings.HasPrefix(u, prefix) {
585 return false
586 }
587 rest := u[len(prefix):]
588 if rest == "" {
589 return false
590 }
591 return asciiSet(rest, "abcdefghijklmnopqrstuvwxyz0123456789._-")
592}
593
594func asciiSet(s, allowed string) bool {
595 for i := 0; i < len(s); i++ {
596 if !strings.ContainsRune(allowed, rune(s[i])) {
597 return false
598 }
599 }
600 return true
601}
602Raw Package Data
Raw JSON data