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