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}