Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}