;; title: satoshitrace
;; version: 1.0.0
;; summary: SatoshiTrace Protocol Contract
;; description: A permissionless, on-chain supply chain tracker
;; --- Errors ---
(define-constant err-product-not-found (err u400))
(define-constant err-not-owner (err u401))
(define-constant err-invalid-stage (err u402))
(define-constant err-checkpoint-not-found (err u403))
(define-constant err-already-flagged (err u404))
(define-constant err-flag-not-found (err u405))
(define-constant err-product-recalled (err u406))
(define-constant err-product-destroyed (err u407))
(define-constant err-invalid-name-length (err u408))
(define-constant err-self-transfer (err u409))
(define-constant err-invalid-status (err u410))
;; --- Variables ---
(define-data-var product-id-counter uint u0)
(define-data-var test-counter int 0)
;; --- Maps ---
(define-map Products uint
{
name: (string-utf8 128),
description: (string-utf8 512),
origin: (string-utf8 128),
creator: principal,
current-owner: principal,
status: (string-ascii 32),
checkpoint-count: uint,
open-flags: uint,
created-at: uint,
metadata-uri: (optional (string-utf8 256))
}
)
(define-map Checkpoints {product-id: uint, seq: uint}
{
logger: principal,
stage: (string-ascii 32),
location: (string-utf8 128),
notes: (string-utf8 512),
timestamp: uint,
metadata-uri: (optional (string-utf8 256)),
flagged: bool
}
)
(define-map Flags {product-id: uint, seq: uint, flagger: principal}
{
reason: (string-utf8 256),
block-height: uint
}
)
(define-map Resolutions {product-id: uint, seq: uint}
{
resolution: (string-utf8 256),
resolver: principal,
block-height: uint
}
)
(define-map OwnershipHistory {product-id: uint, seq: uint}
{
from: principal,
to: principal,
block-height: uint,
notes: (string-utf8 256)
}
)
;; --- Private Helpers ---
;; Validates that a stage string is one of the known lifecycle stages
(define-private (is-valid-stage (stage (string-ascii 32)))
(or
(is-eq stage "REGISTERED")
(is-eq stage "MANUFACTURED")
(is-eq stage "QUALITY_CHECKED")
(is-eq stage "IN_TRANSIT")
(is-eq stage "CUSTOMS_CLEARED")
(is-eq stage "WAREHOUSED")
(is-eq stage "RECEIVED")
(is-eq stage "ON_SHELF")
(is-eq stage "SOLD")
(is-eq stage "DELIVERED")
(is-eq stage "RECALLED")
(is-eq stage "DAMAGED")
(is-eq stage "RETURNED")
(is-eq stage "DESTROYED")
(is-eq stage "DISPUTED")
(is-eq stage "OWNERSHIP_TRANSFERRED")
)
)
;; Validates that a status string is one of the known product statuses
(define-private (is-valid-status (status (string-ascii 32)))
(or
(is-eq status "REGISTERED")
(is-eq status "MANUFACTURED")
(is-eq status "QUALITY_CHECKED")
(is-eq status "IN_TRANSIT")
(is-eq status "CUSTOMS_CLEARED")
(is-eq status "WAREHOUSED")
(is-eq status "RECEIVED")
(is-eq status "ON_SHELF")
(is-eq status "SOLD")
(is-eq status "DELIVERED")
(is-eq status "RECALLED")
(is-eq status "DAMAGED")
(is-eq status "RETURNED")
(is-eq status "DESTROYED")
(is-eq status "DISPUTED")
(is-eq status "OWNERSHIP_TRANSFERRED")
)
)
;; --- Public Functions ---
;; Register a new product
(define-public (register-product
(name (string-utf8 128))
(description (string-utf8 512))
(origin (string-utf8 128))
(metadata-uri (optional (string-utf8 256))))
(let
(
(product-id (+ (var-get product-id-counter) u1))
)
(asserts! (> (len name) u0) err-invalid-name-length)
(var-set product-id-counter product-id)
(map-insert Products product-id {
name: name,
description: description,
origin: origin,
creator: tx-sender,
current-owner: tx-sender,
status: "REGISTERED",
checkpoint-count: u0,
open-flags: u0,
created-at: stacks-block-height,
metadata-uri: metadata-uri
})
(ok product-id)
)
)
;; Log a new checkpoint
;; #[allow(unchecked_data)]
(define-public (log-checkpoint
(product-id uint)
(stage (string-ascii 32))
(location (string-utf8 128))
(notes (string-utf8 512))
(metadata-uri (optional (string-utf8 256))))
(let
(
(product (unwrap! (map-get? Products product-id) err-product-not-found))
(new-seq (+ (get checkpoint-count product) u1))
)
(asserts! (is-valid-stage stage) err-invalid-stage)
(asserts! (not (is-eq (get status product) "RECALLED")) err-product-recalled)
(asserts! (not (is-eq (get status product) "DESTROYED")) err-product-destroyed)
(map-insert Checkpoints {product-id: product-id, seq: new-seq} {
logger: tx-sender,
stage: stage,
location: location,
notes: notes,
timestamp: stacks-block-height,
metadata-uri: metadata-uri,
flagged: false
})
(map-set Products product-id (merge product {
status: stage,
checkpoint-count: new-seq
}))
(ok new-seq)
)
)
;; Transfer custody
;; #[allow(unchecked_data)]
(define-public (transfer-ownership
(product-id uint)
(new-owner principal)
(notes (string-utf8 256)))
(let
(
(product (unwrap! (map-get? Products product-id) err-product-not-found))
(current-owner (get current-owner product))
(new-seq (+ (get checkpoint-count product) u1))
)
(asserts! (is-eq tx-sender current-owner) err-not-owner)
(asserts! (not (is-eq tx-sender new-owner)) err-self-transfer)
;; Add a transfer checkpoint
(map-insert Checkpoints {product-id: product-id, seq: new-seq} {
logger: tx-sender,
stage: "OWNERSHIP_TRANSFERRED",
location: u"",
notes: notes,
timestamp: stacks-block-height,
metadata-uri: none,
flagged: false
})
;; Add ownership history
(map-insert OwnershipHistory {product-id: product-id, seq: new-seq} {
from: current-owner,
to: new-owner,
block-height: stacks-block-height,
notes: notes
})
;; Update product
(map-set Products product-id (merge product {
current-owner: new-owner,
checkpoint-count: new-seq
}))
(ok true)
)
)
;; Flag a checkpoint - permissionless, any address can flag
;; #[allow(unchecked_data)]
(define-public (flag-checkpoint
(product-id uint)
(checkpoint-seq uint)
(reason (string-utf8 256)))
(let
(
(checkpoint (unwrap! (map-get? Checkpoints {product-id: product-id, seq: checkpoint-seq}) err-checkpoint-not-found))
(product (unwrap! (map-get? Products product-id) err-product-not-found))
)
(asserts! (is-none (map-get? Flags {product-id: product-id, seq: checkpoint-seq, flagger: tx-sender})) err-already-flagged)
(map-insert Flags {product-id: product-id, seq: checkpoint-seq, flagger: tx-sender} {
reason: reason,
block-height: stacks-block-height
})
(map-set Checkpoints {product-id: product-id, seq: checkpoint-seq} (merge checkpoint {
flagged: true
}))
(map-set Products product-id (merge product { open-flags: (+ (get open-flags product) u1) }))
(ok true)
)
)
;; Resolve a flag - only current owner
;; #[allow(unchecked_data)]
(define-public (resolve-flag
(product-id uint)
(checkpoint-seq uint)
(resolution (string-utf8 256)))
(let
(
(product (unwrap! (map-get? Products product-id) err-product-not-found))
(checkpoint (unwrap! (map-get? Checkpoints {product-id: product-id, seq: checkpoint-seq}) err-checkpoint-not-found))
(new-flags (if (> (get open-flags product) u0) (- (get open-flags product) u1) u0))
)
(asserts! (is-eq tx-sender (get current-owner product)) err-not-owner)
(asserts! (get flagged checkpoint) err-flag-not-found)
(map-set Resolutions {product-id: product-id, seq: checkpoint-seq} {
resolution: resolution,
resolver: tx-sender,
block-height: stacks-block-height
})
(map-set Checkpoints {product-id: product-id, seq: checkpoint-seq} (merge checkpoint {
flagged: false
}))
(map-set Products product-id (merge product { open-flags: new-flags }))
(ok true)
)
)
;; Update the top-level status manually (e.g. RECALLED) - only current owner
;; #[allow(unchecked_data)]
(define-public (update-product-status
(product-id uint)
(status (string-ascii 32)))
(let
(
(product (unwrap! (map-get? Products product-id) err-product-not-found))
)
(asserts! (is-eq tx-sender (get current-owner product)) err-not-owner)
(asserts! (is-valid-status status) err-invalid-status)
(map-set Products product-id (merge product {
status: status
}))
(ok true)
)
)
;; Custom test counter utilities
(define-public (increment-counter)
(begin
(var-set test-counter (+ (var-get test-counter) 1))
(ok (var-get test-counter))
)
)
(define-public (decrement-counter)
(begin
(var-set test-counter (- (var-get test-counter) 1))
(ok (var-get test-counter))
)
)
;; --- Read Only Functions ---
(define-read-only (get-counter)
(var-get test-counter)
)
(define-read-only (get-product (product-id uint))
(map-get? Products product-id)
)
(define-read-only (get-checkpoint (product-id uint) (seq uint))
(map-get? Checkpoints {product-id: product-id, seq: seq})
)
(define-read-only (get-checkpoint-count (product-id uint))
(match (map-get? Products product-id)
product (get checkpoint-count product)
u0
)
)
(define-read-only (get-current-owner (product-id uint))
(match (map-get? Products product-id)
product (some (get current-owner product))
none
)
)
(define-read-only (get-ownership-record (product-id uint) (seq uint))
(map-get? OwnershipHistory {product-id: product-id, seq: seq})
)
(define-read-only (get-flag (product-id uint) (checkpoint-seq uint) (flagger principal))
(map-get? Flags {product-id: product-id, seq: checkpoint-seq, flagger: flagger})
)
(define-read-only (has-open-flags (product-id uint))
(match (map-get? Products product-id)
product (> (get open-flags product) u0)
false
)
)
(define-read-only (get-product-count)
(var-get product-id-counter)
)
(define-read-only (get-product-stage (product-id uint))
(match (map-get? Products product-id)
product (get status product)
""
)
)
(define-read-only (get-resolution (product-id uint) (checkpoint-seq uint))
(map-get? Resolutions {product-id: product-id, seq: checkpoint-seq})
)