Source Code

;; =============================================================================
;; BiUD - Bitcoin Username Domain (.sBTC) v5
;; =============================================================================
;; TLD: .sBTC
;; Decentralized Username Registrar on Stacks (Bitcoin L2)
;; Clarity 4 Implementation
;; 
;; v5 Changes:
;; - Added transferable admin with pending admin pattern (2-step transfer)
;; - Added admin-import-name function for migration from v4
;; - Migration mode that can be disabled after migration complete
;; - Emergency pause functionality
;; =============================================================================

;; =============================================================================
;; TRAITS
;; =============================================================================

;; Resolver trait that external contracts can implement for custom name resolution
(define-trait resolver-trait
  (
    ;; Resolve a label to arbitrary data (e.g., wallet address, IPFS hash, metadata)
    (resolve ((string-utf8 32) principal) (response (optional (buff 64)) uint))
  )
)

;; =============================================================================
;; CONSTANTS
;; =============================================================================

;; TLD suffix for all registered names
(define-constant TLD ".sBTC")

;; Registration period in blocks (~1 year at ~10 min/block = 52560 blocks)
(define-constant REGISTRATION_PERIOD u52560)

;; Grace period after expiry where only original owner can renew (7 days = ~1008 blocks)
(define-constant GRACE_PERIOD u1008)

;; Premium name threshold (names with <= 4 characters are premium)
(define-constant PREMIUM_LENGTH_THRESHOLD u4)

;; Contract deployer (initial admin - can be transferred)
(define-constant CONTRACT_DEPLOYER tx-sender)

;; Error codes
(define-constant ERR_NAME_TAKEN (err u1001))
(define-constant ERR_NAME_EXPIRED (err u1002))
(define-constant ERR_NOT_OWNER (err u1003))
(define-constant ERR_NOT_ADMIN (err u1004))
(define-constant ERR_INVALID_LABEL (err u1005))
(define-constant ERR_PAYMENT_FAILED (err u1006))
(define-constant ERR_IN_GRACE_PERIOD (err u1007))
(define-constant ERR_RESOLVER_INVALID (err u1008))
(define-constant ERR_NAME_NOT_FOUND (err u1009))
(define-constant ERR_LABEL_TOO_LONG (err u1010))
(define-constant ERR_LABEL_EMPTY (err u1011))
(define-constant ERR_INVALID_CHARACTER (err u1012))
(define-constant ERR_TRANSFER_TO_SELF (err u1013))
(define-constant ERR_ZERO_FEE (err u1014))
(define-constant ERR_LIST_FULL (err u1015))
(define-constant ERR_PERCENT_TOO_HIGH (err u1016))
(define-constant ERR_NOT_NAME_OWNER (err u1017))
(define-constant ERR_CONTRACT_PAUSED (err u1018))
(define-constant ERR_MIGRATION_DISABLED (err u1019))
(define-constant ERR_NO_PENDING_ADMIN (err u1020))
(define-constant ERR_NOT_PENDING_ADMIN (err u1021))

;; =============================================================================
;; DATA VARIABLES
;; =============================================================================

;; Current admin (can be transferred - this is the key security improvement in v5)
(define-data-var contract-admin principal CONTRACT_DEPLOYER)

;; Pending admin for 2-step transfer (new admin must accept)
(define-data-var pending-admin (optional principal) none)

;; Migration mode - allows bulk import without fees
(define-data-var migration-enabled bool true)

;; Contract pause state - for emergencies
(define-data-var contract-paused bool false)

;; Auto-incrementing name ID counter
(define-data-var name-id-counter uint u0)

;; Base registration fee in microSTX (default: 10 STX = 10,000,000 microSTX)
(define-data-var base-fee uint u10000000)

;; Renewal fee in microSTX (default: 5 STX = 5,000,000 microSTX)
(define-data-var renew-fee uint u5000000)

;; Premium multiplier (e.g., 5 = premium names cost 5x base fee)
(define-data-var premium-multiplier uint u5)

;; Fee recipient (defaults to contract deployer)
(define-data-var fee-recipient principal CONTRACT_DEPLOYER)

;; Protocol fee percentage (out of 100) - remaining goes to fee-recipient
(define-data-var protocol-fee-percent uint u10)

;; Protocol treasury address
(define-data-var protocol-treasury principal CONTRACT_DEPLOYER)

;; Total fees collected for analytics
(define-data-var total-fees-collected uint u0)

;; Temporary variable for filter operation
(define-data-var temp-name-id uint u0)

;; =============================================================================
;; DATA MAPS
;; =============================================================================

