read.gno
6.51 Kb · 215 lines
1package nsskin
2
3import (
4 "strings"
5
6 "gno.land/p/nt/grc721/v0"
7 "gno.land/p/nt/ufmt/v0"
8 "gno.land/r/nym-mnsrehearsal001/nsdata/v1"
9)
10
11/*
12ArtForName is the one question the renderer asks, and it is answered here
13so the rule lives with the data it depends on.
14
15It returns the sprite a name should be drawn with, or "" for the ordinary
16card. Everything the renderer would otherwise have to remember to check
17is inside it, because a renderer that has to remember three things before
18drawing is one that eventually forgets one.
19
20The checks, and what each of them makes self-cleaning:
21
22 the name wears a token at all
23 that token's definition has not been WITHDRAWN — so a takedown
24 reverts every name wearing it, everywhere, at once, with no
25 migration and no per-name cleanup
26 the binding is FRESH, pinned to this registration — so a name that
27 lapsed and was taken by somebody else inherits nothing
28 the LOCK still matches — belt and braces, since Attach checks it too
29
30Note what is NOT checked: who holds the token in their wallet. Nobody
31does. It is in this realm's custody for as long as it is attached, which
32is what makes the skin travel with the name instead of with a wallet.
33*/
34func ArtForName(label, domain string) string {
35 tid := SkinOnName(label, domain)
36 if tid == "" {
37 return ""
38 }
39 v := bound.Get(tid)
40 if v == nil {
41 return ""
42 }
43 b := v.(*binding)
44
45 if !nsdata.NameExists(label, domain) {
46 return ""
47 }
48 _, registered, _, _, _, _ := nsdata.GetNameInfo(label, domain)
49 if registered != b.registered {
50 return "" // a different registration of the same label
51 }
52
53 id, ok := skinOfToken(tid)
54 if !ok {
55 return ""
56 }
57 // Normalized on the way in, so a token minted from a non-canonical
58 // id before Buy/Mint were fixed starts rendering instead of staying
59 // a blank card its holder already paid for.
60 sv := skins.Get(strings.ToLower(strings.TrimSpace(id)))
61 if sv == nil {
62 return ""
63 }
64 s := sv.(*skin)
65 if s.withdrawn {
66 return ""
67 }
68 if s.lock != "" && s.lock != domain {
69 return ""
70 }
71 return s.art
72}
73
74// SkinInfo describes a DEFINITION, for a shop listing or a skin's own
75// card. Withdrawn is returned rather than hidden: a holder is entitled
76// to know, and a listing that silently vanishes is worse than one that
77// says what happened.
78func SkinInfo(id string) (name, art, lock string, artist address, withdrawn bool, minted int64) {
79 s := mustSkin(id)
80 return s.name, s.art, s.lock, s.artist, s.withdrawn, s.minted
81}
82
83func SkinExists(id string) bool {
84 return skins.Has(strings.ToLower(strings.TrimSpace(id)))
85}
86
87// ListSkins returns every definition id, in tree order. Small by
88// construction — this is a curated shop, not a submission firehose — and
89// if that ever stops being true it wants paging rather than a bigger
90// answer.
91func ListSkins() []string {
92 var out []string
93 skins.Iterate("", "", func(k string, _ any) bool {
94 out = append(out, k)
95 return false
96 })
97 return out
98}
99
100/*
101AllSkins packs the whole catalogue into ONE read.
102
103A shop page needs every definition's name, lock and availability, and
104asking for them one at a time is sixty round trips before anything
105appears on screen. The ART is deliberately NOT here: it is the bulk of
106the data, it is committed in the repo as the canonical source, and a
107page that already has it does not need to fetch it again.
108
109Six fields: id, name, lock, withdrawn, minted, price in ugnot.
110
111Tab-separated fields, newline-separated records — not JSON, because
112building JSON by string concatenation in a realm is how quoting bugs get
113shipped, and a shop is not worth that risk. No field can contain a tab
114or a newline: ids and locks are [a-z0-9-] by rule, and a name is checked
115here rather than assumed.
116*/
117func AllSkins() string {
118 var b strings.Builder
119 skins.Iterate("", "", func(k string, v any) bool {
120 s := v.(*skin)
121 nm := s.name
122 if strings.ContainsAny(nm, "\t\n") {
123 nm = "?" // never seen: DefineSkin would have to have accepted it
124 }
125 w := "0"
126 if s.withdrawn {
127 w = "1"
128 }
129 // PRICE IS THE SIXTH FIELD, appended rather than inserted, so a
130 // page shipped before per-design pricing keeps reading the five
131 // it knows and simply does not see this one.
132 b.WriteString(k + "\t" + nm + "\t" + s.lock + "\t" + w + "\t" +
133 ufmt.Sprintf("%d", s.minted) + "\t" +
134 ufmt.Sprintf("%d", PriceOf(k)) + "\n")
135 return false
136 })
137 return b.String()
138}
139
140func skinOfToken(tid string) (string, bool) {
141 v := tokenSkin.Get(tid)
142 if v == nil {
143 return "", false
144 }
145 return v.(string), true
146}
147
148// SkinOfToken is skinOfToken for callers outside the realm.
149func SkinOfToken(tid string) string {
150 id, _ := skinOfToken(tid)
151 return id
152}
153
154func GetAdmin() address { return admin }
155func GetTokenURIBase() string { return tokenURIBase }
156func IsTrustedShop(a address) bool {
157 return trustedShop.Has(a.String())
158}
159
160// ---------------------------------------------------------------- grc721
161
162func Name() string { return token.GetName() }
163func Symbol() string { return token.GetSymbol() }
164func GetName() string { return token.GetName() }
165func GetSymbol() string { return token.GetSymbol() }
166func TokenCount() int64 { return token.TotalSupply() }
167func TotalSupply() int64 { return token.TotalSupply() }
168
169// ID is the collection id wallets and indexers key on:
170// "<this realm's path>.MNSKIN.0000000".
171func ID() string { return token.ID() }
172
173func BalanceOf(owner address) int64 {
174 bal, err := token.BalanceOf(owner)
175 if err != nil {
176 panic(err)
177 }
178 return bal
179}
180
181// OwnerOf does NOT hide a withdrawn skin's owner, and that is the
182// opposite of what nsdata does for an expired name.
183//
184// There, hiding the owner is an anti-scam measure: a lapsed name must
185// not be listable, because the seller no longer controls it. Here the
186// holder still owns the token completely — withdrawal only stops it
187// being DRAWN. Hiding the owner would break transfers and approvals for
188// somebody who has done nothing wrong, to solve a problem that is not
189// theirs.
190func OwnerOf(tid grc721.TokenID) address {
191 owner, err := token.OwnerOf(tid)
192 if err != nil {
193 panic(err)
194 }
195 return owner
196}
197
198// Total, the same way nsdata.GetApproved is: the zero address means
199// nobody is approved, and only an unknown token id panics. Collapsing
200// "no approval" into "no such token" would leave no caller able to fall
201// back to the operator grant without catching a panic first.
202func GetApproved(tid grc721.TokenID) address {
203 a, err := token.GetApproved(tid)
204 if err != nil {
205 if _, oerr := token.OwnerOf(tid); oerr != nil {
206 panic(err)
207 }
208 return ""
209 }
210 return a
211}
212
213func IsApprovedForAll(owner, operator address) bool {
214 return token.IsApprovedForAll(owner, operator)
215}