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}
984Raw Package Data
Raw JSON data