;; Main name registry: label -> name record
(define-map name-registry
  { label: (string-utf8 32) }
  {
    name-id: uint,
    full-name: (string-utf8 64),
    owner: principal,
    resolver: (optional principal),
    expiry-height: uint,
    is-premium: bool,
    created-at: uint,
    last-renewed: uint
  }
)

;; Reverse lookup: name-id -> label
(define-map name-id-to-label
  { name-id: uint }
  { label: (string-utf8 32) }
)

;; Owner lookup: principal -> list of owned name IDs (up to 100)
(define-map owner-names
  { owner: principal }
  { name-ids: (list 100 uint) }
)

;; Admin-designated premium labels (override automatic premium detection)
(define-map premium-labels
  { label: (string-utf8 32) }
  { is-premium: bool }
)

;; Primary name map - maps an address to their chosen primary/display name
(define-map primary-names
  { owner: principal }
  { label: (string-utf8 32) }
)

;; =============================================================================
;; ADMIN HELPER FUNCTIONS
;; =============================================================================

;; Check if caller is the current admin
(define-private (is-current-admin)
  (is-eq tx-sender (var-get contract-admin))
)

;; Assert caller is admin
(define-private (assert-admin)
  (begin
    (asserts! (is-current-admin) ERR_NOT_ADMIN)
    (ok true)
  )
)

;; Assert contract is not paused
(define-private (assert-not-paused)
  (begin
    (asserts! (not (var-get contract-paused)) ERR_CONTRACT_PAUSED)
    (ok true)
  )
)

;; =============================================================================
;; PRIVATE HELPER FUNCTIONS
;; =============================================================================

;; Validate label: lowercase ASCII, 1-32 chars
(define-private (validate-label (label (string-utf8 32)))
  (let
    (
      (label-len (len label))
    )
    ;; Check length bounds
    (asserts! (> label-len u0) ERR_LABEL_EMPTY)
    (asserts! (<= label-len u32) ERR_LABEL_TOO_LONG)
    (ok true)
  )
)

;; Generate the next name ID
(define-private (get-next-name-id)
  (let
    (
      (current-id (var-get name-id-counter))
      (next-id (+ current-id u1))
    )
    (var-set name-id-counter next-id)
    next-id
  )
)

;; Check if a name is premium (either by length or admin designation)
(define-private (check-is-premium (label (string-utf8 32)))
  (let
    (
      (admin-premium (map-get? premium-labels { label: label }))
    )
    ;; If admin has explicitly set premium status, use that
    (match admin-premium
      premium-entry (get is-premium premium-entry)
      ;; Otherwise, check if length <= threshold
      (<= (len label) PREMIUM_LENGTH_THRESHOLD)
    )
  )
)

;; Calculate registration fee based on premium status
(define-private (calculate-registration-fee (is-premium bool))
  (if is-premium
    (* (var-get base-fee) (var-get premium-multiplier))
    (var-get base-fee)
  )
)

;; Calculate renewal fee (same for all names currently)
(define-private (calculate-renewal-fee)
  (var-get renew-fee)
)

;; Split and distribute fees between protocol treasury and fee recipient
(define-private (distribute-fees (total-fee uint))
  (let
    (
      (protocol-percent (var-get protocol-fee-percent))
      (protocol-share (/ (* total-fee protocol-percent) u100))
      (recipient-share (- total-fee protocol-share))
      (treasury (var-get protocol-treasury))
      (recipient (var-get fee-recipient))
    )
    ;; Transfer protocol share to treasury (if any)
    (and (> protocol-share u0)
         (unwrap! (stx-transfer? protocol-share tx-sender treasury) (err u1006)))
    ;; Transfer remainder to fee recipient (if any)
    (and (> recipient-share u0)
         (unwrap! (stx-transfer? recipient-share tx-sender recipient) (err u1006)))
    ;; Update total fees collected
    (var-set total-fees-collected (+ (var-get total-fees-collected) total-fee))
    (ok true)
  )
)

;; Check if name is expired (past expiry + grace period)
(define-private (is-name-expired (expiry-height uint))
  (> block-height (+ expiry-height GRACE_PERIOD))
)

;; Check if name is in grace period (expired but within grace period)
(define-private (is-in-grace-period (expiry-height uint))
  (and
    (> block-height expiry-height)
    (<= block-height (+ expiry-height GRACE_PERIOD))
  )
)

;; Helper for filtering out a specific name ID
(define-private (not-matching-id (id uint))
  (not (is-eq id (var-get temp-name-id)))
)

