Source Code

;; StratoSense - Data Registry Contract (v2)
;; Implements Clarity 3 standards

;; Error Codes
(define-constant ERR-NOT-AUTHORIZED (err u401))
(define-constant ERR-DATASET-NOT-FOUND (err u404))
(define-constant ERR-INVALID-PARAMS (err u400))
(define-constant ERR-METADATA-FROZEN (err u403))
(define-constant ERR-CONTRACT-PAUSED (err u503))
(define-constant ERR-DATASET-EXISTS (err u409))

(define-constant MAX-OWNER-DATASETS u1000)
(define-constant MAX-PAGE-LIMIT u100)
(define-constant MAX-ALL-DATASETS u10000)

;; Data Vars
(define-data-var dataset-counter uint u0)
(define-data-var contract-admin principal tx-sender)
(define-data-var contract-paused bool false)
(define-data-var all-dataset-ids (list 10000 uint) (list))

;; Dataset Map
(define-map datasets
  { dataset-id: uint }
  {
    owner: principal,
    name: (string-utf8 100),
    description: (string-utf8 500),
    data-type: (string-utf8 50),
    collection-date: uint,
    altitude-min: uint,
    altitude-max: uint,
    latitude: int,
    longitude: int,
    ipfs-hash: (string-ascii 100),
    is-public: bool,
    metadata-frozen: bool,
    created-at: uint,
    status: (string-ascii 20), ;; "active", "deprecated"
  }
)

;; Datasets by Owner Map
(define-map datasets-by-owner
  { owner: principal }
  { dataset-ids: (list 1000 uint) }
)

;; --- Read-Only Functions ---

(define-read-only (get-contract-admin)
  (ok (var-get contract-admin))
)

(define-read-only (is-contract-paused)
  (ok (var-get contract-paused))
)

(define-read-only (get-dataset (dataset-id uint))
  (match (map-get? datasets { dataset-id: dataset-id })
    data (ok data)
    (err ERR-DATASET-NOT-FOUND)
  )
)

(define-read-only (get-datasets-by-owner (owner principal))
  (default-to (list)
    (get dataset-ids (map-get? datasets-by-owner { owner: owner }))
  )
)

(define-read-only (get-dataset-count)
  (ok (var-get dataset-counter))
)

(define-read-only (get-datasets-by-owner-page
    (owner principal)
    (offset uint)
    (limit uint)
  )
  (let ((all (get-owner-dataset-ids owner)))
    (let ((safe-limit (if (> limit MAX-PAGE-LIMIT)
        MAX-PAGE-LIMIT
        limit
      )))
      (let ((page (slice-list-1000 all offset safe-limit)))
        (ok {
          items: page,
          next-offset: (page-next-offset offset page (len all)),
        })
      )
    )
  )
)

(define-read-only (get-dataset-ids-page
    (offset uint)
    (limit uint)
  )
  (let ((all (var-get all-dataset-ids)))
    (let ((total (len all)))
      (if (or (is-eq total u0) (>= offset total))
        (ok {
          items: (list),
          next-offset: none,
        })
        (let ((safe-limit (if (> limit MAX-PAGE-LIMIT)
            MAX-PAGE-LIMIT
            limit
          )))
          (let ((items (slice-list-10000 all offset safe-limit)))
            (ok {
              items: items,
              next-offset: (page-next-offset offset items total),
            })
          )
        )
      )
    )
  )
)

;; --- Private Helper Functions ---

(define-private (is-dataset-owner (dataset-id uint))
  (let ((dataset (map-get? datasets { dataset-id: dataset-id })))
    (match dataset
      data (is-eq tx-sender (get owner data))
      false
    )
  )
)

(define-private (get-owner-entry (owner principal))
  (default-to { dataset-ids: (list) }
    (map-get? datasets-by-owner { owner: owner })
  )
)

(define-private (get-owner-dataset-ids (owner principal))
  (get dataset-ids (get-owner-entry owner))
)

(define-private (page-next-offset
    (offset uint)
    (page (list 101 uint))
    (total uint)
  )
  (let ((next (+ offset (len page))))
    (if (< next total)
      (some next)
      none
    )
  )
)

