← Back to Realms

Realm

gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/c/adcol

Overview

Realm Path
gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/c/adcol
Exported Functions
50
State Entries
33
Source Files
3
Total Package Entries
114

Exported Functions

50 exported functions

State

33 state entries

Source Code

FILES
adcol.gno
go
1package adcol
2
3import (
4	"chain"
5	"chain/banker"
6	"chain/runtime/unsafe"
7	"strconv"
8	"strings"
9
10	"gno.land/p/nt/grc721/metadata/v0"
11	"gno.land/p/nt/grc721/v0"
12	"gno.land/p/nt/avl/v0"
13	"gno.land/p/nt/seqid/v0"
14	"gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/factory"
15)
16
17const (
18	maxNameLen      = 64
19	maxImageLen     = 200
20	maxSupplyBound  = 1_000_000
21	maxRoyaltyBps   = 1000
22	nativeDenom     = "ugnot"
23	publicOrigin    = "https://bazaar-gno.vercel.app"
24	dropAddCap      = 20
25	dropSlotHardCap = 10000
26	minSlugLen      = 2
27)
28
29type item struct {
30	id       int
31	name     string
32	image    string
33	owner    address
34	seller   address
35	price    int64
36	listed   bool
37	rarity   string
38	traits   string
39	revealed bool
40	slot     int
41}
42
43type dropSlot struct {
44	name   string
45	image  string
46	rarity string
47	traits string
48}
49
50var (
51	items      avl.Tree
52	dropSlots  avl.Tree
53	token      *grc721.Token
54	ledger     *grc721.PrivateLedger
55	metaView   *metadata.Metadata
56	metaLed    *metadata.Ledger
57	tokenSeq   seqid.ID
58	nextID     int
59	minted     int
60	loaded     int
61	maxSupply  int
62	mintPrice  int64
63	royaltyBps int
64	paused     bool
65	inited     bool
66	creator    address
67	self       address
68	colName    string
69	colSymbol  string
70	colCover   string
71	slug       string
72)
73
74func Init(cur realm, name, symbol, cover string, maxSupplyN int, mintPriceUgnot int64, royalty int64) {
75	if !cur.Previous().IsUserCall() {
76		panic("col: EOA only")
77	}
78	if !cur.IsCurrent() {
79		panic("col: spoofed realm")
80	}
81	if len(symbol) < 1 || len(symbol) > grc721.MaxSymbolLen {
82		panic("col: symbol")
83	}
84	pathSlug := lastElem(cur.PkgPath())
85	if !validPkgName(pathSlug) {
86		panic("col: slug")
87	}
88	if maxSupplyN < 0 || maxSupplyN > maxSupplyBound {
89		panic("col: maxSupply")
90	}
91	if mintPriceUgnot < 0 {
92		panic("col: mintPrice")
93	}
94	if royalty < 0 || royalty > maxRoyaltyBps {
95		panic("col: royalty")
96	}
97	if !validName(name) {
98		panic("col: name")
99	}
100	if !validImage(cover) {
101		panic("col: cover")
102	}
103	if inited {
104		panic("col: already initialized")
105	}
106	slug = pathSlug
107	reservedWho := factory.ReservedCreator(slug)
108	takeLaunchFee(cur)
109	tok, led := grc721.NewToken(name, symbol, tokenSeq.Next(), cur)
110	token = tok
111	ledger = led
112	metaView, metaLed = metadata.NewMetadata(led)
113	creator = cur.Previous().Address()
114	self = cur.Address()
115	colName = name
116	colSymbol = symbol
117	colCover = cover
118	maxSupply = maxSupplyN
119	mintPrice = mintPriceUgnot
120	royaltyBps = int(royalty)
121	items = avl.Tree{}
122	dropSlots = avl.Tree{}
123	loaded = 0
124	inited = true
125	if reservedWho == "" {
126		factory.CreditLaunchFee(cross(cur))
127	}
128	factory.Register(cross(cur), name, symbol, cover, maxSupplyN, mintPriceUgnot, royalty)
129	chain.Emit("Init", "creator", creator.String(), "slug", slug)
130}
131
132func Mint(cur realm, name, imageURL string) int {
133	caller := requireUser(cur)
134	if caller != creator {
135		panic("col: not creator")
136	}
137	id := mintTo(cur, caller, name, imageURL)
138	writeTokenMeta(id)
139	chain.Emit("Mint", "id", strconv.Itoa(id), "owner", caller.String())
140	return id
141}
142
143func PublicMint(cur realm) int {
144	caller := requireUser(cur)
145	takeExact(mintPrice)
146	name := colName + " #" + strconv.Itoa(minted+1)
147	image := publicImage()
148	var slot *dropSlot
149	slotIndex := -1
150	if loaded > 0 {
151		if minted >= loaded {
152			panic("col: no slot")
153		}
154		v := dropSlots.Get(slotKey(minted))
155		if v == nil {
156			panic("col: no slot")
157		}
158		slot = v.(*dropSlot)
159		name = slot.name
160		image = slot.image
161		slotIndex = minted
162	}
163	id := mintTo(cur, caller, name, image)
164	if slot != nil {
165		applySlot(id, slot, slotIndex)
166	}
167	writeTokenMeta(id)
168	if mintPrice > 0 {
169		bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
170		bk.SendCoins(self, creator, chain.Coins{{Denom: nativeDenom, Amount: mintPrice}})
171	}
172	chain.Emit("PublicMint", "id", strconv.Itoa(id), "n", strconv.Itoa(minted))
173	return id
174}
175
176func AddDropItems(cur realm, blob string) {
177	caller := requireUser(cur)
178	if caller != creator {
179		panic("col: not creator")
180	}
181	lines := splitDropLines(blob)
182	if len(lines) < 1 || len(lines) > dropAddCap {
183		panic("col: slots")
184	}
185	if loaded+len(lines) > slotCap() {
186		panic("col: slots")
187	}
188	for _, line := range lines {
189		name, image, rarity, traits := parseDropLine(line)
190		if !validName(name) {
191			panic("col: name")
192		}
193		if !validImage(image) {
194			panic("col: image")
195		}
196		if !validRarity(rarity) {
197			panic("col: rarity")
198		}
199		if !validTraits(traits) {
200			panic("col: traits")
201		}
202		dropSlots.Set(slotKey(loaded), &dropSlot{name: name, image: image, rarity: rarity, traits: traits})
203		loaded++
204	}
205	chain.Emit("AddDropItems", "slug", slug, "loaded", strconv.Itoa(loaded))
206}
207
208func ListDropSlots() string {
209	requireInit()
210	out := ""
211	for i := 0; i < loaded; i++ {
212		v := dropSlots.Get(slotKey(i))
213		if v == nil {
214			continue
215		}
216		s := v.(*dropSlot)
217		if out != "" {
218			out += "\n"
219		}
220		out += strconv.Itoa(i) + "|" + s.name + "|" + s.image + "|" + s.rarity + "|" + s.traits
221	}
222	return out
223}
224
225func LoadedOf() int {
226	requireInit()
227	return loaded
228}
229
230func PauseMint(cur realm) {
231	caller := requireUser(cur)
232	if caller != creator {
233		panic("col: not creator")
234	}
235	paused = true
236	chain.Emit("PauseMint", "slug", slug)
237}
238
239func ResumeMint(cur realm) {
240	caller := requireUser(cur)
241	if caller != creator {
242		panic("col: not creator")
243	}
244	paused = false
245	chain.Emit("ResumeMint", "slug", slug)
246}
247
248func OwnerOf(tid string) string {
249	return mustItem(parseTid(tid)).owner.String()
250}
251
252func GrcName() string {
253	requireInit()
254	return token.GetName()
255}
256
257func GrcSymbol() string {
258	requireInit()
259	return token.GetSymbol()
260}
261
262func GrcBalanceOf(addr string) int64 {
263	requireInit()
264	who := address(addr)
265	if !who.IsValid() {
266		return 0
267	}
268	n, err := token.BalanceOf(who)
269	if err != nil {
270		return 0
271	}
272	return n
273}
274
275func GrcOwnerOf(id int) string {
276	requireInit()
277	owner, err := token.OwnerOf(itemTid(id))
278	if err != nil {
279		panic("col: owner")
280	}
281	return owner.String()
282}
283
284func Name() string {
285	return GrcName()
286}
287
288func Symbol() string {
289	return GrcSymbol()
290}
291
292func BalanceOf(addr string) int64 {
293	return GrcBalanceOf(addr)
294}
295
296func TokenURI(tid string) string {
297	id := parseTid(tid)
298	mustItem(id)
299	if metaView != nil {
300		u, err := metaView.TokenURI(itemTid(id))
301		if err == nil && u != "" {
302			return u
303		}
304	}
305	return tokenJSON(mustItem(id))
306}
307
308func guardUnlisted(id int) {
309	if mustItem(id).listed {
310		panic("col: listed")
311	}
312}
313
314func Approve(cur realm, to, tid string) {
315	caller := requireUser(cur)
316	id := parseTid(tid)
317	guardUnlisted(id)
318	who := address(to)
319	if to != "" && !who.IsValid() {
320		panic("col: addr")
321	}
322	if err := ledger.Approve(caller, who, itemTid(id)); err != nil {
323		panic("col: approve")
324	}
325}
326
327func SetApprovalForAll(cur realm, operator string, approved bool) {
328	caller := requireUser(cur)
329	op := address(operator)
330	if !op.IsValid() {
331		panic("col: addr")
332	}
333	if err := ledger.SetApprovalForAll(caller, op, approved); err != nil {
334		panic("col: approve")
335	}
336}
337
338func TransferFrom(cur realm, from, to, tid string) {
339	caller := requireUser(cur)
340	id := parseTid(tid)
341	guardUnlisted(id)
342	src := address(from)
343	dst := address(to)
344	if !src.IsValid() || !dst.IsValid() {
345		panic("col: addr")
346	}
347	if err := ledger.TransferFrom(caller, src, dst, itemTid(id)); err != nil {
348		panic("col: transfer")
349	}
350	it := mustItem(id)
351	it.owner = dst
352	items.Set(itemKey(id), it)
353}
354
355func GetApproved(id int) string {
356	requireInit()
357	who, err := token.GetApproved(itemTid(id))
358	if err != nil {
359		return ""
360	}
361	return who.String()
362}
363
364func IsApprovedForAll(owner, operator string) bool {
365	requireInit()
366	a := address(owner)
367	b := address(operator)
368	if !a.IsValid() || !b.IsValid() {
369		return false
370	}
371	return token.IsApprovedForAll(a, b)
372}
373
374func TotalSupply() int64 {
375	requireInit()
376	return token.TotalSupply()
377}
378
379func SellerOf(id int) address {
380	return mustItem(id).seller
381}
382
383func PriceOf(id int) int64 {
384	return mustItem(id).price
385}
386
387func NameOf(id int) string {
388	return mustItem(id).name
389}
390
391func ImageOf(id int) string {
392	return mustItem(id).image
393}
394
395func Listed(id int) bool {
396	return mustItem(id).listed
397}
398
399func NextID() int {
400	return nextID
401}
402
403func CollectionOf(_ int) string {
404	requireInit()
405	return slug
406}
407
408func ItemLine(id int) string {
409	it := mustItem(id)
410	return strconv.Itoa(it.id) + "|" + it.name + "|" + it.owner.String() + "|" + it.seller.String() + "|" + strconv.FormatInt(it.price, 10) + "|" + strconv.FormatBool(it.listed) + "|" + it.image + "|" + slug + "|" + it.rarity + "|" + it.traits + "|" + strconv.FormatBool(it.revealed) + "|" + strconv.Itoa(it.slot)
411}
412
413func ListOpen() string {
414	requireInit()
415	out := ""
416	n := 0
417	items.Iterate("", "", func(_ string, value any) bool {
418		it := value.(*item)
419		if !it.listed {
420			return false
421		}
422		if n >= 50 {
423			return true
424		}
425		if out != "" {
426			out += "\n"
427		}
428		out += ItemLine(it.id)
429		n++
430		return false
431	})
432	return out
433}
434
435func TokensOf(owner string) string {
436	requireInit()
437	who := address(owner)
438	if !who.IsValid() {
439		return ""
440	}
441	out := ""
442	items.Iterate("", "", func(_ string, value any) bool {
443		it := value.(*item)
444		if it.owner != who {
445			return false
446		}
447		if out != "" {
448			out += ","
449		}
450		out += strconv.Itoa(it.id)
451		return false
452	})
453	return out
454}
455
456func TokenOfOwnerByIndex(owner string, index int64) string {
457	requireInit()
458	who := address(owner)
459	if !who.IsValid() {
460		panic("col: addr")
461	}
462	if index < 0 {
463		panic("col: index")
464	}
465	var n int64
466	found := ""
467	items.Iterate("", "", func(_ string, value any) bool {
468		it := value.(*item)
469		if it.owner != who {
470			return false
471		}
472		if n == index {
473			found = strconv.Itoa(it.id)
474			return true
475		}
476		n++
477		return false
478	})
479	if found == "" {
480		panic("col: index")
481	}
482	return found
483}
484
485func TokenByIndex(index int64) string {
486	requireInit()
487	if index < 0 {
488		panic("col: index")
489	}
490	var n int64
491	found := ""
492	items.Iterate("", "", func(_ string, value any) bool {
493		if n == index {
494			found = strconv.Itoa(value.(*item).id)
495			return true
496		}
497		n++
498		return false
499	})
500	if found == "" {
501		panic("col: index")
502	}
503	return found
504}
505
506func Creator() string {
507	requireInit()
508	return creator.String()
509}
510
511func MintPrice() int64 {
512	requireInit()
513	return mintPrice
514}
515
516func MaxSupply() int {
517	requireInit()
518	return maxSupply
519}
520
521func Minted() int {
522	requireInit()
523	return minted
524}
525
526func RoyaltyBps() int {
527	requireInit()
528	return royaltyBps
529}
530
531func SetRoyalty(cur realm, bps int) {
532	caller := requireUser(cur)
533	if caller != creator {
534		panic("col: not creator")
535	}
536	if minted != 0 {
537		panic("col: minted")
538	}
539	if bps < 0 || bps > maxRoyaltyBps {
540		panic("col: royalty")
541	}
542	royaltyBps = bps
543	factory.NoteRoyalty(cross(cur), bps)
544	chain.Emit("SetRoyalty", "slug", slug, "bps", strconv.Itoa(bps))
545}
546
547func RarityOf(id int) string {
548	return mustItem(id).rarity
549}
550
551func TraitsOf(id int) string {
552	return mustItem(id).traits
553}
554
555func Render(_ string) string {
556	if !inited {
557		return "# Collection\n\nCall Init first.\n"
558	}
559	out := "# " + colName + " ($" + colSymbol + ")\n\n"
560	out += "- minted: " + strconv.Itoa(minted) + "\n"
561	out += "- mint price: " + strconv.FormatInt(mintPrice, 10) + " ugnot\n\n"
562	open := ListOpen()
563	if open == "" {
564		out += "_No open listings._\n"
565	} else {
566		out += "```\n" + open + "\n```\n"
567	}
568	return out
569}
570
571func mintTo(cur realm, caller address, name, imageURL string) int {
572	requireInit()
573	if !cur.IsCurrent() {
574		panic("col: spoofed realm")
575	}
576	if paused {
577		panic("col: paused")
578	}
579	if maxSupply > 0 && minted >= maxSupply {
580		panic("col: sold out")
581	}
582	if !validName(name) {
583		panic("col: name")
584	}
585	if !validImage(imageURL) {
586		panic("col: image")
587	}
588	nextID++
589	id := nextID
590	it := &item{
591		id:       id,
592		name:     name,
593		image:    imageURL,
594		owner:    caller,
595		rarity:   rarityOf(id),
596		revealed: true,
597		slot:     -1,
598	}
599	items.Set(itemKey(id), it)
600	err := ledger.Mint(caller, itemTid(id))
601	if err != nil {
602		panic("col: mint")
603	}
604	minted++
605	factory.NoteMinted(cross(cur), minted)
606	return id
607}
608
609func takeLaunchFee(cur realm) {
610	fee := factory.LaunchFee()
611	who := factory.ReservedCreator(lastElem(cur.PkgPath()))
612	if who != "" && who == cur.Previous().Address().String() {
613		takeExact(0)
614		return
615	}
616	takeExact(fee)
617	if fee > 0 {
618		bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
619		bk.SendCoins(cur.Address(), factory.Address(), chain.Coins{{Denom: nativeDenom, Amount: fee}})
620	}
621}
622
623func takeExact(want int64) {
624	sent := unsafe.OriginSend()
625	if want == 0 {
626		if len(sent) != 0 {
627			panic("col: extra denoms")
628		}
629		return
630	}
631	if len(sent) != 1 || sent[0].Denom != nativeDenom || sent[0].Amount != want {
632		panic("col: wrong payment")
633	}
634}
635
636func publicImage() string {
637	img := "/samples/" + slug + "-0" + strconv.Itoa(minted+1) + ".png"
638	if validImage(img) {
639		return img
640	}
641	return colCover
642}
643
644func requireInit() {
645	if !inited {
646		panic("col: call Init first")
647	}
648}
649
650func requireUser(cur realm) address {
651	requireInit()
652	if !cur.IsCurrent() {
653		panic("col: spoofed realm")
654	}
655	if !cur.Previous().IsUserCall() {
656		panic("col: EOA only")
657	}
658	return cur.Previous().Address()
659}
660
661func itemTid(id int) grc721.TokenID {
662	return grc721.TokenID(strconv.Itoa(id))
663}
664
665func parseTid(tid string) int {
666	id, err := strconv.Atoi(tid)
667	if err != nil || id < 1 {
668		panic("col: tid")
669	}
670	return id
671}
672
673func mustItem(id int) *item {
674	v := items.Get(itemKey(id))
675	if v == nil {
676		panic("col: unknown item")
677	}
678	return v.(*item)
679}
680
681func itemKey(id int) string {
682	return strconv.Itoa(id)
683}
684
685func lastElem(pkg string) string {
686	i := strings.LastIndex(pkg, "/")
687	if i < 0 || i+1 >= len(pkg) {
688		panic("col: pkg")
689	}
690	return pkg[i+1:]
691}
692
693func slotKey(i int) string {
694	return strconv.Itoa(i)
695}
696
697func slotCap() int {
698	if maxSupply > 0 {
699		return maxSupply
700	}
701	return dropSlotHardCap
702}
703
704func applySlot(id int, s *dropSlot, slotIndex int) {
705	it := mustItem(id)
706	it.name = s.name
707	it.image = s.image
708	if s.rarity != "" {
709		it.rarity = s.rarity
710	}
711	it.traits = s.traits
712	it.revealed = true
713	it.slot = slotIndex
714	items.Set(itemKey(id), it)
715}
716
717func writeTokenMeta(id int) {
718	it := mustItem(id)
719	uri := tokenJSON(it)
720	if metaLed == nil {
721		return
722	}
723	if err := metaLed.SetTokenURI(itemTid(id), uri); err != nil {
724		panic("col: tokenURI")
725	}
726	if err := metaLed.SetTokenMetadata(itemTid(id), metadata.Data{
727		Name:        it.name,
728		Description: colName + " #" + strconv.Itoa(id),
729		Image:       absImage(it.image),
730		Attributes:  traitAttrs(it),
731	}); err != nil {
732		panic("col: metadata")
733	}
734}
735
736func tokenJSON(it *item) string {
737	attrs := ""
738	for _, tr := range traitAttrs(it) {
739		if attrs != "" {
740			attrs += ","
741		}
742		attrs += `{"trait_type":` + jsonStr(tr.TraitType) + `,"value":` + jsonStr(tr.Value) + `}`
743	}
744	body := `{"name":` + jsonStr(it.name) + `,"description":` + jsonStr(colName+" #"+strconv.Itoa(it.id)) + `,"image":` + jsonStr(absImage(it.image)) + `,"attributes":[` + attrs + `]}`
745	return "data:application/json;charset=utf-8," + pctURI(body)
746}
747
748func traitAttrs(it *item) []metadata.Trait {
749	out := []metadata.Trait{}
750	if it.rarity != "" {
751		out = append(out, metadata.Trait{TraitType: "Rarity", Value: it.rarity})
752	}
753	for _, part := range strings.Split(it.traits, ";") {
754		part = strings.TrimSpace(part)
755		colon := strings.Index(part, ":")
756		if colon <= 0 || colon >= len(part)-1 {
757			continue
758		}
759		key := strings.TrimSpace(part[:colon])
760		val := strings.TrimSpace(part[colon+1:])
761		if key == "" || val == "" {
762			continue
763		}
764		out = append(out, metadata.Trait{TraitType: key, Value: val})
765	}
766	return out
767}
768
769func absImage(u string) string {
770	if u == "" {
771		return u
772	}
773	if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "ipfs://") {
774		return u
775	}
776	if strings.HasPrefix(u, "/") {
777		return publicOrigin + u
778	}
779	return u
780}
781
782func jsonStr(s string) string {
783	out := `"`
784	for _, r := range s {
785		switch r {
786		case '"':
787			out += `\"`
788		case '\\':
789			out += `\\`
790		case '\n':
791			out += `\n`
792		case '\r':
793			out += `\r`
794		case '\t':
795			out += `\t`
796		default:
797			if r < 0x20 {
798				out += `\u00`
799				hex := "0123456789abcdef"
800				out += string([]byte{hex[r>>4], hex[r&15]})
801			} else {
802				out += string(r)
803			}
804		}
805	}
806	return out + `"`
807}
808
809func pctURI(s string) string {
810	out := ""
811	for i := 0; i < len(s); i++ {
812		c := s[i]
813		if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~' {
814			out += string(c)
815			continue
816		}
817		hex := "0123456789ABCDEF"
818		out += "%"
819		out += string([]byte{hex[c>>4], hex[c&15]})
820	}
821	return out
822}
823
824func splitDropLines(blob string) []string {
825	raw := strings.Split(blob, "\n")
826	out := make([]string, 0, len(raw))
827	for _, line := range raw {
828		line = strings.TrimSpace(line)
829		if line == "" {
830			continue
831		}
832		out = append(out, line)
833	}
834	return out
835}
836
837func parseDropLine(line string) (string, string, string, string) {
838	parts := strings.Split(line, "|")
839	name := ""
840	image := ""
841	rarity := ""
842	traits := ""
843	if len(parts) > 0 {
844		name = strings.TrimSpace(parts[0])
845	}
846	if len(parts) > 1 {
847		image = strings.TrimSpace(parts[1])
848	}
849	if len(parts) > 2 {
850		rarity = strings.TrimSpace(parts[2])
851	}
852	if len(parts) > 3 {
853		traits = strings.TrimSpace(parts[3])
854	}
855	return name, image, rarity, traits
856}
857
858func validRarity(s string) bool {
859	if s == "" {
860		return true
861	}
862	return s == "Common" || s == "Uncommon" || s == "Rare" || s == "Epic" || s == "Legendary"
863}
864
865func validTraits(s string) bool {
866	if s == "" {
867		return true
868	}
869	if len(s) > 200 {
870		return false
871	}
872	if strings.ContainsAny(s, "\n\r|") {
873		return false
874	}
875	n := 0
876	for _, part := range strings.Split(s, ";") {
877		part = strings.TrimSpace(part)
878		if part == "" {
879			continue
880		}
881		colon := strings.Index(part, ":")
882		if colon <= 0 || colon >= len(part)-1 {
883			return false
884		}
885		key := strings.TrimSpace(part[:colon])
886		val := strings.TrimSpace(part[colon+1:])
887		if key == "" || val == "" {
888			return false
889		}
890		if !asciiSet(key, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _-") {
891			return false
892		}
893		if !asciiSet(val, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _-") {
894			return false
895		}
896		n++
897		if n > 8 {
898			return false
899		}
900	}
901	return n >= 1
902}
903
904func validPkgName(s string) bool {
905	if len(s) < minSlugLen || len(s) > grc721.MaxSymbolLen {
906		return false
907	}
908	if s[0] < 'a' || s[0] > 'z' {
909		return false
910	}
911	return asciiSet(s, "abcdefghijklmnopqrstuvwxyz0123456789")
912}
913
914func rarityOf(id int) string {
915	r := id % 10
916	if r == 0 {
917		return "Legendary"
918	}
919	if r == 1 {
920		return "Common"
921	}
922	if r == 2 {
923		return "Uncommon"
924	}
925	if r == 3 {
926		return "Rare"
927	}
928	if r == 4 {
929		return "Epic"
930	}
931	if r <= 7 {
932		return "Uncommon"
933	}
934	return "Common"
935}
936
937func validName(name string) bool {
938	if len(name) < 1 || len(name) > maxNameLen {
939		return false
940	}
941	if strings.ContainsAny(name, "\n\r|\t\"\\") {
942		return false
943	}
944	for _, r := range name {
945		if r < 0x20 {
946			return false
947		}
948	}
949	return true
950}
951
952func validImage(u string) bool {
953	if u == "" {
954		return true
955	}
956	if len(u) > maxImageLen {
957		return false
958	}
959	if strings.ContainsAny(u, " \t\n\r|<>\"'`") {
960		return false
961	}
962	if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {
963		return true
964	}
965	const prefix = "/samples/"
966	if !strings.HasPrefix(u, prefix) {
967		return false
968	}
969	rest := u[len(prefix):]
970	if rest == "" {
971		return false
972	}
973	return asciiSet(rest, "abcdefghijklmnopqrstuvwxyz0123456789._-")
974}
975
976func asciiSet(s, allowed string) bool {
977	for i := 0; i < len(s); i++ {
978		if !strings.ContainsRune(allowed, rune(s[i])) {
979			return false
980		}
981	}
982	return true
983}
984

Raw Package Data

Raw JSON data