;; Add name ID to owner's list (best effort - silently fails if list is full)
(define-private (add-name-to-owner (owner principal) (name-id uint))
  (let
    (
      (current-names (default-to { name-ids: (list) } (map-get? owner-names { owner: owner })))
      (current-list (get name-ids current-names))
    )
    (match (as-max-len? (append current-list name-id) u100)
      new-list (begin
        (map-set owner-names { owner: owner } { name-ids: new-list })
        true)
      false
    )
  )
)

;; Check if name exists and handle takeover if expired - always returns response type
(define-private (check-and-handle-existing-name
  (existing (optional { name-id: uint, full-name: (string-utf8 64), owner: principal, resolver: (optional principal), expiry-height: uint, is-premium: bool, created-at: uint, last-renewed: uint }))
  (label (string-utf8 32)))
  (match existing
    name-record (handle-expired-name-takeover name-record label)
    (ok true)
  )
)

;; Handle takeover of an expired name - returns response type for consistent typing
(define-private (handle-expired-name-takeover 
  (name-record { name-id: uint, full-name: (string-utf8 64), owner: principal, resolver: (optional principal), expiry-height: uint, is-premium: bool, created-at: uint, last-renewed: uint })
  (label (string-utf8 32)))
  (begin
    (asserts! (is-name-expired (get expiry-height name-record)) ERR_NAME_TAKEN)
    (remove-name-from-owner (get owner name-record) (get name-id name-record))
    (if (is-eq (some label) (match (map-get? primary-names { owner: (get owner name-record) })
                              entry (some (get label entry))
                              none))
      (map-delete primary-names { owner: (get owner name-record) })
      true
    )
    (ok true)
  )
)

;; Remove name ID from owner's list
(define-private (remove-name-from-owner (owner principal) (name-id uint))
  (let
    (
      (current-names (default-to { name-ids: (list) } (map-get? owner-names { owner: owner })))
      (current-list (get name-ids current-names))
    )
    (var-set temp-name-id name-id)
    (map-set owner-names
      { owner: owner }
      { name-ids: (filter not-matching-id current-list) }
    )
    true
  )
)

;; Check if owner has a primary name set
(define-private (has-primary-name (owner principal))
  (is-some (map-get? primary-names { owner: owner }))
)

;; =============================================================================
;; ADMIN TRANSFER FUNCTIONS (NEW IN V5)
;; =============================================================================

;; Step 1: Current admin initiates transfer to new admin
(define-public (transfer-admin (new-admin principal))
  (begin
    (try! (assert-admin))
    ;; Cannot transfer to self
    (asserts! (not (is-eq new-admin (var-get contract-admin))) ERR_TRANSFER_TO_SELF)
    
    ;; Set pending admin
    (var-set pending-admin (some new-admin))
    
    (print {
      event: "AdminTransferInitiated",
      current-admin: (var-get contract-admin),
      pending-admin: new-admin,
      block-height: block-height
    })
    
    (ok true)
  )
)

;; Step 2: New admin accepts the transfer
(define-public (accept-admin)
  (let
    (
      (pending (var-get pending-admin))
    )
    ;; Must have a pending admin
    (asserts! (is-some pending) ERR_NO_PENDING_ADMIN)
    ;; Caller must be the pending admin
    (asserts! (is-eq tx-sender (unwrap-panic pending)) ERR_NOT_PENDING_ADMIN)
    
    ;; Transfer admin role
    (var-set contract-admin tx-sender)
    (var-set pending-admin none)
    
    (print {
      event: "AdminTransferCompleted",
      new-admin: tx-sender,
      block-height: block-height
    })
    
    (ok true)
  )
)

;; Cancel pending admin transfer
(define-public (cancel-admin-transfer)
  (begin
    (try! (assert-admin))
    (var-set pending-admin none)
    
    (print {
      event: "AdminTransferCancelled",
      block-height: block-height
    })
    
    (ok true)
  )
)

;; =============================================================================
;; MIGRATION FUNCTIONS (NEW IN V5)
;; =============================================================================

;; Import a name from the old contract (admin only, migration mode only)
;; This allows restoring user names without charging fees
(define-public (admin-import-name 
  (label (string-utf8 32))
  (owner principal)
  (expiry-height uint)
  (is-premium-flag uint)
  (created-at uint)
)
  (let
    (
      (full-name (unwrap! (as-max-len? (concat label u".sBTC") u64) ERR_INVALID_LABEL))
      (new-name-id (get-next-name-id))
      (is-premium (> is-premium-flag u0))
    )
    ;; Only admin can import
    (try! (assert-admin))
    ;; Migration must be enabled
    (asserts! (var-get migration-enabled) ERR_MIGRATION_DISABLED)
    ;; Name must not already exist
    (asserts! (is-none (map-get? name-registry { label: label })) ERR_NAME_TAKEN)
    
    ;; Create the name record
    (map-set name-registry
      { label: label }
      {
        name-id: new-name-id,
        full-name: full-name,
        owner: owner,
        resolver: none,
        expiry-height: expiry-height,
        is-premium: is-premium,
        created-at: created-at,
        last-renewed: block-height
      }
    )
    
    ;; Set reverse lookup
    (map-set name-id-to-label
      { name-id: new-name-id }
      { label: label }
    )
    
    ;; Add to owner's name list
    (add-name-to-owner owner new-name-id)
    
    ;; Emit migration event
    (print {
      event: "NameImported",
      label: label,
      full-name: full-name,
      owner: owner,
      name-id: new-name-id,
      expiry-height: expiry-height,
      is-premium: is-premium,
      block-height: block-height
    })
    
    (ok new-name-id)
  )
)

