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.

name.gno

12.26 Kb · 315 lines
  1package nslogic
  2
  3import (
  4	"chain"
  5	"strings"
  6	"time"
  7
  8	"gno.land/r/nym-mnsrehearsal001/nsdata/v1"
  9)
 10
 11// Profile field names inside a name's extension slot. The vault stores
 12// them as opaque strings and bounds their size; what they MEAN is this
 13// realm's convention, so new profile fields need no vault change.
 14const (
 15	keyDisplayName = "displayName"
 16	keyBio         = "bio"
 17	keyWebsite     = "website"
 18	keyAvatar      = "avatar"
 19)
 20
 21// RegisterName mints `label*domain` under an existing domain. Anyone may
 22// call this. Minting still works while the parent domain is past its own
 23// due date, as long as its grace period hasn't fully lapsed and it isn't
 24// frozen — nsdata's RegisterNameRecord enforces that.
 25func RegisterName(cur realm, label, domainLabel string) {
 26	registerName(cur, label, domainLabel, "")
 27}
 28
 29// RegisterNameRef is RegisterName with a referral code — the NAME of
 30// whoever sent the buyer here, as it appears in the link.
 31//
 32// It is a second entry point rather than a third argument on the first
 33// one because the first one is already deployed and already being called
 34// by wallets and by the site. Changing its arity would break every one
 35// of those callers at the same instant, which is exactly the failure the
 36// voting realm produced last time; adding a function breaks nothing and
 37// lets the front end pick whichever the deployed realm actually has.
 38//
 39// A code that does not earn — misspelt, lapsed, the buyer's own name, a
 40// free-tier name, or one not yet held for thirty days (referral.gno) — is
 41// IGNORED, not rejected. The registration goes through at list price.
 42// Refusing to sell somebody a name because they mistyped somebody else's
 43// would be an absurd way to lose a sale.
 44func RegisterNameRef(cur realm, label, domainLabel, refName string) {
 45	registerName(cur, label, domainLabel, refName)
 46}
 47
 48func registerName(cur realm, label, domainLabel, refName string) {
 49	assertNotRestoring()
 50	validateLabel(label)
 51	assertNotBlocked(label)
 52	if !DomainOpen(domainLabel) {
 53		panic("nslogic: this domain is not accepting new names")
 54	}
 55	caller := cur.Previous().Address()
 56
 57	// Resolved BEFORE anything is written, so the code is judged in the
 58	// world the buyer was quoted in.
 59	ref := resolveReferrer(refName, caller)
 60	price := NamePrice(domainLabel, label)
 61	if ref != "" {
 62		if _, pct := RefRates(); pct > 0 {
 63			price -= price * pct / 100
 64		}
 65	}
 66	payTo, cut := referralCut(ref, price)
 67	takePaymentSplit(cur, price, payTo, cut)
 68
 69	if err := nsdata.RegisterNameRecord(cross(cur), label, domainLabel, caller, time.Now().Unix(), true); err != nil {
 70		panic(err)
 71	}
 72
 73	// Written after the record exists, because the extension slot is
 74	// created by registration. Only recorded when the cut was actually
 75	// paid: the record says who brought this buyer, and a referrer who
 76	// earned nothing did not.
 77	if ref != "" && cut > 0 {
 78		setExtra(cur, label, domainLabel, keyRef, ref.String())
 79		creditReferral(cur, ref)
 80		// Money left the treasury's share: say so, so a monitor can add up
 81		// what referral rules are costing without reading every tx.
 82		chain.Emit("ReferralPaid", "tid", label+"*"+domainLabel, "referrer", ref.String(),
 83			"amount", fmtInt(cut), "price", fmtInt(price))
 84	}
 85}
 86
 87// RenewName — same anchoring rule as RenewDomain: extends from the
 88// previous due date, not from the payment date. Deliberately does not
 89// call validateLabel — an already-registered name must keep renewing
 90// under whatever pattern was in force when it was minted.
 91func RenewName(cur realm, label, domainLabel string) {
 92	// Not while a restore window is open: the prices may not be back yet
 93	// (restore.gno, assertNotRestoring; review follow-up).
 94	assertNotRestoring()
 95	// FIRST YEAR ONLY (referral.gno): a renewal pays the full price, all
 96	// of it to the treasury, and nobody's cut. Renewals used to pay the
 97	// referrer for as long as the name survived, which made a second
 98	// wallet's code a permanent discount on its owner's own renewals.
 99	assertRoomToRenew(label, domainLabel)
100	takePayment(cur, NamePrice(domainLabel, label))
101
102	caller := cur.Previous().Address()
103	if err := nsdata.RenewNameRecord(cross(cur), label, domainLabel, caller, time.Now().Unix()); err != nil {
104		panic(err)
105	}
106}
107
108// SetProfile writes the four conventional profile fields. Each lands in
109// the name's extension slot, where the vault enforces size bounds.
110func SetProfile(cur realm, label, domainLabel, displayName, bio, website, avatar string) {
111	assertNoSend(cur)
112	assertNameOwner(cur, label, domainLabel)
113	setExtra(cur, label, domainLabel, keyDisplayName, displayName)
114	setExtra(cur, label, domainLabel, keyBio, bio)
115	setExtra(cur, label, domainLabel, keyWebsite, website)
116	setExtra(cur, label, domainLabel, keyAvatar, avatar)
117}
118
119// SetNameField writes a single arbitrary key — cross-chain addresses and
120// anything else a future feature needs, with no vault change required.
121// Keys used by this realm's own machinery are reserved.
122func SetNameField(cur realm, label, domainLabel, key, value string) {
123	assertNoSend(cur)
124	assertNameOwner(cur, label, domainLabel)
125	if strings.HasPrefix(key, "_") {
126		panic("nslogic: keys beginning with '_' are reserved")
127	}
128	// `ref` decides who gets paid out of every renewal of this name. A
129	// holder who could write it would set it to an address they control
130	// and take a permanent cut of their own renewals out of the
131	// treasury — so the one key that moves money is the one key its
132	// owner may not touch.
133	if key == keyRef {
134		panic("nslogic: '" + keyRef + "' is set at registration and cannot be edited")
135	}
136	setExtra(cur, label, domainLabel, key, value)
137}
138
139func setExtra(cur realm, label, domainLabel, key, value string) {
140	if err := nsdata.SetNameExtraRecord(cross(cur), label, domainLabel, key, value); err != nil {
141		panic(err)
142	}
143}
144
145func assertNameOwner(cur realm, label, domainLabel string) {
146	if nsdata.GetNameOwner(label, domainLabel) != cur.Previous().Address() {
147		panic("nslogic: restricted to name owner")
148	}
149}
150
151// SetPrimaryName lets a wallet explicitly choose (and later change) which
152// of its names is used for reverse resolution.
153func SetPrimaryName(cur realm, label, domainLabel string) {
154	assertNoSend(cur)
155	caller := cur.Previous().Address()
156	if nsdata.GetNameOwner(label, domainLabel) != caller {
157		panic("nslogic: restricted to name owner")
158	}
159	if err := nsdata.SetPrimaryRecord(cross(cur), caller, label, domainLabel); err != nil {
160		panic(err)
161	}
162}
163
164/*
165ClearPrimaryName switches reverse resolution OFF for the caller.
166
167WHY THIS HAS TO EXIST, and it is a hole rather than a feature request.
168
169nsdata sets the reverse pointer AUTOMATICALLY on a wallet's first
170registration — RegisterNameRecord does it, and nothing asked the person
171first. From that moment their address answers with their name, so anybody
172who sees that address anywhere can find the persona attached to it, and
173every transaction it has ever made becomes attributable. Retroactively:
174the chain is permanent and indexers keep copies.
175
176And until now there was NO WAY OUT. ClearPrimaryRecord is gated to
177trusted logic, and no logic realm exposed it, so the one person the
178pointer is about could not remove it. A name service is a bridge between
179a persona and a wallet; publishing that bridge without consent and
180without an exit is the worst version of it.
181
182This is the exit. It takes no arguments on purpose: a wallet has exactly
183one reverse pointer, and the only address it can clear is its own, so
184there is nothing to pass and nothing to get wrong.
185
186WHAT IT DOES NOT DO, said plainly because a promise we cannot keep would
187be worse than the hole: clearing stops FUTURE lookups. It does not
188unpublish what was already visible, and it cannot — the history is on
189chain and other people have copies. The honest sequence is to choose
190before publishing, which is why the site now shows what the link exposes
191before it offers to make it.
192*/
193func ClearPrimaryName(cur realm) {
194	assertNoSend(cur)
195	nsdata.ClearPrimaryRecord(cross(cur), cur.Previous().Address())
196}
197
198// maxTTL caps a cache policy, a name's or a domain's: 2^31-1 seconds,
199// about 68 years, the ceiling DNS itself uses (RFC 2181). Anything longer
200// reads as forever to every resolver, and an unbounded value did not
201// survive a snapshot (restore.gno, ttlSecs).
202const maxTTL = int64(1<<31 - 1)
203
204// SetNameTTL is the owner's cache policy for their own name, in
205// seconds, where 0 MEANS NEVER CACHE — copied from the ENS registry so
206// integrators need not read anything of ours to handle it.
207//
208// The owner sets it rather than the registry because only they know the
209// risk: a name being used to receive payments wants 0, since a cached
210// answer after a sale pays the previous owner; a name used as a display
211// handle can safely be cached for a long time. At most maxTTL.
212func SetNameTTL(cur realm, label, domainLabel string, seconds int64) {
213	assertNoSend(cur)
214	assertNameOwner(cur, label, domainLabel)
215	if seconds < 0 || seconds > maxTTL {
216		panic("nslogic: a ttl is 0.." + fmtInt(maxTTL) + " seconds")
217	}
218	if err := nsdata.SetNameTTLRecord(cross(cur), label, domainLabel, seconds); err != nil {
219		panic(err)
220	}
221}
222
223func FreezeName(cur realm, label, domainLabel string) {
224	assertNoSend(cur)
225	assertIsAdmin(cur)
226	emitAdmin(cur, "FreezeName", "label", label, "domain", domainLabel)
227	nsdata.SetNameFrozen(cross(cur), label, domainLabel, true)
228}
229
230// UnfreezeName is TIMELOCKED (timelock.gno); freezing stays instant. A
231// freeze is the takedown and dispute lever — a disputed or stolen name is
232// held while it is sorted out — so releasing one instantly would let a
233// stolen admin key free a name and move it in the same transaction.
234func UnfreezeName(cur realm, label, domainLabel string) {
235	assertNoSend(cur)
236	assertIsAdmin(cur)
237	emitAdmin(cur, "UnfreezeName", "label", label, "domain", domainLabel)
238	requireAction(cur, actionID("UnfreezeName", label, domainLabel))
239	nsdata.SetNameFrozen(cross(cur), label, domainLabel, false)
240}
241
242// UnfreezeNames releases several names behind ONE timelocked action:
243// propose exactly "UnfreezeNames a*meme,b*doge,…" (token ids, comma-
244// separated, at most maxUnfreezeBatch). A stolen key freezes names one call
245// each and instantly, about a thousand a block; releasing them one action
246// each, with at most 32 actions pending, took ⌈N/32⌉ timelocks (re-audit
247// 2026-10-16, L-3). One unknown or malformed name refuses the whole call.
248const maxUnfreezeBatch = 64
249
250func UnfreezeNames(cur realm, tids string) {
251	assertNoSend(cur)
252	assertIsAdmin(cur)
253	emitAdmin(cur, "UnfreezeNames", "tids", tids)
254	list := strings.Split(tids, ",")
255	if tids == "" || len(list) > maxUnfreezeBatch {
256		panic("nslogic: 1.." + fmtI(maxUnfreezeBatch) + " token ids, comma-separated")
257	}
258	type ref struct{ label, domain string }
259	refs := make([]ref, 0, len(list))
260	for _, tid := range list {
261		label, domainLabel := splitTokenID(tid)
262		if label == "" || domainLabel == "" || !nsdata.NameExists(label, domainLabel) {
263			panic("nslogic: not a registered name: " + tid)
264		}
265		refs = append(refs, ref{label, domainLabel})
266	}
267	requireAction(cur, actionID("UnfreezeNames", tids))
268	for _, r := range refs {
269		nsdata.SetNameFrozen(cross(cur), r.label, r.domain, false)
270	}
271}
272
273// -- resolution --
274
275func ResolveName(label, domainLabel string) address {
276	if !nsdata.IsNameValid(label, domainLabel) {
277		panic("nslogic: name is expired")
278	}
279	return nsdata.GetNameOwner(label, domainLabel)
280}
281
282// ResolveAddress performs reverse resolution: address -> name.
283//
284// It must verify the ownership it is asserting, not merely that the
285// pointed-at name exists and is unexpired. nsdata's `primary` tree is a
286// record of what an address last SELECTED, not proof of what it still
287// owns — and although nsdata now clears the pointer on transfer and
288// reclaim, this check is the half that lives in the swappable realm and
289// therefore stays correct even against pointers written by an older
290// version of the logic. Without it, an address that sold or lost a name
291// kept reverse-resolving to it, impersonating the new owner.
292func ResolveAddress(addr address) string {
293	tid := nsdata.GetPrimary(addr)
294	if tid == "" {
295		return ""
296	}
297	label, domainLabel := splitTokenID(tid)
298	if !nsdata.IsNameValid(label, domainLabel) {
299		return ""
300	}
301	if nsdata.GetNameOwner(label, domainLabel) != addr {
302		return ""
303	}
304	return tid
305}
306
307// splitTokenID splits a `label*domain` token ID.
308func splitTokenID(tid string) (label, domainLabel string) {
309	for i := 0; i < len(tid); i++ {
310		if tid[i] == '*' {
311			return tid[:i], tid[i+1:]
312		}
313	}
314	return tid, ""
315}