Source Code

;; Loiters Core Contract
;; Main contract for user profiles, reputation, check-ins, and endorsements

;; Constants
(define-constant CONTRACT-OWNER tx-sender)
(define-constant CHECKIN-COOLDOWN u86400) ;; 24 hours in seconds
(define-constant ENDORSEMENT-COOLDOWN u604800) ;; 7 days in seconds
(define-constant BASE-CHECKIN-REWARD u10000000) ;; 10 LOIT (with 6 decimals)
(define-constant ENDORSEMENT-REWARD u5000000) ;; 5 LOIT
(define-constant MAX-USERNAME-LENGTH u32)
(define-constant MIN-USERNAME-LENGTH u3)

;; Error codes (importing from loiters-errors)
(define-constant ERR-NOT-AUTHORIZED (err u1000))
(define-constant ERR-USER-NOT-REGISTERED (err u1100))
(define-constant ERR-USERNAME-TAKEN (err u1101))
(define-constant ERR-INVALID-USERNAME (err u1102))
(define-constant ERR-USER-ALREADY-REGISTERED (err u1103))
(define-constant ERR-CHECKIN-TOO-SOON (err u1300))
(define-constant ERR-CANNOT-ENDORSE-SELF (err u1400))
(define-constant ERR-ENDORSEMENT-COOLDOWN (err u1401))
(define-constant ERR-CONTRACT-PAUSED (err u1004))

;; Reputation tiers
(define-constant TIER-BRONZE u0)
(define-constant TIER-SILVER u1000)
(define-constant TIER-GOLD u5000)
(define-constant TIER-PLATINUM u15000)
(define-constant TIER-DIAMOND u50000)

;; Data Variables
(define-data-var contract-paused bool false)
(define-data-var total-users uint u0)
(define-data-var total-checkins uint u0)
(define-data-var loiters-token-contract principal tx-sender) ;; Will be set to token contract

;; Data Maps

;; User profiles
(define-map users
  principal
  {
    username: (string-utf8 32),
    bio: (string-utf8 256),
    avatar-uri: (string-utf8 256),
    reputation-score: uint,
    total-checkins: uint,
    current-streak: uint,
    longest-streak: uint,
    total-endorsements-received: uint,
    total-endorsements-given: uint,
    joined-at: uint,
    last-checkin: uint
  }
)

;; Username to principal mapping
(define-map usernames (string-utf8 32) principal)

;; Check-in history
(define-map checkins
  {user: principal, checkin-id: uint}
  {
    latitude: int,
    longitude: int,
    timestamp: uint,
    reward-amount: uint
  }
)

;; User check-in counter
(define-map user-checkin-count principal uint)

;; Endorsements
(define-map endorsements
  {endorser: principal, endorsed: principal}
  {
    timestamp: uint,
    message: (string-utf8 256)
  }
)

;; Endorsement cooldown tracking
(define-map endorsement-cooldowns
  {endorser: principal, endorsed: principal}
  uint
)

;; Read-only functions

(define-read-only (get-user (user principal))
  (map-get? users user)
)

(define-read-only (get-user-by-username (username (string-utf8 32)))
  (match (map-get? usernames username)
    user-principal (get-user user-principal)
    none
  )
)

(define-read-only (get-total-users)
  (var-get total-users)
)

(define-read-only (get-total-checkins)
  (var-get total-checkins)
)

(define-read-only (get-checkin (user principal) (checkin-id uint))
  (map-get? checkins {user: user, checkin-id: checkin-id})
)

(define-read-only (get-user-checkin-count (user principal))
  (default-to u0 (map-get? user-checkin-count user))
)

(define-read-only (get-endorsement (endorser principal) (endorsed principal))
  (map-get? endorsements {endorser: endorser, endorsed: endorsed})
)

(define-read-only (get-reputation-tier (reputation uint))
  (if (>= reputation TIER-DIAMOND)
    "Diamond"
    (if (>= reputation TIER-PLATINUM)
      "Platinum"
      (if (>= reputation TIER-GOLD)
        "Gold"
        (if (>= reputation TIER-SILVER)
          "Silver"
          "Bronze"
        )
      )
    )
  )
)

