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

5.68 Kb · 179 lines
  1package nsnft
  2
  3import (
  4	"chain"
  5	"encoding/base64"
  6	"strings"
  7
  8	"gno.land/p/nt/grc721/v0"
  9)
 10
 11/*
 12WHAT A WALLET SHOWS, AND WHY THIS FROZEN REALM DOES NOT DRAW IT.
 13
 14Adena puts TokenURI(tid) straight into an <img src>, so it must return
 15the image itself (a data:image/svg+xml URI), and it reads TokenMetadata
 16as a camelCase JSON string. Both have to be answered HERE, because the
 17collection id names this realm. But the card is the most cosmetic and
 18most-revised thing in the project, and this realm can never change — so
 19the drawing is delegated to a view realm through a Renderer, and only
 20the delegation is frozen.
 21
 22This realm cannot import the view: the view reads the registry, which
 23reads this ledger, and imports may not form a cycle. So the view hands
 24this realm an object at runtime instead. gno runs a method declared in
 25another realm with THAT realm's storage authority (the declaring-realm
 26borrow), so the renderer can read anything and write nothing of ours —
 27it gets no `cur`, so it cannot cross into anything as us either. What it
 28can do is draw something wrong, slow or broken, which is a display
 29problem: ClearRenderer withdraws it in one transaction (admin, recovery
 30or guardian) and wallets fall back to the built-in card below.
 31
 32Who may register is pinned to ONE package path, approved through the
 33timelock (ProposeRenderer), because what every wallet shows for every
 34name is worth a waiting period: a hostile card could tell holders to pay
 35somebody. The ledger is born approving its own generation's view.
 36*/
 37
 38// Renderer draws one token for wallets. Implemented by the view realm.
 39type Renderer interface {
 40	// TokenImage returns the image itself, as a data: URI.
 41	TokenImage(tid string) string
 42	// TokenMetadata returns the metadata as a JSON string: name,
 43	// description, externalUrl, attributes [{traitType, value}].
 44	TokenMetadata(tid string) string
 45}
 46
 47var (
 48	// The one realm allowed to register a renderer; "" for none.
 49	rendererPath string
 50	// What it registered; nil means the built-in card.
 51	renderer Renderer
 52)
 53
 54// RegisterRenderer is called BY the approved view realm, handing over
 55// the object that draws. Refused from anywhere else.
 56func RegisterRenderer(cur realm, r Renderer) {
 57	assertNoSend(cur)
 58	registerRenderer(cur.Previous().PkgPath(), r)
 59}
 60
 61func registerRenderer(callerPath string, r Renderer) {
 62	if rendererPath == "" || callerPath != rendererPath {
 63		panic("nsnft: only the approved view realm may register a renderer")
 64	}
 65	if r == nil {
 66		panic("nsnft: no renderer given")
 67	}
 68	renderer = r
 69	chain.Emit("RendererRegistered", "path", callerPath)
 70}
 71
 72// ClearRenderer withdraws the art at once, approval included, so the
 73// realm that drew it cannot simply register again: approving one takes
 74// the timelock. Admin, recovery or guardian.
 75func ClearRenderer(cur realm) {
 76	assertNoSend(cur)
 77	clearRenderer(cur.Previous().Address())
 78}
 79
 80func clearRenderer(caller address) {
 81	assertCanRevoke(caller)
 82	prev := rendererPath
 83	rendererPath = ""
 84	renderer = nil
 85	chain.Emit("RendererCleared", "path", prev, "by", caller.String())
 86}
 87
 88func RendererPath() string { return rendererPath }
 89func HasRenderer() bool    { return renderer != nil }
 90
 91// TokenURI is the token's image, as a data: URI, for a token that exists.
 92// A renderer that answers nothing gets the built-in card instead: an
 93// empty <img src> is a broken picture in every wallet.
 94func TokenURI(tid string) (string, error) {
 95	if !Exists(tid) {
 96		return "", grc721.ErrInvalidTokenId
 97	}
 98	if renderer != nil {
 99		if img := renderer.TokenImage(tid); img != "" {
100			return img, nil
101		}
102	}
103	return builtinImage(tid), nil
104}
105
106// TokenMetadata is the token's metadata as a camelCase JSON string, the
107// shape Adena reads. The image is not in it; that is TokenURI.
108func TokenMetadata(tid string) (string, error) {
109	if !Exists(tid) {
110		return "", grc721.ErrInvalidTokenId
111	}
112	if renderer != nil {
113		if meta := renderer.TokenMetadata(tid); meta != "" {
114			return meta, nil
115		}
116	}
117	return builtinMetadata(tid), nil
118}
119
120/*
121THE BUILT-IN CARD, for when no view is registered: the name, plainly.
122
123Deliberately plain and deliberately free of anything that could go
124stale. No URL — a domain this realm named could lapse and be bought by
125somebody else, and the link would then live in every wallet forever — and
126no colours or claims that a policy change could make wrong.
127*/
128func builtinImage(tid string) string {
129	size := 40
130	if n := len(tid); n > 16 {
131		size = 640 / n
132		if size < 8 {
133			size = 8
134		}
135	}
136	svg := `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">` +
137		`<rect width="400" height="400" fill="#0b0d10"/>` +
138		`<text x="200" y="200" text-anchor="middle" font-family="monospace" font-size="` +
139		itoa(int64(size)) + `" fill="#f4f4f5">` + escapeXML(tid) + `</text>` +
140		`<text x="200" y="372" text-anchor="middle" font-family="monospace" font-size="13"` +
141		` letter-spacing="3" fill="#71717a">MEME NAME SERVICE</text></svg>`
142	return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))
143}
144
145func builtinMetadata(tid string) string {
146	return `{"name":"` + escapeJSON(tid) + `",` +
147		`"description":"A name on the Meme Name Service.",` +
148		`"attributes":[]}`
149}
150
151func escapeXML(s string) string {
152	s = strings.ReplaceAll(s, "&", "&amp;")
153	s = strings.ReplaceAll(s, "<", "&lt;")
154	s = strings.ReplaceAll(s, ">", "&gt;")
155	s = strings.ReplaceAll(s, `"`, "&quot;")
156	s = strings.ReplaceAll(s, "'", "&apos;")
157	return s
158}
159
160func escapeJSON(s string) string {
161	var b strings.Builder
162	for i := 0; i < len(s); i++ {
163		c := s[i]
164		switch {
165		case c == '"':
166			b.WriteString(`\"`)
167		case c == '\\':
168			b.WriteString(`\\`)
169		case c < 0x20:
170			const hex = "0123456789abcdef"
171			b.WriteString(`\u00`)
172			b.WriteByte(hex[c>>4])
173			b.WriteByte(hex[c&0xf])
174		default:
175			b.WriteByte(c)
176		}
177	}
178	return b.String()
179}