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.

attach.gno

10.37 Kb · 282 lines
  1package nsskin
  2
  3import (
  4	"gno.land/p/nt/grc721/v0"
  5	"gno.land/p/nt/ufmt/v0"
  6	"gno.land/r/nym-mnsrehearsal001/nsdata/v2"
  7)
  8
  9func nameKey(label, domain string) string { return domain + "*" + label }
 10
 11// bindingIsLive reports whether a token's binding still points at the
 12// registration it was made against. Same test ArtForName and Claim
 13// already apply; named once so the four copies cannot drift.
 14func bindingIsLive(tid string) bool {
 15	v := bound.Get(tid)
 16	if v == nil {
 17		return false
 18	}
 19	b := v.(*binding)
 20	if !nsdata.NameExists(b.label, b.domain) {
 21		return false
 22	}
 23	_, registered, _, _, _, _ := nsdata.GetNameInfo(b.label, b.domain)
 24	return registered == b.registered
 25}
 26
 27// grcID saves every call site from importing grc721 just to cast.
 28func grcID(tid string) grc721.TokenID { return grc721.TokenID(tid) }
 29
 30/*
 31Attach binds a skin to a name and puts the token into this realm's
 32custody. See the long note in types.gno for why custody rather than a
 33lock in place: it is what makes a marketplace's escrow of the NAME
 34irrelevant to the skin.
 35
 36ONE SKIN, ONE NAME, both ways. A token may be attached to one name, and
 37a name may wear one skin. Letting one copy dress every name a person owns
 38would make the second copy pointless, and the object stops meaning
 39anything.
 40*/
 41func Attach(cur realm, tid, label, domain string) {
 42	assertNoSend(cur)
 43	wear(cur, tid, label, domain, cur.Previous().Address())
 44}
 45
 46// wear is Attach's body, shared with Buy so that buying and wearing
 47// settle in one transaction. Splitting them would let a buyer stop
 48// halfway and end up holding a skin they paid for and did not put on.
 49func wear(cur realm, tid, label, domain string, caller address) {
 50	id, ok := skinOfToken(tid)
 51	if !ok {
 52		panic("nsskin: no such skin token: " + tid)
 53	}
 54	s := mustSkin(id)
 55	if s.withdrawn {
 56		panic("nsskin: " + id + " has been withdrawn and cannot be worn")
 57	}
 58	if bound.Has(tid) {
 59		panic("nsskin: " + tid + " is already on a name — claim it back first")
 60	}
 61	/* A STALE BINDING IS NOT A BINDING, and this used to be the one
 62	   place in the realm that forgot it.
 63	   Every other consumer — ArtForName, Refresh, refresh, Claim — pins
 64	   on `registered` and treats a mismatch as no binding at all. This
 65	   guard tested existence only, so: somebody attaches a skin, lets
 66	   the name lapse, never claims. A stranger re-registers the label
 67	   and can NEVER wear a skin on it — their Attach panics here, and
 68	   Claim refuses them because the stale binding entitles its own
 69	   binder. No admin call could clear it either. One registration
 70	   permanently killed the cosmetic slot on any label, and an honest
 71	   holder who simply let a name go did it by accident.
 72	   So a dead binding is evicted rather than obeyed. Only the reverse
 73	   index goes: `bound[tid]` stays, so the old binder can still Claim
 74	   their own token back — they paid for it and it is not forfeit. */
 75	nk := nameKey(label, domain)
 76	if cur := nameBound.Get(nk); cur != nil {
 77		if !bindingIsLive(cur.(string)) {
 78			nameBound.Remove(nk)
 79		} else {
 80			panic("nsskin: " + label + "*" + domain + " already wears a skin")
 81		}
 82	}
 83
 84	owner, err := token.OwnerOf(grcID(tid))
 85	if err != nil {
 86		panic("nsskin: " + err.Error())
 87	}
 88	if owner != caller {
 89		panic("nsskin: you do not own " + tid)
 90	}
 91
 92	if !nsdata.NameExists(label, domain) {
 93		panic("nsskin: no such name: " + label + "*" + domain)
 94	}
 95	if !nsdata.IsNameValid(label, domain) {
 96		panic("nsskin: " + label + "*" + domain + " has expired")
 97	}
 98	nameOwner, registered, _, _, _, _ := nsdata.GetNameInfo(label, domain)
 99	if nameOwner != caller {
100		panic("nsskin: you do not own " + label + "*" + domain)
101	}
102	if s.lock != "" && s.lock != domain {
103		panic(ufmt.Sprintf("nsskin: %s only goes on *%s names, not *%s", id, s.lock, domain))
104	}
105
106	// Into custody. From here the token is in nobody's wallet, so it
107	// cannot be listed anywhere or sold out from under the name.
108	if err := ledger.TransferFrom(caller, caller, cur.Address(), grcID(tid)); err != nil {
109		panic("nsskin: could not take custody: " + err.Error())
110	}
111	bound.Set(tid, &binding{label: label, domain: domain, registered: registered, binder: caller})
112	nameBound.Set(nameKey(label, domain), tid)
113}
114
115/*
116Refresh points a binding's `binder` at the name's CURRENT owner.
117
118WHY THIS EXISTS, and it is the fix for a real hole. `binder` decides who
119gets the skin back once the name record is gone. Set once at Attach and
120never updated, it names whoever ATTACHED the skin — so: Alice attaches,
121sells the name to Bob, the skin travels with it as intended, Bob lets the
122name lapse, and the skin goes back to ALICE. Bob paid for it. That is
123not a limitation, it is taking something from the person who bought it.
124
125nsdata cannot tell us a name changed hands — it is permanent, it has no
126hook, and it never will. So `binder` cannot be maintained; it can only be
127CAUGHT UP. This is that, and it is deliberately open to anyone:
128
129	it only ever writes the name's real current owner, so calling it can
130	never move the skin anywhere it should not go, and there is nothing
131	to gain by calling it on somebody else's binding
132
133	which means the site can call it on a purchase, a keeper can call it
134	on a schedule, the buyer can call it themselves, or nobody calls it
135	at all and the next Attach or Claim does it — all with the same
136	result and none of them needing to be trusted
137
138Nothing on chain is automatic here. gno has no scheduler and nsdata has
139no callback, so every version of "the skin follows the sale" is one extra
140call by someone. Making that call safe for ANYONE to make is as close to
141automatic as this can get, and it is close enough.
142*/
143func Refresh(cur realm, tid string) {
144	// Permissionless and routinely called by keepers, so it is exactly
145	// the entry point a script attaches coins to by accident — and this
146	// realm's only banker moves that transaction's own OriginSend, so
147	// anything left here is stranded at the realm address forever.
148	assertNoSend(cur)
149	v := bound.Get(tid)
150	if v == nil {
151		return
152	}
153	b := v.(*binding)
154	if !nsdata.NameExists(b.label, b.domain) {
155		return
156	}
157	owner, registered, _, _, frozen, _ := nsdata.GetNameInfo(b.label, b.domain)
158	// Only for THIS registration. If the name lapsed and somebody else
159	// took it, the current owner is a stranger to this binding and must
160	// not be recorded as entitled to anything. Nor while the name is
161	// frozen: a disputed or stolen name's holder is not who the skin
162	// travels with (re-audit 2026-10-16, L-5).
163	if registered != b.registered || frozen {
164		return
165	}
166	b.binder = owner
167}
168
169// refresh is Refresh without the realm ceremony, for the callers below.
170func refresh(b *binding) {
171	if !nsdata.NameExists(b.label, b.domain) {
172		return
173	}
174	owner, registered, _, _, frozen, _ := nsdata.GetNameInfo(b.label, b.domain)
175	if registered != b.registered || frozen {
176		return
177	}
178	b.binder = owner
179}
180
181/*
182claimEntitled is Claim's rule on its own, with the record handed in (exists
183false and zeros when the record is gone), so it can be tested without a
184registry under it. A live, FROZEN registration is refused outright: the
185admin freezes a stolen or disputed name so nothing moves while it is sorted
186out, and its skin used to be claimable and sellable all the same (re-audit
1872026-10-16, L-5). The record's frozen bit, not the ledger's holds, so a
188free-tier name's skin still travels.
189*/
190func claimEntitled(b *binding, caller address, exists bool, owner address, registered int64, frozen bool) (bool, string) {
191	if exists && registered == b.registered {
192		if frozen {
193			return false, b.label + "*" + b.domain + " is frozen; its skin stays put until the name is released"
194		}
195		return owner == caller, ""
196	}
197	// The name lapsed and was taken by somebody else, or the record is
198	// gone: back to whoever the binding last knew. Another registration's
199	// freeze is not this binding's.
200	return caller == b.binder, ""
201}
202
203/*
204Claim takes a skin out of custody and into the caller's wallet.
205
206WHO MAY, and this is the whole travels-with-the-name rule:
207
208	the CURRENT OWNER of the name, while the name record still exists.
209	Buy the name, claim the skin — including a name bought from someone
210	else, which is the point.
211
212	the LAST KNOWN NAME OWNER, once the record is gone entirely. That is
213	the "it comes back to your wallet when the name expires" case — and
214	"last known" rather than "whoever attached it", because after a sale
215	those are different people and only one of them paid for it. See
216	Refresh above for how that is kept current without a hook nsdata
217	does not have.
218
219The binding is pinned to one REGISTRATION. If a name lapsed and somebody
220else registered it, its timestamp has changed, so the new registrant is
221not the owner of anything here and cannot claim.
222*/
223func Claim(cur realm, tid string) {
224	assertNoSend(cur)
225	caller := cur.Previous().Address()
226	v := bound.Get(tid)
227	if v == nil {
228		panic("nsskin: " + tid + " is not on a name")
229	}
230	b := v.(*binding)
231
232	// Catch `binder` up before deciding, so a name that changed hands
233	// since it was attached settles on the person who holds it now
234	// rather than the person who first attached it.
235	refresh(b)
236
237	exists := nsdata.NameExists(b.label, b.domain)
238	var owner address
239	var registered int64
240	frozen := false
241	if exists {
242		owner, registered, _, _, frozen, _ = nsdata.GetNameInfo(b.label, b.domain)
243	}
244	entitled, why := claimEntitled(b, caller, exists, owner, registered, frozen)
245	if why != "" {
246		panic("nsskin: " + why)
247	}
248	if !entitled {
249		panic("nsskin: " + tid + " belongs to " + b.label + "*" + b.domain +
250			", and you are neither its owner nor the person who attached this")
251	}
252
253	if err := ledger.TransferFrom(cur.Address(), cur.Address(), caller, grcID(tid)); err != nil {
254		panic("nsskin: could not release: " + err.Error())
255	}
256	bound.Remove(tid)
257	// Only clear the reverse entry if it still points at THIS token — a
258	// stale binding may have been superseded by a new registrant's own
259	// skin, and clearing it blindly would strip theirs.
260	if cur := nameBound.Get(nameKey(b.label, b.domain)); cur != nil && cur.(string) == tid {
261		nameBound.Remove(nameKey(b.label, b.domain))
262	}
263}
264
265// SkinOnName returns the token currently worn by a name, or "".
266func SkinOnName(label, domain string) string {
267	v := nameBound.Get(nameKey(label, domain))
268	if v == nil {
269		return ""
270	}
271	return v.(string)
272}
273
274// BindingOf reports where a token is attached, for a wallet or a shop.
275func BindingOf(tid string) (label, domain string, registered int64, binder address) {
276	v := bound.Get(tid)
277	if v == nil {
278		return "", "", 0, ""
279	}
280	b := v.(*binding)
281	return b.label, b.domain, b.registered, b.binder
282}