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.

art.gno

6.62 Kb · 226 lines
  1package nsskin
  2
  3import (
  4	"chain/runtime"
  5	"encoding/base64"
  6	"strconv"
  7	"strings"
  8
  9	"gno.land/p/nt/grc721/v0"
 10)
 11
 12/*
 13WHAT A WALLET SHOWS FOR A SKIN.
 14
 15Adena puts TokenURI(tid) straight into an <img src>, so it must return
 16the image itself, as a data URI, and it reads TokenMetadata as a camelCase
 17JSON string. The previous TokenURI returned a gnoweb path, which no wallet
 18can draw.
 19
 20The picture is drawn HERE, from the definition's own sprite, rather than
 21by nsview: nsview imports this realm to put skins on name cards, so this
 22realm cannot import it back. It is the bare artwork on a dark matte —
 23nsview's matted shop card stays the gnoweb page, linked as externalUrl.
 24This realm is replaceable, so a richer wallet image is a redeploy away.
 25
 26A WITHDRAWN SKIN DRAWS NOTHING. Withdrawal is the takedown control
 27(SKINS.md §6): a design that turns out to be somebody else's character or
 28something vile must stop appearing in wallets with our name beside it the
 29moment it is withdrawn. So its image is a neutral placeholder, its
 30metadata omits its name and link, and nothing else about the token
 31changes — the holder keeps it.
 32*/
 33
 34// TokenURI is the skin's image, as a data URI, for a token that exists.
 35func TokenURI(tid grc721.TokenID) (string, error) {
 36	if _, err := token.OwnerOf(tid); err != nil {
 37		return "", err
 38	}
 39	s, ok := skinOfTID(string(tid))
 40	if !ok {
 41		return placeholderImage("UNKNOWN SKIN"), nil
 42	}
 43	if s.withdrawn {
 44		return placeholderImage("WITHDRAWN"), nil
 45	}
 46	if svg, ok := spriteSVG(s.art); ok {
 47		return dataURI(svg), nil
 48	}
 49	return placeholderImage("ART WILL NOT RENDER"), nil
 50}
 51
 52// TokenMetadata is the skin's metadata as a camelCase JSON string: name,
 53// description, externalUrl and attributes of {traitType, value}. No image
 54// in it; that is TokenURI.
 55func TokenMetadata(tid grc721.TokenID) (string, error) {
 56	if _, err := token.OwnerOf(tid); err != nil {
 57		return "", err
 58	}
 59	t := string(tid)
 60	id, serial := t, ""
 61	if h := strings.LastIndex(t, "#"); h > 0 {
 62		id, serial = t[:h], t[h+1:]
 63	}
 64	s, ok := skinOfTID(t)
 65	if !ok || s.withdrawn {
 66		return `{"name":"Withdrawn skin #` + escapeJSON(serial) + `",` +
 67			`"description":"This skin has been withdrawn and no longer draws on any name.",` +
 68			`"attributes":[{"traitType":"Skin","value":"` + escapeJSON(id) + `"},` +
 69			`{"traitType":"Status","value":"withdrawn"}]}`, nil
 70	}
 71	goesOn := "any name"
 72	if s.lock != "" {
 73		goesOn = "*" + s.lock + " names"
 74	}
 75	worn := "no"
 76	if bindingIsLive(t) {
 77		b := bound.Get(t).(*binding)
 78		worn = b.label + "*" + b.domain
 79	}
 80	out := `{"name":"` + escapeJSON(s.name) + ` #` + escapeJSON(serial) + `",` +
 81		`"description":"Card art for a Meme Name Service name. Wear it and it travels with the name.",`
 82	if u := externalURL(t); u != "" {
 83		out += `"externalUrl":"` + escapeJSON(u) + `",`
 84	}
 85	out += `"attributes":[` +
 86		`{"traitType":"Skin","value":"` + escapeJSON(id) + `"},` +
 87		`{"traitType":"Edition","value":"` + escapeJSON(serial) + `"},` +
 88		`{"traitType":"Goes on","value":"` + escapeJSON(goesOn) + `"},` +
 89		`{"traitType":"Worn by","value":"` + escapeJSON(worn) + `"}]}`
 90	return out, nil
 91}
 92
 93func skinOfTID(tid string) (*skin, bool) {
 94	id, ok := skinOfToken(tid)
 95	if !ok {
 96		return nil, false
 97	}
 98	v := skins.Get(strings.ToLower(strings.TrimSpace(id)))
 99	if v == nil {
100		return nil, false
101	}
102	return v.(*skin), true
103}
104
105// externalURL is the skin's own page: tokenURIBase plus the token id, on
106// the chain's gnoweb when the base is a path. "#" would start a URL
107// fragment, so it is escaped.
108func externalURL(tid string) string {
109	esc := strings.ReplaceAll(tid, "#", "%23")
110	switch {
111	case strings.HasPrefix(tokenURIBase, "https://"):
112		return tokenURIBase + esc
113	case strings.HasPrefix(tokenURIBase, "/"):
114		return "https://" + runtime.ChainDomain() + tokenURIBase + esc
115	}
116	return ""
117}
118
119/*
120spriteSVG draws the sprite format DefineSkin validates ("A#rrggbb,..|row|row")
121as one rect per horizontal run, on a dark matte. It re-checks every rule
122assertValidArt applies rather than trusting that it ran, and refuses the
123whole sprite on any fault — a picture with holes in it is worse than an
124honest placeholder. Colours are emitted only after they are checked to be
125exactly # and six hex digits, so nothing from the definition reaches the
126SVG unvalidated.
127*/
128func spriteSVG(art string) (string, bool) {
129	parts := strings.Split(art, "|")
130	n := len(parts) - 1
131	if n < 4 || n > 64 {
132		return "", false
133	}
134	pal := map[byte]string{}
135	for _, e := range strings.Split(parts[0], ",") {
136		if len(e) != 8 || e[1] != '#' || !isHex6(e[2:]) {
137			return "", false
138		}
139		pal[e[0]] = e[1:]
140	}
141	size := strconv.Itoa(n)
142	var b strings.Builder
143	b.WriteString(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ` + size + ` ` + size +
144		`" shape-rendering="crispEdges"><rect width="` + size + `" height="` + size + `" fill="#0b0d10"/>`)
145	for y := 0; y < n; y++ {
146		row := parts[y+1]
147		if len(row) != n {
148			return "", false
149		}
150		for x := 0; x < n; {
151			c := row[x]
152			if c == '.' {
153				x++
154				continue
155			}
156			col, ok := pal[c]
157			if !ok {
158				return "", false
159			}
160			x0 := x
161			for x < n && row[x] == c {
162				x++
163			}
164			b.WriteString(`<rect x="` + strconv.Itoa(x0) + `" y="` + strconv.Itoa(y) +
165				`" width="` + strconv.Itoa(x-x0) + `" height="1" fill="` + col + `"/>`)
166		}
167	}
168	b.WriteString(`</svg>`)
169	return b.String(), true
170}
171
172func isHex6(s string) bool {
173	if len(s) != 6 {
174		return false
175	}
176	for i := 0; i < 6; i++ {
177		c := s[i]
178		if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') {
179			continue
180		}
181		return false
182	}
183	return true
184}
185
186func placeholderImage(msg string) string {
187	return dataURI(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">` +
188		`<rect width="400" height="400" fill="#0b0d10"/>` +
189		`<rect x="40" y="40" width="320" height="320" fill="none" stroke="#3f3f46" stroke-width="2" stroke-dasharray="7 6"/>` +
190		`<text x="200" y="206" text-anchor="middle" font-family="monospace" font-size="16" fill="#a1a1aa">` +
191		escapeXML(msg) + `</text></svg>`)
192}
193
194func dataURI(svg string) string {
195	return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))
196}
197
198func escapeXML(s string) string {
199	s = strings.ReplaceAll(s, "&", "&amp;")
200	s = strings.ReplaceAll(s, "<", "&lt;")
201	s = strings.ReplaceAll(s, ">", "&gt;")
202	s = strings.ReplaceAll(s, `"`, "&quot;")
203	s = strings.ReplaceAll(s, "'", "&apos;")
204	return s
205}
206
207func escapeJSON(s string) string {
208	var b strings.Builder
209	for i := 0; i < len(s); i++ {
210		c := s[i]
211		switch {
212		case c == '"':
213			b.WriteString(`\"`)
214		case c == '\\':
215			b.WriteString(`\\`)
216		case c < 0x20:
217			const hex = "0123456789abcdef"
218			b.WriteString(`\u00`)
219			b.WriteByte(hex[c>>4])
220			b.WriteByte(hex[c&0xf])
221		default:
222			b.WriteByte(c)
223		}
224	}
225	return b.String()
226}