gns.gno
66.54 Kb · 2469 lines
1// Package gns implements GNS (Gno Name Service): an ENS-equivalent naming
2// system expressed as a single Gno-native realm.
3//
4// The design goal is NOT byte-for-byte ENS compatibility. Instead it provides
5// the same user-facing capabilities (registration, renewal, expiry/grace,
6// forward & reverse resolution, primary names, typed + arbitrary records,
7// subnames with policies, delegated operators, pagination and
8// events) through ONE realm, ONE ownership model, ONE record model, ONE
9// authorization function and ONE registration lifecycle.
10//
11// Key deviations from a naive port of the spec, forced by gno semantics:
12//
13// - State-mutating exported functions are "crossing" functions: they take
14// `cur realm` as the first parameter and PANIC (abort) on failure rather
15// than returning an error, because in gno only a panic/abort reverts state.
16// Failures panic with a stable machine-readable code (see the error_* set).
17// - Persistent, enumerable collections use avl.Tree (ordered, paginatable)
18// instead of Go maps, so every listing API is bounded and cursor-based.
19// - Names are stored string-native (canonical "label.parent"), not namehashed.
20//
21// See README.md for the full compatibility statement.
22package gns
23
24import (
25 "chain"
26 "chain/banker"
27 "chain/runtime"
28 "chain/runtime/unsafe"
29 "crypto/sha256"
30 "encoding/hex"
31 "errors"
32 "strconv"
33 "strings"
34 "time"
35
36 "gno.land/p/nt/avl/v0"
37 "gno.land/p/nt/ufmt/v0"
38)
39
40// ---------------------------------------------------------------------------
41// 2. Constants
42// ---------------------------------------------------------------------------
43
44const (
45 maxLabelLen = 63
46 maxNameLen = 255
47 maxDepth = 16
48
49 sep = "|" // index-key separator; never appears in names or bech32 addresses
50)
51
52// Registration-policy modes.
53const (
54 ModeClosed = "closed"
55 ModeOwner = "owner"
56 ModeOpen = "open"
57 ModeAllowlist = "allowlist"
58 ModePaid = "paid"
59)
60
61// Reserved record namespaces (cannot be used by the generic SetRecord API).
62var reservedNamespaces = map[string]bool{
63 "gno": true, "addr": true, "text": true, "content": true,
64 "abi": true, "interface": true, "system": true,
65}
66
67// Stable machine-readable error codes. Messages MAY add context, but client
68// logic should depend on these codes.
69var (
70 errInvalidName = errors.New("invalid_name")
71 errInvalidLabel = errors.New("invalid_label")
72 errNameUnavailable = errors.New("name_unavailable")
73 errNameReserved = errors.New("name_reserved")
74 errNameExpired = errors.New("name_expired")
75 errNameInGrace = errors.New("name_in_grace")
76 errUnauthorized = errors.New("unauthorized")
77 errCommitmentMissing = errors.New("commitment_missing")
78 errCommitmentTooNew = errors.New("commitment_too_new")
79 errCommitmentExpired = errors.New("commitment_expired")
80 errCommitmentMismatch = errors.New("commitment_mismatch")
81 errPriceChanged = errors.New("price_changed")
82 errInsufficientPay = errors.New("insufficient_payment")
83 errDurationTooShort = errors.New("duration_too_short")
84 errDurationTooLong = errors.New("duration_too_long")
85 errRecordTooLarge = errors.New("record_too_large")
86 errRecordLimit = errors.New("record_limit_reached")
87 errOperatorLimit = errors.New("operator_limit_reached")
88 errPolicyLocked = errors.New("policy_locked")
89 errParentInvalid = errors.New("parent_invalid")
90 errPaused = errors.New("paused")
91 errNotFound = errors.New("not_found")
92 errNotUser = errors.New("not_user")
93 errSpoofedRealm = errors.New("spoofed_realm")
94 errEmptyAddress = errors.New("empty_address")
95 errRegistrationClosed = errors.New("registration_closed")
96 errBadRequest = errors.New("bad_request")
97)
98
99// ---------------------------------------------------------------------------
100// 3. Public types
101// ---------------------------------------------------------------------------
102
103// Permission enumerates the delegable operator capabilities.
104type Permission int
105
106const (
107 PermManageRecords Permission = iota
108 PermManageOperators
109 PermManageSubnames
110 PermRenew
111 PermTransfer
112 PermManagePolicy
113)
114
115// Permissions is an operator grant. ExpiresAt == 0 means no expiry.
116type Permissions struct {
117 ManageRecords bool
118 ManageOperators bool
119 ManageSubnames bool
120 Renew bool
121 Transfer bool
122 ManagePolicy bool
123 ExpiresAt int64
124}
125
126func (p Permissions) has(perm Permission) bool {
127 switch perm {
128 case PermManageRecords:
129 return p.ManageRecords
130 case PermManageOperators:
131 return p.ManageOperators
132 case PermManageSubnames:
133 return p.ManageSubnames
134 case PermRenew:
135 return p.Renew
136 case PermTransfer:
137 return p.Transfer
138 case PermManagePolicy:
139 return p.ManagePolicy
140 }
141 return false
142}
143
144// ControlPolicy holds the explicit, readable ownership/parent control flags
145// that replace ENS Name Wrapper fuses.
146type ControlPolicy struct {
147 OwnerCanTransfer bool
148 OwnerCanCreateSubnames bool
149 RecordsMutable bool
150
151 ParentCanReclaim bool
152 ParentCanTransfer bool
153 ParentCanDelete bool
154 ParentCanChangePolicy bool
155
156 // Permanent means the policy can only become MORE restrictive. It does not
157 // make records immutable by itself.
158 Permanent bool
159}
160
161// RegistrationPolicy governs how subnames of a name may be created. The
162// realm-global config drives second-level registration.
163type RegistrationPolicy struct {
164 Mode string // closed | owner | open | allowlist | paid
165
166 MinDuration int64
167 MaxDuration int64
168
169 PricePerSecond int64
170 PaymentDenom string
171
172 Allowlist *avl.Tree // address string -> bool
173
174 DefaultControlPolicy ControlPolicy
175}
176
177// Records is the built-in resolver state for a single name.
178type Records struct {
179 NativeAddress string
180 ContentHash []byte
181 PublicKey []byte
182
183 Addresses *avl.Tree // coinType (decimal string) -> []byte
184 Text *avl.Tree // key -> string
185 ABIs *avl.Tree // contentType -> []byte
186 Interfaces *avl.Tree // interfaceID -> string
187 Arbitrary *avl.Tree // "namespace/key" -> []byte
188
189 Count int // number of stored entries, for MaxRecordsPerName enforcement
190}
191
192func newRecords() *Records {
193 return &Records{
194 Addresses: avl.NewTree(),
195 Text: avl.NewTree(),
196 ABIs: avl.NewTree(),
197 Interfaces: avl.NewTree(),
198 Arbitrary: avl.NewTree(),
199 }
200}
201
202// Name is the single object the whole realm operates on.
203type Name struct {
204 Canonical string
205 Owner address
206
207 CreatedAt int64
208 UpdatedAt int64
209 ExpiresAt int64 // 0 == permanent subname (follows parent validity)
210 GraceEndsAt int64
211
212 Parent string
213 Label string
214 Depth uint8
215
216 TTL uint64
217
218 Generation uint64
219 ParentGeneration uint64
220
221 RegistrationPolicy RegistrationPolicy
222 ControlPolicy ControlPolicy
223
224 Records *Records
225
226 Operators *avl.Tree // address string -> Permissions
227 OperatorCount int
228
229 Revision uint64
230 Reserved bool
231 Deleted bool
232}
233
234// NameView is the read-only projection returned by GetName.
235type NameView struct {
236 Canonical string
237 Owner string
238 Status string
239 CreatedAt int64
240 UpdatedAt int64
241 ExpiresAt int64
242 GraceEndsAt int64
243 Parent string
244 Label string
245 Depth uint8
246 TTL uint64
247 Generation uint64
248 Revision uint64
249 Reserved bool
250 NativeAddr string
251}
252
253// Config is the realm-global configuration.
254type Config struct {
255 Admin address
256 PendingAdmin address
257
258 RegistrationOpen bool
259
260 MinCommitAge int64
261 MaxCommitAge int64
262
263 MinRegistrationDuration int64
264 MaxRegistrationDuration int64
265
266 GracePeriod int64
267
268 BasePricePerSecond int64
269 PremiumByLength map[uint8]int64
270
271 PaymentDenom string
272 Treasury address
273
274 MaxTextValueBytes uint32
275 MaxBinaryValueBytes uint32
276 MaxRecordsPerName uint16
277 MaxOperatorsPerName uint16
278
279 PolicyRevision uint64 // bumped whenever pricing/registration rules change
280 Paused bool
281}
282
283// PricingConfig is the admin-settable pricing surface.
284type PricingConfig struct {
285 BasePricePerSecond int64
286 PremiumByLength map[uint8]int64
287 PaymentDenom string
288}
289
290// PriceQuote is returned by Price.
291type PriceQuote struct {
292 Amount int64
293 Denom string
294 ValidUntil int64
295 Revision uint64
296}
297
298// RegisterRequest is the reveal payload for Register.
299//
300// The commitment the client submits via Commit MUST equal
301// sha256hex(Name|Owner|Duration|Secret|RecordsHash|PolicyRevision) using the
302// same field values. RecordsHash is an opaque client-computed hex digest of
303// the intended initial records; it binds the reveal so a front-runner cannot
304// change records. NativeAddress/SetPrimary are optional conveniences applied
305// after creation.
306type RegisterRequest struct {
307 Name string
308 Owner address
309 Duration int64
310 Secret string
311 RecordsHash string
312 PolicyRevision uint64
313
314 NativeAddress string
315 SetPrimary bool
316}
317
318// RegistrationResult is returned by Register.
319type RegistrationResult struct {
320 Canonical string
321 Owner string
322 ExpiresAt int64
323 Generation uint64
324 Paid int64
325 Refunded int64
326}
327
328// RenewalResult is returned by Renew.
329type RenewalResult struct {
330 Canonical string
331 ExpiresAt int64
332 Paid int64
333}
334
335// SubnameOptions configures CreateSubname.
336type SubnameOptions struct {
337 Duration int64 // 0 == permanent (follows parent)
338 ControlPolicy ControlPolicy
339 NativeAddress string
340}
341
342// RecordQuery selects which record Resolve should return.
343type RecordQuery struct {
344 Kind string // "address" | "text" | "coin" | "content" | "pubkey" | "abi" | "interface" | "record"
345 Key1 string // text key / coin type / abi content type / interface id / namespace
346 Key2 string // arbitrary record key (with namespace in Key1)
347}
348
349// ResolveMode selects exact vs inherited resolution.
350type ResolveMode int
351
352const (
353 Exact ResolveMode = iota
354 NearestAncestor
355)
356
357// ResolveResult is returned by Resolve.
358type ResolveResult struct {
359 Found bool
360 Requested string
361 SourceName string
362 Value []byte
363 Revision uint64
364 ExpiresAt int64
365}
366
367// Event is an append-only change record for indexers.
368type Event struct {
369 ID uint64
370 Height int64
371 Timestamp int64
372
373 Type string
374 Name string
375
376 Actor string
377 Owner string
378 Target string
379
380 Revision uint64
381
382 Key string
383 OldDigest string
384 NewDigest string
385}
386
387// Event types.
388const (
389 EvNameRegistered = "NameRegistered"
390 EvNameRenewed = "NameRenewed"
391 EvNameTransferred = "NameTransferred"
392 EvNameExpired = "NameExpired"
393 EvNameDeleted = "NameDeleted"
394 EvSubnameCreated = "SubnameCreated"
395 EvPolicyChanged = "PolicyChanged"
396 EvOperatorChanged = "OperatorChanged"
397 EvRecordChanged = "RecordChanged"
398 EvPrimaryNameChange = "PrimaryNameChanged"
399 EvConfigChanged = "ConfigChanged"
400 EvPaused = "Paused"
401 EvUnpaused = "Unpaused"
402)
403
404// Paged result types (gno avoids generics; concrete types keep it simple).
405type StringPage struct {
406 Items []string
407 Next string
408}
409
410type OperatorView struct {
411 Address string
412 Permissions Permissions
413}
414
415type OperatorPage struct {
416 Items []OperatorView
417 Next string
418}
419
420type EventPage struct {
421 Items []Event
422 Next string
423}
424
425// NameStatus mirrors the lifecycle states.
426type NameStatus string
427
428const (
429 StatusAvailable NameStatus = "Available"
430 StatusCommitted NameStatus = "Committed"
431 StatusActive NameStatus = "Active"
432 StatusGrace NameStatus = "Grace"
433 StatusExpired NameStatus = "Expired"
434 StatusDeleted NameStatus = "Deleted"
435 StatusReserved NameStatus = "Reserved"
436)
437
438// CommitmentView is the read projection of a pending commitment.
439type CommitmentView struct {
440 Exists bool
441 Committer string
442 CreatedAt int64
443 ReadyAt int64
444 ExpiresAt int64
445}
446
447// commitment is the stored commit record.
448type commitment struct {
449 Committer address
450 CreatedAt int64
451}
452
453// ---------------------------------------------------------------------------
454// 4. Persistent state
455// ---------------------------------------------------------------------------
456
457var (
458 config Config
459 names = avl.NewTree() // canonical -> *Name
460 commitments = avl.NewTree() // commitment hex -> *commitment
461 reverse = avl.NewTree() // address string -> canonical (primary name)
462 events = avl.NewTree() // zero-padded id -> *Event
463 byOwner = avl.NewTree() // "owner|canonical" -> canonical
464 byParent = avl.NewTree() // "parent|canonical" -> canonical
465 nextEventID uint64
466)
467
468// ---------------------------------------------------------------------------
469// 5. Initialization
470// ---------------------------------------------------------------------------
471
472func init() {
473 deployer := unsafe.OriginCaller()
474 config = Config{
475 Admin: deployer,
476 RegistrationOpen: true,
477
478 MinCommitAge: 60, // 1 minute
479 MaxCommitAge: 24 * 3600, // 1 day
480
481 MinRegistrationDuration: 28 * 24 * 3600, // 28 days
482 MaxRegistrationDuration: 10 * 365 * 24 * 3600, // 10 years
483
484 GracePeriod: 90 * 24 * 3600, // 90 days
485
486 BasePricePerSecond: 1, // 1 ugnot / second (deterministic, boring)
487 PremiumByLength: map[uint8]int64{
488 1: 100,
489 2: 25,
490 3: 5,
491 4: 2,
492 },
493
494 PaymentDenom: "ugnot",
495 Treasury: deployer,
496
497 MaxTextValueBytes: 4096,
498 MaxBinaryValueBytes: 8192,
499 MaxRecordsPerName: 128,
500 MaxOperatorsPerName: 32,
501
502 PolicyRevision: 1,
503 Paused: false,
504 }
505}
506
507// ---------------------------------------------------------------------------
508// 6. Normalization
509// ---------------------------------------------------------------------------
510
511// Normalize canonicalizes a name: lowercases ASCII, validates every label, and
512// enforces length/depth limits. Non-ASCII input is rejected outright.
513func Normalize(name string) (string, error) {
514 if name == "" {
515 return "", errInvalidName
516 }
517 if len(name) > maxNameLen {
518 return "", errInvalidName
519 }
520 lower := strings.ToLower(name)
521 // reject non-ASCII (ToLower only folds ASCII deterministically for us; any
522 // byte >= 0x80 is disallowed)
523 for i := 0; i < len(lower); i++ {
524 if lower[i] >= 0x80 {
525 return "", errInvalidName
526 }
527 }
528 labels := strings.Split(lower, ".")
529 if len(labels) > maxDepth {
530 return "", errInvalidName
531 }
532 for _, l := range labels {
533 if err := validateLabel(l); err != nil {
534 return "", err
535 }
536 }
537 return lower, nil
538}
539
540func validateLabel(l string) error {
541 n := len(l)
542 if n < 1 || n > maxLabelLen {
543 return errInvalidLabel
544 }
545 for i := 0; i < n; i++ {
546 c := l[i]
547 isDigit := c >= '0' && c <= '9'
548 isAlpha := c >= 'a' && c <= 'z'
549 isHyphen := c == '-'
550 if !isDigit && !isAlpha && !isHyphen {
551 return errInvalidLabel
552 }
553 if isHyphen && (i == 0 || i == n-1) {
554 return errInvalidLabel // no leading/trailing hyphen
555 }
556 }
557 return nil
558}
559
560func mustNormalize(name string) string {
561 c, err := Normalize(name)
562 if err != nil {
563 panic(err)
564 }
565 return c
566}
567
568// splitLabel returns (label, parent) for a canonical name.
569func splitLabel(canonical string) (string, string) {
570 i := strings.Index(canonical, ".")
571 if i < 0 {
572 return canonical, ""
573 }
574 return canonical[:i], canonical[i+1:]
575}
576
577func depthOf(canonical string) uint8 {
578 return uint8(strings.Count(canonical, ".") + 1)
579}
580
581// ---------------------------------------------------------------------------
582// 7. Hashing
583// ---------------------------------------------------------------------------
584
585// MakeCommitment is the public helper clients use to derive the commitment
586// hex to pass to Commit. It normalizes the name first so the value matches what
587// Register recomputes at reveal. Returns an error if the name is invalid.
588func MakeCommitment(name string, owner address, duration int64, secret, recordsHash string, policyRevision uint64) (string, error) {
589 canonical, err := Normalize(name)
590 if err != nil {
591 return "", err
592 }
593 return computeCommitment(canonical, owner, duration, secret, recordsHash, policyRevision), nil
594}
595
596// computeCommitment derives the canonical commitment hex string. Clients MUST
597// compute it identically (see RegisterRequest docs).
598func computeCommitment(name string, owner address, duration int64, secret, recordsHash string, policyRev uint64) string {
599 preimage := name + sep + owner.String() + sep + strconv.FormatInt(duration, 10) + sep + secret + sep + recordsHash + sep + strconv.FormatUint(policyRev, 10)
600 sum := sha256.Sum256([]byte(preimage))
601 return hex.EncodeToString(sum[:])
602}
603
604// digest returns a short hex digest of a byte value, for event payloads (never
605// store full record values in events).
606func digest(b []byte) string {
607 if len(b) == 0 {
608 return ""
609 }
610 sum := sha256.Sum256(b)
611 return hex.EncodeToString(sum[:])[:16]
612}
613
614func digestStr(s string) string { return digest([]byte(s)) }
615
616// ---------------------------------------------------------------------------
617// 8. Time and lifecycle
618// ---------------------------------------------------------------------------
619
620func now() int64 { return time.Now().Unix() }
621func height() int64 { return runtime.ChainHeight() }
622
623// statusOf computes the lifecycle status of a (possibly nil) name.
624func statusOf(n *Name) NameStatus {
625 if n == nil {
626 return StatusAvailable
627 }
628 if n.Deleted {
629 return StatusDeleted
630 }
631 if n.Reserved && n.Owner == (address("")) {
632 return StatusReserved
633 }
634 if !parentChainValid(n) {
635 return StatusExpired
636 }
637 t := now()
638 if n.ExpiresAt == 0 { // permanent subname; valid while ancestors valid
639 return StatusActive
640 }
641 if t < n.ExpiresAt {
642 return StatusActive
643 }
644 if t < n.GraceEndsAt {
645 return StatusGrace
646 }
647 return StatusExpired
648}
649
650func isActive(n *Name) bool { return statusOf(n) == StatusActive }
651
652// parentChainValid verifies every ancestor exists, is active, and matches the
653// stored ParentGeneration (recycling safety, invariant 16 & recycling).
654func parentChainValid(n *Name) bool {
655 if n.Parent == "" {
656 return true
657 }
658 p := getRaw(n.Parent)
659 if p == nil || p.Deleted {
660 return false
661 }
662 if n.ParentGeneration != p.Generation {
663 return false
664 }
665 // parent must itself be active (not grace/expired) and its own chain valid
666 if p.ExpiresAt != 0 {
667 t := now()
668 if t >= p.ExpiresAt {
669 return false
670 }
671 }
672 return parentChainValid(p)
673}
674
675// available reports whether a name may be registered now.
676func available(canonical string) bool {
677 n := getRaw(canonical)
678 if n == nil {
679 return true
680 }
681 if n.Reserved {
682 return false
683 }
684 switch statusOf(n) {
685 case StatusExpired, StatusDeleted:
686 return true
687 default:
688 return false
689 }
690}
691
692// ---------------------------------------------------------------------------
693// 9. Pricing
694// ---------------------------------------------------------------------------
695
696func lengthMultiplier(label string) int64 {
697 l := uint8(len(label))
698 if m, ok := config.PremiumByLength[l]; ok {
699 return m
700 }
701 return 1
702}
703
704// priceFor computes the deterministic, overflow-checked price.
705func priceFor(canonical string, duration int64) (int64, error) {
706 if duration <= 0 {
707 return 0, errDurationTooShort
708 }
709 label, _ := splitLabel(canonical)
710 mult := lengthMultiplier(label)
711 base := config.BasePricePerSecond
712 // price = duration * base * mult, checked for overflow at each step.
713 p := duration
714 var err error
715 if p, err = mulChecked(p, base); err != nil {
716 return 0, err
717 }
718 if p, err = mulChecked(p, mult); err != nil {
719 return 0, err
720 }
721 return p, nil
722}
723
724func mulChecked(a, b int64) (int64, error) {
725 if a == 0 || b == 0 {
726 return 0, nil
727 }
728 c := a * b
729 if c/b != a || c < 0 {
730 return 0, errRecordTooLarge // reuse as overflow marker; documented
731 }
732 return c, nil
733}
734
735// Price returns a price quote for registering/renewing name for duration.
736func Price(name string, duration int64) (PriceQuote, error) {
737 canonical, err := Normalize(name)
738 if err != nil {
739 return PriceQuote{}, err
740 }
741 amt, err := priceFor(canonical, duration)
742 if err != nil {
743 return PriceQuote{}, err
744 }
745 return PriceQuote{
746 Amount: amt,
747 Denom: config.PaymentDenom,
748 ValidUntil: now() + config.MaxCommitAge,
749 Revision: config.PolicyRevision,
750 }, nil
751}
752
753// ---------------------------------------------------------------------------
754// 10. Authorization
755// ---------------------------------------------------------------------------
756
757// caller authenticates a crossing frame and returns the immediate caller.
758func caller(cur realm) address {
759 if !cur.IsCurrent() {
760 panic(errSpoofedRealm)
761 }
762 return cur.Previous().Address()
763}
764
765// callerUser authenticates and requires an end-user (EOA) caller.
766func callerUser(cur realm) address {
767 if !cur.IsCurrent() {
768 panic(errSpoofedRealm)
769 }
770 prev := cur.Previous()
771 if !prev.IsUser() {
772 panic(errNotUser)
773 }
774 return prev.Address()
775}
776
777// authorize is the single gate for all name mutations. Authority order:
778// 1. admin — NOT here (admin has no routine power over user names);
779// 2. direct owner of an active name;
780// 3. active operator with the matching permission;
781// 4. parent authority, when the child policy allows.
782func authorize(callerAddr address, n *Name, perm Permission) error {
783 if n == nil {
784 return errNotFound
785 }
786 // (2) direct owner, but only while active (invariant 2).
787 if n.Owner == callerAddr && isActive(n) {
788 return nil
789 }
790 // (3) operator
791 if op, ok := getOperator(n, callerAddr); ok && isActive(n) {
792 if op.has(perm) && (op.ExpiresAt == 0 || op.ExpiresAt > now()) {
793 return nil
794 }
795 }
796 // (4) parent authority
797 if parentAuthorized(callerAddr, n, perm) {
798 return nil
799 }
800 return errUnauthorized
801}
802
803func mustAuthorize(callerAddr address, n *Name, perm Permission) {
804 if err := authorize(callerAddr, n, perm); err != nil {
805 panic(err)
806 }
807}
808
809// parentAuthorized checks whether callerAddr controls the parent AND the
810// child's policy grants the parent that specific power.
811func parentAuthorized(callerAddr address, n *Name, perm Permission) bool {
812 if n.Parent == "" {
813 return false
814 }
815 p := getRaw(n.Parent)
816 if p == nil || !isActive(p) {
817 return false
818 }
819 // caller must control the parent (owner or ManageSubnames operator)
820 controls := p.Owner == callerAddr
821 if !controls {
822 if op, ok := getOperator(p, callerAddr); ok && op.ManageSubnames {
823 controls = true
824 }
825 }
826 if !controls {
827 return false
828 }
829 cp := n.ControlPolicy
830 switch perm {
831 case PermTransfer:
832 return cp.ParentCanTransfer || cp.ParentCanReclaim
833 case PermManagePolicy:
834 return cp.ParentCanChangePolicy
835 }
836 return false
837}
838
839// ---------------------------------------------------------------------------
840// 11. Registration
841// ---------------------------------------------------------------------------
842
843// Commit stores a registration commitment. The commitment hides the intended
844// name; only its hash is recorded together with the committer and timestamp.
845func Commit(cur realm, commit string) {
846 requireNotPaused()
847 c := callerUser(cur)
848 if commit == "" {
849 panic(errBadRequest)
850 }
851 if existing, ok := getCommitment(commit); ok {
852 // reject only if the existing commitment is still within its usable
853 // window; stale ones may be overwritten.
854 if now()-existing.CreatedAt <= config.MaxCommitAge {
855 panic(errCommitmentMismatch)
856 }
857 }
858 commitments.Set(commit, &commitment{Committer: c, CreatedAt: now()})
859}
860
861// Register reveals and consumes a commitment to create a second-level name.
862func Register(cur realm, request RegisterRequest) RegistrationResult {
863 requireNotPaused()
864 c := callerUser(cur)
865
866 if !config.RegistrationOpen {
867 panic(errRegistrationClosed)
868 }
869 canonical, err := Normalize(request.Name)
870 if err != nil {
871 panic(err)
872 }
873 // second-level only (no dots) via Register; subnames use CreateSubname.
874 if strings.Contains(canonical, ".") {
875 panic(errInvalidName)
876 }
877 if request.PolicyRevision != config.PolicyRevision {
878 panic(errPriceChanged)
879 }
880 if !request.Owner.IsValid() {
881 panic(errEmptyAddress)
882 }
883
884 // commitment checks
885 key := computeCommitment(canonical, request.Owner, request.Duration, request.Secret, request.RecordsHash, request.PolicyRevision)
886 cm, ok := getCommitment(key)
887 if !ok {
888 panic(errCommitmentMissing)
889 }
890 if cm.Committer != c {
891 panic(errUnauthorized)
892 }
893 age := now() - cm.CreatedAt
894 if age < config.MinCommitAge {
895 panic(errCommitmentTooNew)
896 }
897 if age > config.MaxCommitAge {
898 panic(errCommitmentExpired)
899 }
900
901 // availability + duration
902 if !available(canonical) {
903 existing := getRaw(canonical)
904 switch statusOf(existing) {
905 case StatusReserved:
906 panic(errNameReserved)
907 case StatusGrace:
908 panic(errNameInGrace)
909 default:
910 panic(errNameUnavailable)
911 }
912 }
913 if request.Duration < config.MinRegistrationDuration {
914 panic(errDurationTooShort)
915 }
916 if request.Duration > config.MaxRegistrationDuration {
917 panic(errDurationTooLong)
918 }
919
920 price, err := priceFor(canonical, request.Duration)
921 if err != nil {
922 panic(err)
923 }
924 paid, refunded := collectPayment(cur, price)
925
926 // build/recycle the name
927 prev := getRaw(canonical)
928 var gen uint64 = 1
929 if prev != nil {
930 gen = prev.Generation + 1 // recycle: increment generation
931 }
932 t := now()
933 label, parent := splitLabel(canonical)
934 n := &Name{
935 Canonical: canonical,
936 Owner: request.Owner,
937 CreatedAt: t,
938 UpdatedAt: t,
939 ExpiresAt: t + request.Duration,
940 Parent: parent,
941 Label: label,
942 Depth: depthOf(canonical),
943 Generation: gen,
944 Revision: 1,
945 Records: newRecords(),
946 Operators: avl.NewTree(),
947 ControlPolicy: ControlPolicy{
948 OwnerCanTransfer: true,
949 OwnerCanCreateSubnames: true,
950 RecordsMutable: true,
951 },
952 RegistrationPolicy: RegistrationPolicy{Mode: ModeClosed},
953 }
954 n.GraceEndsAt = n.ExpiresAt + config.GracePeriod
955
956 // clear any stale owner index from a previous generation before storing.
957 if prev != nil {
958 byOwner.Remove(ownerKey(prev.Owner, canonical))
959 }
960 putName(n)
961
962 // optional convenience records
963 if request.NativeAddress != "" {
964 setNativeAddressInternal(n, request.NativeAddress)
965 }
966 if request.SetPrimary && request.NativeAddress != "" && address(request.NativeAddress) == c {
967 reverse.Set(c.String(), canonical)
968 emit(EvPrimaryNameChange, canonical, c, n.Owner, "")
969 }
970
971 commitments.Remove(key)
972 emitOwner(EvNameRegistered, n)
973
974 return RegistrationResult{
975 Canonical: canonical,
976 Owner: n.Owner.String(),
977 ExpiresAt: n.ExpiresAt,
978 Generation: n.Generation,
979 Paid: paid,
980 Refunded: refunded,
981 }
982}
983
984// collectPayment reads the attached ugnot, requires it to cover price, and
985// forwards price to the treasury while refunding any overpayment.
986func collectPayment(cur realm, price int64) (paid, refunded int64) {
987 if price <= 0 {
988 return 0, 0
989 }
990 if !cur.Previous().IsUserCall() {
991 panic(errNotUser)
992 }
993 sent := unsafe.OriginSend()
994 got := sent.AmountOf(config.PaymentDenom)
995 if got < price {
996 panic(errInsufficientPay)
997 }
998 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
999 self := cur.Address()
1000 // forward the price to the treasury
1001 if config.Treasury != self {
1002 bk.SendCoins(self, config.Treasury, coins(config.PaymentDenom, price))
1003 }
1004 // refund the remainder to the caller
1005 over := got - price
1006 if over > 0 {
1007 bk.SendCoins(self, cur.Previous().Address(), coins(config.PaymentDenom, over))
1008 }
1009 return price, over
1010}
1011
1012// ---------------------------------------------------------------------------
1013// 12. Renewal and expiry
1014// ---------------------------------------------------------------------------
1015
1016// Renew extends a name's expiry. Anyone may pay to renew (a socially useful
1017// property: third parties can prevent expiry but gain no authority). Renewal
1018// is allowed while Active or in Grace, never once fully Expired.
1019func Renew(cur realm, name string, duration int64) RenewalResult {
1020 requireNotPaused()
1021 _ = caller(cur) // authenticate frame; no authority needed to sponsor renewal
1022 canonical := mustNormalize(name)
1023 n := getRaw(canonical)
1024 if n == nil || n.Deleted {
1025 panic(errNotFound)
1026 }
1027 st := statusOf(n)
1028 if st != StatusActive && st != StatusGrace {
1029 panic(errNameExpired)
1030 }
1031 if n.ExpiresAt == 0 {
1032 panic(errBadRequest) // permanent subname has no independent expiry
1033 }
1034 if duration <= 0 {
1035 panic(errDurationTooShort)
1036 }
1037
1038 // new expiry base: max(now, current expiry) so grace renewals extend from
1039 // the original expiry, active renewals from current expiry.
1040 base := n.ExpiresAt
1041 if st == StatusGrace {
1042 // during grace, extend from now to avoid free grace time abuse
1043 base = n.ExpiresAt
1044 }
1045 newExpiry := base + duration
1046 // enforce the maximum expiry horizon (invariant 11).
1047 maxHorizon := now() + config.MaxRegistrationDuration
1048 if newExpiry > maxHorizon {
1049 panic(errDurationTooLong)
1050 }
1051
1052 price, err := priceFor(canonical, duration)
1053 if err != nil {
1054 panic(err)
1055 }
1056 paid, _ := collectPayment(cur, price)
1057
1058 n.ExpiresAt = newExpiry
1059 n.GraceEndsAt = newExpiry + config.GracePeriod
1060 n.UpdatedAt = now()
1061 n.Revision++
1062 putName(n)
1063 emitOwner(EvNameRenewed, n)
1064
1065 return RenewalResult{Canonical: canonical, ExpiresAt: n.ExpiresAt, Paid: paid}
1066}
1067
1068// ---------------------------------------------------------------------------
1069// 13. Ownership
1070// ---------------------------------------------------------------------------
1071
1072// Transfer moves ownership of a name.
1073func Transfer(cur realm, name string, newOwner address, clearOperators, clearRecords bool) {
1074 requireNotPaused()
1075 c := caller(cur)
1076 canonical := mustNormalize(name)
1077 n := getRaw(canonical)
1078 if n == nil {
1079 panic(errNotFound)
1080 }
1081 if !newOwner.IsValid() {
1082 panic(errEmptyAddress)
1083 }
1084 if newOwner == n.Owner {
1085 panic(errBadRequest) // self-transfer rejected (cleaner than no-op)
1086 }
1087 // owner path additionally requires the policy to allow transfer.
1088 if c == n.Owner && isActive(n) {
1089 if !n.ControlPolicy.OwnerCanTransfer {
1090 panic(errPolicyLocked)
1091 }
1092 } else {
1093 mustAuthorize(c, n, PermTransfer)
1094 }
1095
1096 old := n.Owner
1097 byOwner.Remove(ownerKey(old, canonical))
1098
1099 n.Owner = newOwner
1100 n.Revision++
1101 n.UpdatedAt = now()
1102 if clearOperators {
1103 n.Operators = avl.NewTree()
1104 n.OperatorCount = 0
1105 }
1106 if clearRecords {
1107 n.Records = newRecords()
1108 }
1109 putName(n)
1110
1111 // invalidate reverse mappings that no longer pass forward verification.
1112 invalidateReverseFor(old, canonical)
1113
1114 emit(EvNameTransferred, canonical, c, newOwner, old.String())
1115}
1116
1117// ---------------------------------------------------------------------------
1118// 14. Subnames and policy
1119// ---------------------------------------------------------------------------
1120
1121// CreateSubname creates label.parent according to the parent registration
1122// policy.
1123func CreateSubname(cur realm, parent string, label string, owner address, options SubnameOptions) {
1124 requireNotPaused()
1125 c := callerUser(cur)
1126
1127 pcanon := mustNormalize(parent)
1128 if err := validateLabel(strings.ToLower(label)); err != nil {
1129 panic(err)
1130 }
1131 label = strings.ToLower(label)
1132 canonical := label + "." + pcanon
1133 if len(canonical) > maxNameLen {
1134 panic(errInvalidName)
1135 }
1136 if depthOf(canonical) > maxDepth {
1137 panic(errInvalidName)
1138 }
1139
1140 p := getRaw(pcanon)
1141 if p == nil || !isActive(p) {
1142 panic(errParentInvalid)
1143 }
1144 if !owner.IsValid() {
1145 panic(errEmptyAddress)
1146 }
1147 if !available(canonical) {
1148 panic(errNameUnavailable)
1149 }
1150
1151 pol := p.RegistrationPolicy
1152 // authorize + charge according to policy mode.
1153 switch pol.Mode {
1154 case ModeClosed, "":
1155 panic(errRegistrationClosed)
1156 case ModeOwner:
1157 mustAuthorize(c, p, PermManageSubnames)
1158 case ModeOpen:
1159 // anyone
1160 case ModeAllowlist:
1161 if pol.Allowlist == nil || !allowlistHas(pol.Allowlist, c) {
1162 panic(errUnauthorized)
1163 }
1164 case ModePaid:
1165 charge := int64(0)
1166 if pol.PricePerSecond > 0 && options.Duration > 0 {
1167 var err error
1168 if charge, err = mulChecked(pol.PricePerSecond, options.Duration); err != nil {
1169 panic(err)
1170 }
1171 }
1172 denom := pol.PaymentDenom
1173 if denom == "" {
1174 denom = config.PaymentDenom
1175 }
1176 collectPaymentDenom(cur, charge, denom, p.Owner)
1177 default:
1178 panic(errBadRequest)
1179 }
1180
1181 t := now()
1182 prev := getRaw(canonical)
1183 var gen uint64 = 1
1184 if prev != nil {
1185 gen = prev.Generation + 1
1186 byOwner.Remove(ownerKey(prev.Owner, canonical))
1187 }
1188 cpol := options.ControlPolicy
1189 if (cpol == ControlPolicy{}) {
1190 cpol = pol.DefaultControlPolicy
1191 }
1192 n := &Name{
1193 Canonical: canonical,
1194 Owner: owner,
1195 CreatedAt: t,
1196 UpdatedAt: t,
1197 Parent: pcanon,
1198 Label: label,
1199 Depth: depthOf(canonical),
1200 Generation: gen,
1201 ParentGeneration: p.Generation,
1202 Revision: 1,
1203 Records: newRecords(),
1204 Operators: avl.NewTree(),
1205 ControlPolicy: cpol,
1206 RegistrationPolicy: RegistrationPolicy{Mode: ModeClosed},
1207 }
1208 if options.Duration > 0 {
1209 n.ExpiresAt = t + options.Duration
1210 n.GraceEndsAt = n.ExpiresAt + config.GracePeriod
1211 } // else permanent (ExpiresAt == 0)
1212
1213 putName(n)
1214 if options.NativeAddress != "" {
1215 setNativeAddressInternal(n, options.NativeAddress)
1216 }
1217 emit(EvSubnameCreated, canonical, c, owner, pcanon)
1218}
1219
1220// DeleteSubname removes a subname. Callable by the owner, or by the parent when
1221// ParentCanDelete is set.
1222func DeleteSubname(cur realm, name string) {
1223 requireNotPaused()
1224 c := caller(cur)
1225 canonical := mustNormalize(name)
1226 n := getRaw(canonical)
1227 if n == nil || n.Deleted {
1228 panic(errNotFound)
1229 }
1230 if n.Parent == "" {
1231 panic(errBadRequest) // not a subname
1232 }
1233 authorized := false
1234 if c == n.Owner && isActive(n) {
1235 authorized = true
1236 } else if p := getRaw(n.Parent); p != nil && isActive(p) && n.ControlPolicy.ParentCanDelete {
1237 if p.Owner == c {
1238 authorized = true
1239 } else if op, ok := getOperator(p, c); ok && op.ManageSubnames {
1240 authorized = true
1241 }
1242 }
1243 if !authorized {
1244 panic(errUnauthorized)
1245 }
1246 deleteName(n)
1247 emit(EvNameDeleted, canonical, c, n.Owner, "")
1248}
1249
1250// SetRegistrationPolicy sets the subname-issuance policy for a name.
1251func SetRegistrationPolicy(cur realm, name string, policy RegistrationPolicy) {
1252 requireNotPaused()
1253 c := caller(cur)
1254 canonical := mustNormalize(name)
1255 n := getRaw(canonical)
1256 if n == nil {
1257 panic(errNotFound)
1258 }
1259 if c == n.Owner && isActive(n) {
1260 if !n.ControlPolicy.OwnerCanCreateSubnames && policy.Mode != ModeClosed {
1261 panic(errPolicyLocked)
1262 }
1263 } else {
1264 mustAuthorize(c, n, PermManagePolicy)
1265 }
1266 n.RegistrationPolicy = policy
1267 n.UpdatedAt = now()
1268 n.Revision++
1269 putName(n)
1270 emit(EvPolicyChanged, canonical, c, n.Owner, "registration")
1271}
1272
1273// LockPolicy makes a name's control policy strictly more restrictive
1274// (emancipation). Flags may only move true->false; once Permanent, no field
1275// may be relaxed. Only the owner may lock.
1276func LockPolicy(cur realm, name string, restrictions ControlPolicy) {
1277 requireNotPaused()
1278 c := caller(cur)
1279 canonical := mustNormalize(name)
1280 n := getRaw(canonical)
1281 if n == nil {
1282 panic(errNotFound)
1283 }
1284 if c != n.Owner || !isActive(n) {
1285 panic(errUnauthorized)
1286 }
1287 cur0 := n.ControlPolicy
1288 if cur0.Permanent && !restrictions.Permanent {
1289 panic(errPolicyLocked)
1290 }
1291 next := mergeRestrictive(cur0, restrictions)
1292 n.ControlPolicy = next
1293 n.UpdatedAt = now()
1294 n.Revision++
1295 putName(n)
1296 emit(EvPolicyChanged, canonical, c, n.Owner, "control")
1297}
1298
1299// mergeRestrictive returns a policy where each boolean can only go from
1300// permissive (true) to restrictive (false); Permanent can only be turned on.
1301func mergeRestrictive(cur, req ControlPolicy) ControlPolicy {
1302 andRestrict := func(cur, req bool) bool { return cur && req }
1303 return ControlPolicy{
1304 OwnerCanTransfer: andRestrict(cur.OwnerCanTransfer, req.OwnerCanTransfer),
1305 OwnerCanCreateSubnames: andRestrict(cur.OwnerCanCreateSubnames, req.OwnerCanCreateSubnames),
1306 RecordsMutable: andRestrict(cur.RecordsMutable, req.RecordsMutable),
1307 ParentCanReclaim: andRestrict(cur.ParentCanReclaim, req.ParentCanReclaim),
1308 ParentCanTransfer: andRestrict(cur.ParentCanTransfer, req.ParentCanTransfer),
1309 ParentCanDelete: andRestrict(cur.ParentCanDelete, req.ParentCanDelete),
1310 ParentCanChangePolicy: andRestrict(cur.ParentCanChangePolicy, req.ParentCanChangePolicy),
1311 Permanent: cur.Permanent || req.Permanent,
1312 }
1313}
1314
1315// SetOperator grants (or updates) an operator's permissions on a name.
1316func SetOperator(cur realm, name string, operator address, permissions Permissions) {
1317 requireNotPaused()
1318 c := caller(cur)
1319 canonical := mustNormalize(name)
1320 n := getRaw(canonical)
1321 if n == nil {
1322 panic(errNotFound)
1323 }
1324 mustAuthorizeOwnerOrOp(c, n, PermManageOperators)
1325 if !operator.IsValid() {
1326 panic(errEmptyAddress)
1327 }
1328 _, existed := getOperator(n, operator)
1329 if !existed {
1330 if int(config.MaxOperatorsPerName) > 0 && n.OperatorCount >= int(config.MaxOperatorsPerName) {
1331 panic(errOperatorLimit)
1332 }
1333 n.OperatorCount++
1334 }
1335 n.Operators.Set(operator.String(), permissions)
1336 n.UpdatedAt = now()
1337 n.Revision++
1338 putName(n)
1339 emit(EvOperatorChanged, canonical, c, operator, "set")
1340}
1341
1342// RemoveOperator revokes an operator.
1343func RemoveOperator(cur realm, name string, operator address) {
1344 requireNotPaused()
1345 c := caller(cur)
1346 canonical := mustNormalize(name)
1347 n := getRaw(canonical)
1348 if n == nil {
1349 panic(errNotFound)
1350 }
1351 mustAuthorizeOwnerOrOp(c, n, PermManageOperators)
1352 if _, removed := n.Operators.Remove(operator.String()); removed {
1353 n.OperatorCount--
1354 n.UpdatedAt = now()
1355 n.Revision++
1356 putName(n)
1357 emit(EvOperatorChanged, canonical, c, operator, "remove")
1358 }
1359}
1360
1361// mustAuthorizeOwnerOrOp allows the active owner directly or an operator with
1362// the given permission (used for record/operator management).
1363func mustAuthorizeOwnerOrOp(c address, n *Name, perm Permission) {
1364 if c == n.Owner && isActive(n) {
1365 return
1366 }
1367 if op, ok := getOperator(n, c); ok && isActive(n) && op.has(perm) && (op.ExpiresAt == 0 || op.ExpiresAt > now()) {
1368 return
1369 }
1370 panic(errUnauthorized)
1371}
1372
1373// ---------------------------------------------------------------------------
1374// 15. Records
1375// ---------------------------------------------------------------------------
1376
1377func requireRecordsMutable(n *Name) {
1378 if !n.ControlPolicy.RecordsMutable {
1379 panic(errPolicyLocked)
1380 }
1381 // records may not be mutated during grace (spec: normal updates disabled in
1382 // grace, except clearing reverse).
1383 if statusOf(n) != StatusActive {
1384 panic(errNameExpired)
1385 }
1386}
1387
1388func bumpRecords(n *Name) {
1389 n.UpdatedAt = now()
1390 n.Revision++
1391 putName(n)
1392}
1393
1394func (n *Name) recordCountGuard(added int) {
1395 if config.MaxRecordsPerName > 0 && n.Records.Count+added > int(config.MaxRecordsPerName) {
1396 panic(errRecordLimit)
1397 }
1398}
1399
1400// recordMutation is the shared preamble for every typed record setter: pause
1401// gate, caller authentication, name resolution, record-management authority,
1402// and records-mutable check. Returns the name and the caller address.
1403func recordMutation(cur realm, name string) (*Name, address) {
1404 requireNotPaused()
1405 c := caller(cur)
1406 n := mustName(name)
1407 mustAuthorizeOwnerOrOp(c, n, PermManageRecords)
1408 requireRecordsMutable(n)
1409 return n, c
1410}
1411
1412// SetAddress sets the native address record.
1413func SetAddress(cur realm, name string, addr string) {
1414 n, c := recordMutation(cur, name)
1415 if addr != "" && !address(addr).IsValid() {
1416 panic(errEmptyAddress)
1417 }
1418 setNativeAddressInternal(n, addr)
1419 bumpRecords(n)
1420 emitRecord(EvRecordChanged, n, c, "addr", digestStr(addr))
1421}
1422
1423func setNativeAddressInternal(n *Name, addr string) {
1424 n.Records.NativeAddress = addr
1425}
1426
1427// Address returns the native address record.
1428func Address(name string) (string, bool) {
1429 n := resolvableName(name)
1430 if n == nil || n.Records.NativeAddress == "" {
1431 return "", false
1432 }
1433 return n.Records.NativeAddress, true
1434}
1435
1436// SetCoinAddress sets a multichain address for coinType (decimal string).
1437func SetCoinAddress(cur realm, name string, coinType string, value []byte) {
1438 n, c := recordMutation(cur, name)
1439 guardBinary(value)
1440 if !n.Records.Addresses.Has(coinType) {
1441 n.recordCountGuard(1)
1442 n.Records.Count++
1443 }
1444 n.Records.Addresses.Set(coinType, value)
1445 bumpRecords(n)
1446 emitRecord(EvRecordChanged, n, c, "coin:"+coinType, digest(value))
1447}
1448
1449// CoinAddress returns a multichain address.
1450func CoinAddress(name string, coinType string) ([]byte, bool) {
1451 n := resolvableName(name)
1452 if n == nil {
1453 return nil, false
1454 }
1455 if v, ok := treeGet(n.Records.Addresses, coinType); ok {
1456 return v.([]byte), true
1457 }
1458 return nil, false
1459}
1460
1461// SetText sets a text record; empty value deletes it (physical removal).
1462func SetText(cur realm, name string, key string, value string) {
1463 n, c := recordMutation(cur, name)
1464 if key == "" {
1465 panic(errBadRequest)
1466 }
1467 if value == "" {
1468 if _, removed := n.Records.Text.Remove(key); removed {
1469 n.Records.Count--
1470 bumpRecords(n)
1471 emitRecord(EvRecordChanged, n, c, "text:"+key, "")
1472 }
1473 return
1474 }
1475 if uint32(len(value)) > config.MaxTextValueBytes {
1476 panic(errRecordTooLarge)
1477 }
1478 if !n.Records.Text.Has(key) {
1479 n.recordCountGuard(1)
1480 n.Records.Count++
1481 }
1482 n.Records.Text.Set(key, value)
1483 bumpRecords(n)
1484 emitRecord(EvRecordChanged, n, c, "text:"+key, digestStr(value))
1485}
1486
1487// Text returns a text record.
1488func Text(name string, key string) (string, bool) {
1489 n := resolvableName(name)
1490 if n == nil {
1491 return "", false
1492 }
1493 if v, ok := treeGet(n.Records.Text, key); ok {
1494 return v.(string), true
1495 }
1496 return "", false
1497}
1498
1499// SetContentHash sets the content hash record.
1500func SetContentHash(cur realm, name string, value []byte) {
1501 n, c := recordMutation(cur, name)
1502 guardBinary(value)
1503 n.Records.ContentHash = value
1504 bumpRecords(n)
1505 emitRecord(EvRecordChanged, n, c, "content", digest(value))
1506}
1507
1508// ContentHash returns the content hash record.
1509func ContentHash(name string) ([]byte, bool) {
1510 n := resolvableName(name)
1511 if n == nil || len(n.Records.ContentHash) == 0 {
1512 return nil, false
1513 }
1514 return n.Records.ContentHash, true
1515}
1516
1517// SetPublicKey sets the public key record.
1518func SetPublicKey(cur realm, name string, value []byte) {
1519 n, c := recordMutation(cur, name)
1520 guardBinary(value)
1521 n.Records.PublicKey = value
1522 bumpRecords(n)
1523 emitRecord(EvRecordChanged, n, c, "pubkey", digest(value))
1524}
1525
1526// PublicKey returns the public key record.
1527func PublicKey(name string) ([]byte, bool) {
1528 n := resolvableName(name)
1529 if n == nil || len(n.Records.PublicKey) == 0 {
1530 return nil, false
1531 }
1532 return n.Records.PublicKey, true
1533}
1534
1535// SetABI sets an ABI blob by content type.
1536func SetABI(cur realm, name string, contentType string, value []byte) {
1537 n, c := recordMutation(cur, name)
1538 guardBinary(value)
1539 if !n.Records.ABIs.Has(contentType) {
1540 n.recordCountGuard(1)
1541 n.Records.Count++
1542 }
1543 n.Records.ABIs.Set(contentType, value)
1544 bumpRecords(n)
1545 emitRecord(EvRecordChanged, n, c, "abi:"+contentType, digest(value))
1546}
1547
1548// ABI returns an ABI blob.
1549func ABI(name string, contentType string) ([]byte, bool) {
1550 n := resolvableName(name)
1551 if n == nil {
1552 return nil, false
1553 }
1554 if v, ok := treeGet(n.Records.ABIs, contentType); ok {
1555 return v.([]byte), true
1556 }
1557 return nil, false
1558}
1559
1560// SetInterface sets an interface target by interface ID.
1561func SetInterface(cur realm, name string, interfaceID string, target string) {
1562 n, c := recordMutation(cur, name)
1563 if !n.Records.Interfaces.Has(interfaceID) {
1564 n.recordCountGuard(1)
1565 n.Records.Count++
1566 }
1567 n.Records.Interfaces.Set(interfaceID, target)
1568 bumpRecords(n)
1569 emitRecord(EvRecordChanged, n, c, "interface:"+interfaceID, digestStr(target))
1570}
1571
1572// Interface returns an interface target.
1573func Interface(name string, interfaceID string) (string, bool) {
1574 n := resolvableName(name)
1575 if n == nil {
1576 return "", false
1577 }
1578 if v, ok := treeGet(n.Records.Interfaces, interfaceID); ok {
1579 return v.(string), true
1580 }
1581 return "", false
1582}
1583
1584// SetRecord sets an arbitrary namespaced record. Reserved namespaces are
1585// rejected; use the typed setters for those.
1586func SetRecord(cur realm, name string, namespace string, key string, value []byte) {
1587 n, c := recordMutation(cur, name)
1588 if namespace == "" || key == "" {
1589 panic(errBadRequest)
1590 }
1591 if reservedNamespaces[namespace] {
1592 panic(errBadRequest)
1593 }
1594 guardBinary(value)
1595 k := namespace + "/" + key
1596 if !n.Records.Arbitrary.Has(k) {
1597 n.recordCountGuard(1)
1598 n.Records.Count++
1599 }
1600 n.Records.Arbitrary.Set(k, value)
1601 bumpRecords(n)
1602 emitRecord(EvRecordChanged, n, c, "record:"+k, digest(value))
1603}
1604
1605// Record returns an arbitrary namespaced record.
1606func Record(name string, namespace string, key string) ([]byte, bool) {
1607 n := resolvableName(name)
1608 if n == nil {
1609 return nil, false
1610 }
1611 if v, ok := treeGet(n.Records.Arbitrary, namespace+"/"+key); ok {
1612 return v.([]byte), true
1613 }
1614 return nil, false
1615}
1616
1617// SetTTL sets the name's TTL metadata.
1618func SetTTL(cur realm, name string, ttl uint64) {
1619 n, c := recordMutation(cur, name)
1620 n.TTL = ttl
1621 bumpRecords(n)
1622 emitRecord(EvRecordChanged, n, c, "ttl", strconv.FormatUint(ttl, 10))
1623}
1624
1625func guardBinary(value []byte) {
1626 if config.MaxBinaryValueBytes > 0 && uint32(len(value)) > config.MaxBinaryValueBytes {
1627 panic(errRecordTooLarge)
1628 }
1629}
1630
1631// Resolve returns a record either at the exact name or from the nearest valid
1632// ancestor (wildcard-style). Inheritance is explicit, never implicit in the
1633// primitive getters.
1634func Resolve(name string, query RecordQuery, mode ResolveMode) ResolveResult {
1635 canonical, err := Normalize(name)
1636 if err != nil {
1637 return ResolveResult{Requested: name}
1638 }
1639 cur := canonical
1640 for {
1641 n := resolvableName(cur)
1642 if n != nil {
1643 if val, ok := lookupRecord(n, query); ok {
1644 return ResolveResult{
1645 Found: true,
1646 Requested: canonical,
1647 SourceName: cur,
1648 Value: val,
1649 Revision: n.Revision,
1650 ExpiresAt: n.ExpiresAt,
1651 }
1652 }
1653 }
1654 if mode == Exact {
1655 break
1656 }
1657 _, parent := splitLabel(cur)
1658 if parent == "" {
1659 break
1660 }
1661 cur = parent
1662 }
1663 return ResolveResult{Requested: canonical}
1664}
1665
1666func lookupRecord(n *Name, q RecordQuery) ([]byte, bool) {
1667 switch q.Kind {
1668 case "address":
1669 if n.Records.NativeAddress != "" {
1670 return []byte(n.Records.NativeAddress), true
1671 }
1672 case "text":
1673 if v, ok := treeGet(n.Records.Text, q.Key1); ok {
1674 return []byte(v.(string)), true
1675 }
1676 case "coin":
1677 if v, ok := treeGet(n.Records.Addresses, q.Key1); ok {
1678 return v.([]byte), true
1679 }
1680 case "content":
1681 if len(n.Records.ContentHash) > 0 {
1682 return n.Records.ContentHash, true
1683 }
1684 case "pubkey":
1685 if len(n.Records.PublicKey) > 0 {
1686 return n.Records.PublicKey, true
1687 }
1688 case "abi":
1689 if v, ok := treeGet(n.Records.ABIs, q.Key1); ok {
1690 return v.([]byte), true
1691 }
1692 case "interface":
1693 if v, ok := treeGet(n.Records.Interfaces, q.Key1); ok {
1694 return []byte(v.(string)), true
1695 }
1696 case "record":
1697 if v, ok := treeGet(n.Records.Arbitrary, q.Key1+"/"+q.Key2); ok {
1698 return v.([]byte), true
1699 }
1700 }
1701 return nil, false
1702}
1703
1704// ---------------------------------------------------------------------------
1705// 16. Reverse resolution and primary names
1706// ---------------------------------------------------------------------------
1707
1708// SetPrimaryName sets the caller's primary (reverse) name. The name must be
1709// active and forward-resolve (Address) to the caller.
1710func SetPrimaryName(cur realm, name string) {
1711 requireNotPaused()
1712 c := callerUser(cur)
1713 canonical := mustNormalize(name)
1714 n := getRaw(canonical)
1715 if n == nil || !isActive(n) {
1716 panic(errNotFound)
1717 }
1718 if n.Records.NativeAddress != c.String() {
1719 panic(errUnauthorized)
1720 }
1721 reverse.Set(c.String(), canonical)
1722 emit(EvPrimaryNameChange, canonical, c, n.Owner, "")
1723}
1724
1725// PrimaryName returns the verified primary name for an address, checking that
1726// (1) a reverse record exists, (2) the name is active, and (3) forward
1727// resolution still matches. Any failure returns not-found.
1728func PrimaryName(addr string) (string, bool) {
1729 v, ok := treeGet(reverse, addr)
1730 if !ok {
1731 return "", false
1732 }
1733 canonical := v.(string)
1734 n := getRaw(canonical)
1735 if n == nil || !isActive(n) {
1736 return "", false
1737 }
1738 if n.Records.NativeAddress != addr {
1739 return "", false
1740 }
1741 return canonical, true
1742}
1743
1744// ClearPrimaryName clears the caller's reverse record.
1745func ClearPrimaryName(cur realm) {
1746 c := caller(cur) // allowed even when paused / during grace
1747 if _, removed := reverse.Remove(c.String()); removed {
1748 emit(EvPrimaryNameChange, "", c, "", "cleared")
1749 }
1750}
1751
1752// invalidateReverseFor lazily clears a reverse record if the just-changed name
1753// no longer forward-verifies for the old owner. PrimaryName also re-verifies,
1754// so this is best-effort cleanup.
1755func invalidateReverseFor(oldOwner address, canonical string) {
1756 if v, ok := treeGet(reverse, oldOwner.String()); ok && v.(string) == canonical {
1757 n := getRaw(canonical)
1758 if n == nil || n.Records.NativeAddress != oldOwner.String() {
1759 reverse.Remove(oldOwner.String())
1760 }
1761 }
1762}
1763
1764// ---------------------------------------------------------------------------
1765// 17. Enumeration (bounded, cursor-based)
1766// ---------------------------------------------------------------------------
1767
1768func capLimit(limit uint16) int {
1769 const hardCap = 200
1770 if limit == 0 || int(limit) > hardCap {
1771 return hardCap
1772 }
1773 return int(limit)
1774}
1775
1776// NamesByOwner lists canonical names owned by owner.
1777func NamesByOwner(owner string, cursor string, limit uint16) StringPage {
1778 prefix := owner + sep
1779 start := prefix
1780 if cursor != "" {
1781 start = prefix + cursor
1782 }
1783 max := capLimit(limit)
1784 items := []string{}
1785 next := ""
1786 byOwner.Iterate(start, prefixEnd(prefix), func(k string, v any) bool {
1787 if cursor != "" && k <= prefix+cursor {
1788 return false
1789 }
1790 if len(items) == max {
1791 next = strings.TrimPrefix(k, prefix)
1792 return true
1793 }
1794 items = append(items, v.(string))
1795 return false
1796 })
1797 return StringPage{Items: items, Next: next}
1798}
1799
1800// Subnames lists direct subnames of parent.
1801func Subnames(parent string, cursor string, limit uint16) StringPage {
1802 pcanon, err := Normalize(parent)
1803 if err != nil {
1804 return StringPage{}
1805 }
1806 prefix := pcanon + sep
1807 start := prefix
1808 if cursor != "" {
1809 start = prefix + cursor
1810 }
1811 max := capLimit(limit)
1812 items := []string{}
1813 next := ""
1814 byParent.Iterate(start, prefixEnd(prefix), func(k string, v any) bool {
1815 if cursor != "" && k <= prefix+cursor {
1816 return false
1817 }
1818 if len(items) == max {
1819 next = strings.TrimPrefix(k, prefix)
1820 return true
1821 }
1822 items = append(items, v.(string))
1823 return false
1824 })
1825 return StringPage{Items: items, Next: next}
1826}
1827
1828// TextKeys lists text-record keys for a name.
1829func TextKeys(name string, cursor string, limit uint16) StringPage {
1830 n := mustName(name)
1831 return treeKeys(n.Records.Text, cursor, limit)
1832}
1833
1834// CoinTypes lists multichain coin types set for a name.
1835func CoinTypes(name string, cursor string, limit uint16) StringPage {
1836 n := mustName(name)
1837 return treeKeys(n.Records.Addresses, cursor, limit)
1838}
1839
1840// Operators lists operators and their permissions for a name.
1841func Operators(name string, cursor string, limit uint16) OperatorPage {
1842 n := mustName(name)
1843 max := capLimit(limit)
1844 items := []OperatorView{}
1845 next := ""
1846 n.Operators.Iterate(cursor, "", func(k string, v any) bool {
1847 if cursor != "" && k <= cursor {
1848 return false
1849 }
1850 if len(items) == max {
1851 next = k
1852 return true
1853 }
1854 items = append(items, OperatorView{Address: k, Permissions: v.(Permissions)})
1855 return false
1856 })
1857 return OperatorPage{Items: items, Next: next}
1858}
1859
1860func treeKeys(tree *avl.Tree, cursor string, limit uint16) StringPage {
1861 max := capLimit(limit)
1862 items := []string{}
1863 next := ""
1864 tree.Iterate(cursor, "", func(k string, v any) bool {
1865 if cursor != "" && k <= cursor {
1866 return false
1867 }
1868 if len(items) == max {
1869 next = k
1870 return true
1871 }
1872 items = append(items, k)
1873 return false
1874 })
1875 return StringPage{Items: items, Next: next}
1876}
1877
1878// ---------------------------------------------------------------------------
1879// 18. Events
1880// ---------------------------------------------------------------------------
1881
1882func eventKey(id uint64) string {
1883 // zero-pad to 20 digits for lexicographic ordering.
1884 s := strconv.FormatUint(id, 10)
1885 return strings.Repeat("0", 20-len(s)) + s
1886}
1887
1888func recordEvent(e *Event) {
1889 nextEventID++
1890 e.ID = nextEventID
1891 e.Height = height()
1892 e.Timestamp = now()
1893 events.Set(eventKey(e.ID), e)
1894 // also surface as a native gno event for tx-level indexers.
1895 chain.Emit(e.Type, "name", e.Name, "actor", e.Actor, "id", strconv.FormatUint(e.ID, 10))
1896}
1897
1898func emit(typ, name string, actor address, owner interface{}, key string) {
1899 ownerStr := ""
1900 switch o := owner.(type) {
1901 case address:
1902 ownerStr = o.String()
1903 case string:
1904 ownerStr = o
1905 }
1906 recordEvent(&Event{Type: typ, Name: name, Actor: actor.String(), Owner: ownerStr, Key: key})
1907}
1908
1909func emitOwner(typ string, n *Name) {
1910 recordEvent(&Event{Type: typ, Name: n.Canonical, Actor: n.Owner.String(), Owner: n.Owner.String(), Revision: n.Revision})
1911}
1912
1913func emitRecord(typ string, n *Name, actor address, key, newDigest string) {
1914 recordEvent(&Event{Type: typ, Name: n.Canonical, Actor: actor.String(), Owner: n.Owner.String(), Revision: n.Revision, Key: key, NewDigest: newDigest})
1915}
1916
1917// EventsAfter returns events with ID strictly greater than id.
1918func EventsAfter(id uint64, limit uint16) EventPage {
1919 max := capLimit(limit)
1920 items := []Event{}
1921 next := ""
1922 start := eventKey(id + 1)
1923 events.Iterate(start, "", func(k string, v any) bool {
1924 if len(items) == max {
1925 next = k
1926 return true
1927 }
1928 items = append(items, *v.(*Event))
1929 return false
1930 })
1931 return EventPage{Items: items, Next: next}
1932}
1933
1934// EventsForName returns events for a specific name with ID greater than after.
1935func EventsForName(name string, after uint64, limit uint16) EventPage {
1936 canonical, err := Normalize(name)
1937 if err != nil {
1938 return EventPage{}
1939 }
1940 max := capLimit(limit)
1941 items := []Event{}
1942 next := ""
1943 start := eventKey(after + 1)
1944 events.Iterate(start, "", func(k string, v any) bool {
1945 e := v.(*Event)
1946 if e.Name != canonical {
1947 return false
1948 }
1949 if len(items) == max {
1950 next = k
1951 return true
1952 }
1953 items = append(items, *e)
1954 return false
1955 })
1956 return EventPage{Items: items, Next: next}
1957}
1958
1959// ---------------------------------------------------------------------------
1960// 19. Administration
1961// ---------------------------------------------------------------------------
1962
1963func requireAdmin(cur realm) address {
1964 c := caller(cur)
1965 if c != config.Admin {
1966 panic(errUnauthorized)
1967 }
1968 return c
1969}
1970
1971func requireNotPaused() {
1972 if config.Paused {
1973 panic(errPaused)
1974 }
1975}
1976
1977// SetPaused toggles the global pause. Paused blocks registration, subname
1978// creation, transfers and record mutation; reads, renewals and primary-name
1979// clearing remain available. It never confiscates or mutates ownership.
1980func SetPaused(cur realm, paused bool) {
1981 requireAdmin(cur)
1982 config.Paused = paused
1983 if paused {
1984 emit(EvPaused, "", config.Admin, "", "")
1985 } else {
1986 emit(EvUnpaused, "", config.Admin, "", "")
1987 }
1988}
1989
1990// SetRegistrationOpen toggles whether new second-level registrations are open.
1991func SetRegistrationOpen(cur realm, open bool) {
1992 requireAdmin(cur)
1993 config.RegistrationOpen = open
1994 emit(EvConfigChanged, "", config.Admin, "", "registration_open")
1995}
1996
1997// SetPricing updates future pricing and bumps the policy revision so pending
1998// commitments that priced against the old rules are rejected at reveal.
1999func SetPricing(cur realm, pricing PricingConfig) {
2000 requireAdmin(cur)
2001 if pricing.BasePricePerSecond < 0 {
2002 panic(errBadRequest)
2003 }
2004 config.BasePricePerSecond = pricing.BasePricePerSecond
2005 if pricing.PremiumByLength != nil {
2006 config.PremiumByLength = pricing.PremiumByLength
2007 }
2008 if pricing.PaymentDenom != "" {
2009 config.PaymentDenom = pricing.PaymentDenom
2010 }
2011 config.PolicyRevision++
2012 emit(EvConfigChanged, "", config.Admin, "", "pricing")
2013}
2014
2015// SetTreasury updates the treasury address.
2016func SetTreasury(cur realm, treasury address) {
2017 requireAdmin(cur)
2018 if !treasury.IsValid() {
2019 panic(errEmptyAddress)
2020 }
2021 config.Treasury = treasury
2022 emit(EvConfigChanged, "", config.Admin, "", "treasury")
2023}
2024
2025// ReserveName reserves (or unreserves) an unregistered name so it cannot be
2026// publicly registered. Admin may not reserve an actively-owned name.
2027func ReserveName(cur realm, name string, reserved bool) {
2028 requireAdmin(cur)
2029 canonical := mustNormalize(name)
2030 n := getRaw(canonical)
2031 if n != nil && isActive(n) && n.Owner != (address("")) {
2032 panic(errNameUnavailable) // cannot confiscate an active name
2033 }
2034 if n == nil {
2035 label, parent := splitLabel(canonical)
2036 n = &Name{
2037 Canonical: canonical,
2038 Label: label,
2039 Parent: parent,
2040 Depth: depthOf(canonical),
2041 Records: newRecords(),
2042 Operators: avl.NewTree(),
2043 CreatedAt: now(),
2044 }
2045 }
2046 // If unreserving a bare placeholder (never registered), physically remove
2047 // it so the name becomes Available again rather than lingering as an
2048 // ExpiresAt==0 node (which statusOf would read as a permanent Active name).
2049 if !reserved && !n.Owner.IsValid() {
2050 names.Remove(canonical)
2051 emit(EvConfigChanged, canonical, config.Admin, "", "reserve")
2052 return
2053 }
2054 n.Reserved = reserved
2055 names.Set(canonical, n)
2056 emit(EvConfigChanged, canonical, config.Admin, "", "reserve")
2057}
2058
2059// TransferAdmin begins a two-step admin handover.
2060func TransferAdmin(cur realm, next address) {
2061 requireAdmin(cur)
2062 if !next.IsValid() {
2063 panic(errEmptyAddress)
2064 }
2065 config.PendingAdmin = next
2066 emit(EvConfigChanged, "", config.Admin, next, "transfer_admin")
2067}
2068
2069// AcceptAdmin completes the two-step admin handover.
2070func AcceptAdmin(cur realm) {
2071 c := caller(cur)
2072 if config.PendingAdmin == (address("")) || c != config.PendingAdmin {
2073 panic(errUnauthorized)
2074 }
2075 config.Admin = c
2076 config.PendingAdmin = address("")
2077 emit(EvConfigChanged, "", c, "", "accept_admin")
2078}
2079
2080// SetLimits updates operational storage/abuse limits (future registrations and
2081// mutations). Existing names are unaffected until next mutation.
2082func SetLimits(cur realm, maxRecords, maxOperators uint16, maxText, maxBinary uint32) {
2083 requireAdmin(cur)
2084 config.MaxRecordsPerName = maxRecords
2085 config.MaxOperatorsPerName = maxOperators
2086 config.MaxTextValueBytes = maxText
2087 config.MaxBinaryValueBytes = maxBinary
2088 emit(EvConfigChanged, "", config.Admin, "", "limits")
2089}
2090
2091// ---------------------------------------------------------------------------
2092// 20. Read API (status/lookup)
2093// ---------------------------------------------------------------------------
2094
2095// Status returns the lifecycle status of a name.
2096func Status(name string) NameStatus {
2097 canonical, err := Normalize(name)
2098 if err != nil {
2099 return StatusAvailable
2100 }
2101 return statusOf(getRaw(canonical))
2102}
2103
2104// Exists reports whether a name currently resolves to a live registration.
2105func Exists(name string) bool {
2106 canonical, err := Normalize(name)
2107 if err != nil {
2108 return false
2109 }
2110 n := getRaw(canonical)
2111 if n == nil {
2112 return false
2113 }
2114 switch statusOf(n) {
2115 case StatusActive, StatusGrace:
2116 return true
2117 }
2118 return false
2119}
2120
2121// OwnerOf returns the owner of an active/grace name.
2122func OwnerOf(name string) (string, bool) {
2123 canonical, err := Normalize(name)
2124 if err != nil {
2125 return "", false
2126 }
2127 n := getRaw(canonical)
2128 if n == nil {
2129 return "", false
2130 }
2131 switch statusOf(n) {
2132 case StatusActive, StatusGrace:
2133 return n.Owner.String(), true
2134 }
2135 return "", false
2136}
2137
2138// GetName returns a read-only view of a name.
2139func GetName(name string) (NameView, bool) {
2140 canonical, err := Normalize(name)
2141 if err != nil {
2142 return NameView{}, false
2143 }
2144 n := getRaw(canonical)
2145 if n == nil {
2146 return NameView{}, false
2147 }
2148 return NameView{
2149 Canonical: n.Canonical,
2150 Owner: n.Owner.String(),
2151 Status: string(statusOf(n)),
2152 CreatedAt: n.CreatedAt,
2153 UpdatedAt: n.UpdatedAt,
2154 ExpiresAt: n.ExpiresAt,
2155 GraceEndsAt: n.GraceEndsAt,
2156 Parent: n.Parent,
2157 Label: n.Label,
2158 Depth: n.Depth,
2159 TTL: n.TTL,
2160 Generation: n.Generation,
2161 Revision: n.Revision,
2162 Reserved: n.Reserved,
2163 NativeAddr: n.Records.NativeAddress,
2164 }, true
2165}
2166
2167// CommitmentStatus returns the state of a pending commitment.
2168func CommitmentStatus(commit string) CommitmentView {
2169 cm, ok := getCommitment(commit)
2170 if !ok {
2171 return CommitmentView{}
2172 }
2173 return CommitmentView{
2174 Exists: true,
2175 Committer: cm.Committer.String(),
2176 CreatedAt: cm.CreatedAt,
2177 ReadyAt: cm.CreatedAt + config.MinCommitAge,
2178 ExpiresAt: cm.CreatedAt + config.MaxCommitAge,
2179 }
2180}
2181
2182// ---------------------------------------------------------------------------
2183// 21. Rendering
2184// ---------------------------------------------------------------------------
2185
2186// Render is a human-readable explorer. It never mutates state.
2187func Render(path string) string {
2188 path = strings.TrimPrefix(path, "/")
2189 switch {
2190 case path == "":
2191 return renderHome()
2192 case strings.HasPrefix(path, "name/"):
2193 return renderName(strings.TrimPrefix(path, "name/"))
2194 case strings.HasPrefix(path, "address/"):
2195 return renderAddress(strings.TrimPrefix(path, "address/"))
2196 case strings.HasPrefix(path, "available/"):
2197 return renderAvailable(strings.TrimPrefix(path, "available/"))
2198 case path == "events":
2199 return renderEvents()
2200 case path == "help":
2201 return renderHelp()
2202 default:
2203 return "# GNS\n\nUnknown route. See [/help](/r/moul/gns/v1:help).\n"
2204 }
2205}
2206
2207func renderHome() string {
2208 var b strings.Builder
2209 b.WriteString("# GNS — Gno Name Service\n\n")
2210 b.WriteString("A single-realm, ENS-equivalent naming system for gno.land.\n\n")
2211 b.WriteString(ufmt.Sprintf("- Registered names: **%d**\n", names.Size()))
2212 b.WriteString(ufmt.Sprintf("- Events: **%d**\n", int(nextEventID)))
2213 b.WriteString(ufmt.Sprintf("- Registration open: **%t**\n", config.RegistrationOpen))
2214 b.WriteString(ufmt.Sprintf("- Paused: **%t**\n\n", config.Paused))
2215 b.WriteString("## Routes\n\n")
2216 b.WriteString("- `/name/<name>` — details for a name\n")
2217 b.WriteString("- `/address/<g1...>` — primary name + owned names\n")
2218 b.WriteString("- `/available/<name>` — availability and price\n")
2219 b.WriteString("- `/events` — recent events\n")
2220 b.WriteString("- `/help` — public API summary\n")
2221 return b.String()
2222}
2223
2224func renderName(name string) string {
2225 canonical, err := Normalize(name)
2226 if err != nil {
2227 return "# " + name + "\n\nInvalid name: " + err.Error() + "\n"
2228 }
2229 n := getRaw(canonical)
2230 if n == nil {
2231 return "# " + canonical + "\n\n_Available._ See [/available/" + canonical + "](/r/moul/gns/v1:available/" + canonical + ").\n"
2232 }
2233 var b strings.Builder
2234 b.WriteString("# " + canonical + "\n\n")
2235 b.WriteString("Owner: `" + n.Owner.String() + "`\n\n")
2236 b.WriteString("Status: " + string(statusOf(n)) + "\n\n")
2237 if n.ExpiresAt > 0 {
2238 b.WriteString(ufmt.Sprintf("Expires: %s\n\n", time.Unix(n.ExpiresAt, 0).UTC().Format("2006-01-02")))
2239 } else {
2240 b.WriteString("Expires: never (permanent subname)\n\n")
2241 }
2242 b.WriteString(ufmt.Sprintf("Generation: %d · Revision: %d\n\n", int(n.Generation), int(n.Revision)))
2243 if n.Records.NativeAddress != "" {
2244 b.WriteString("Primary address: `" + n.Records.NativeAddress + "`\n\n")
2245 }
2246 // text records
2247 b.WriteString("## Records\n\n")
2248 hasText := false
2249 n.Records.Text.Iterate("", "", func(k string, v any) bool {
2250 b.WriteString("- " + k + ": " + v.(string) + "\n")
2251 hasText = true
2252 return false
2253 })
2254 if !hasText {
2255 b.WriteString("_No text records._\n")
2256 }
2257 // subnames
2258 b.WriteString("\n## Subnames\n\n")
2259 subs := Subnames(canonical, "", 50)
2260 if len(subs.Items) == 0 {
2261 b.WriteString("_None._\n")
2262 } else {
2263 for _, s := range subs.Items {
2264 b.WriteString("- " + s + "\n")
2265 }
2266 }
2267 return b.String()
2268}
2269
2270func renderAddress(addr string) string {
2271 var b strings.Builder
2272 b.WriteString("# " + addr + "\n\n")
2273 if pn, ok := PrimaryName(addr); ok {
2274 b.WriteString("Primary name: **" + pn + "**\n\n")
2275 } else {
2276 b.WriteString("_No verified primary name._\n\n")
2277 }
2278 b.WriteString("## Owned names\n\n")
2279 page := NamesByOwner(addr, "", 50)
2280 if len(page.Items) == 0 {
2281 b.WriteString("_None._\n")
2282 } else {
2283 for _, s := range page.Items {
2284 b.WriteString("- " + s + "\n")
2285 }
2286 }
2287 return b.String()
2288}
2289
2290func renderAvailable(name string) string {
2291 canonical, err := Normalize(name)
2292 if err != nil {
2293 return "# " + name + "\n\nInvalid: " + err.Error() + "\n"
2294 }
2295 var b strings.Builder
2296 b.WriteString("# " + canonical + "\n\n")
2297 if available(canonical) {
2298 b.WriteString("**Available.**\n\n")
2299 q, _ := Price(canonical, config.MinRegistrationDuration)
2300 b.WriteString(ufmt.Sprintf("Price for %d seconds: %d %s\n", int(config.MinRegistrationDuration), int(q.Amount), q.Denom))
2301 } else {
2302 b.WriteString("**Not available** (status: " + string(Status(canonical)) + ").\n")
2303 }
2304 return b.String()
2305}
2306
2307func renderEvents() string {
2308 var b strings.Builder
2309 b.WriteString("# Recent events\n\n")
2310 from := uint64(0)
2311 if nextEventID > 20 {
2312 from = nextEventID - 20
2313 }
2314 page := EventsAfter(from, 20)
2315 if len(page.Items) == 0 {
2316 b.WriteString("_No events yet._\n")
2317 return b.String()
2318 }
2319 b.WriteString("| ID | Type | Name | Actor |\n| ---: | --- | --- | --- |\n")
2320 for _, e := range page.Items {
2321 b.WriteString(ufmt.Sprintf("| %d | %s | %s | `%s` |\n", int(e.ID), e.Type, e.Name, e.Actor))
2322 }
2323 return b.String()
2324}
2325
2326func renderHelp() string {
2327 return "# GNS API\n\n" +
2328 "Read: `Normalize`, `Status`, `Exists`, `OwnerOf`, `GetName`, `Resolve`, " +
2329 "`Address`, `CoinAddress`, `Text`, `ContentHash`, `PublicKey`, `ABI`, " +
2330 "`Interface`, `Record`, `PrimaryName`, `Price`, `CommitmentStatus`, " +
2331 "`NamesByOwner`, `Subnames`, `TextKeys`, `CoinTypes`, `Operators`, " +
2332 "`EventsAfter`, `EventsForName`.\n\n" +
2333 "Mutations (crossing, panic on failure): `Commit`, `Register`, `Renew`, " +
2334 "`Transfer`, `CreateSubname`, `DeleteSubname`, `SetRegistrationPolicy`, " +
2335 "`LockPolicy`, `SetOperator`, `RemoveOperator`, `SetPrimaryName`, " +
2336 "`ClearPrimaryName`, and the typed record setters.\n\n" +
2337 "Admin: `SetPaused`, `SetRegistrationOpen`, `SetPricing`, `SetTreasury`, " +
2338 "`ReserveName`, `TransferAdmin`, `AcceptAdmin`, `SetLimits`.\n"
2339}
2340
2341// ---------------------------------------------------------------------------
2342// 22. Internal storage helpers
2343// ---------------------------------------------------------------------------
2344
2345func coins(denom string, amount int64) chain.Coins {
2346 return chain.NewCoins(chain.NewCoin(denom, amount))
2347}
2348
2349// treeGet adapts the avl v0 API (Get returns a single value; existence is via
2350// Has) to the (value, ok) idiom used throughout this file.
2351func treeGet(t *avl.Tree, key string) (any, bool) {
2352 if !t.Has(key) {
2353 return nil, false
2354 }
2355 return t.Get(key), true
2356}
2357
2358func getRaw(canonical string) *Name {
2359 v, ok := treeGet(names, canonical)
2360 if !ok {
2361 return nil
2362 }
2363 return v.(*Name)
2364}
2365
2366// resolvableName returns the name only if it is currently Active (records
2367// resolve only for active names).
2368func resolvableName(name string) *Name {
2369 canonical, err := Normalize(name)
2370 if err != nil {
2371 return nil
2372 }
2373 n := getRaw(canonical)
2374 if n == nil || !isActive(n) {
2375 return nil
2376 }
2377 return n
2378}
2379
2380// mustName returns an existing name or panics with not_found.
2381func mustName(name string) *Name {
2382 canonical := mustNormalize(name)
2383 n := getRaw(canonical)
2384 if n == nil {
2385 panic(errNotFound)
2386 }
2387 return n
2388}
2389
2390func putName(n *Name) {
2391 names.Set(n.Canonical, n)
2392 byOwner.Set(ownerKey(n.Owner, n.Canonical), n.Canonical)
2393 if n.Parent != "" {
2394 byParent.Set(n.Parent+sep+n.Canonical, n.Canonical)
2395 }
2396}
2397
2398func deleteName(n *Name) {
2399 n.Deleted = true
2400 byOwner.Remove(ownerKey(n.Owner, n.Canonical))
2401 if n.Parent != "" {
2402 byParent.Remove(n.Parent + sep + n.Canonical)
2403 }
2404 names.Remove(n.Canonical)
2405}
2406
2407func ownerKey(owner address, canonical string) string {
2408 return owner.String() + sep + canonical
2409}
2410
2411func getCommitment(key string) (*commitment, bool) {
2412 v, ok := treeGet(commitments, key)
2413 if !ok {
2414 return nil, false
2415 }
2416 return v.(*commitment), true
2417}
2418
2419func getOperator(n *Name, addr address) (Permissions, bool) {
2420 if n.Operators == nil {
2421 return Permissions{}, false
2422 }
2423 v, ok := treeGet(n.Operators, addr.String())
2424 if !ok {
2425 return Permissions{}, false
2426 }
2427 return v.(Permissions), true
2428}
2429
2430func allowlistHas(tree *avl.Tree, addr address) bool {
2431 v, ok := treeGet(tree, addr.String())
2432 return ok && v.(bool)
2433}
2434
2435func collectPaymentDenom(cur realm, price int64, denom string, treasury address) {
2436 if price <= 0 {
2437 return
2438 }
2439 if !cur.Previous().IsUserCall() {
2440 panic(errNotUser)
2441 }
2442 sent := unsafe.OriginSend()
2443 got := sent.AmountOf(denom)
2444 if got < price {
2445 panic(errInsufficientPay)
2446 }
2447 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
2448 self := cur.Address()
2449 if treasury != self {
2450 bk.SendCoins(self, treasury, coins(denom, price))
2451 }
2452 if over := got - price; over > 0 {
2453 bk.SendCoins(self, cur.Previous().Address(), coins(denom, over))
2454 }
2455}
2456
2457func prefixEnd(prefix string) string {
2458 if prefix == "" {
2459 return ""
2460 }
2461 b := []byte(prefix)
2462 for i := len(b) - 1; i >= 0; i-- {
2463 if b[i] < 0xff {
2464 b[i]++
2465 return string(b[:i+1])
2466 }
2467 }
2468 return "" // prefix is all 0xff; iterate to end
2469}