Source Code

;; 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})
)

Functions (21)

FunctionAccessArgs
is-valid-stageprivatestage: (string-ascii 32
is-valid-statusprivatestatus: (string-ascii 32
register-productpublicname: (string-utf8 128
log-checkpointpublicproduct-id: uint, stage: (string-ascii 32
transfer-ownershippublicproduct-id: uint, new-owner: principal, notes: (string-utf8 256
flag-checkpointpublicproduct-id: uint, checkpoint-seq: uint, reason: (string-utf8 256
resolve-flagpublicproduct-id: uint, checkpoint-seq: uint, resolution: (string-utf8 256
update-product-statuspublicproduct-id: uint, status: (string-ascii 32
increment-counterpublic
decrement-counterpublic
get-counterread-only
get-productread-onlyproduct-id: uint
get-checkpointread-onlyproduct-id: uint, seq: uint
get-checkpoint-countread-onlyproduct-id: uint
get-current-ownerread-onlyproduct-id: uint
get-ownership-recordread-onlyproduct-id: uint, seq: uint
get-flagread-onlyproduct-id: uint, checkpoint-seq: uint, flagger: principal
has-open-flagsread-onlyproduct-id: uint
get-product-countread-only
get-product-stageread-onlyproduct-id: uint
get-resolutionread-onlyproduct-id: uint, checkpoint-seq: uint