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