(define-private (slice-fold
    (item uint)
    (acc {
      result: (list 101 uint),
      idx: uint,
      offset: uint,
      end: uint,
    })
  )
  (let ((idx (get idx acc)))
    (let ((next-idx (+ idx u1)))
      (if (and (>= idx (get offset acc)) (< idx (get end acc)))
        (match (as-max-len? (append (get result acc) item) u101)
          updated {
            result: updated,
            idx: next-idx,
            offset: (get offset acc),
            end: (get end acc),
          }
          {
            result: (get result acc),
            idx: next-idx,
            offset: (get offset acc),
            end: (get end acc),
          }
        )
        {
          result: (get result acc),
          idx: next-idx,
          offset: (get offset acc),
          end: (get end acc),
        }
      )
    )
  )
)

(define-private (slice-list-1000
    (items (list 1000 uint))
    (offset uint)
    (limit uint)
  )
  (let ((end (+ offset limit)))
    (get result
      (fold slice-fold items {
        result: (list),
        idx: u0,
        offset: offset,
        end: end,
      })
    )
  )
)

(define-private (slice-list-10000
    (items (list 10000 uint))
    (offset uint)
    (limit uint)
  )
  (let ((end (+ offset limit)))
    (get result
      (fold slice-fold items {
        result: (list),
        idx: u0,
        offset: offset,
        end: end,
      })
    )
  )
)

(define-private (add-dataset-to-owner
    (owner principal)
    (dataset-id uint)
  )
  (let ((current (get-owner-dataset-ids owner)))
    (if (list-contains-1000 current dataset-id)
      (ok true)
      (match (as-max-len? (append current dataset-id) u1000)
        updated (ok (map-set datasets-by-owner { owner: owner } { dataset-ids: updated }))
        (err ERR-INVALID-PARAMS)
      )
    )
  )
)

(define-private (add-dataset-id (dataset-id uint))
  (let ((current (var-get all-dataset-ids)))
    (if (list-contains-10000 current dataset-id)
      (ok true)
      (match (as-max-len? (append current dataset-id) u10000)
        updated (ok (var-set all-dataset-ids updated))
        (err ERR-INVALID-PARAMS)
      )
    )
  )
)

(define-private (contains-fold
    (item uint)
    (acc {
      target: uint,
      found: bool,
    })
  )
  (if (get found acc)
    acc
    (if (is-eq item (get target acc))
      {
        target: (get target acc),
        found: true,
      }
      acc
    )
  )
)

(define-private (list-contains-1000
    (items (list 1000 uint))
    (target uint)
  )
  (get found
    (fold contains-fold items {
      target: target,
      found: false,
    })
  )
)

(define-private (list-contains-10000
    (items (list 10000 uint))
    (target uint)
  )
  (get found
    (fold contains-fold items {
      target: target,
      found: false,
    })
  )
)

(define-private (remove-fold
    (item uint)
    (acc {
      result: (list 1000 uint),
      target: uint,
    })
  )
  (if (is-eq item (get target acc))
    acc
    (match (as-max-len? (append (get result acc) item) u1000)
      updated {
        result: updated,
        target: (get target acc),
      }
      acc
    )
  )
)

(define-private (remove-dataset-from-owner
    (owner principal)
    (dataset-id uint)
  )
  (let ((current (get-owner-dataset-ids owner)))
    (let ((updated (get result
        (fold remove-fold current {
          result: (list),
          target: dataset-id,
        })
      )))
      (map-set datasets-by-owner { owner: owner } { dataset-ids: updated })
    )
  )
)

(define-private (is-valid-status (status (string-ascii 20)))
  (or (is-eq status "active") (is-eq status "deprecated"))
)

