;; The name registry contract acts as the central hub for storing
;; name information. Each 'name' record has two parts - the 'name' (domain)
;; and the 'namespace' (TLD). Each record has a unique ID, which is an integer.
;;
;; Registering a new name is not publicly exposed through this contract. Instead,
;; registrations must be initiated via a different contract. The originating contract
;; must have the "registry" role in the [`.bnsx-extensions`](`../bnsx-extension.md`) contract.
;;
;; The name registry includes functionality for "managed namespaces". A managed namespace
;; is controlled by an external set of contracts - such as a separate DAO. The set
;; namespace/manager relationships is stored in this contract. If a namespace has a 'manager',
;; that manager is allowed to call privileged functions for names in their namespace.
;;
;; This contract keeps track of an account's "primary name", as well as their other names, in a
;; linked list data structure. This allows for querying the entire set of an account's names.
;; If an account has at least one name, they will always have a 'primary' name. If their primary
;; name is transfered, the next name in the linked list is automatically set as the primary.
;;
;; This contract also exposes an NFT asset, which represents ownership of a given name. The contract
;; exposes a SIP9-compatible interface for interacting with the NFT.
(impl-trait .nft-trait.nft-trait)
;; Variables
(define-constant ROLE "registry")
(define-constant ERR_UNAUTHORIZED (err u4000))
(define-constant ERR_ALREADY_REGISTERED (err u4001))
(define-constant ERR_CANNOT_SET_PRIMARY (err u4002))
(define-constant ERR_INVALID_ID (err u4003))
(define-constant ERR_EXPIRED (err u4004))
(define-constant ERR_TRANSFER_BLOCKED (err u4005))
(define-constant ERR_NOT_OWNER (err u4))
(define-data-var last-id-var uint u0)
(define-data-var token-uri-var (string-ascii 256) "")
(define-map namespace-token-uri-map (buff 20) (string-ascii 256))
(define-map namespace-managers-map { manager: principal, namespace: (buff 20) } bool)
(define-map dao-namespace-manager-map (buff 20) bool)
(define-map namespace-transfers-allowed (buff 20) bool)
;; Data
(define-non-fungible-token BNSx-Names uint)
;; linked list for account->names
(define-map owner-primary-name-map principal uint)
(define-map owner-last-name-map principal uint)
(define-map owner-name-next-map uint uint)
(define-map owner-name-prev-map uint uint)
(define-map name-owner-map uint principal)
(define-map name-id-map { name: (buff 48), namespace: (buff 20) } uint)
(define-map id-name-map uint { name: (buff 48), namespace: (buff 20) })
(define-map name-encoding-map uint (buff 1))
(define-map owner-balance-map principal uint)
;; Authorization
;; Validate an action that can only be executed by a BNS X extension.
(define-read-only (is-dao-or-extension)
(ok (asserts! (or (is-eq tx-sender .bnsx-extensions) (contract-call? .bnsx-extensions has-role-or-extension contract-caller ROLE)) ERR_UNAUTHORIZED))
;; (ok (asserts! (contract-call? .bnsx-extensions has-role-or-extension contract-caller ROLE) ERR_UNAUTHORIZED))
)
;; Mutators
;; Register a name in the registry
;;
;; If the owner does not have a primary name, this name will be set as their primary
;;
;; @throws if not called by an authorized contract
;;
;; @throws if the name is already registered
(define-public (register
(name { name: (buff 48), namespace: (buff 20) })
(owner principal)
)
(let
(
(id (increment-id))
)
(try! (validate-namespace-action (get namespace name)))
(asserts! (map-insert name-id-map name id) ERR_ALREADY_REGISTERED)
(asserts! (map-insert id-name-map id name) ERR_ALREADY_REGISTERED)
(asserts! (map-insert name-owner-map id owner) ERR_ALREADY_REGISTERED)
(print {
topic: "new-name",
owner: owner,
name: name,
id: id,
})
(unwrap-panic (nft-mint? BNSx-Names id owner))
(add-node owner id)
(ok id)
)
)
;; Set a name as a user's primary name. Only the owner of the name
;; can set it as their primary.
;;
;; @param id; the ID of the name
(define-public (set-primary-name (id uint))
(let
(
(owner (unwrap! (map-get? name-owner-map id) ERR_INVALID_ID))
)
(asserts! (is-eq owner tx-sender) ERR_UNAUTHORIZED)
(try! (set-first tx-sender id))
(print {
topic: "set-primary",
id: id,
owner: owner,
})
(ok true)
)
)
;; Burn a name. This burns the name NFT and removes all ownership data.
;;
;; If the name being burnt is the account's primary, and the account
;; owns another name, a different name is automatically set as the account's
;; new primary.
;;
;; @throws if not called by the owner
(define-public (burn (id uint))
(match (map-get? name-owner-map id)
owner (begin
(asserts! (is-eq tx-sender owner) ERR_NOT_OWNER)
(burn-name id)
)
ERR_NOT_OWNER
)
)
;; Private method to handle burning a name. See [`burn`](#burn) and
;; [`mng-burn`](#mng-burn)
(define-private (burn-name (id uint))
(let
(
(name (unwrap! (map-get? id-name-map id) ERR_INVALID_ID))
(owner (unwrap-panic (map-get? name-owner-map id)))
)
(remove-node owner id)
(try! (nft-burn? BNSx-Names id owner))
(map-delete name-id-map name)
(map-delete id-name-map id)
(map-delete name-owner-map id)
(print {
topic: "burn",
id: id,
})
(ok true)
)
)
;; Private method to handle transfering ownership of a name. This updates internal
;; data tracking name ownership, and transfers the NFT to the recipient.
(define-private (transfer-ownership (id uint) (sender principal) (recipient principal))
;; #[allow(unchecked_data)]
(begin
(map-set name-owner-map id recipient)
(unwrap-panic (nft-transfer? BNSx-Names id sender recipient))
(print {
topic: "transfer-ownership",
id: id,
recipient: recipient,
})
(remove-node sender id)
(add-node recipient id)
)
)
;; Getters
;; Get properties of a given name. Returns `optional` with the following properties:
;;
;; - id
;; - name
;; - namespace
;; - owner
(define-read-only (get-name-properties (name { name: (buff 48), namespace: (buff 20) }))
(match (map-get? name-id-map name)
id (merge-name-props name id)
none
)
)
;; Get name properties of a name, with lookup via ID. See [`get-name-properties`](#get-name-properties)
(define-read-only (get-name-properties-by-id (id uint))
(match (map-get? id-name-map id)
name (merge-name-props name id)
none
)
)
;; #[allow(unchecked_data)]
(define-private (merge-name-props (name { name: (buff 48), namespace: (buff 20) }) (id uint))
(some (merge name {
id: id,
owner: (unwrap-panic (map-get? name-owner-map id))
}))
)
;; Return the primary name for a given account. Returns an optional
;; tuple with `name` and `namespace`
(define-read-only (get-primary-name (account principal))
(match (map-get? owner-primary-name-map account)
id (map-get? id-name-map id)
none
)
)
;; Return properties of an account's primary name. See [`get-name-properties`](#get-name-properties)
(define-read-only (get-primary-name-properties (account principal))
(match (map-get? owner-primary-name-map account)
id (get-name-properties-by-id id)
none
)
)
;; Reverse lookup the ID of a name
(define-read-only (get-id-for-name (name { name: (buff 48), namespace: (buff 20) }))
(map-get? name-id-map name)
)
;; Returns the namespace for a given name. Returns a `response`
;; with the namespace, or `ERR_INVALID_ID` otherwise.
(define-read-only (get-namespace-for-id (id uint))
(ok (get namespace (unwrap! (map-get? id-name-map id) ERR_INVALID_ID)))
)
;; Returns the owner of a name
(define-read-only (get-name-owner (id uint))
(map-get? name-owner-map id)
)
;; NFT
;; Set the Token URI for NFT metadata.
;;
;; @throws if called by an unauthorized contract
;; #[allow(unchecked_data)]
(define-public (mng-set-token-uri (uri (string-ascii 256)))
(begin
(try! (is-dao-or-extension))
(ok (var-set token-uri-var uri))
)
)
;; Set a namespace-specific metadata URI
;;
;; @param namespace; the namespace to be associated with this URI
;; @param uri; a URI that returns valid metadata according to SIP16
(define-public (mng-set-namespace-token-uri (namespace (buff 20)) (uri (string-ascii 256)))
(begin
;; #[filter(namespace, uri)]
(try! (validate-namespace-action namespace))
(map-set namespace-token-uri-map namespace uri)
(ok true)
)
)
;; Trait methods
(define-read-only (get-last-token-id)
(let ((last (var-get last-id-var)))
(ok (if (is-eq last u0) u0 (- last u1)))
)
)
;; SIP9 - get a SIP16-valid token URI.
;;
;; If the namespace for the name associated with `id` has
;; a namespace-specific token-uri, that URI is returned.
;;
;; Otherwise, the contract's base token URI is returned.
(define-read-only (get-token-uri (id uint))
(let
(
(base-uri (var-get token-uri-var))
)
(match (get-namespace-for-id id)
namespace (ok (some (default-to base-uri (get-token-uri-for-namespace namespace))))
e (ok (some base-uri))
)
)
)
;; Fetch a namespace-specific token URI.
(define-read-only (get-token-uri-for-namespace (namespace (buff 20)))
(map-get? namespace-token-uri-map namespace)
)
;; SIP9 - fetch owner of an NFT
(define-read-only (get-owner (id uint))
(ok (nft-get-owner? BNSx-Names id))
)
;; Returns the total number of names owned by an account
(define-read-only (get-balance (account principal))
(default-to u0 (map-get? owner-balance-map account))
)
(define-read-only (get-balance-of (account principal)) (ok (get-balance account)))
;; Transfer a name
;;
;; @throws if transfers are not allowed for a given namespace.
;;
;; @throws if not called by the name owner
(define-public (transfer (id uint) (sender principal) (recipient principal))
(let
(
(owner (unwrap! (map-get? name-owner-map id) ERR_NOT_OWNER))
)
(asserts! (is-eq tx-sender owner) ERR_NOT_OWNER)
(asserts! (is-eq owner sender) ERR_NOT_OWNER)
(asserts! (are-transfers-allowed (try! (get-namespace-for-id id))) ERR_TRANSFER_BLOCKED)
;; #[filter(recipient)]
(transfer-ownership id sender recipient)
(ok true)
)
)
;; DAO / controller methods
;; Returns `bool` specifying whether BNS X contracts can manage a given namespace
(define-read-only (can-dao-manage-ns (namespace (buff 20)))
(default-to true (map-get? dao-namespace-manager-map namespace))
)
;; Removes the ability for BNS X contracts to manage a specific namespace. Once BNS X
;; is "ejected" from a namespace, only managers of that namespace can perform
;; name-related actions (like registration) for that namespace.
(define-public (remove-dao-namespace-manager (namespace (buff 20)))
(begin
;; #[filter(namespace)]
(try! (is-dao-or-extension))
(map-set dao-namespace-manager-map namespace false)
(ok true)
)
)
;; Authorization check for namespace action.
;;
;; If `contract-caller` is a manager: OK
;; Otherwise:
;; - Ensure that DAO is allowed to manage namespace
;; - Check that caller is an authorized extension
(define-read-only (validate-namespace-action (namespace (buff 20)))
(if (is-namespace-manager namespace contract-caller) (ok true)
(if (can-dao-manage-ns namespace)
(is-dao-or-extension)
ERR_UNAUTHORIZED
)
)
)
;; #[filter(id)]
(define-read-only (validate-namespace-action-by-id (id uint))
(validate-namespace-action (try! (get-namespace-for-id id)))
)
;; Returns `bool` of whether a principal is a valid manager for a given namespace.
(define-read-only (is-namespace-manager (namespace (buff 20)) (manager principal))
(default-to false (map-get? namespace-managers-map { namespace: namespace, manager: manager }))
)
;; Privileged method for transfering a name. This allows external (authorized)
;; contracts to allow transfers based on flexible conditions.
(define-public (mng-transfer (id uint) (recipient principal))
(begin
;; #[filter(id, recipient)]
(try! (validate-namespace-action-by-id id))
(transfer-ownership id (unwrap-panic (map-get? name-owner-map id)) recipient)
(ok true)
)
)
;; Privileged method for burning a name. This allows external contracts to
;; allow transfers based on flexible conditions.
(define-public (mng-burn (id uint))
(begin
(try! (validate-namespace-action-by-id id))
(burn-name id)
)
)
;; Add a manager for a specific namespace. Only BNS X contracts can set the first
;; manager. After that, existing managers can add other managers. See
;; [`validate-namespace-action`](#validate-namespace-action) for authorization rules.
(define-public (set-namespace-manager (namespace (buff 20)) (manager principal) (enabled bool))
(begin
;; #[filter(namespace, manager)]
(try! (validate-namespace-action namespace))
(map-set namespace-managers-map { namespace: namespace, manager: manager } enabled)
(ok true)
)
)
;; Enable or disable transfers of names for a specific namespace. See
;; [`validate-namespace-action`](#validate-namespace-action) for authorization rules.
(define-public (set-namespace-transfers-allowed (namespace (buff 20)) (enabled bool))
(begin
;; #[filter(namespace)]
(try! (validate-namespace-action namespace))
(map-set namespace-transfers-allowed namespace enabled)
(ok true)
)
)
;; Returns a `bool` indicating whether transfers are allowed for a given namespace.
(define-read-only (are-transfers-allowed (namespace (buff 20)))
(default-to true (map-get? namespace-transfers-allowed namespace))
)
;; Helpers
(define-private (increment-id)
(let
(
(last (var-get last-id-var))
)
(var-set last-id-var (+ last u1))
last
)
)
;; Linked list for keeping track of account
;; primary names
;; Helper method to traverse the linked list structure for an account's names.
(define-read-only (get-next-node-id (id uint))
(map-get? owner-name-next-map id)
)
;; Internal method for adding a node to an account's linked list of names.
;; The name is always added to the 'end' of the list. If this is the account's
;; first name, that means it will also be the primary name.
;;
;; #[allow(unchecked_data)]
(define-private (add-node (account principal) (id uint))
(let
(
(last-opt (map-get? owner-last-name-map account))
)
(map-set owner-balance-map account (+ (get-balance account) u1))
(print-primary-update account (some id))
;; Set "first" if it doesnt exist
(map-insert owner-primary-name-map account id)
(map-set owner-last-name-map account id)
(match last-opt
last (begin
(map-set owner-name-next-map last id)
(map-set owner-name-prev-map id last)
)
true
)
true
)
)
;; Internal method to indicate that an account's primary name has been updated.
;; This only prints out information, which can be indexed off-chain.
(define-private (print-primary-update (account principal) (id (optional uint)))
(begin
(print {
topic: "primary-update",
id: id,
account: account,
prev: (map-get? owner-primary-name-map account)
})
true
)
)
;; Remove a name from an account's list of names.
(define-private (remove-node (account principal) (id uint))
(let
(
(prev-opt (map-get? owner-name-prev-map id))
(next-opt (map-get? owner-name-next-map id))
(first (unwrap-panic (map-get? owner-primary-name-map account)))
(last (unwrap-panic (map-get? owner-last-name-map account)))
(balance (unwrap-panic (map-get? owner-balance-map account)))
)
(print {topic: "remove", account: account})
;; #[filter(account)]
(map-set owner-balance-map account (- balance u1))
;; We're removing the first
(and (is-eq first id)
(if (is-some next-opt)
(and
(print-primary-update account next-opt)
(map-set owner-primary-name-map account (unwrap-panic next-opt))
)
(and
(print-primary-update account none)
(map-delete owner-primary-name-map account)
)
)
)
;; removing the last
(and (is-eq last id)
(if (is-some prev-opt)
(map-set owner-last-name-map account (unwrap-panic prev-opt))
;; Removing the _only_ node:
(map-delete owner-last-name-map account)
)
)
(match next-opt
next (if (is-some prev-opt)
(map-set owner-name-prev-map next (unwrap-panic prev-opt))
(map-delete owner-name-prev-map next)
)
true
)
(match prev-opt
prev (if (is-some next-opt)
(map-set owner-name-next-map prev (unwrap-panic next-opt))
(map-delete owner-name-next-map prev)
)
true
)
(map-delete owner-name-next-map id)
(map-delete owner-name-prev-map id)
true
)
)
;; Set a name as an account's primary. This internal method is updates the internal data
;; structure for an account's names.
(define-private (set-first (account principal) (node uint))
(let
(
(first (unwrap! (map-get? owner-primary-name-map account) ERR_CANNOT_SET_PRIMARY))
)
;; make sure this isn't the existing first
(asserts! (not (is-eq first node)) ERR_CANNOT_SET_PRIMARY)
(remove-node account node)
(print-primary-update account (some node))
(map-set owner-primary-name-map account node)
(map-set owner-name-prev-map first node)
(map-set owner-name-next-map node first)
(ok true)
)
)