package nsnft import ( "chain" "encoding/base64" "strings" "gno.land/p/nt/grc721/v0" ) /* WHAT A WALLET SHOWS, AND WHY THIS FROZEN REALM DOES NOT DRAW IT. Adena puts TokenURI(tid) straight into an , so it must return the image itself (a data:image/svg+xml URI), and it reads TokenMetadata as a camelCase JSON string. Both have to be answered HERE, because the collection id names this realm. But the card is the most cosmetic and most-revised thing in the project, and this realm can never change — so the drawing is delegated to a view realm through a Renderer, and only the delegation is frozen. This realm cannot import the view: the view reads the registry, which reads this ledger, and imports may not form a cycle. So the view hands this realm an object at runtime instead. gno runs a method declared in another realm with THAT realm's storage authority (the declaring-realm borrow), so the renderer can read anything and write nothing of ours — it gets no `cur`, so it cannot cross into anything as us either. What it can do is draw something wrong, slow or broken, which is a display problem: ClearRenderer withdraws it in one transaction (admin, recovery or guardian) and wallets fall back to the built-in card below. Who may register is pinned to ONE package path, approved through the timelock (ProposeRenderer), because what every wallet shows for every name is worth a waiting period: a hostile card could tell holders to pay somebody. The ledger is born approving its own generation's view. */ // Renderer draws one token for wallets. Implemented by the view realm. type Renderer interface { // TokenImage returns the image itself, as a data: URI. TokenImage(tid string) string // TokenMetadata returns the metadata as a JSON string: name, // description, externalUrl, attributes [{traitType, value}]. TokenMetadata(tid string) string } var ( // The one realm allowed to register a renderer; "" for none. rendererPath string // What it registered; nil means the built-in card. renderer Renderer ) // RegisterRenderer is called BY the approved view realm, handing over // the object that draws. Refused from anywhere else. func RegisterRenderer(cur realm, r Renderer) { assertNoSend(cur) registerRenderer(cur.Previous().PkgPath(), r) } func registerRenderer(callerPath string, r Renderer) { if rendererPath == "" || callerPath != rendererPath { panic("nsnft: only the approved view realm may register a renderer") } if r == nil { panic("nsnft: no renderer given") } renderer = r chain.Emit("RendererRegistered", "path", callerPath) } // ClearRenderer withdraws the art at once, approval included, so the // realm that drew it cannot simply register again: approving one takes // the timelock. Admin, recovery or guardian. func ClearRenderer(cur realm) { assertNoSend(cur) clearRenderer(cur.Previous().Address()) } func clearRenderer(caller address) { assertCanRevoke(caller) prev := rendererPath rendererPath = "" renderer = nil chain.Emit("RendererCleared", "path", prev, "by", caller.String()) } func RendererPath() string { return rendererPath } func HasRenderer() bool { return renderer != nil } // TokenURI is the token's image, as a data: URI, for a token that exists. // A renderer that answers nothing gets the built-in card instead: an // empty is a broken picture in every wallet. func TokenURI(tid string) (string, error) { if !Exists(tid) { return "", grc721.ErrInvalidTokenId } if renderer != nil { if img := renderer.TokenImage(tid); img != "" { return img, nil } } return builtinImage(tid), nil } // TokenMetadata is the token's metadata as a camelCase JSON string, the // shape Adena reads. The image is not in it; that is TokenURI. func TokenMetadata(tid string) (string, error) { if !Exists(tid) { return "", grc721.ErrInvalidTokenId } if renderer != nil { if meta := renderer.TokenMetadata(tid); meta != "" { return meta, nil } } return builtinMetadata(tid), nil } /* THE BUILT-IN CARD, for when no view is registered: the name, plainly. Deliberately plain and deliberately free of anything that could go stale. No URL — a domain this realm named could lapse and be bought by somebody else, and the link would then live in every wallet forever — and no colours or claims that a policy change could make wrong. */ func builtinImage(tid string) string { size := 40 if n := len(tid); n > 16 { size = 640 / n if size < 8 { size = 8 } } svg := `` + `` + `` + escapeXML(tid) + `` + `MEME NAME SERVICE` return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg)) } func builtinMetadata(tid string) string { return `{"name":"` + escapeJSON(tid) + `",` + `"description":"A name on the Meme Name Service.",` + `"attributes":[]}` } func escapeXML(s string) string { s = strings.ReplaceAll(s, "&", "&") s = strings.ReplaceAll(s, "<", "<") s = strings.ReplaceAll(s, ">", ">") s = strings.ReplaceAll(s, `"`, """) s = strings.ReplaceAll(s, "'", "'") return s } func escapeJSON(s string) string { var b strings.Builder for i := 0; i < len(s); i++ { c := s[i] switch { case c == '"': b.WriteString(`\"`) case c == '\\': b.WriteString(`\\`) case c < 0x20: const hex = "0123456789abcdef" b.WriteString(`\u00`) b.WriteByte(hex[c>>4]) b.WriteByte(hex[c&0xf]) default: b.WriteByte(c) } } return b.String() }