Realm
gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/c/bazaargens
Overview
Realm Path
gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/c/bazaargens
Exported Functions
71
State Entries
49
Source Files
3
Total Package Entries
162
Exported Functions
71 exported functions
State
49 state entries
Source Code
FILES
bazaargens.gno
go
1package bazaargens
2
3// Mainnet factory template: genesis p/nt/grc721 so Adena indexes NewToken/Transfer,
4// plus List/Buy escrow on this realm. Listed OwnerOf is the collection (wallet hides
5// the token until Cancel or Buy). Do not import p/bazaar/grc721 here.
6
7import (
8 "chain"
9 "chain/banker"
10 "chain/runtime/unsafe"
11 "errors"
12 "strconv"
13 "strings"
14 "time"
15
16 "gno.land/p/nt/grc721/metadata/v0"
17 "gno.land/p/nt/grc721/v0"
18 "gno.land/p/nt/avl/v0"
19 "gno.land/p/nt/seqid/v0"
20 factory "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/bazaarv5"
21)
22
23const (
24 maxNameLen = 64
25 maxImageLen = 200
26 maxSupplyBound = 1_000_000
27 maxRoyaltyBps = 1000
28 nativeDenom = "ugnot"
29 publicOrigin = "https://bazaar-gno.vercel.app"
30 dropAddCap = 20
31 dropSlotHardCap = 10000
32 allowAddCap = 50
33 minSlugLen = 2
34)
35
36type item struct {
37 id int
38 name string
39 image string
40 owner address
41 seller address
42 price int64
43 listed bool
44 rarity string
45 traits string
46 revealed bool
47 slot int
48}
49
50type dropSlot struct {
51 name string
52 image string
53 rarity string
54 traits string
55}
56
57type phaseCount struct {
58 wl int
59 pub int
60}
61
62var (
63 items avl.Tree
64 dropSlots avl.Tree
65 allowlist avl.Tree
66 mintedBy avl.Tree
67 token *grc721.Token
68 ledger *grc721.PrivateLedger
69 metaView *metadata.Metadata
70 metaLed *metadata.Ledger
71 tokenSeq seqid.ID
72 nextID int
73 minted int
74 loaded int
75 allowN int
76 mintCap int
77 wlMintCap int
78 pubMintCap int
79 maxSupply int
80 mintPrice int64
81 wlPrice int64
82 wlSupply int
83 wlStart int64
84 wlEnd int64
85 publicStart int64
86 publicEnd int64
87 publicForced bool
88 royaltyBps int
89 paused bool
90 hidden bool
91 inited bool
92 creator address
93 self address
94 colName string
95 colSymbol string
96 colCover string
97 prereveal string
98 slug string
99)
100
101func Init(cur realm, name, symbol, cover string, maxSupplyN int, mintPriceUgnot int64, royalty int64) {
102 if !cur.Previous().IsUserCall() {
103 panic("col: EOA only")
104 }
105 if !cur.IsCurrent() {
106 panic("col: spoofed realm")
107 }
108 if len(symbol) < 1 || len(symbol) > grc721.MaxSymbolLen {
109 panic("col: symbol")
110 }
111 pathSlug := lastElem(cur.PkgPath())
112 if !validPkgName(pathSlug) {
113 panic("col: slug")
114 }
115 if maxSupplyN < 0 || maxSupplyN > maxSupplyBound {
116 panic("col: maxSupply")
117 }
118 if mintPriceUgnot < 0 {
119 panic("col: mintPrice")
120 }
121 if royalty < 0 || royalty > maxRoyaltyBps {
122 panic("col: royalty")
123 }
124 if !validName(name) {
125 panic("col: name")
126 }
127 if !validImage(cover) {
128 panic("col: cover")
129 }
130 if inited {
131 panic("col: already initialized")
132 }
133 slug = pathSlug
134 reservedWho := factory.ReservedCreator(slug)
135 if reservedWho == "" || reservedWho != cur.Previous().Address().String() {
136 panic("col: reserve first")
137 }
138 takeExact(0)
139 tok, led := grc721.NewToken(name, symbol, tokenSeq.Next(), cur)
140 token = tok
141 ledger = led
142 metaView, metaLed = metadata.NewMetadata(led)
143 creator = cur.Previous().Address()
144 self = cur.Address()
145 colName = name
146 colSymbol = symbol
147 colCover = cover
148 prereveal = ""
149 maxSupply = maxSupplyN
150 mintPrice = mintPriceUgnot
151 royaltyBps = int(royalty)
152 items = avl.Tree{}
153 dropSlots = avl.Tree{}
154 allowlist = avl.Tree{}
155 mintedBy = avl.Tree{}
156 loaded = 0
157 allowN = 0
158 mintCap = 0
159 wlMintCap = 0
160 pubMintCap = 0
161 wlPrice = 0
162 wlSupply = 0
163 wlStart = 0
164 wlEnd = 0
165 publicStart = 0
166 publicEnd = 0
167 publicForced = false
168 hidden = false
169 paused = maxSupplyN > 0
170 inited = true
171 factory.Register(cross(cur), name, symbol, cover, maxSupplyN, mintPriceUgnot, royalty)
172 chain.Emit("Init", "creator", creator.String(), "slug", slug)
173}
174
175func Mint(cur realm, name, imageURL string) int {
176 caller := requireUser(cur)
177 if caller != creator {
178 panic("col: not creator")
179 }
180 id := mintTo(cur, caller, name, imageURL)
181 writeTokenMeta(id)
182 chain.Emit("Mint", "id", strconv.Itoa(id), "owner", caller.String())
183 return id
184}
185
186func PublicMint(cur realm) int {
187 caller := requireUser(cur)
188 if paused {
189 panic("col: paused")
190 }
191 if mintPhase(time.Now().Unix()) == "closed" {
192 panic("col: not open")
193 }
194 requireAllow(caller)
195 checkMintCap(caller)
196 if maxSupply > 0 && loaded == 0 {
197 panic("col: no slot")
198 }
199 pay := mintPrice
200 if wlPhase() {
201 pay = wlPrice
202 }
203 takeExact(pay)
204 name := colName + " #" + strconv.Itoa(minted+1)
205 image := publicImage()
206 var slot *dropSlot
207 slotIndex := -1
208 if loaded > 0 {
209 if minted >= loaded {
210 panic("col: no slot")
211 }
212 v := dropSlots.Get(slotKey(minted))
213 if v == nil {
214 panic("col: no slot")
215 }
216 slot = v.(*dropSlot)
217 slotIndex = minted
218 if hidden {
219 name = "Unrevealed"
220 image = prerevealImage()
221 } else {
222 name = slot.name
223 image = slot.image
224 }
225 }
226 id := mintTo(cur, caller, name, image)
227 if slot != nil {
228 if hidden {
229 it := mustItem(id)
230 it.revealed = false
231 it.slot = slotIndex
232 items.Set(itemKey(id), it)
233 } else {
234 applySlot(id, slot, slotIndex)
235 }
236 }
237 writeTokenMeta(id)
238 noteMint(caller)
239 if pay > 0 {
240 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
241 bk.SendCoins(self, creator, chain.Coins{{Denom: nativeDenom, Amount: pay}})
242 }
243 chain.Emit("PublicMint", "id", strconv.Itoa(id), "n", strconv.Itoa(minted))
244 return id
245}
246
247func AddDropItems(cur realm, blob string) {
248 caller := requireUser(cur)
249 if caller != creator {
250 panic("col: not creator")
251 }
252 lines := splitDropLines(blob)
253 if len(lines) < 1 || len(lines) > dropAddCap {
254 panic("col: slots")
255 }
256 if loaded+len(lines) > slotCap() {
257 panic("col: slots")
258 }
259 for _, line := range lines {
260 name, image, rarity, traits := parseDropLine(line)
261 if !validName(name) {
262 panic("col: name")
263 }
264 if !validImage(image) {
265 panic("col: image")
266 }
267 if !validRarity(rarity) {
268 panic("col: rarity")
269 }
270 if !validTraits(traits) {
271 panic("col: traits")
272 }
273 dropSlots.Set(slotKey(loaded), &dropSlot{name: name, image: image, rarity: rarity, traits: traits})
274 loaded++
275 }
276 chain.Emit("AddDropItems", "slug", slug, "loaded", strconv.Itoa(loaded))
277}
278
279func ListDropSlots() string {
280 requireInit()
281 if hidden {
282 return ""
283 }
284 out := ""
285 for i := 0; i < loaded; i++ {
286 v := dropSlots.Get(slotKey(i))
287 if v == nil {
288 continue
289 }
290 s := v.(*dropSlot)
291 if out != "" {
292 out += "\n"
293 }
294 out += strconv.Itoa(i) + "|" + s.name + "|" + s.image + "|" + s.rarity + "|" + s.traits
295 }
296 return out
297}
298
299func LoadedOf() int {
300 requireInit()
301 return loaded
302}
303
304func PauseMint(cur realm) {
305 caller := requireUser(cur)
306 if caller != creator {
307 panic("col: not creator")
308 }
309 paused = true
310 chain.Emit("PauseMint", "slug", slug)
311}
312
313func ResumeMint(cur realm) {
314 caller := requireUser(cur)
315 if caller != creator {
316 panic("col: not creator")
317 }
318 if maxSupply > 0 && loaded < 1 {
319 panic("col: no slot")
320 }
321 paused = false
322 chain.Emit("ResumeMint", "slug", slug)
323}
324
325func AddAllowlist(cur realm, blob string) {
326 caller := requireUser(cur)
327 if caller != creator {
328 panic("col: not creator")
329 }
330 lines := splitDropLines(blob)
331 if len(lines) < 1 || len(lines) > allowAddCap {
332 panic("col: allowlist")
333 }
334 for _, line := range lines {
335 who := strings.ToLower(strings.TrimSpace(line))
336 if !validG1(who) {
337 panic("col: address")
338 }
339 if allowlist.Get(who) != nil {
340 continue
341 }
342 allowlist.Set(who, true)
343 allowN++
344 }
345 chain.Emit("AddAllowlist", "n", strconv.Itoa(allowN))
346}
347
348func AllowN() int {
349 requireInit()
350 return allowN
351}
352
353func OnAllowlist(addr string) bool {
354 requireInit()
355 who := strings.ToLower(strings.TrimSpace(addr))
356 return allowlist.Get(who) != nil
357}
358
359func SetDropSale(cur realm, wlPriceUgnot int64, wlQty int) {
360 caller := requireUser(cur)
361 if caller != creator {
362 panic("col: not creator")
363 }
364 if minted > 0 {
365 panic("col: minted")
366 }
367 if wlPriceUgnot < 0 {
368 panic("col: wlPrice")
369 }
370 if wlQty < 0 {
371 panic("col: wlSupply")
372 }
373 if maxSupply > 0 && wlQty > maxSupply {
374 panic("col: wlSupply")
375 }
376 wlPrice = wlPriceUgnot
377 wlSupply = wlQty
378 chain.Emit("SetDropSale", "wlSupply", strconv.Itoa(wlQty))
379}
380
381func StartPublic(cur realm) {
382 caller := requireUser(cur)
383 if caller != creator {
384 panic("col: not creator")
385 }
386 if wlSupply <= 0 || minted >= wlSupply {
387 panic("col: public")
388 }
389 publicForced = true
390 wlSupply = minted
391 now := time.Now().Unix()
392 if wlStart > now {
393 wlStart = now
394 }
395 if publicStart > now {
396 publicStart = now
397 }
398 if publicEnd > 0 && publicEnd <= now {
399 publicEnd = 0
400 }
401 chain.Emit("StartPublic", "minted", strconv.Itoa(minted))
402}
403
404// SetMintSchedule sets unix-second bounds for both rounds. 0,0,0,0 clears the clock.
405// An end of 0 means that round has no hard close. wlStart > 0 requires publicStart > wlStart.
406// A set end must be after its own start. publicStart must not fall inside the whitelist window.
407// Locked once any token has minted.
408func SetMintSchedule(cur realm, wlStartUnix, wlEndUnix, publicStartUnix, publicEndUnix int64) {
409 caller := requireUser(cur)
410 if caller != creator {
411 panic("col: not creator")
412 }
413 if minted > 0 {
414 panic("col: minted")
415 }
416 if wlStartUnix < 0 || wlEndUnix < 0 || publicStartUnix < 0 || publicEndUnix < 0 {
417 panic("col: schedule")
418 }
419 if wlStartUnix == 0 && wlEndUnix == 0 && publicStartUnix == 0 && publicEndUnix == 0 {
420 wlStart = 0
421 wlEnd = 0
422 publicStart = 0
423 publicEnd = 0
424 chain.Emit("SetMintSchedule", "wlStart", "0", "wlEnd", "0", "publicStart", "0", "publicEnd", "0")
425 return
426 }
427 if wlStartUnix > 0 && publicStartUnix <= wlStartUnix {
428 panic("col: schedule")
429 }
430 if wlEndUnix > 0 && (wlStartUnix == 0 || wlEndUnix <= wlStartUnix) {
431 panic("col: schedule")
432 }
433 if publicEndUnix > 0 && (publicStartUnix == 0 || publicEndUnix <= publicStartUnix) {
434 panic("col: schedule")
435 }
436 if wlEndUnix > 0 && publicStartUnix > 0 && publicStartUnix < wlEndUnix {
437 panic("col: schedule")
438 }
439 wlStart = wlStartUnix
440 wlEnd = wlEndUnix
441 publicStart = publicStartUnix
442 publicEnd = publicEndUnix
443 chain.Emit(
444 "SetMintSchedule",
445 "wlStart", strconv.FormatInt(wlStartUnix, 10),
446 "wlEnd", strconv.FormatInt(wlEndUnix, 10),
447 "publicStart", strconv.FormatInt(publicStartUnix, 10),
448 "publicEnd", strconv.FormatInt(publicEndUnix, 10),
449 )
450}
451
452func MintScheduleOf() string {
453 requireInit()
454 return strconv.FormatInt(wlStart, 10) + "|" + strconv.FormatInt(wlEnd, 10) + "|" + strconv.FormatInt(publicStart, 10) + "|" + strconv.FormatInt(publicEnd, 10)
455}
456
457func SetMintCap(cur realm, n int) {
458 if n < 0 {
459 panic("col: mintCap")
460 }
461 caller := requireUser(cur)
462 if caller != creator {
463 panic("col: not creator")
464 }
465 mintCap = n
466 chain.Emit("SetMintCap", "cap", strconv.Itoa(n))
467}
468
469// SetPhaseMintCap sets how many times one wallet may PublicMint in each round.
470// 0 means that round has no per-wallet limit. Counts are separate: a whitelist
471// mint does not consume the public allowance. mintCap still caps the total.
472func SetPhaseMintCap(cur realm, wlN, pubN int) {
473 if wlN < 0 || pubN < 0 {
474 panic("col: phase cap")
475 }
476 caller := requireUser(cur)
477 if caller != creator {
478 panic("col: not creator")
479 }
480 wlMintCap = wlN
481 pubMintCap = pubN
482 chain.Emit("SetPhaseMintCap", "wl", strconv.Itoa(wlN), "public", strconv.Itoa(pubN))
483}
484
485func SetMintPrice(cur realm, priceUgnot int64) {
486 if priceUgnot < 0 {
487 panic("col: price")
488 }
489 caller := requireUser(cur)
490 if caller != creator {
491 panic("col: not creator")
492 }
493 mintPrice = priceUgnot
494 chain.Emit("SetMintPrice", "price", strconv.FormatInt(priceUgnot, 10))
495}
496
497func MintCapOf() int {
498 requireInit()
499 return mintCap
500}
501
502func PhaseMintCapOf() string {
503 requireInit()
504 return strconv.Itoa(wlMintCap) + "|" + strconv.Itoa(pubMintCap)
505}
506
507func DropSaleOf() string {
508 requireInit()
509 return strconv.Itoa(royaltyBps) + "|" + strconv.FormatInt(wlPrice, 10) + "|" + strconv.Itoa(wlSupply) + "|" + mintPhase(time.Now().Unix()) + "|" + strconv.FormatInt(mintPrice, 10)
510}
511
512func wlPhase() bool {
513 return mintPhase(time.Now().Unix()) == "whitelist"
514}
515
516// mintPhase is closed, whitelist, or public. A future publicStart does not
517// fall through to public price when the allowlist quantity is already used.
518// After wlEnd, and after publicEnd, the sale is closed until the next open bound.
519func mintPhase(now int64) string {
520 if publicForced {
521 if publicEnd > 0 && now >= publicEnd {
522 return "closed"
523 }
524 return "public"
525 }
526 if wlStart > 0 && now < wlStart {
527 return "closed"
528 }
529 if publicStart > 0 && now >= publicStart {
530 if publicEnd > 0 && now >= publicEnd {
531 return "closed"
532 }
533 return "public"
534 }
535 if publicStart > 0 && now < publicStart {
536 if wlEnd > 0 && now >= wlEnd {
537 return "closed"
538 }
539 if wlSupply > 0 && minted < wlSupply {
540 return "whitelist"
541 }
542 return "closed"
543 }
544 if wlEnd > 0 && now >= wlEnd {
545 return "public"
546 }
547 if wlSupply > 0 && minted < wlSupply {
548 return "whitelist"
549 }
550 return "public"
551}
552
553func requireAllow(caller address) {
554 if publicForced {
555 return
556 }
557 who := strings.ToLower(caller.String())
558 if wlPhase() {
559 if allowN <= 0 {
560 panic("col: allowlist")
561 }
562 if allowlist.Get(who) == nil {
563 panic("col: not allowed")
564 }
565 return
566 }
567 if wlSupply > 0 {
568 return
569 }
570 if allowN <= 0 {
571 return
572 }
573 if allowlist.Get(who) == nil {
574 panic("col: not allowed")
575 }
576}
577
578func walletPhase(who string) phaseCount {
579 if v := mintedBy.Get(who); v != nil {
580 return v.(phaseCount)
581 }
582 return phaseCount{}
583}
584
585func checkMintCap(caller address) {
586 who := strings.ToLower(caller.String())
587 got := walletPhase(who)
588 if mintCap > 0 && got.wl+got.pub >= mintCap {
589 panic("col: mint cap")
590 }
591 if wlPhase() {
592 if wlMintCap > 0 && got.wl >= wlMintCap {
593 panic("col: wl mint cap")
594 }
595 return
596 }
597 if pubMintCap > 0 && got.pub >= pubMintCap {
598 panic("col: public mint cap")
599 }
600}
601
602func noteMint(caller address) {
603 who := strings.ToLower(caller.String())
604 got := walletPhase(who)
605 if wlPhase() {
606 got.wl++
607 } else {
608 got.pub++
609 }
610 mintedBy.Set(who, got)
611}
612
613func Hidden() bool {
614 requireInit()
615 return hidden
616}
617
618func SetHidden(cur realm, hide bool) {
619 caller := requireUser(cur)
620 if caller != creator {
621 panic("col: not creator")
622 }
623 if !hide {
624 panic("col: reveal")
625 }
626 if loaded < 1 {
627 panic("col: no slots")
628 }
629 if !hidden && minted > 0 {
630 panic("col: revealed")
631 }
632 hidden = true
633 chain.Emit("SetHidden", "hidden", "true")
634}
635
636// SetPrereveal is the image every sealed NFT shows. Empty uses the cover.
637// Creator only. After RevealCollection the real slot image is already public.
638func SetPrereveal(cur realm, image string) {
639 caller := requireUser(cur)
640 if caller != creator {
641 panic("col: not creator")
642 }
643 image = strings.TrimSpace(image)
644 if image != "" && !validImage(image) {
645 panic("col: image")
646 }
647 if !hidden && minted > 0 {
648 panic("col: revealed")
649 }
650 prereveal = image
651 if hidden {
652 paintSealed(prerevealImage())
653 }
654 chain.Emit("SetPrereveal", "image", image)
655}
656
657func Prereveal() string {
658 requireInit()
659 return prereveal
660}
661
662// RevealCollection opens every minted NFT. Creator only, and only while sealed.
663func RevealCollection(cur realm) {
664 caller := requireUser(cur)
665 if caller != creator {
666 panic("col: not creator")
667 }
668 if !hidden {
669 panic("col: revealed")
670 }
671 if loaded < 1 {
672 panic("col: no slots")
673 }
674 hidden = false
675 for _, id := range sealedIDs() {
676 it := mustItem(id)
677 v := dropSlots.Get(slotKey(it.slot))
678 if v == nil {
679 panic("col: no slot")
680 }
681 applySlot(id, v.(*dropSlot), it.slot)
682 writeTokenMeta(id)
683 }
684 chain.Emit("RevealCollection", "slug", slug)
685}
686
687func Revealed(id int) bool {
688 return mustItem(id).revealed
689}
690
691func Reveal(cur realm, id int) {
692 caller := requireUser(cur)
693 it := mustItem(id)
694 if caller != it.owner {
695 panic("col: not owner")
696 }
697 if hidden {
698 panic("col: sealed")
699 }
700 if it.revealed {
701 panic("col: revealed")
702 }
703 if it.slot < 0 {
704 panic("col: no slot")
705 }
706 v := dropSlots.Get(slotKey(it.slot))
707 if v == nil {
708 panic("col: no slot")
709 }
710 applySlot(id, v.(*dropSlot), it.slot)
711 writeTokenMeta(id)
712 chain.Emit("Reveal", "id", strconv.Itoa(id))
713}
714
715// Adena IIFE: owner, err := OwnerOf("1"); owner.String() == wallet
716func OwnerOf(tid string) (address, error) {
717 requireInit()
718 id, err := strconv.Atoi(tid)
719 if err != nil || id < 1 {
720 return address(""), errors.New("col: tid")
721 }
722 v := items.Get(itemKey(id))
723 if v == nil {
724 return address(""), errors.New("col: unknown item")
725 }
726 return v.(*item).owner, nil
727}
728
729func GrcName() string {
730 requireInit()
731 return token.GetName()
732}
733
734func GrcSymbol() string {
735 requireInit()
736 return token.GetSymbol()
737}
738
739func GrcBalanceOf(addr string) int64 {
740 requireInit()
741 who := address(addr)
742 if !who.IsValid() {
743 return 0
744 }
745 n, err := token.BalanceOf(who)
746 if err != nil {
747 return 0
748 }
749 return n
750}
751
752func GrcOwnerOf(id int) string {
753 requireInit()
754 owner, err := token.OwnerOf(itemTid(id))
755 if err != nil {
756 panic("col: owner")
757 }
758 return owner.String()
759}
760
761func Name() string {
762 return GrcName()
763}
764
765func Symbol() string {
766 return GrcSymbol()
767}
768
769func BalanceOf(addr string) int64 {
770 return GrcBalanceOf(addr)
771}
772
773func TokenURI(tid string) string {
774 id := parseTid(tid)
775 mustItem(id)
776 if metaView != nil {
777 u, err := metaView.TokenURI(itemTid(id))
778 if err == nil && u != "" {
779 return u
780 }
781 }
782 return tokenJSON(mustItem(id))
783}
784
785func guardUnlisted(id int) {
786 if mustItem(id).listed {
787 panic("col: listed")
788 }
789}
790
791func Approve(cur realm, to, tid string) {
792 caller := requireUser(cur)
793 id := parseTid(tid)
794 guardUnlisted(id)
795 who := address(to)
796 if to != "" && !who.IsValid() {
797 panic("col: addr")
798 }
799 if err := ledger.Approve(caller, who, itemTid(id)); err != nil {
800 panic("col: approve")
801 }
802}
803
804func SetApprovalForAll(cur realm, operator string, approved bool) {
805 caller := requireUser(cur)
806 op := address(operator)
807 if !op.IsValid() {
808 panic("col: addr")
809 }
810 if err := ledger.SetApprovalForAll(caller, op, approved); err != nil {
811 panic("col: approve")
812 }
813}
814
815func TransferFrom(cur realm, from, to, tid string) {
816 caller := requireUser(cur)
817 id := parseTid(tid)
818 guardUnlisted(id)
819 src := address(from)
820 dst := address(to)
821 if !src.IsValid() || !dst.IsValid() {
822 panic("col: addr")
823 }
824 if err := ledger.TransferFrom(caller, src, dst, itemTid(id)); err != nil {
825 panic("col: transfer")
826 }
827 it := mustItem(id)
828 it.owner = dst
829 items.Set(itemKey(id), it)
830}
831
832func GetApproved(id int) string {
833 requireInit()
834 who, err := token.GetApproved(itemTid(id))
835 if err != nil {
836 return ""
837 }
838 return who.String()
839}
840
841func IsApprovedForAll(owner, operator string) bool {
842 requireInit()
843 a := address(owner)
844 b := address(operator)
845 if !a.IsValid() || !b.IsValid() {
846 return false
847 }
848 return token.IsApprovedForAll(a, b)
849}
850
851func TotalSupply() int64 {
852 requireInit()
853 return token.TotalSupply()
854}
855
856func SellerOf(id int) address {
857 return mustItem(id).seller
858}
859
860func PriceOf(id int) int64 {
861 return mustItem(id).price
862}
863
864func NameOf(id int) string {
865 it := mustItem(id)
866 if !it.revealed {
867 return "Unrevealed"
868 }
869 return it.name
870}
871
872func ImageOf(id int) string {
873 it := mustItem(id)
874 if !it.revealed {
875 return prerevealImage()
876 }
877 return it.image
878}
879
880func Listed(id int) bool {
881 return mustItem(id).listed
882}
883
884func NextID() int {
885 return nextID
886}
887
888func CollectionOf(_ int) string {
889 requireInit()
890 return slug
891}
892
893func ItemLine(id int) string {
894 it := mustItem(id)
895 name := it.name
896 image := it.image
897 rarity := it.rarity
898 traits := it.traits
899 if !it.revealed {
900 name = "Unrevealed"
901 image = prerevealImage()
902 rarity = ""
903 traits = ""
904 }
905 return strconv.Itoa(it.id) + "|" + name + "|" + it.owner.String() + "|" + it.seller.String() + "|" + strconv.FormatInt(it.price, 10) + "|" + strconv.FormatBool(it.listed) + "|" + image + "|" + slug + "|" + rarity + "|" + traits + "|" + strconv.FormatBool(it.revealed) + "|" + strconv.Itoa(it.slot)
906}
907
908func ListOpen() string {
909 requireInit()
910 out := ""
911 n := 0
912 items.Iterate("", "", func(_ string, value any) bool {
913 it := value.(*item)
914 if !it.listed {
915 return false
916 }
917 if n >= 50 {
918 return true
919 }
920 if out != "" {
921 out += "\n"
922 }
923 out += ItemLine(it.id)
924 n++
925 return false
926 })
927 return out
928}
929
930func TokensOf(owner string) string {
931 requireInit()
932 who := address(owner)
933 if !who.IsValid() {
934 return ""
935 }
936 out := ""
937 items.Iterate("", "", func(_ string, value any) bool {
938 it := value.(*item)
939 if it.owner != who {
940 return false
941 }
942 if out != "" {
943 out += ","
944 }
945 out += strconv.Itoa(it.id)
946 return false
947 })
948 return out
949}
950
951func TokenOfOwnerByIndex(owner string, index int64) string {
952 requireInit()
953 who := address(owner)
954 if !who.IsValid() {
955 panic("col: addr")
956 }
957 if index < 0 {
958 panic("col: index")
959 }
960 var n int64
961 found := ""
962 items.Iterate("", "", func(_ string, value any) bool {
963 it := value.(*item)
964 if it.owner != who {
965 return false
966 }
967 if n == index {
968 found = strconv.Itoa(it.id)
969 return true
970 }
971 n++
972 return false
973 })
974 if found == "" {
975 panic("col: index")
976 }
977 return found
978}
979
980func TokenByIndex(index int64) string {
981 requireInit()
982 if index < 0 {
983 panic("col: index")
984 }
985 var n int64
986 found := ""
987 items.Iterate("", "", func(_ string, value any) bool {
988 if n == index {
989 found = strconv.Itoa(value.(*item).id)
990 return true
991 }
992 n++
993 return false
994 })
995 if found == "" {
996 panic("col: index")
997 }
998 return found
999}
1000
1001func Creator() string {
1002 requireInit()
1003 return creator.String()
1004}
1005
1006func MintPrice() int64 {
1007 requireInit()
1008 return mintPrice
1009}
1010
1011func MaxSupply() int {
1012 requireInit()
1013 return maxSupply
1014}
1015
1016func Minted() int {
1017 requireInit()
1018 return minted
1019}
1020
1021func RoyaltyBps() int {
1022 requireInit()
1023 return royaltyBps
1024}
1025
1026func SetRoyalty(cur realm, bps int) {
1027 caller := requireUser(cur)
1028 if caller != creator {
1029 panic("col: not creator")
1030 }
1031 if minted != 0 {
1032 panic("col: minted")
1033 }
1034 if bps < 0 || bps > maxRoyaltyBps {
1035 panic("col: royalty")
1036 }
1037 royaltyBps = bps
1038 factory.NoteRoyalty(cross(cur), bps)
1039 chain.Emit("SetRoyalty", "slug", slug, "bps", strconv.Itoa(bps))
1040}
1041
1042func RarityOf(id int) string {
1043 it := mustItem(id)
1044 if !it.revealed {
1045 return ""
1046 }
1047 return it.rarity
1048}
1049
1050func TraitsOf(id int) string {
1051 it := mustItem(id)
1052 if !it.revealed {
1053 return ""
1054 }
1055 return it.traits
1056}
1057
1058func Paused() bool {
1059 requireInit()
1060 return paused
1061}
1062
1063func Render(_ string) string {
1064 if !inited {
1065 return "# Collection\n\nCall Init first.\n"
1066 }
1067 out := "# " + colName + " ($" + colSymbol + ")\n\n"
1068 out += "- minted: " + strconv.Itoa(minted) + "\n"
1069 out += "- mint price: " + strconv.FormatInt(mintPrice, 10) + " ugnot\n\n"
1070 open := ListOpen()
1071 if open == "" {
1072 out += "_No open listings._\n"
1073 } else {
1074 out += "```\n" + open + "\n```\n"
1075 }
1076 return out
1077}
1078
1079func mintTo(cur realm, caller address, name, imageURL string) int {
1080 requireInit()
1081 if !cur.IsCurrent() {
1082 panic("col: spoofed realm")
1083 }
1084 if maxSupply > 0 && minted >= maxSupply {
1085 panic("col: sold out")
1086 }
1087 if !validName(name) {
1088 panic("col: name")
1089 }
1090 if !validImage(imageURL) {
1091 panic("col: image")
1092 }
1093 nextID++
1094 id := nextID
1095 it := &item{
1096 id: id,
1097 name: name,
1098 image: imageURL,
1099 owner: caller,
1100 rarity: rarityOf(id),
1101 revealed: true,
1102 slot: -1,
1103 }
1104 items.Set(itemKey(id), it)
1105 err := ledger.Mint(caller, itemTid(id))
1106 if err != nil {
1107 panic("col: mint")
1108 }
1109 minted++
1110 factory.NoteMinted(cross(cur), minted)
1111 return id
1112}
1113
1114func takeExact(want int64) {
1115 sent := unsafe.OriginSend()
1116 if want == 0 {
1117 if len(sent) != 0 {
1118 panic("col: extra denoms")
1119 }
1120 return
1121 }
1122 if len(sent) != 1 || sent[0].Denom != nativeDenom || sent[0].Amount != want {
1123 panic("col: wrong payment")
1124 }
1125}
1126
1127func prerevealImage() string {
1128 if prereveal != "" {
1129 return prereveal
1130 }
1131 return colCover
1132}
1133
1134func sealedIDs() []int {
1135 out := []int{}
1136 items.Iterate("", "", func(_ string, value any) bool {
1137 it := value.(*item)
1138 if !it.revealed && it.slot >= 0 {
1139 out = append(out, it.id)
1140 }
1141 return false
1142 })
1143 return out
1144}
1145
1146func paintSealed(image string) {
1147 for _, id := range sealedIDs() {
1148 it := mustItem(id)
1149 it.image = image
1150 items.Set(itemKey(id), it)
1151 writeTokenMeta(id)
1152 }
1153}
1154
1155func publicImage() string {
1156 img := "/samples/" + slug + "-0" + strconv.Itoa(minted+1) + ".png"
1157 if validImage(img) {
1158 return img
1159 }
1160 return colCover
1161}
1162
1163func requireInit() {
1164 if !inited {
1165 panic("col: call Init first")
1166 }
1167}
1168
1169func requireUser(cur realm) address {
1170 requireInit()
1171 if !cur.IsCurrent() {
1172 panic("col: spoofed realm")
1173 }
1174 if !cur.Previous().IsUserCall() {
1175 panic("col: EOA only")
1176 }
1177 return cur.Previous().Address()
1178}
1179
1180func itemTid(id int) grc721.TokenID {
1181 return grc721.TokenID(strconv.Itoa(id))
1182}
1183
1184func parseTid(tid string) int {
1185 id, err := strconv.Atoi(tid)
1186 if err != nil || id < 1 {
1187 panic("col: tid")
1188 }
1189 return id
1190}
1191
1192func mustItem(id int) *item {
1193 v := items.Get(itemKey(id))
1194 if v == nil {
1195 panic("col: unknown item")
1196 }
1197 return v.(*item)
1198}
1199
1200func itemKey(id int) string {
1201 return strconv.Itoa(id)
1202}
1203
1204func lastElem(pkg string) string {
1205 i := strings.LastIndex(pkg, "/")
1206 if i < 0 || i+1 >= len(pkg) {
1207 panic("col: pkg")
1208 }
1209 return pkg[i+1:]
1210}
1211
1212func slotKey(i int) string {
1213 return strconv.Itoa(i)
1214}
1215
1216func slotCap() int {
1217 if maxSupply > 0 {
1218 return maxSupply
1219 }
1220 return dropSlotHardCap
1221}
1222
1223func applySlot(id int, s *dropSlot, slotIndex int) {
1224 it := mustItem(id)
1225 it.name = s.name
1226 it.image = s.image
1227 if s.rarity != "" {
1228 it.rarity = s.rarity
1229 }
1230 it.traits = s.traits
1231 it.revealed = true
1232 it.slot = slotIndex
1233 items.Set(itemKey(id), it)
1234}
1235
1236func writeTokenMeta(id int) {
1237 it := mustItem(id)
1238 name, image, attrs := publicMeta(it)
1239 uri := tokenJSON(it)
1240 if metaLed == nil {
1241 return
1242 }
1243 if err := metaLed.SetTokenURI(itemTid(id), uri); err != nil {
1244 panic("col: tokenURI")
1245 }
1246 if err := metaLed.SetTokenMetadata(itemTid(id), metadata.Data{
1247 Name: name,
1248 Description: colName + " #" + strconv.Itoa(id),
1249 Image: absImage(image),
1250 Attributes: attrs,
1251 }); err != nil {
1252 panic("col: metadata")
1253 }
1254}
1255
1256func publicMeta(it *item) (string, string, []metadata.Trait) {
1257 if !it.revealed {
1258 return "Unrevealed", prerevealImage(), []metadata.Trait{}
1259 }
1260 return it.name, it.image, traitAttrs(it)
1261}
1262
1263func tokenJSON(it *item) string {
1264 name, image, traits := publicMeta(it)
1265 attrs := ""
1266 for _, tr := range traits {
1267 if attrs != "" {
1268 attrs += ","
1269 }
1270 attrs += `{"trait_type":` + jsonStr(tr.TraitType) + `,"value":` + jsonStr(tr.Value) + `}`
1271 }
1272 body := `{"name":` + jsonStr(name) + `,"description":` + jsonStr(colName+" #"+strconv.Itoa(it.id)) + `,"image":` + jsonStr(absImage(image)) + `,"attributes":[` + attrs + `]}`
1273 return "data:application/json;charset=utf-8," + pctURI(body)
1274}
1275
1276func traitAttrs(it *item) []metadata.Trait {
1277 out := []metadata.Trait{}
1278 if it.rarity != "" {
1279 out = append(out, metadata.Trait{TraitType: "Rarity", Value: it.rarity})
1280 }
1281 for _, part := range strings.Split(it.traits, ";") {
1282 part = strings.TrimSpace(part)
1283 colon := strings.Index(part, ":")
1284 if colon <= 0 || colon >= len(part)-1 {
1285 continue
1286 }
1287 key := strings.TrimSpace(part[:colon])
1288 val := strings.TrimSpace(part[colon+1:])
1289 if key == "" || val == "" {
1290 continue
1291 }
1292 out = append(out, metadata.Trait{TraitType: key, Value: val})
1293 }
1294 return out
1295}
1296
1297func absImage(u string) string {
1298 if u == "" {
1299 return u
1300 }
1301 if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "ipfs://") {
1302 return u
1303 }
1304 if strings.HasPrefix(u, "/") {
1305 return publicOrigin + u
1306 }
1307 return u
1308}
1309
1310func jsonStr(s string) string {
1311 out := `"`
1312 for _, r := range s {
1313 switch r {
1314 case '"':
1315 out += `\"`
1316 case '\\':
1317 out += `\\`
1318 case '\n':
1319 out += `\n`
1320 case '\r':
1321 out += `\r`
1322 case '\t':
1323 out += `\t`
1324 default:
1325 if r < 0x20 {
1326 out += `\u00`
1327 hex := "0123456789abcdef"
1328 out += string([]byte{hex[r>>4], hex[r&15]})
1329 } else {
1330 out += string(r)
1331 }
1332 }
1333 }
1334 return out + `"`
1335}
1336
1337func pctURI(s string) string {
1338 out := ""
1339 for i := 0; i < len(s); i++ {
1340 c := s[i]
1341 if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~' {
1342 out += string(c)
1343 continue
1344 }
1345 hex := "0123456789ABCDEF"
1346 out += "%"
1347 out += string([]byte{hex[c>>4], hex[c&15]})
1348 }
1349 return out
1350}
1351
1352func splitDropLines(blob string) []string {
1353 blob = strings.ReplaceAll(blob, "\x1e", "\n")
1354 raw := strings.Split(blob, "\n")
1355 out := make([]string, 0, len(raw))
1356 for _, line := range raw {
1357 line = strings.TrimSpace(line)
1358 if line == "" {
1359 continue
1360 }
1361 out = append(out, line)
1362 }
1363 return out
1364}
1365
1366func parseDropLine(line string) (string, string, string, string) {
1367 parts := strings.Split(line, "|")
1368 name := ""
1369 image := ""
1370 rarity := ""
1371 traits := ""
1372 if len(parts) > 0 {
1373 name = strings.TrimSpace(parts[0])
1374 }
1375 if len(parts) > 1 {
1376 image = strings.TrimSpace(parts[1])
1377 }
1378 if len(parts) > 2 {
1379 rarity = strings.TrimSpace(parts[2])
1380 }
1381 if len(parts) > 3 {
1382 traits = strings.TrimSpace(parts[3])
1383 }
1384 return name, image, rarity, traits
1385}
1386
1387func validRarity(s string) bool {
1388 if s == "" {
1389 return true
1390 }
1391 return s == "Common" || s == "Uncommon" || s == "Rare" || s == "Epic" || s == "Legendary" || s == "Mythic"
1392}
1393
1394func validTraits(s string) bool {
1395 if s == "" {
1396 return true
1397 }
1398 if len(s) > 200 {
1399 return false
1400 }
1401 if strings.ContainsAny(s, "\n\r|") {
1402 return false
1403 }
1404 n := 0
1405 for _, part := range strings.Split(s, ";") {
1406 part = strings.TrimSpace(part)
1407 if part == "" {
1408 continue
1409 }
1410 colon := strings.Index(part, ":")
1411 if colon <= 0 || colon >= len(part)-1 {
1412 return false
1413 }
1414 key := strings.TrimSpace(part[:colon])
1415 val := strings.TrimSpace(part[colon+1:])
1416 if key == "" || val == "" {
1417 return false
1418 }
1419 if !asciiSet(key, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _-") {
1420 return false
1421 }
1422 if !asciiSet(val, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _-") {
1423 return false
1424 }
1425 n++
1426 if n > 8 {
1427 return false
1428 }
1429 }
1430 return n >= 1
1431}
1432
1433func validG1(s string) bool {
1434 if len(s) < 39 || len(s) > 64 {
1435 return false
1436 }
1437 if s[0] != 'g' || s[1] != '1' {
1438 return false
1439 }
1440 return asciiSet(s, "abcdefghijklmnopqrstuvwxyz0123456789")
1441}
1442
1443func validPkgName(s string) bool {
1444 if len(s) < minSlugLen || len(s) > grc721.MaxSymbolLen {
1445 return false
1446 }
1447 if s[0] < 'a' || s[0] > 'z' {
1448 return false
1449 }
1450 return asciiSet(s, "abcdefghijklmnopqrstuvwxyz0123456789")
1451}
1452
1453func rarityOf(id int) string {
1454 r := id % 10
1455 if r == 0 {
1456 return "Legendary"
1457 }
1458 if r == 1 {
1459 return "Common"
1460 }
1461 if r == 2 {
1462 return "Uncommon"
1463 }
1464 if r == 3 {
1465 return "Rare"
1466 }
1467 if r == 4 {
1468 return "Epic"
1469 }
1470 if r <= 7 {
1471 return "Uncommon"
1472 }
1473 return "Common"
1474}
1475
1476func validName(name string) bool {
1477 if len(name) < 1 || len(name) > maxNameLen {
1478 return false
1479 }
1480 if strings.ContainsAny(name, "\n\r|\t\"\\") {
1481 return false
1482 }
1483 for _, r := range name {
1484 if r < 0x20 {
1485 return false
1486 }
1487 }
1488 return true
1489}
1490
1491func validImage(u string) bool {
1492 if u == "" {
1493 return true
1494 }
1495 if len(u) > maxImageLen {
1496 return false
1497 }
1498 if strings.ContainsAny(u, " \t\n\r|<>\"'`") {
1499 return false
1500 }
1501 if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {
1502 return true
1503 }
1504 const prefix = "/samples/"
1505 if !strings.HasPrefix(u, prefix) {
1506 return false
1507 }
1508 rest := u[len(prefix):]
1509 if rest == "" {
1510 return false
1511 }
1512 return asciiSet(rest, "abcdefghijklmnopqrstuvwxyz0123456789._-")
1513}
1514
1515func asciiSet(s, allowed string) bool {
1516 for i := 0; i < len(s); i++ {
1517 if !strings.ContainsRune(allowed, rune(s[i])) {
1518 return false
1519 }
1520 }
1521 return true
1522}
1523Raw Package Data
Raw JSON data