;; Set primary name during migration (admin only)
(define-public (admin-set-primary-name (owner principal) (label (string-utf8 32)))
  (begin
    (try! (assert-admin))
    (asserts! (var-get migration-enabled) ERR_MIGRATION_DISABLED)
    
    ;; Verify the name exists and belongs to owner
    (let
      (
        (name-record (unwrap! (map-get? name-registry { label: label }) ERR_NAME_NOT_FOUND))
      )
      (asserts! (is-eq (get owner name-record) owner) ERR_NOT_NAME_OWNER)
      
      (map-set primary-names
        { owner: owner }
        { label: label }
      )
      
      (print {
        event: "PrimaryNameImported",
        owner: owner,
        label: label,
        block-height: block-height
      })
      
      (ok true)
    )
  )
)

;; Disable migration mode (permanent - cannot be re-enabled)
(define-public (disable-migration)
  (begin
    (try! (assert-admin))
    (var-set migration-enabled false)
    
    (print {
      event: "MigrationDisabled",
      admin: tx-sender,
      block-height: block-height
    })
    
    (ok true)
  )
)

;; =============================================================================
;; EMERGENCY FUNCTIONS (NEW IN V5)
;; =============================================================================

;; Pause the contract (admin only)
(define-public (pause-contract)
  (begin
    (try! (assert-admin))
    (var-set contract-paused true)
    
    (print {
      event: "ContractPaused",
      admin: tx-sender,
      block-height: block-height
    })
    
    (ok true)
  )
)

;; Unpause the contract (admin only)
(define-public (unpause-contract)
  (begin
    (try! (assert-admin))
    (var-set contract-paused false)
    
    (print {
      event: "ContractUnpaused",
      admin: tx-sender,
      block-height: block-height
    })
    
    (ok true)
  )
)

;; =============================================================================
;; PUBLIC FUNCTIONS - PRIMARY NAME
;; =============================================================================

;; Set your primary/display name
(define-public (set-primary-name (label (string-utf8 32)))
  (let
    (
      (name-record (unwrap! (map-get? name-registry { label: label }) ERR_NAME_NOT_FOUND))
      (owner (get owner name-record))
      (expiry (get expiry-height name-record))
    )
    (try! (assert-not-paused))
    ;; Only the owner of the name can set it as primary
    (asserts! (is-eq tx-sender owner) ERR_NOT_NAME_OWNER)
    
    ;; Name must not be expired
    (asserts! (<= block-height expiry) ERR_NAME_EXPIRED)
    
    ;; Set primary name
    (map-set primary-names
      { owner: tx-sender }
      { label: label }
    )
    
    (print {
      event: "PrimaryNameSet",
      owner: tx-sender,
      label: label,
      full-name: (get full-name name-record),
      block-height: block-height
    })
    
    (ok true)
  )
)

;; Clear your primary name
(define-public (clear-primary-name)
  (begin
    (try! (assert-not-paused))
    (map-delete primary-names { owner: tx-sender })
    
    (print {
      event: "PrimaryNameCleared",
      owner: tx-sender,
      block-height: block-height
    })
    
    (ok true)
  )
)

;; =============================================================================
;; PUBLIC FUNCTIONS - CORE REGISTRATION
;; =============================================================================

