ledgerart.gno
4.18 Kb · 124 lines
1package nsview
2
3import (
4 "chain/runtime"
5 "chain/runtime/unsafe"
6 "strconv"
7 "strings"
8 "time"
9
10 "gno.land/r/nym-mnsrehearsal001/nsdata/v1"
11 "gno.land/r/nym-mnsrehearsal001/nsnft/v1"
12 "gno.land/r/nym-mnsrehearsal001/nsskin/v1"
13)
14
15/*
16THIS REALM DRAWS THE NAMES IN WALLETS.
17
18A wallet asks the LEDGER, nsnft, for TokenURI and TokenMetadata, because
19the collection id names it. nsnft can never change, so it does not draw:
20it holds a Renderer handed to it by the one view realm it has approved,
21and that realm is this one. Swapping the art is deploying a new nsview and
22approving it on nsnft (a timelocked change), nothing more.
23
24The renderer's methods run with THIS realm's authority (gno's
25declaring-realm borrow), so drawing for the ledger can read anything and
26write nothing of the ledger's. This realm holds no state worth writing
27anyway; selfPath is set once, at init.
28*/
29
30// selfPath is this realm's path without the chain domain, for links to
31// its own gnoweb pages. Set at init, because inside a renderer call the
32// realm context belongs to whoever asked, not to this realm.
33var selfPath string
34
35type ledgerArt struct{}
36
37// TokenImage is nsview's TokenURI: the full card, drawn now.
38func (ledgerArt) TokenImage(tid string) string { return TokenURI(tid) }
39
40func (ledgerArt) TokenMetadata(tid string) string { return tokenMetadata(tid) }
41
42func init(cur realm) {
43 selfPath = strings.TrimPrefix(cur.PkgPath(), runtime.ChainDomain())
44 // Register only when this realm is the approved one. Registering
45 // unconditionally would make deploying a NEW view generation fail
46 // until its approval had cleared the timelock; this way it deploys,
47 // and RegisterWithLedger finishes the job once it is approved.
48 if nsnft.RendererPath() == cur.PkgPath() {
49 nsnft.RegisterRenderer(cross(cur), ledgerArt{})
50 }
51}
52
53// RegisterWithLedger hands the ledger this realm's renderer. Anyone may
54// call it: it only ever registers THIS realm's own drawing, and the ledger
55// refuses unless this realm is the one it has approved.
56func RegisterWithLedger(cur realm) {
57 // This realm has no banker and no way to return coins.
58 if cur.Previous().IsUserCall() && len(unsafe.OriginSend()) > 0 {
59 panic("nsview: this call takes no payment")
60 }
61 nsnft.RegisterRenderer(cross(cur), ledgerArt{})
62}
63
64/*
65tokenMetadata is the camelCase JSON Adena reads: name, description,
66externalUrl, attributes of {traitType, value}. Everything in it is read
67from the registry at the moment of asking, like the card, so a renewal or
68a sale shows up without anybody writing anything.
69*/
70func tokenMetadata(tid string) string {
71 label, dom := splitTokenID(tid)
72 if label == "" || dom == "" || !nsdata.NameExists(label, dom) {
73 return `{"name":"` + escapeJSON(tid) + `","description":"A name on the Meme Name Service.","attributes":[]}`
74 }
75 _, registered, expires, _, frozen, paid := nsdata.GetNameInfo(label, dom)
76 status, _ := statusFields(expires, frozen)
77 tier := "paid"
78 if !paid {
79 tier = "free tier"
80 }
81 skin := "none"
82 if worn := nsskin.SkinOnName(label, dom); worn != "" && nsskin.ArtForName(label, dom) != "" {
83 skin = worn
84 }
85 out := `{"name":"` + escapeJSON(label+"*"+dom) + `",` +
86 `"description":"A name on the Meme Name Service. You own a word now. Renew it, or somebody else will.",`
87 if selfPath != "" {
88 out += `"externalUrl":"` + escapeJSON("https://"+runtime.ChainDomain()+selfPath+":"+label+"*"+dom) + `",`
89 }
90 out += `"attributes":[` +
91 attr("Domain", "*"+dom) + `,` +
92 attr("Length", strconv.Itoa(len(label))) + `,` +
93 attr("Status", strings.ToLower(status)) + `,` +
94 attr("Registered", time.Unix(registered, 0).UTC().Format("2006-01-02")) + `,` +
95 attr("Expires", time.Unix(expires, 0).UTC().Format("2006-01-02")) + `,` +
96 attr("Tier", tier) + `,` +
97 attr("Skin", skin) + `]}`
98 return out
99}
100
101func attr(k, v string) string {
102 return `{"traitType":"` + escapeJSON(k) + `","value":"` + escapeJSON(v) + `"}`
103}
104
105func escapeJSON(s string) string {
106 var b strings.Builder
107 for i := 0; i < len(s); i++ {
108 c := s[i]
109 switch {
110 case c == '"':
111 b.WriteString(`\"`)
112 case c == '\\':
113 b.WriteString(`\\`)
114 case c < 0x20:
115 const hex = "0123456789abcdef"
116 b.WriteString(`\u00`)
117 b.WriteByte(hex[c>>4])
118 b.WriteByte(hex[c&0xf])
119 default:
120 b.WriteByte(c)
121 }
122 }
123 return b.String()
124}