json.gno
4.73 Kb · 153 lines
1package skins
2
3import (
4 "errors"
5 "strings"
6
7 "gno.land/p/nt/grc721/v0"
8 "gno.land/p/onbloc/json/v0"
9)
10
11// CatalogJSON returns every design, sorted by id, as a JSON array:
12//
13// [ { "id": "baron", "name": "Red Baron", "model": "biplane", "pilot": "none",
14// "body": "#c1121f", "wing": "#780000", "accent": "#fdf0d5",
15// "price": 1000000, "listed": true, "minted": 3, "maxSupply": 0 } ]
16//
17// The output has no whitespace. maxSupply 0 is unlimited. Classic, the free
18// look, is not in it: the client draws it without a token.
19func CatalogJSON() string {
20 items := []*json.Node{}
21 designs.Iterate("", "", func(_ string, v any) bool {
22 d := v.(*design)
23 b := json.Builder().
24 WriteString("id", d.id).
25 WriteString("name", d.name).
26 WriteString("model", d.model).
27 WriteString("pilot", d.pilot).
28 WriteString("body", d.body).
29 WriteString("wing", d.wing).
30 WriteString("accent", d.accent).
31 WriteNumber("price", float64(d.price)).
32 WriteBool("listed", d.listed).
33 WriteNumber("minted", float64(d.minted)).
34 WriteNumber("maxSupply", float64(d.maxSupply))
35 items = append(items, b.Node())
36 return false
37 })
38 return encode(json.ArrayNode("", items))
39}
40
41// SkinsOf returns the designs addr owns a token of, sorted by id, as a JSON
42// array: ["baron","goldie"]. It is [] for an address without skins.
43func SkinsOf(addr address) string {
44 ids := []*json.Node{}
45 prefix := addr.String() + "/"
46 if validAddress(addr) {
47 // Keys sort as "<addr>/<design>": the designs of addr are one range,
48 // "/" + 1 is "0".
49 owned.Iterate(prefix, addr.String()+"0", func(key string, _ any) bool {
50 ids = append(ids, json.StringNode("", strings.TrimPrefix(key, prefix)))
51 return false
52 })
53 }
54 return encode(json.ArrayNode("", ids))
55}
56
57// The read functions a wallet calls take a grc721.TokenID and return an
58// error rather than panic, the way Adena calls them: OwnerOf("1") into two
59// values, TokenURI("1") as the image it shows, TokenMetadata("1") as JSON
60// for the traits. The errors are made on the call, not stored (errors.gno).
61
62// OwnerOf returns who owns skin token tid, or an error when there is none.
63func OwnerOf(tid grc721.TokenID) (address, error) {
64 id, err := mintedID(tid)
65 if err != nil {
66 return "", err
67 }
68 return ownerOf(id), nil
69}
70
71// BalanceOf returns how many skin tokens addr owns, one per design at most.
72func BalanceOf(addr address) (int64, error) {
73 return token.BalanceOf(addr)
74}
75
76// TokenMetadata returns the metadata of skin token tid as JSON, the same for
77// every token of a design, with the keys Adena reads:
78//
79// {"name":"Red Baron","description":"A gnofly skin, a biplane. ...",
80// "image":"https://gnofly.xyz/nft/skins/baron.png",
81// "attributes":[{"traitType":"model","value":"biplane"}, ...]}
82func TokenMetadata(tid grc721.TokenID) (string, error) {
83 id, err := mintedID(tid)
84 if err != nil {
85 return "", err
86 }
87 d := getDesign(skinOf(id))
88 if d == nil {
89 return "", errors.New(errNotMinted)
90 }
91 pilot := ""
92 if d.pilot == "gnome" {
93 pilot = " flown by a gnome"
94 }
95 b := json.Builder().
96 WriteString("name", d.name).
97 WriteString("description", "A gnofly skin, a "+d.model+pilot+". Bound to its buyer: it cannot be transferred.").
98 WriteString("image", uriBase+d.id+".png").
99 WriteArray("attributes", func(ab *json.ArrayBuilder) {
100 for _, t := range [][2]string{
101 {"model", d.model}, {"pilot", d.pilot}, {"body", d.body}, {"wing", d.wing},
102 {"accent", d.accent}, {"soulbound", "yes"},
103 } {
104 ab.WriteObject(func(o *json.NodeBuilder) {
105 o.WriteString("traitType", t[0]).WriteString("value", t[1])
106 })
107 }
108 })
109 return encode(b.Node()), nil
110}
111
112// mintedID reads a token id, "1" and up in decimal, and checks it exists.
113func mintedID(tid grc721.TokenID) (int64, error) {
114 s := string(tid)
115 n := int64(0)
116 for i := 0; i < len(s); i++ {
117 if s[i] < '0' || s[i] > '9' || i > 15 || (i == 0 && s[i] == '0') {
118 return 0, errors.New(errNotMinted)
119 }
120 n = n*10 + int64(s[i]-'0')
121 }
122 if n == 0 || ownerOf(n) == "" {
123 return 0, errors.New(errNotMinted)
124 }
125 return n, nil
126}
127
128// SkinOf returns the design of skin token id, "" when there is no such token.
129func SkinOf(id int64) string {
130 return skinOf(id)
131}
132
133// TokenURI returns the image of skin token tid,
134// "https://gnofly.xyz/nft/skins/<design>.png": what a wallet shows (Adena
135// puts it in an <img>). The metadata is TokenMetadata, or
136// "https://gnofly.xyz/nft/skins/<design>.json" on the website. It is
137// computed, not stored: a token costs no URI in deposit.
138func TokenURI(tid grc721.TokenID) (string, error) {
139 id, err := mintedID(tid)
140 if err != nil {
141 return "", err
142 }
143 return uriBase + skinOf(id) + ".png", nil
144}
145
146// encode marshals a document built above; it cannot fail on one.
147func encode(n *json.Node) string {
148 out, err := json.Marshal(n)
149 if err != nil {
150 panic(err)
151 }
152 return string(out)
153}