;; Register a new username with .sBTC TLD
(define-public (register-name (label (string-utf8 32)))
  (let
    (
      (validation-result (try! (validate-label label)))
      (full-name (unwrap! (as-max-len? (concat label u".sBTC") u64) ERR_INVALID_LABEL))
      (is-premium (check-is-premium label))
      (reg-fee (calculate-registration-fee is-premium))
      (existing (map-get? name-registry { label: label }))
      (new-name-id (get-next-name-id))
      (expiry (+ block-height REGISTRATION_PERIOD))
      (user-has-primary (has-primary-name tx-sender))
    )
    (try! (assert-not-paused))
    
    ;; Check if name is available - if exists, must be expired and we take it over
    (try! (check-and-handle-existing-name existing label))
    
    ;; Collect registration fee
    (asserts! (> reg-fee u0) ERR_ZERO_FEE)
    (try! (distribute-fees reg-fee))
    
    ;; Create the name record
    (map-set name-registry
      { label: label }
      {
        name-id: new-name-id,
        full-name: full-name,
        owner: tx-sender,
        resolver: none,
        expiry-height: expiry,
        is-premium: is-premium,
        created-at: block-height,
        last-renewed: block-height
      }
    )
    
    ;; Set reverse lookup
    (map-set name-id-to-label
      { name-id: new-name-id }
      { label: label }
    )
    
    ;; Add to owner's name list
    (add-name-to-owner tx-sender new-name-id)

    ;; Auto-set primary name if user doesn't have one
    (if (not user-has-primary)
      (map-set primary-names
        { owner: tx-sender }
        { label: label }
      )
      true
    )

    (print {
      event: "NameRegistered",
      label: label,
      full-name: full-name,
      owner: tx-sender,
      name-id: new-name-id,
      expiry-height: expiry,
      fee-paid: reg-fee,
      is-premium: is-premium,
      is-primary: (not user-has-primary),
      block-height: block-height
    })
    
    (ok {
      name-id: new-name-id,
      full-name: full-name,
      expiry-height: expiry,
      fee-paid: reg-fee,
      is-primary: (not user-has-primary)
    })
  )
)

;; =============================================================================
;; PUBLIC FUNCTIONS - RENEWAL
;; =============================================================================

;; Renew an existing name registration
(define-public (renew-name (label (string-utf8 32)))
  (let
    (
      (name-record (unwrap! (map-get? name-registry { label: label }) ERR_NAME_NOT_FOUND))
      (current-expiry (get expiry-height name-record))
      (owner (get owner name-record))
      (renewal-fee (calculate-renewal-fee))
    )
    (try! (assert-not-paused))
    ;; Check if name is completely expired (past grace period)
    (asserts! (not (is-name-expired current-expiry)) ERR_NAME_EXPIRED)
    
    ;; During grace period, only original owner can renew
    (asserts! (or (not (is-in-grace-period current-expiry))
                  (is-eq tx-sender owner)) 
              ERR_IN_GRACE_PERIOD)
    
    ;; Collect renewal fee
    (asserts! (> renewal-fee u0) ERR_ZERO_FEE)
    (try! (distribute-fees renewal-fee))
    
    (let
      (
        (new-expiry (+ current-expiry REGISTRATION_PERIOD))
      )
      (map-set name-registry
        { label: label }
        (merge name-record {
          expiry-height: new-expiry,
          last-renewed: block-height
        })
      )
      
      (print {
        event: "NameRenewed",
        label: label,
        owner: owner,
        new-expiry-height: new-expiry,
        fee-paid: renewal-fee,
        block-height: block-height
      })
      
      (ok {
        new-expiry-height: new-expiry,
        fee-paid: renewal-fee
      })
    )
  )
)

;; =============================================================================
;; PUBLIC FUNCTIONS - TRANSFER
;; =============================================================================

;; Transfer name ownership to another principal
(define-public (transfer-name (label (string-utf8 32)) (new-owner principal))
  (let
    (
      (name-record (unwrap! (map-get? name-registry { label: label }) ERR_NAME_NOT_FOUND))
      (current-owner (get owner name-record))
      (name-id (get name-id name-record))
      (expiry (get expiry-height name-record))
    )
    (try! (assert-not-paused))
    ;; Only current owner can transfer
    (asserts! (is-eq tx-sender current-owner) ERR_NOT_OWNER)
    ;; Cannot transfer to self
    (asserts! (not (is-eq current-owner new-owner)) ERR_TRANSFER_TO_SELF)
    ;; Name must not be expired
    (asserts! (<= block-height expiry) ERR_NAME_EXPIRED)
    
    ;; Update ownership in registry
    (map-set name-registry
      { label: label }
      (merge name-record { owner: new-owner })
    )
    
    ;; Update owner lists
    (remove-name-from-owner current-owner name-id)
    (add-name-to-owner new-owner name-id)

    ;; Clear sender's primary name if this was it
    (match (map-get? primary-names { owner: current-owner })
      primary-entry (if (is-eq label (get label primary-entry))
                      (map-delete primary-names { owner: current-owner })
                      true)
      true
    )
    
    ;; Auto-set as primary for receiver if they don't have one
    (if (not (has-primary-name new-owner))
      (map-set primary-names
        { owner: new-owner }
        { label: label }
      )
      true
    )

    (print {
      event: "NameTransferred",
      label: label,
      from: current-owner,
      to: new-owner,
      block-height: block-height
    })
    
    (ok true)
  )
)

