Source Code

;; Passkey Registry Contract
;; Manages passkey registration and verification using Clarity 4 features
;; Features: principal-from-slice, principal-destruct?, list-concat

;; Constants
(define-constant CONTRACT-OWNER tx-sender)
(define-constant ERR-NOT-AUTHORIZED (err u100))
(define-constant ERR-PASSKEY-EXISTS (err u101))
(define-constant ERR-PASSKEY-NOT-FOUND (err u102))
(define-constant ERR-INVALID-PRINCIPAL (err u103))
(define-constant ERR-MAX-PASSKEYS-REACHED (err u104))
(define-constant MAX-PASSKEYS-PER-ACCOUNT u10)

;; Data structures
(define-map passkey-registry
  {
    owner: principal,
    passkey-id: (buff 65),
  }
  {
    credential-id: (buff 65),
    created-at: uint,
    last-used: uint,
    device-type: (string-ascii 50),
    is-active: bool,
  }
)

(define-map account-passkeys
  { owner: principal }
  { passkey-list: (list 10 (buff 65)) }
)

(define-map passkey-metadata
  { passkey-id: (buff 65) }
  {
    derived-principal: (optional principal),
    registration-height: uint,
  }
)

;; Read-only functions

;; Get passkey details
(define-read-only (get-passkey-info
    (owner principal)
    (passkey-id (buff 65))
  )
  (map-get? passkey-registry {
    owner: owner,
    passkey-id: passkey-id,
  })
)

;; Get all passkeys for an account
(define-read-only (get-account-passkeys (owner principal))
  (default-to { passkey-list: (list) }
    (map-get? account-passkeys { owner: owner })
  )
)

;; Check if passkey is registered
(define-read-only (is-passkey-registered
    (owner principal)
    (passkey-id (buff 65))
  )
  (is-some (map-get? passkey-registry {
    owner: owner,
    passkey-id: passkey-id,
  }))
)

;; Verify passkey-derived principal
;; NOTE: In full Clarity 4, this would use principal-destruct?
;; For now, we validate the principal format is correct
(define-read-only (verify-passkey-principal (derived-principal principal))
  ;; Future: (match (principal-destruct? derived-principal) ...)
  ;; For now, just verify it's a valid principal (non-standard address check)
  (ok (is-standard derived-principal))
)

;; Get passkey metadata including derived principal
(define-read-only (get-passkey-metadata (passkey-id (buff 65)))
  (map-get? passkey-metadata { passkey-id: passkey-id })
)

;; Public functions

;; Register a new passkey for an account
;; NOTE: Uses Clarity 4's list-concat feature
;; Future versions will use principal-from-slice when fully supported in tooling
(define-public (register-passkey
    (passkey-id (buff 65))
    (device-type (string-ascii 50))
  )
  (let (
      (owner tx-sender)
      (current-passkeys (get passkey-list (get-account-passkeys owner)))
      (passkey-count (len current-passkeys))
    )
    ;; Check if passkey already exists
    (asserts! (not (is-passkey-registered owner passkey-id)) ERR-PASSKEY-EXISTS)

    ;; Check max passkeys limit
    (asserts! (< passkey-count MAX-PASSKEYS-PER-ACCOUNT) ERR-MAX-PASSKEYS-REACHED)

    ;; Store passkey registry entry
    (map-set passkey-registry {
      owner: owner,
      passkey-id: passkey-id,
    } {
      credential-id: passkey-id,
      created-at: stacks-block-height,
      last-used: stacks-block-height,
      device-type: device-type,
      is-active: true,
    })

    ;; Store metadata
    ;; In production with full Clarity 4 support, derived-principal would use:
    ;; (principal-from-slice passkey-id)
    (map-set passkey-metadata { passkey-id: passkey-id } {
      derived-principal: none, ;; Future: use principal-from-slice
      registration-height: stacks-block-height,
    })

    ;; Add to account's passkey list using concat (Clarity 4 feature - list-concat)
    (map-set account-passkeys { owner: owner } { passkey-list: (unwrap-panic (as-max-len? (concat current-passkeys (list passkey-id)) u10)) })

    (ok true)
  )
)

;; Update last-used timestamp for a passkey
(define-public (update-passkey-usage (passkey-id (buff 65)))
  (let (
      (owner tx-sender)
      (passkey-data (unwrap!
        (map-get? passkey-registry {
          owner: owner,
          passkey-id: passkey-id,
        })
        ERR-PASSKEY-NOT-FOUND
      ))
    )
    (map-set passkey-registry {
      owner: owner,
      passkey-id: passkey-id,
    }
      (merge passkey-data { last-used: stacks-block-height })
    )

    (ok true)
  )
)

;; Deactivate a passkey (doesn't delete, just marks inactive)
(define-public (deactivate-passkey (passkey-id (buff 65)))
  (let (
      (owner tx-sender)
      (passkey-data (unwrap!
        (map-get? passkey-registry {
          owner: owner,
          passkey-id: passkey-id,
        })
        ERR-PASSKEY-NOT-FOUND
      ))
    )
    (map-set passkey-registry {
      owner: owner,
      passkey-id: passkey-id,
    }
      (merge passkey-data { is-active: false })
    )

    (ok true)
  )
)

;; Reactivate a passkey
(define-public (reactivate-passkey (passkey-id (buff 65)))
  (let (
      (owner tx-sender)
      (passkey-data (unwrap!
        (map-get? passkey-registry {
          owner: owner,
          passkey-id: passkey-id,
        })
        ERR-PASSKEY-NOT-FOUND
      ))
    )
    (map-set passkey-registry {
      owner: owner,
      passkey-id: passkey-id,
    }
      (merge passkey-data { is-active: true })
    )

    (ok true)
  )
)

;; Merge multiple passkey lists (demonstrates list-concat - Clarity 4)
;; Useful for account recovery or migration
(define-public (merge-passkey-accounts (source-owner principal))
  (let (
      (dest-owner tx-sender)
      (source-passkeys (get passkey-list (get-account-passkeys source-owner)))
      (dest-passkeys (get passkey-list (get-account-passkeys dest-owner)))
      ;; Combine lists using concat
      (merged-list (concat dest-passkeys source-passkeys))
    )
    ;; Ensure merged list doesn't exceed maximum
    (asserts! (<= (len merged-list) MAX-PASSKEYS-PER-ACCOUNT)
      ERR-MAX-PASSKEYS-REACHED
    )

    (map-set account-passkeys { owner: dest-owner } { passkey-list: (unwrap-panic (as-max-len? merged-list u10)) })

    (ok true)
  )
)

Functions (10)

FunctionAccessArgs
get-passkey-inforead-onlyowner: principal, passkey-id: (buff 65
get-account-passkeysread-onlyowner: principal
is-passkey-registeredread-onlyowner: principal, passkey-id: (buff 65
verify-passkey-principalread-onlyderived-principal: principal
get-passkey-metadataread-onlypasskey-id: (buff 65
register-passkeypublicpasskey-id: (buff 65
update-passkey-usagepublicpasskey-id: (buff 65
deactivate-passkeypublicpasskey-id: (buff 65
reactivate-passkeypublicpasskey-id: (buff 65
merge-passkey-accountspublicsource-owner: principal