(define-private (validate-fields
    (name (string-utf8 100))
    (description (string-utf8 500))
    (data-type (string-utf8 50))
    (collection-date uint)
    (altitude-min uint)
    (altitude-max uint)
    (latitude int)
    (longitude int)
    (ipfs-hash (string-ascii 100))
    (status (string-ascii 20))
  )
  (and
    (>= (len name) u1)
    (>= (len description) u1)
    (>= (len data-type) u1)
    (> collection-date u0)
    (>= altitude-max altitude-min)
    (and (>= latitude (* -90 1000000)) (<= latitude (* 90 1000000)))
    (and (>= longitude (* -180 1000000)) (<= longitude (* 180 1000000)))
    (or (is-eq (len ipfs-hash) u0) (>= (len ipfs-hash) u1))
    (is-valid-status status)
  )
)

(define-private (check-not-paused)
  (not (var-get contract-paused))
)

;; --- Public Functions ---

;; Register a new dataset
(define-public (register-dataset
    (name (string-utf8 100))
    (description (string-utf8 500))
    (data-type (string-utf8 50))
    (collection-date uint)
    (altitude-min uint)
    (altitude-max uint)
    (latitude int)
    (longitude int)
    (ipfs-hash (string-ascii 100))
    (is-public bool)
  )
  (let (
      (dataset-id (+ (var-get dataset-counter) u1))
      (owner-principal tx-sender)
      (current-time burn-block-height)
    )
    (asserts! (check-not-paused) ERR-CONTRACT-PAUSED)
    (asserts!
      (validate-fields name description data-type collection-date altitude-min
        altitude-max latitude longitude ipfs-hash "active"
      )
      ERR-INVALID-PARAMS
    )

    (map-set datasets { dataset-id: dataset-id } {
      owner: owner-principal,
      name: name,
      description: description,
      data-type: data-type,
      collection-date: collection-date,
      altitude-min: altitude-min,
      altitude-max: altitude-max,
      latitude: latitude,
      longitude: longitude,
      ipfs-hash: ipfs-hash,
      is-public: is-public,
      metadata-frozen: false,
      created-at: current-time,
      status: "active",
    })

    (unwrap! (add-dataset-id dataset-id) ERR-INVALID-PARAMS)
    (unwrap! (add-dataset-to-owner owner-principal dataset-id) ERR-INVALID-PARAMS)

    (var-set dataset-counter dataset-id)
    (ok dataset-id)
  )
)

;; Update dataset metadata
(define-public (update-dataset-metadata
    (dataset-id uint)
    (name (string-utf8 100))
    (description (string-utf8 500))
    (data-type (string-utf8 50))
    (is-public bool)
  )
  (let ((dataset (unwrap! (map-get? datasets { dataset-id: dataset-id }) ERR-DATASET-NOT-FOUND)))
    (asserts! (check-not-paused) ERR-CONTRACT-PAUSED)
    (asserts! (is-dataset-owner dataset-id) ERR-NOT-AUTHORIZED)
    (asserts! (not (get metadata-frozen dataset)) ERR-METADATA-FROZEN)
    (asserts!
      (and
        (>= (len name) u1)
        (>= (len description) u1)
        (>= (len data-type) u1)
      )
      ERR-INVALID-PARAMS
    )

    (map-set datasets { dataset-id: dataset-id }
      (merge dataset {
        name: name,
        description: description,
        data-type: data-type,
        is-public: is-public,
      })
    )
    (ok true)
  )
)

;; Freeze dataset metadata
(define-public (freeze-dataset-metadata (dataset-id uint))
  (let ((dataset (unwrap! (map-get? datasets { dataset-id: dataset-id }) ERR-DATASET-NOT-FOUND)))
    (asserts! (check-not-paused) ERR-CONTRACT-PAUSED)
    (asserts! (is-dataset-owner dataset-id) ERR-NOT-AUTHORIZED)
    (map-set datasets { dataset-id: dataset-id }
      (merge dataset { metadata-frozen: true })
    )
    (ok true)
  )
)

;; Transfer dataset
(define-public (transfer-dataset
    (dataset-id uint)
    (new-owner principal)
  )
  (let (
      (dataset (unwrap! (map-get? datasets { dataset-id: dataset-id })
        ERR-DATASET-NOT-FOUND
      ))
      (old-owner (get owner dataset))
    )
    (asserts! (check-not-paused) ERR-CONTRACT-PAUSED)
    (asserts! (is-dataset-owner dataset-id) ERR-NOT-AUTHORIZED)

    (map-set datasets { dataset-id: dataset-id }
      (merge dataset { owner: new-owner })
    )

    (remove-dataset-from-owner old-owner dataset-id)
    (unwrap! (add-dataset-to-owner new-owner dataset-id) ERR-INVALID-PARAMS)
    (ok true)
  )
)