(define-read-only (calculate-checkin-reward (streak uint))
  (let
    (
      (multiplier (if (>= streak u30)
                    u5
                    (if (>= streak u14)
                      u3
                      (if (>= streak u7)
                        u2
                        u1
                      )
                    )
                  ))
    )
    (* BASE-CHECKIN-REWARD multiplier)
  )
)

;; Public functions

;; Register a new user
(define-public (register-user (username (string-utf8 32)) (bio (string-utf8 256)) (avatar-uri (string-utf8 256)))
  (let
    (
      (username-len (len username))
      (existing-user (map-get? users tx-sender))
      (existing-username (map-get? usernames username))
    )
    (asserts! (not (var-get contract-paused)) ERR-CONTRACT-PAUSED)
    (asserts! (is-none existing-user) ERR-USER-ALREADY-REGISTERED)
    (asserts! (is-none existing-username) ERR-USERNAME-TAKEN)
    (asserts! (and (>= username-len MIN-USERNAME-LENGTH) (<= username-len MAX-USERNAME-LENGTH)) ERR-INVALID-USERNAME)
    
    (map-set users tx-sender {
      username: username,
      bio: bio,
      avatar-uri: avatar-uri,
      reputation-score: u0,
      total-checkins: u0,
      current-streak: u0,
      longest-streak: u0,
      total-endorsements-received: u0,
      total-endorsements-given: u0,
      joined-at: stacks-block-height,
      last-checkin: u0
    })
    
    (map-set usernames username tx-sender)
    (var-set total-users (+ (var-get total-users) u1))
    
    (print {
      type: "user-registered",
      user: tx-sender,
      username: username,
      timestamp: stacks-block-height
    })
    
    (ok true)
  )
)

;; Update user profile
(define-public (update-profile (bio (string-utf8 256)) (avatar-uri (string-utf8 256)))
  (let
    (
      (user-data (unwrap! (map-get? users tx-sender) ERR-USER-NOT-REGISTERED))
    )
    (asserts! (not (var-get contract-paused)) ERR-CONTRACT-PAUSED)
    
    (map-set users tx-sender (merge user-data {
      bio: bio,
      avatar-uri: avatar-uri
    }))
    
    (ok true)
  )
)

;; Check-in at a location
(define-public (checkin (latitude int) (longitude int))
  (let
    (
      (user-data (unwrap! (map-get? users tx-sender) ERR-USER-NOT-REGISTERED))
      (last-checkin (get last-checkin user-data))
      (current-time stacks-block-height)
      (time-since-last (- current-time last-checkin))
      (checkin-count (get-user-checkin-count tx-sender))
      (new-streak (if (and (> last-checkin u0) (<= time-since-last CHECKIN-COOLDOWN))
                    (+ (get current-streak user-data) u1)
                    u1))
      (reward (calculate-checkin-reward new-streak))
    )
    (asserts! (not (var-get contract-paused)) ERR-CONTRACT-PAUSED)
    (asserts! (or (is-eq last-checkin u0) (>= time-since-last CHECKIN-COOLDOWN)) ERR-CHECKIN-TOO-SOON)
    
    ;; Record check-in
    (map-set checkins {user: tx-sender, checkin-id: checkin-count} {
      latitude: latitude,
      longitude: longitude,
      timestamp: current-time,
      reward-amount: reward
    })
    
    ;; Update user data
    (map-set users tx-sender (merge user-data {
      total-checkins: (+ (get total-checkins user-data) u1),
      current-streak: new-streak,
      longest-streak: (if (> new-streak (get longest-streak user-data)) new-streak (get longest-streak user-data)),
      last-checkin: current-time,
      reputation-score: (+ (get reputation-score user-data) u10) ;; +10 reputation per check-in
    }))
    
    (map-set user-checkin-count tx-sender (+ checkin-count u1))
    (var-set total-checkins (+ (var-get total-checkins) u1))
    
    ;; Mint reward tokens (will be implemented when token contract is integrated)
    ;; (try! (contract-call? .loiters-token-v2 mint reward tx-sender))
    
    (print {
      type: "checkin",
      user: tx-sender,
      checkin-id: checkin-count,
      latitude: latitude,
      longitude: longitude,
      streak: new-streak,
      reward: reward,
      timestamp: current-time
    })
    
    (ok reward)
  )
)