;; =============================================================================
;; PUBLIC FUNCTIONS - RESOLVER
;; =============================================================================

;; Set a resolver contract for a name
(define-public (set-resolver (label (string-utf8 32)) (resolver principal))
  (let
    (
      (name-record (unwrap! (map-get? name-registry { label: label }) ERR_NAME_NOT_FOUND))
      (owner (get owner name-record))
      (expiry (get expiry-height name-record))
    )
    (try! (assert-not-paused))
    ;; Only owner can set resolver
    (asserts! (is-eq tx-sender owner) ERR_NOT_OWNER)
    ;; Name must not be expired
    (asserts! (<= block-height expiry) ERR_NAME_EXPIRED)
    
    (map-set name-registry
      { label: label }
      (merge name-record { resolver: (some resolver) })
    )
    
    (print {
      event: "ResolverSet",
      label: label,
      owner: owner,
      resolver: resolver,
      block-height: block-height
    })
    
    (ok true)
  )
)

;; Clear the resolver for a name
(define-public (clear-resolver (label (string-utf8 32)))
  (let
    (
      (name-record (unwrap! (map-get? name-registry { label: label }) ERR_NAME_NOT_FOUND))
      (owner (get owner name-record))
    )
    (try! (assert-not-paused))
    ;; Only owner can clear resolver
    (asserts! (is-eq tx-sender owner) ERR_NOT_OWNER)
    
    (map-set name-registry
      { label: label }
      (merge name-record { resolver: none })
    )
    
    (ok true)
  )
)

;; =============================================================================
;; PUBLIC FUNCTIONS - ADMIN
;; =============================================================================

;; Set a label as premium (admin only)
(define-public (set-premium-label (label (string-utf8 32)) (is-premium bool))
  (begin
    (try! (assert-admin))
    
    (map-set premium-labels
      { label: label }
      { is-premium: is-premium }
    )
    
    (print {
      event: "PremiumLabelSet",
      label: label,
      is-premium: is-premium
    })
    
    (ok true)
  )
)

;; Update base registration fee (admin only)
(define-public (set-base-fee (new-fee uint))
  (begin
    (try! (assert-admin))
    (asserts! (> new-fee u0) ERR_ZERO_FEE)
    (var-set base-fee new-fee)
    (print {
      event: "FeeConfigUpdated",
      base-fee: new-fee,
      renew-fee: (var-get renew-fee),
      premium-multiplier: (var-get premium-multiplier),
      fee-recipient: (var-get fee-recipient),
      block-height: block-height
    })
    (ok true)
  )
)

;; Update renewal fee (admin only)
(define-public (set-renew-fee (new-fee uint))
  (begin
    (try! (assert-admin))
    (asserts! (> new-fee u0) ERR_ZERO_FEE)
    (var-set renew-fee new-fee)
    (print {
      event: "FeeConfigUpdated",
      base-fee: (var-get base-fee),
      renew-fee: new-fee,
      premium-multiplier: (var-get premium-multiplier),
      fee-recipient: (var-get fee-recipient),
      block-height: block-height
    })
    (ok true)
  )
)

;; Update premium multiplier (admin only)
(define-public (set-premium-multiplier (new-multiplier uint))
  (begin
    (try! (assert-admin))
    (asserts! (> new-multiplier u0) ERR_ZERO_FEE)
    (var-set premium-multiplier new-multiplier)
    (print {
      event: "FeeConfigUpdated",
      base-fee: (var-get base-fee),
      renew-fee: (var-get renew-fee),
      premium-multiplier: new-multiplier,
      fee-recipient: (var-get fee-recipient),
      block-height: block-height
    })
    (ok true)
  )
)

;; Update fee recipient (admin only)
(define-public (set-fee-recipient (new-recipient principal))
  (begin
    (try! (assert-admin))
    (var-set fee-recipient new-recipient)
    (print {
      event: "FeeConfigUpdated",
      base-fee: (var-get base-fee),
      renew-fee: (var-get renew-fee),
      premium-multiplier: (var-get premium-multiplier),
      fee-recipient: new-recipient,
      block-height: block-height
    })
    (ok true)
  )
)

;; Update protocol treasury (admin only)
(define-public (set-protocol-treasury (new-treasury principal))
  (begin
    (try! (assert-admin))
    (var-set protocol-treasury new-treasury)
    (print {
      event: "TreasuryUpdated",
      treasury: new-treasury,
      protocol-fee-percent: (var-get protocol-fee-percent),
      block-height: block-height
    })
    (ok true)
  )
)

