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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}