Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}