;; Update protocol fee percentage (admin only)
(define-public (set-protocol-fee-percent (new-percent uint))
  (begin
    (try! (assert-admin))
    (asserts! (<= new-percent u100) ERR_PERCENT_TOO_HIGH)
    (var-set protocol-fee-percent new-percent)
    (print {
      event: "TreasuryUpdated",
      treasury: (var-get protocol-treasury),
      protocol-fee-percent: new-percent,
      block-height: block-height
    })
    (ok true)
  )
)

;; =============================================================================
;; READ-ONLY FUNCTIONS
;; =============================================================================

;; Get full name record for a label
(define-read-only (get-name (label (string-utf8 32)))
  (match (map-get? name-registry { label: label })
    name-record (some {
      label: label,
      full-name: (get full-name name-record),
      owner: (get owner name-record),
      expiry-height: (get expiry-height name-record),
      resolver: (get resolver name-record),
      is-premium: (get is-premium name-record),
      name-id: (get name-id name-record),
      created-at: (get created-at name-record),
      last-renewed: (get last-renewed name-record)
    })
    none
  )
)

;; Check if a name is available for registration
(define-read-only (is-available (label (string-utf8 32)))
  (match (map-get? name-registry { label: label })
    name-record (is-name-expired (get expiry-height name-record))
    true
  )
)

;; Get owner of a name
(define-read-only (get-owner (label (string-utf8 32)))
  (match (map-get? name-registry { label: label })
    name-record (some (get owner name-record))
    none
  )
)

;; Get expiry height of a name
(define-read-only (get-expiry (label (string-utf8 32)))
  (match (map-get? name-registry { label: label })
    name-record (some (get expiry-height name-record))
    none
  )
)

;; Check if a name is premium
(define-read-only (is-premium-name (label (string-utf8 32)))
  (check-is-premium label)
)

;; Get the resolver for a name
(define-read-only (get-resolver (label (string-utf8 32)))
  (match (map-get? name-registry { label: label })
    name-record (get resolver name-record)
    none
  )
)

;; Get current fee configuration
(define-read-only (get-fee-config)
  {
    base-fee: (var-get base-fee),
    renew-fee: (var-get renew-fee),
    premium-multiplier: (var-get premium-multiplier),
    fee-recipient: (var-get fee-recipient),
    protocol-treasury: (var-get protocol-treasury),
    protocol-fee-percent: (var-get protocol-fee-percent)
  }
)

;; Calculate registration fee for a specific label
(define-read-only (get-registration-fee (label (string-utf8 32)))
  (calculate-registration-fee (check-is-premium label))
)

;; Get all names owned by a principal
(define-read-only (get-names-by-owner (owner principal))
  (default-to { name-ids: (list) } (map-get? owner-names { owner: owner }))
)

;; Get label by name ID (reverse lookup)
(define-read-only (get-label-by-id (name-id uint))
  (match (map-get? name-id-to-label { name-id: name-id })
    entry (some (get label entry))
    none
  )
)

;; Check if a name is in grace period
(define-read-only (is-name-in-grace-period (label (string-utf8 32)))
  (match (map-get? name-registry { label: label })
    name-record (is-in-grace-period (get expiry-height name-record))
    false
  )
)

;; Check if a name is completely expired (past grace period)
(define-read-only (is-name-fully-expired (label (string-utf8 32)))
  (match (map-get? name-registry { label: label })
    name-record (is-name-expired (get expiry-height name-record))
    true
  )
)

;; Get total number of registered names
(define-read-only (get-total-names)
  (var-get name-id-counter)
)

;; Get total fees collected
(define-read-only (get-total-fees-collected)
  (var-get total-fees-collected)
)

;; Get registration period constant
(define-read-only (get-registration-period)
  REGISTRATION_PERIOD
)

;; Get grace period constant
(define-read-only (get-grace-period)
  GRACE_PERIOD
)

;; Get current contract admin
(define-read-only (get-admin)
  (var-get contract-admin)
)

;; Get pending admin (if any)
(define-read-only (get-pending-admin)
  (var-get pending-admin)
)

;; Check if caller is admin
(define-read-only (is-admin (caller principal))
  (is-eq caller (var-get contract-admin))
)

;; Check if contract is paused
(define-read-only (is-paused)
  (var-get contract-paused)
)

;; Check if migration is enabled
(define-read-only (is-migration-enabled)
  (var-get migration-enabled)
)

;; =============================================================================
;; PRIMARY NAME READ-ONLY FUNCTIONS
;; =============================================================================

