← Back to Realms

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}
1523

Raw Package Data

Raw JSON data