types.gno
6.15 Kb · 148 lines
1// Package nsskin holds purchasable card art — the skins — as tokens of
2// their own, and binds them to names.
3//
4// WHY THIS IS ITS OWN REALM AND NOT PART OF nsdata. SKINS.md §8 listed
5// it as undecided; it is decided here, because the reasoning turned out
6// to be one-sided rather than balanced.
7//
8// nsdata IS PERMANENT. It cannot be replaced, ever. A second kind of
9// token in it means every mistake in the skin model is permanent too,
10// and the skin model is the newest and least settled thing here.
11//
12// nsdata.OwnerOf DELIBERATELY PANICS FOR EXPIRED TOKENS so a lapsed
13// name cannot be listed anywhere. Skins do not expire. That machinery
14// is exactly wrong for them, and a conditional on token kind inside
15// the one realm that can never be fixed is the worst place for it.
16//
17// A NEW REALM CAN BE REPLACED while it holds a handful of tokens. The
18// vault cannot, at any point, for any reason.
19//
20// So being wrong here costs a migration nobody notices. Being wrong
21// inside nsdata costs forever.
22//
23// # A SKIN IS TWO THINGS
24//
25// A DEFINITION is the artwork: a name, a sprite, an optional extension
26// lock, and the artist to pay. One per design, stored once — a
27// thousand holders of one design cost one sprite, not a thousand.
28//
29// A TOKEN is somebody's copy. Editions are unlimited, because SKINS.md
30// is explicit that a cap is a floor price with extra steps and resale
31// value is never part of the pitch.
32//
33// # A SKIN BINDS TO ONE NAME, AND TRAVELS WITH IT
34//
35// This reverses an earlier recommendation of mine, and the new answer is
36// better. I had argued for keeping them separate — a pointer from the
37// name, reverting on sale — because names expire and skins should not.
38// The objection to bundling was that a buyer sees a skinned card on a
39// marketplace and receives an unskinned name.
40//
41// Binding solves that rather than working around it. The skin genuinely
42// comes with the name, so the card is not misleading, and the small
43// disclosure line the old model needed inside the artwork disappears.
44// And a skin still does not die with a name: when the name goes, the
45// skin comes back.
46//
47// # WHY ATTACHING ESCROWS THE TOKEN, WHICH IS THE WHOLE CUSTODY ANSWER
48//
49// The worry: a name listed on a marketplace that takes CUSTODY. If the
50// skin lived in the seller's wallet, a custodial listing would hold the
51// name while the skin sat somewhere else, and untangling that on expiry
52// would need cooperation we cannot compel.
53//
54// So attaching moves the token to THIS REALM'S OWN ADDRESS. While
55// attached it is in nobody's wallet: it cannot be listed, cannot be sold
56// out from under the name, and a marketplace escrowing the NAME never
57// touches it. The question of freeing a skin from somebody else's
58// custody does not arise, because it was never in it.
59//
60// Taking it out again is one rule with one fallback:
61//
62// WHILE THE NAME EXISTS, its CURRENT OWNER may claim the skin. That is
63// what makes it travel: buy the name, claim the skin.
64//
65// IF THE NAME RECORD IS GONE — lapsed and reclaimed — whoever attached
66// it may take it back, which is the "it returns to your wallet" case.
67//
68// One limitation, stated rather than hidden: buy a name with a skin,
69// never claim it, and let that name lapse completely, and the skin
70// returns to whoever attached it. Claiming on purchase avoids it
71// entirely, and the site prompts for exactly that.
72package nsskin
73
74import (
75 "gno.land/p/nt/avl/v0"
76 "gno.land/p/nt/grc721/v0"
77)
78
79type skin struct {
80 name string // as the shop shows it
81 art string // the sprite string, in the format nsview parses
82 lock string // an extension label, or "" for one that goes anywhere
83 artist address
84 // WITHDRAWN IS NOT DELETED, and the distinction is the whole
85 // takedown mechanism (SKINS.md §6). Deleting a definition orphans
86 // every token pointing at it. Withdrawing leaves everything in place
87 // and stops it drawing — every name wearing it reverts at once, with
88 // no migration and no per-name cleanup, because the card is
89 // generated on every read.
90 withdrawn bool
91 minted int64 // how many copies exist, and the next serial
92}
93
94// A live attachment. `registered` pins it to one REGISTRATION of the
95// name, not to the label: if the name lapses and somebody else takes it,
96// the timestamp changes and this binding is stale, so the new registrant
97// inherits nothing.
98type binding struct {
99 label string
100 domain string
101 registered int64
102 binder address // who attached it, and who gets it back if the name dies
103}
104
105var (
106 // Constructed in init, which is where it can be handed the realm:
107 // the chain's own GRC-721, so every mint and transfer is stamped
108 // gno.land/p/nt/grc721/v0. token is the read side; ledger, the write
109 // side, never leaves this realm.
110 token *grc721.Token
111 ledger *grc721.PrivateLedger
112 skins = avl.NewTree() // definition id -> *skin
113
114 // token id -> definition id. Derivable by splitting the token id,
115 // but stored so a lookup is a tree read rather than string surgery
116 // that has to agree with the minter forever.
117 tokenSkin = avl.NewTree()
118
119 bound = avl.NewTree() // token id -> *binding
120 nameBound = avl.NewTree() // "domain*label" -> token id
121
122 // Where a skin's own page lives, for the externalUrl in its metadata.
123 // A gnoweb path ("/r/.../nsview:skin/") or a full https URL. The
124 // IMAGE is drawn here (art.gno), from the definition's own sprite,
125 // because wallets put TokenURI straight into an <img>.
126 tokenURIBase = "/r/demo/nsview:skin/"
127
128 // Day-to-day authority: define, mint, withdraw. Deliberately a plain
129 // address rather than the vault's timelocked governance — this realm
130 // is replaceable, so it does not need machinery built for one that
131 // is not.
132 admin address
133
134 // Realms allowed to mint on somebody's behalf, which is what a shop
135 // is. Empty at first: the launch batch is minted by the admin.
136 trustedShop = avl.NewTree()
137
138 // What one costs, in ugnot. Zero until an admin sets it, so a
139 // misconfigured deploy gives skins away rather than charging an
140 // accidental amount — the failure that costs nobody money.
141 price int64
142)
143
144const (
145 maxSkinName = 40
146 maxSkinArt = 2048 // a 32x32 sprite is about 1,100 bytes
147 maxSkinID = 24 // appears in every token id and every binding
148)