;; Get the primary/display name for any address
(define-read-only (get-primary-name (address principal))
  (match (map-get? primary-names { owner: address })
    entry (let
      (
        (label (get label entry))
        (name-record (map-get? name-registry { label: label }))
      )
      (match name-record
        record (if (and 
                    (is-eq (get owner record) address)
                    (<= block-height (get expiry-height record)))
                  (some {
                    label: label,
                    full-name: (get full-name record),
                    expiry-height: (get expiry-height record)
                  })
                  none)
        none
      )
    )
    none
  )
)

;; Get just the primary label
(define-read-only (get-primary-label (address principal))
  (match (get-primary-name address)
    result (some (get label result))
    none
  )
)

;; Get primary full name with TLD
(define-read-only (get-primary-full-name (address principal))
  (match (get-primary-name address)
    result (some (get full-name result))
    none
  )
)

;; Reverse resolution: Given an address, return the display name
(define-read-only (resolve-address (address principal))
  (match (get-primary-full-name address)
    full-name full-name
    u""
  )
)

;; =============================================================================
;; RESOLUTION FUNCTION
;; =============================================================================

;; Resolve a name using its configured resolver contract
(define-public (resolve-name (label (string-utf8 32)) (resolver-contract <resolver-trait>))
  (let
    (
      (name-record (unwrap! (map-get? name-registry { label: label }) ERR_NAME_NOT_FOUND))
      (stored-resolver (get resolver name-record))
      (owner (get owner name-record))
    )
    ;; Verify the resolver contract matches what's stored
    (asserts! (is-some stored-resolver) ERR_RESOLVER_INVALID)
    (asserts! (is-eq (some (contract-of resolver-contract)) stored-resolver) ERR_RESOLVER_INVALID)
    
    ;; Call the resolver contract
    (contract-call? resolver-contract resolve label owner)
  )
)

Functions (63)

FunctionAccessArgs
is-current-adminprivate
assert-adminprivate
assert-not-pausedprivate
validate-labelprivatelabel: (string-utf8 32
get-next-name-idprivate
check-is-premiumprivatelabel: (string-utf8 32
calculate-registration-feeprivateis-premium: bool
calculate-renewal-feeprivate
distribute-feesprivatetotal-fee: uint
is-name-expiredprivateexpiry-height: uint
is-in-grace-periodprivateexpiry-height: uint
not-matching-idprivateid: uint
add-name-to-ownerprivateowner: principal, name-id: uint
remove-name-from-ownerprivateowner: principal, name-id: uint
has-primary-nameprivateowner: principal
transfer-adminpublicnew-admin: principal
accept-adminpublic
cancel-admin-transferpublic
admin-import-namepubliclabel: (string-utf8 32
admin-set-primary-namepublicowner: principal, label: (string-utf8 32
disable-migrationpublic
pause-contractpublic
unpause-contractpublic
set-primary-namepubliclabel: (string-utf8 32
clear-primary-namepublic
register-namepubliclabel: (string-utf8 32
renew-namepubliclabel: (string-utf8 32
transfer-namepubliclabel: (string-utf8 32
set-resolverpubliclabel: (string-utf8 32
clear-resolverpubliclabel: (string-utf8 32
set-premium-labelpubliclabel: (string-utf8 32
set-base-feepublicnew-fee: uint
set-renew-feepublicnew-fee: uint
set-premium-multiplierpublicnew-multiplier: uint
set-fee-recipientpublicnew-recipient: principal
set-protocol-treasurypublicnew-treasury: principal
set-protocol-fee-percentpublicnew-percent: uint
get-nameread-onlylabel: (string-utf8 32
is-availableread-onlylabel: (string-utf8 32
get-ownerread-onlylabel: (string-utf8 32
get-expiryread-onlylabel: (string-utf8 32
is-premium-nameread-onlylabel: (string-utf8 32
get-resolverread-onlylabel: (string-utf8 32
get-fee-configread-only
get-registration-feeread-onlylabel: (string-utf8 32
get-names-by-ownerread-onlyowner: principal
get-label-by-idread-onlyname-id: uint
is-name-in-grace-periodread-onlylabel: (string-utf8 32
is-name-fully-expiredread-onlylabel: (string-utf8 32
get-total-namesread-only
get-total-fees-collectedread-only
get-registration-periodread-only
get-grace-periodread-only
get-adminread-only
get-pending-adminread-only
is-adminread-onlycaller: principal
is-pausedread-only
is-migration-enabledread-only
get-primary-nameread-onlyaddress: principal
get-primary-labelread-onlyaddress: principal
get-primary-full-nameread-onlyaddress: principal
resolve-addressread-onlyaddress: principal
resolve-namepubliclabel: (string-utf8 32