Source Code

;; title: source
;; version:
;; summary:
;; description:
(define-constant ERR-NOT-AUTHORIZED (err u100))
(define-constant ERR-INVALID-ROLE (err u101))
(define-constant ERR-NOT-FOUND (err u102))
(define-constant ERR-INVALID-INPUT (err u103))
(define-constant ERR-ALREADY-EXISTS (err u104))

;; traits
;;
(define-data-var contract-owner principal tx-sender)

;; token definitions
;;
(define-constant MANUFACTURER u"manufacturer")
(define-constant TRANSPORTER u"transporter")
(define-constant RETAILER u"retailer")
(define-constant MIN-STRING-LENGTH u1)
(define-constant MAX-STRING-LENGTH-100 u100)
(define-constant MAX-STRING-LENGTH-50 u50)

;; constants
;;
(define-map roles
  principal
  {
    role: (string-utf8 20),
    is-active: bool,
  }
)

;; data vars
;;
(define-map products
  uint
  {
    id: uint,
    name: (string-utf8 100),
    manufacturer: principal,
    origin: (string-utf8 50),
    timestamp: uint,
    current-location: (string-utf8 100),
    status: (string-utf8 20),
  }
)

;; data maps
;;
;; New map for product history
(define-map product-history
  {
    product-id: uint,
    change-id: uint,
  }
  {
    timestamp: uint,
    location: (string-utf8 100),
    status: (string-utf8 20),
  }
)

;; public functions
;;
(define-data-var product-counter uint u0)
(define-data-var change-counter uint u0) ;; To track the number of changes for history

;; read only functions
;;
(define-private (is-valid-role (role (string-utf8 20)))
  (or
    (is-eq role MANUFACTURER)
    (is-eq role TRANSPORTER)
    (is-eq role RETAILER)
  )
)

;; private functions
;;
(define-private (is-valid-string-length
    (str (string-utf8 100))
    (max-len uint)
  )
  (and
    (>= (len str) MIN-STRING-LENGTH)
    (<= (len str) max-len)
  )
)

(define-private (validate-strings
    (name (string-utf8 100))
    (origin (string-utf8 50))
    (location (string-utf8 100))
  )
  (and
    (is-valid-string-length name MAX-STRING-LENGTH-100)
    (is-valid-string-length origin MAX-STRING-LENGTH-50)
    (is-valid-string-length location MAX-STRING-LENGTH-100)
  )
)

(define-private (is-contract-owner)
  (is-eq tx-sender (var-get contract-owner))
)

(define-private (check-role
    (address principal)
    (required-role (string-utf8 20))
  )
  (match (map-get? roles address)
    role (and
      (is-eq (get role role) required-role)
      (get is-active role)
    )
    false
  )
)

(define-private (safe-get-role (address principal))
  (default-to {
    role: u"",
    is-active: false,
  } (map-get? roles address)
  )
)

(define-public (assign-role
    (address principal)
    (new-role (string-utf8 20))
  )
  (let ((current-role (safe-get-role address)))
    (begin
      (asserts! (is-contract-owner) ERR-NOT-AUTHORIZED)
      (asserts! (is-valid-role new-role) ERR-INVALID-INPUT)
      (asserts! (not (get is-active current-role)) ERR-ALREADY-EXISTS)
      (ok (map-set roles address {
        role: new-role,
        is-active: true,
      }))
    )
  )
)

(define-public (revoke-role (address principal))
  (let ((current-role (safe-get-role address)))
    (begin
      (asserts! (is-contract-owner) ERR-NOT-AUTHORIZED)
      (asserts! (get is-active current-role) ERR-NOT-FOUND)
      (ok (map-set roles address {
        role: u"",
        is-active: false,
      }))
    )
  )
)

(define-private (safe-get-product (product-id uint))
  (map-get? products product-id)
)

(define-public (add-product
    (name (string-utf8 100))
    (origin (string-utf8 50))
    (location (string-utf8 100))
  )
  (let (
      (safe-id (var-get product-counter))
      (existing-product (safe-get-product safe-id))
      (validated-strings (validate-strings name origin location))
    )
    (begin
      (asserts! (check-role tx-sender MANUFACTURER) ERR-NOT-AUTHORIZED)
      (asserts! validated-strings ERR-INVALID-INPUT)
      (asserts! (is-none existing-product) ERR-ALREADY-EXISTS)
      (map-set products safe-id {
        id: safe-id,
        name: name,
        manufacturer: tx-sender,
        origin: origin,
        timestamp: stacks-block-height,
        current-location: location,
        status: u"created",
      })
      (var-set product-counter (+ safe-id u1))
      (ok safe-id)
    )
  )
)

;; Update location and add history entry
(define-public (update-location
    (product-id uint)
    (new-location (string-utf8 100))
  )
  (let (
      (product (safe-get-product product-id))
      (valid-location (is-valid-string-length new-location MAX-STRING-LENGTH-100))
      (change-id (var-get change-counter))
    )
    (begin
      (asserts!
        (or
          (check-role tx-sender TRANSPORTER)
          (check-role tx-sender MANUFACTURER)
        )
        ERR-NOT-AUTHORIZED
      )
      (asserts! valid-location ERR-INVALID-INPUT)
      (asserts! (is-some product) ERR-NOT-FOUND)
      ;; Update the product with the new location
      (map-set products product-id
        (merge (unwrap! product ERR-NOT-FOUND) { current-location: new-location })
      )
      ;; Add a new entry to the product history
      (map-set product-history {
        product-id: product-id,
        change-id: change-id,
      } {
        timestamp: stacks-block-height,
        location: new-location,
        status: (get status (unwrap! product ERR-NOT-FOUND)),
      })
      ;; Increment the change counter
      (var-set change-counter (+ change-id u1))
      (ok change-id)
    )
  )
)

;; New function to get the product's history
(define-read-only (get-product-history (product-id uint))
  (map-get? product-history {
    product-id: product-id,
    change-id: u0,
  })
)

(define-read-only (get-product (product-id uint))
  (safe-get-product product-id)
)

(define-read-only (get-role (address principal))
  (safe-get-role address)
)

Functions (14)

FunctionAccessArgs
is-valid-roleprivaterole: (string-utf8 20
is-valid-string-lengthprivatestr: (string-utf8 100
validate-stringsprivatename: (string-utf8 100
is-contract-ownerprivate
check-roleprivateaddress: principal, required-role: (string-utf8 20
safe-get-roleprivateaddress: principal
assign-rolepublicaddress: principal, new-role: (string-utf8 20
revoke-rolepublicaddress: principal
safe-get-productprivateproduct-id: uint
add-productpublicname: (string-utf8 100
update-locationpublicproduct-id: uint, new-location: (string-utf8 100
get-product-historyread-onlyproduct-id: uint
get-productread-onlyproduct-id: uint
get-roleread-onlyaddress: principal