;; Admin: Import dataset (migration helper)
(define-public (import-dataset
    (dataset-id uint)
    (owner principal)
    (name (string-utf8 100))
    (description (string-utf8 500))
    (data-type (string-utf8 50))
    (collection-date uint)
    (altitude-min uint)
    (altitude-max uint)
    (latitude int)
    (longitude int)
    (ipfs-hash (string-ascii 100))
    (is-public bool)
    (metadata-frozen bool)
    (created-at uint)
    (status (string-ascii 20))
  )
  (begin
    (asserts! (is-eq tx-sender (var-get contract-admin)) ERR-NOT-AUTHORIZED)
    (asserts! (check-not-paused) ERR-CONTRACT-PAUSED)
    (asserts! (> dataset-id u0) ERR-INVALID-PARAMS)
    (asserts! (is-none (map-get? datasets { dataset-id: dataset-id }))
      ERR-DATASET-EXISTS
    )
    (asserts!
      (validate-fields name description data-type collection-date altitude-min
        altitude-max latitude longitude ipfs-hash status
      )
      ERR-INVALID-PARAMS
    )

    (map-set datasets { dataset-id: dataset-id } {
      owner: owner,
      name: name,
      description: description,
      data-type: data-type,
      collection-date: collection-date,
      altitude-min: altitude-min,
      altitude-max: altitude-max,
      latitude: latitude,
      longitude: longitude,
      ipfs-hash: ipfs-hash,
      is-public: is-public,
      metadata-frozen: metadata-frozen,
      created-at: created-at,
      status: status,
    })

    (unwrap! (add-dataset-id dataset-id) ERR-INVALID-PARAMS)
    (unwrap! (add-dataset-to-owner owner dataset-id) ERR-INVALID-PARAMS)

    (if (> dataset-id (var-get dataset-counter))
      (var-set dataset-counter dataset-id)
      true
    )

    (ok dataset-id)
  )
)

;; Admin: Set Contract Admin
(define-public (set-contract-admin (new-admin principal))
  (begin
    (asserts! (is-eq tx-sender (var-get contract-admin)) ERR-NOT-AUTHORIZED)
    (var-set contract-admin new-admin)
    (ok new-admin)
  )
)

;; Admin: Pause/Unpause Contract
(define-public (set-paused (paused bool))
  (begin
    (asserts! (is-eq tx-sender (var-get contract-admin)) ERR-NOT-AUTHORIZED)
    (var-set contract-paused paused)
    (ok paused)
  )
)

Functions (23)

FunctionAccessArgs
get-contract-adminread-only
is-contract-pausedread-only
get-datasetread-onlydataset-id: uint
get-datasets-by-ownerread-onlyowner: principal
get-dataset-countread-only
is-dataset-ownerprivatedataset-id: uint
get-owner-entryprivateowner: principal
get-owner-dataset-idsprivateowner: principal
page-next-offsetprivateoffset: uint, page: (list 101 uint
slice-list-1000privateitems: (list 1000 uint
slice-list-10000privateitems: (list 10000 uint
add-dataset-idprivatedataset-id: uint
list-contains-1000privateitems: (list 1000 uint
list-contains-10000privateitems: (list 10000 uint
is-valid-statusprivatestatus: (string-ascii 20
validate-fieldsprivatename: (string-utf8 100
check-not-pausedprivate
register-datasetpublicname: (string-utf8 100
update-dataset-metadatapublicdataset-id: uint, name: (string-utf8 100
freeze-dataset-metadatapublicdataset-id: uint
import-datasetpublicdataset-id: uint, owner: principal, name: (string-utf8 100
set-contract-adminpublicnew-admin: principal
set-pausedpublicpaused: bool