;; Endorse another user
(define-public (endorse-user (endorsed principal) (message (string-utf8 256)))
  (let
    (
      (endorser-data (unwrap! (map-get? users tx-sender) ERR-USER-NOT-REGISTERED))
      (endorsed-data (unwrap! (map-get? users endorsed) ERR-USER-NOT-REGISTERED))
      (last-endorsement (default-to u0 (map-get? endorsement-cooldowns {endorser: tx-sender, endorsed: endorsed})))
      (current-time stacks-block-height)
      (time-since-last (- current-time last-endorsement))
      (endorser-reputation (get reputation-score endorser-data))
      (reputation-bonus (/ endorser-reputation u100)) ;; 1% of endorser's reputation
    )
    (asserts! (not (var-get contract-paused)) ERR-CONTRACT-PAUSED)
    (asserts! (not (is-eq tx-sender endorsed)) ERR-CANNOT-ENDORSE-SELF)
    (asserts! (or (is-eq last-endorsement u0) (>= time-since-last ENDORSEMENT-COOLDOWN)) ERR-ENDORSEMENT-COOLDOWN)
    
    ;; Record endorsement
    (map-set endorsements {endorser: tx-sender, endorsed: endorsed} {
      timestamp: current-time,
      message: message
    })
    
    (map-set endorsement-cooldowns {endorser: tx-sender, endorsed: endorsed} current-time)
    
    ;; Update endorser stats
    (map-set users tx-sender (merge endorser-data {
      total-endorsements-given: (+ (get total-endorsements-given endorser-data) u1)
    }))
    
    ;; Update endorsed user stats and reputation
    (map-set users endorsed (merge endorsed-data {
      total-endorsements-received: (+ (get total-endorsements-received endorsed-data) u1),
      reputation-score: (+ (+ (get reputation-score endorsed-data) u50) reputation-bonus) ;; Base +50 + bonus
    }))
    
    ;; Reward endorsed user (will be implemented when token contract is integrated)
    ;; (try! (contract-call? .loiters-token-v2 mint ENDORSEMENT-REWARD endorsed))
    
    (print {
      type: "endorsement",
      endorser: tx-sender,
      endorsed: endorsed,
      message: message,
      reputation-bonus: reputation-bonus,
      timestamp: current-time
    })
    
    (ok true)
  )
)

;; Admin functions

(define-public (set-token-contract (token-contract principal))
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (ok (var-set loiters-token-contract token-contract))
  )
)

(define-public (pause-contract)
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (ok (var-set contract-paused true))
  )
)

(define-public (unpause-contract)
  (begin
    (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-AUTHORIZED)
    (ok (var-set contract-paused false))
  )
)

Functions (16)

FunctionAccessArgs
get-userread-onlyuser: principal
get-user-by-usernameread-onlyusername: (string-utf8 32
get-total-usersread-only
get-total-checkinsread-only
get-checkinread-onlyuser: principal, checkin-id: uint
get-user-checkin-countread-onlyuser: principal
get-endorsementread-onlyendorser: principal, endorsed: principal
get-reputation-tierread-onlyreputation: uint
calculate-checkin-rewardread-onlystreak: uint
register-userpublicusername: (string-utf8 32
update-profilepublicbio: (string-utf8 256
checkinpubliclatitude: int, longitude: int
endorse-userpublicendorsed: principal, message: (string-utf8 256
set-token-contractpublictoken-contract: principal
pause-contractpublic
unpause-contractpublic