write.gno
7.94 Kb · 265 lines
1package nsskin
2
3import (
4 "strings"
5
6 "gno.land/p/nt/grc721/v0"
7 "gno.land/p/nt/ufmt/v0"
8)
9
10func assertIsAdmin(cur realm) {
11 if cur.Previous().Address() != admin {
12 panic("nsskin: not the admin")
13 }
14}
15
16func assertIsAdminOrShop(cur realm) {
17 a := cur.Previous().Address()
18 if a == admin {
19 return
20 }
21 if !trustedShop.Has(a.String()) {
22 panic("nsskin: not the admin or a trusted shop")
23 }
24}
25
26/*
27DefineSkin registers a design. It does not mint anything.
28
29Definitions and copies are separate calls on purpose: defining is the
30expensive, careful, one-off act that puts an artist's work and a sprite
31on chain, while minting is the cheap repeated one. Rolling them together
32would mean re-sending the artwork with every sale.
33
34A definition may be REDEFINED only while nothing has been minted from it.
35After the first copy exists, the art is what its holders bought and it
36stops being ours to edit — the only lever left is withdrawal, which is
37honest about what it is doing.
38*/
39func DefineSkin(cur realm, id, name, art, lock string, artist address) {
40 assertIsAdmin(cur)
41 id = strings.ToLower(strings.TrimSpace(id))
42 assertValidID(id)
43 if len(name) == 0 || len(name) > maxSkinName {
44 panic("nsskin: a skin needs a name, of at most 40 characters")
45 }
46 if len(art) == 0 || len(art) > maxSkinArt {
47 panic(ufmt.Sprintf("nsskin: art is %d bytes, the limit is %d", len(art), maxSkinArt))
48 }
49 // The SHAPE of the sprite is checked here; whether it renders is
50 // nsview's business. Storing something the renderer will reject
51 // silently is the failure mode this exists to prevent — a skin that
52 // draws nothing, sold to somebody, discovered later.
53 assertValidArt(art)
54 if lock != "" {
55 assertValidLabel(lock)
56 }
57 if artist == "" {
58 panic("nsskin: a skin needs an artist to pay")
59 }
60
61 if v := skins.Get(id); v != nil {
62 s := v.(*skin)
63 if s.minted > 0 {
64 panic(ufmt.Sprintf(
65 "nsskin: %s has %d copies out — redefining it would change "+
66 "what those holders bought; withdraw it instead", id, s.minted))
67 }
68 s.name, s.art, s.lock, s.artist = name, art, lock, artist
69 return
70 }
71 skins.Set(id, &skin{name: name, art: art, lock: lock, artist: artist})
72}
73
74/*
75Mint issues one copy of a definition to somebody.
76
77The serial is the definition's own running count, so token ids read as
78"ember#1", "ember#2". Never reused: the counter only goes up, even across
79burns, because a reissued id would let a new token inherit whatever an
80old one was pointed at.
81*/
82func Mint(cur realm, id string, to address) string {
83 assertIsAdminOrShop(cur)
84 assertNoSend(cur)
85 /* NORMALIZED BEFORE THE TOKEN ID IS BUILT.
86 mustSkin and PriceOf lowercase what they are given; this did not,
87 so Mint("Ember", …) resolved the right definition and then minted
88 "Ember#3" while tokenSkin recorded the raw id. ArtForName looks the
89 definition up in a tree DefineSkin lowercased, so the token bound,
90 charged, and then rendered nothing, forever. An admin typing a
91 capital into a form was all it took. */
92 id = strings.ToLower(strings.TrimSpace(id))
93 s := mustSkin(id)
94 if s.withdrawn {
95 panic("nsskin: " + id + " has been withdrawn and cannot be minted")
96 }
97 if to == "" {
98 panic("nsskin: no recipient")
99 }
100 s.minted++
101 tid := ufmt.Sprintf("%s#%d", id, s.minted)
102 if err := ledger.Mint(to, grc721.TokenID(tid)); err != nil {
103 panic("nsskin: mint failed: " + err.Error())
104 }
105 tokenSkin.Set(tid, id)
106 return tid
107}
108
109/*
110Withdraw is the takedown control, and SKINS.md §6 makes it
111non-negotiable: it has to exist before a single outside submission is
112accepted, which means it has to exist now, while there is nothing to take
113down and it is cheap to build.
114
115Somebody will submit a character somebody else owns, or a bank's logo, or
116something vile, and it will render inside other people's wallets with our
117name beside it. When that happens the fix has to be one call, not a
118migration.
119
120It does NOT burn anything. The holder keeps the token; it simply stops
121rendering, and every name pointing at it falls back to the ordinary card
122at once. Burning somebody's property to solve our problem is a different
123act with different consequences, and it is not this one.
124*/
125func Withdraw(cur realm, id string, withdrawn bool) {
126 assertNoSend(cur)
127 assertIsAdmin(cur)
128 mustSkin(id).withdrawn = withdrawn
129}
130
131func SetTokenURIBase(cur realm, base string) {
132 assertNoSend(cur)
133 assertIsAdmin(cur)
134 tokenURIBase = base
135}
136
137func SetAdmin(cur realm, a address) {
138 assertNoSend(cur)
139 assertIsAdmin(cur)
140 if a == "" {
141 panic("nsskin: refusing to leave the realm with no admin")
142 }
143 admin = a
144}
145
146func TrustShop(cur realm, a address, trusted bool) {
147 assertNoSend(cur)
148 assertIsAdmin(cur)
149 if trusted {
150 trustedShop.Set(a.String(), true)
151 return
152 }
153 trustedShop.Remove(a.String())
154}
155
156// ---------------------------------------------------------------- grc721
157
158func TransferFrom(cur realm, from, to address, tid grc721.TokenID) {
159 assertNoSend(cur)
160 if err := ledger.TransferFrom(cur.Previous().Address(), from, to, tid); err != nil {
161 panic("nsskin: " + err.Error())
162 }
163}
164
165func Approve(cur realm, to address, tid grc721.TokenID) {
166 assertNoSend(cur)
167 if err := ledger.Approve(cur.Previous().Address(), to, tid); err != nil {
168 panic("nsskin: " + err.Error())
169 }
170}
171
172func SetApprovalForAll(cur realm, operator address, approved bool) {
173 assertNoSend(cur)
174 if err := ledger.SetApprovalForAll(cur.Previous().Address(), operator, approved); err != nil {
175 panic("nsskin: " + err.Error())
176 }
177}
178
179// ---------------------------------------------------------------- checks
180
181func mustSkin(id string) *skin {
182 v := skins.Get(strings.ToLower(strings.TrimSpace(id)))
183 if v == nil {
184 panic("nsskin: no such skin: " + id)
185 }
186 return v.(*skin)
187}
188
189// An id goes into every token id and into the pointer a name stores, so
190// it has to survive being split on "#" and compared byte for byte.
191func assertValidID(id string) {
192 if len(id) == 0 || len(id) > maxSkinID {
193 panic("nsskin: a skin id must be 1 to 24 characters")
194 }
195 for i := 0; i < len(id); i++ {
196 c := id[i]
197 if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' {
198 continue
199 }
200 panic("nsskin: a skin id may only use a-z, 0-9 and -: " + id)
201 }
202}
203
204func assertValidLabel(l string) {
205 if len(l) == 0 || len(l) > 63 {
206 panic("nsskin: a lock must be a domain label")
207 }
208 for i := 0; i < len(l); i++ {
209 c := l[i]
210 if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' {
211 continue
212 }
213 panic("nsskin: a lock may only use a-z, 0-9 and -: " + l)
214 }
215}
216
217/*
218assertValidArt mirrors the sprite checks nsview makes, and it is here for
219one reason: nsview REJECTS THE WHOLE SPRITE SILENTLY. A malformed skin
220does not draw a broken picture, it draws nothing at all, with no error
221anywhere a person would see it.
222
223Storing one would mean selling somebody a blank card and finding out
224weeks later. So the shape is checked at the point of storage, where the
225failure is a panic in a transaction nobody has paid for yet.
226
227Deliberately checks the SHAPE and not the rendering. This realm does not
228know how a card is composed and should not learn — that is the whole
229reason the renderer is swappable.
230*/
231func assertValidArt(enc string) {
232 parts := strings.Split(enc, "|")
233 n := len(parts) - 1
234 if n < 4 || n > 64 {
235 panic(ufmt.Sprintf("nsskin: a sprite needs 4 to 64 rows, this has %d", n))
236 }
237 keys := map[string]bool{}
238 for _, e := range strings.Split(parts[0], ",") {
239 if len(e) != 8 || e[1] != '#' {
240 panic("nsskin: a palette entry must be a letter, #, and six hex digits: " + e)
241 }
242 for i := 2; i < 8; i++ {
243 c := e[i]
244 if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') {
245 continue
246 }
247 panic("nsskin: not a hex colour: " + e)
248 }
249 keys[e[:1]] = true
250 }
251 for y := 1; y <= n; y++ {
252 if len(parts[y]) != n {
253 panic(ufmt.Sprintf(
254 "nsskin: row %d is %d wide but the sprite is %d rows tall — it must be square",
255 y, len(parts[y]), n))
256 }
257 for x := 0; x < n; x++ {
258 ch := parts[y][x : x+1]
259 if ch == "." || keys[ch] {
260 continue
261 }
262 panic(ufmt.Sprintf("nsskin: row %d uses colour %s, which the palette does not define", y, ch))
263 }
264 }
265}