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.

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}