diff --git a/reality-experience/.gitignore b/reality-experience/.gitignore new file mode 100644 index 0000000..f18b582 --- /dev/null +++ b/reality-experience/.gitignore @@ -0,0 +1,4 @@ + +settings/Mainnet.toml +settings/Testnet.toml +history.txt diff --git a/reality-experience/.vscode/settings.json b/reality-experience/.vscode/settings.json new file mode 100644 index 0000000..02e21eb --- /dev/null +++ b/reality-experience/.vscode/settings.json @@ -0,0 +1,4 @@ + +{ + "deno.enable": true, +} diff --git a/reality-experience/.vscode/tasks.json b/reality-experience/.vscode/tasks.json new file mode 100644 index 0000000..22af91c --- /dev/null +++ b/reality-experience/.vscode/tasks.json @@ -0,0 +1,18 @@ + +{ + "version": "2.0.0", + "tasks": [ + { + "label": "check contracts", + "group": "test", + "type": "shell", + "command": "clarinet check" + }, + { + "label": "test contracts", + "group": "test", + "type": "shell", + "command": "clarinet test" + } + ] +} diff --git a/reality-experience/Clarinet.toml b/reality-experience/Clarinet.toml new file mode 100644 index 0000000..19aa31f --- /dev/null +++ b/reality-experience/Clarinet.toml @@ -0,0 +1,11 @@ +[project] +name = "reality-experience" +authors = [] +description = "" +telemetry = true +requirements = [] +analysis = ["check_checker"] +costs_version = 2 +[contracts.augmented] +path = "contracts/augmented.clar" +depends_on = [] diff --git a/reality-experience/contracts/augmented.clar b/reality-experience/contracts/augmented.clar new file mode 100644 index 0000000..ab6b341 --- /dev/null +++ b/reality-experience/contracts/augmented.clar @@ -0,0 +1,339 @@ +;; Augmented Reality Experiences Smart Contract +;; A comprehensive contract for managing AR experiences on Stacks blockchain + +;; Constants +(define-constant contract-owner tx-sender) +(define-constant err-owner-only (err u100)) +(define-constant err-not-found (err u101)) +(define-constant err-unauthorized (err u102)) +(define-constant err-already-exists (err u103)) +(define-constant err-insufficient-payment (err u104)) +(define-constant err-invalid-rating (err u105)) +(define-constant err-invalid-input (err u106)) + +;; Data Variables +(define-data-var next-experience-id uint u1) +(define-data-var platform-fee-percentage uint u5) ;; 5% platform fee + +;; Data Maps +(define-map ar-experiences + { experience-id: uint } + { + creator: principal, + title: (string-ascii 100), + description: (string-ascii 500), + category: (string-ascii 50), + price: uint, + metadata-uri: (string-ascii 200), + created-at: uint, + is-active: bool, + total-downloads: uint, + average-rating: uint, + rating-count: uint + } +) + +(define-map user-purchases + { user: principal, experience-id: uint } + { purchased-at: uint, access-granted: bool } +) + +(define-map experience-ratings + { user: principal, experience-id: uint } + { rating: uint, review: (string-ascii 300), rated-at: uint } +) + +(define-map creator-profiles + { creator: principal } + { + display-name: (string-ascii 50), + bio: (string-ascii 200), + total-experiences: uint, + total-earnings: uint, + verified: bool + } +) + +;; Private Functions +(define-private (is-contract-owner) + (is-eq tx-sender contract-owner) +) + +(define-private (calculate-platform-fee (amount uint)) + (/ (* amount (var-get platform-fee-percentage)) u100) +) + +(define-private (update-experience-rating (experience-id uint) (new-rating uint)) + (let ( + (experience (unwrap! (map-get? ar-experiences { experience-id: experience-id }) (err err-not-found))) + (current-total (* (get average-rating experience) (get rating-count experience))) + (new-count (+ (get rating-count experience) u1)) + (new-average (if (> new-count u0) (/ (+ current-total new-rating) new-count) u0)) + ) + (map-set ar-experiences + { experience-id: experience-id } + (merge experience { + average-rating: new-average, + rating-count: new-count + }) + ) + (ok true) + ) +) + +;; Public Functions + +;; Create AR Experience +(define-public (create-ar-experience + (title (string-ascii 100)) + (description (string-ascii 500)) + (category (string-ascii 50)) + (price uint) + (metadata-uri (string-ascii 200)) +) + (let ( + (experience-id (var-get next-experience-id)) + ) + ;; Input validation to fix warnings + (asserts! (> (len title) u0) (err err-invalid-input)) + (asserts! (> (len description) u0) (err err-invalid-input)) + (asserts! (> (len category) u0) (err err-invalid-input)) + (asserts! (<= price u1000000000000) (err err-invalid-input)) ;; Max price limit + (asserts! (> (len metadata-uri) u0) (err err-invalid-input)) + + (map-set ar-experiences + { experience-id: experience-id } + { + creator: tx-sender, + title: title, + description: description, + category: category, + price: price, + metadata-uri: metadata-uri, + created-at: block-height, + is-active: true, + total-downloads: u0, + average-rating: u0, + rating-count: u0 + } + ) + + ;; Update creator profile + (match (map-get? creator-profiles { creator: tx-sender }) + existing-profile (map-set creator-profiles + { creator: tx-sender } + (merge existing-profile { total-experiences: (+ (get total-experiences existing-profile) u1) }) + ) + (map-set creator-profiles + { creator: tx-sender } + { + display-name: "", + bio: "", + total-experiences: u1, + total-earnings: u0, + verified: false + } + ) + ) + + (var-set next-experience-id (+ experience-id u1)) + (ok experience-id) + ) +) + +;; Purchase AR Experience +(define-public (purchase-experience (experience-id uint)) + (let ( + (experience (unwrap! (map-get? ar-experiences { experience-id: experience-id }) (err err-not-found))) + (price (get price experience)) + (creator (get creator experience)) + (platform-fee (calculate-platform-fee price)) + (creator-payment (- price platform-fee)) + ) + ;; Input validation + (asserts! (> experience-id u0) (err err-invalid-input)) + (asserts! (< experience-id (var-get next-experience-id)) (err err-not-found)) + (asserts! (get is-active experience) (err err-unauthorized)) + (asserts! (is-none (map-get? user-purchases { user: tx-sender, experience-id: experience-id })) + (err err-already-exists)) + + ;; Handle payment transfers + (if (> price u0) + (begin + (unwrap! (stx-transfer? creator-payment tx-sender creator) (err err-insufficient-payment)) + (unwrap! (stx-transfer? platform-fee tx-sender contract-owner) (err err-insufficient-payment)) + ) + true + ) + + ;; Record purchase + (map-set user-purchases + { user: tx-sender, experience-id: experience-id } + { purchased-at: block-height, access-granted: true } + ) + + ;; Update experience stats + (map-set ar-experiences + { experience-id: experience-id } + (merge experience { total-downloads: (+ (get total-downloads experience) u1) }) + ) + + ;; Update creator earnings + (match (map-get? creator-profiles { creator: creator }) + existing-profile (map-set creator-profiles + { creator: creator } + (merge existing-profile { total-earnings: (+ (get total-earnings existing-profile) creator-payment) }) + ) + true + ) + + (ok true) + ) +) + +;; Rate AR Experience +(define-public (rate-experience + (experience-id uint) + (rating uint) + (review (string-ascii 300)) +) + (begin + ;; Input validation + (asserts! (> experience-id u0) (err err-invalid-input)) + (asserts! (< experience-id (var-get next-experience-id)) (err err-not-found)) + (asserts! (and (>= rating u1) (<= rating u5)) (err err-invalid-rating)) + (asserts! (<= (len review) u300) (err err-invalid-input)) + (asserts! (is-some (map-get? ar-experiences { experience-id: experience-id })) (err err-not-found)) + (asserts! (is-some (map-get? user-purchases { user: tx-sender, experience-id: experience-id })) + (err err-unauthorized)) + + (map-set experience-ratings + { user: tx-sender, experience-id: experience-id } + { rating: rating, review: review, rated-at: block-height } + ) + + (try! (update-experience-rating experience-id rating)) + (ok true) + ) +) + +;; Update Creator Profile +(define-public (update-creator-profile + (display-name (string-ascii 50)) + (bio (string-ascii 200)) +) + (begin + ;; Input validation + (asserts! (<= (len display-name) u50) (err err-invalid-input)) + (asserts! (<= (len bio) u200) (err err-invalid-input)) + + (match (map-get? creator-profiles { creator: tx-sender }) + existing-profile (map-set creator-profiles + { creator: tx-sender } + (merge existing-profile { display-name: display-name, bio: bio }) + ) + (map-set creator-profiles + { creator: tx-sender } + { + display-name: display-name, + bio: bio, + total-experiences: u0, + total-earnings: u0, + verified: false + } + ) + ) + (ok true) + ) +) + +;; Toggle Experience Status (Creator only) +(define-public (toggle-experience-status (experience-id uint)) + (let ( + (experience (unwrap! (map-get? ar-experiences { experience-id: experience-id }) (err err-not-found))) + ) + ;; Input validation + (asserts! (> experience-id u0) (err err-invalid-input)) + (asserts! (< experience-id (var-get next-experience-id)) (err err-not-found)) + (asserts! (is-eq tx-sender (get creator experience)) (err err-unauthorized)) + + (map-set ar-experiences + { experience-id: experience-id } + (merge experience { is-active: (not (get is-active experience)) }) + ) + (ok true) + ) +) + +;; Admin Functions +(define-public (verify-creator (creator principal)) + (begin + (asserts! (is-contract-owner) (err err-owner-only)) + ;; Input validation - ensure creator is not contract owner + (asserts! (not (is-eq creator contract-owner)) (err err-invalid-input)) + + (match (map-get? creator-profiles { creator: creator }) + existing-profile (begin + (map-set creator-profiles + { creator: creator } + (merge existing-profile { verified: true }) + ) + (ok true) + ) + (err err-not-found) + ) + ) +) + +(define-public (set-platform-fee (new-fee uint)) + (begin + (asserts! (is-contract-owner) (err err-owner-only)) + (asserts! (<= new-fee u20) (err err-invalid-input)) + (var-set platform-fee-percentage new-fee) + (ok true) + ) +) + +;; Read-only Functions +(define-read-only (get-experience (experience-id uint)) + (map-get? ar-experiences { experience-id: experience-id }) +) + +(define-read-only (get-user-purchase (user principal) (experience-id uint)) + (map-get? user-purchases { user: user, experience-id: experience-id }) +) + +(define-read-only (get-experience-rating (user principal) (experience-id uint)) + (map-get? experience-ratings { user: user, experience-id: experience-id }) +) + +(define-read-only (get-creator-profile (creator principal)) + (map-get? creator-profiles { creator: creator }) +) + +(define-read-only (has-access (user principal) (experience-id uint)) + (match (map-get? user-purchases { user: user, experience-id: experience-id }) + purchase (get access-granted purchase) + false + ) +) + +(define-read-only (get-next-experience-id) + (var-get next-experience-id) +) + +(define-read-only (get-platform-fee) + (var-get platform-fee-percentage) +) + +(define-read-only (get-experience-stats (experience-id uint)) + (match (map-get? ar-experiences { experience-id: experience-id }) + experience (ok { + downloads: (get total-downloads experience), + rating: (get average-rating experience), + rating-count: (get rating-count experience), + is-active: (get is-active experience) + }) + (err err-not-found) + ) +) \ No newline at end of file diff --git a/reality-experience/settings/Devnet.toml b/reality-experience/settings/Devnet.toml new file mode 100644 index 0000000..8a5ff75 --- /dev/null +++ b/reality-experience/settings/Devnet.toml @@ -0,0 +1,127 @@ +[network] +name = "devnet" +deployment_fee_rate = 10 + +[accounts.deployer] +mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw" +balance = 100_000_000_000_000 +# secret_key: 753b7cc01a1a2e86221266a154af739463fce51219d97e4f856cd7200c3bd2a601 +# stx_address: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM +# btc_address: mqVnk6NPRdhntvfm4hh9vvjiRkFDUuSYsH + +[accounts.wallet_1] +mnemonic = "sell invite acquire kitten bamboo drastic jelly vivid peace spawn twice guilt pave pen trash pretty park cube fragile unaware remain midnight betray rebuild" +balance = 100_000_000_000_000 +# secret_key: 7287ba251d44a4d3fd9276c88ce34c5c52a038955511cccaf77e61068649c17801 +# stx_address: ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5 +# btc_address: mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC + +[accounts.wallet_2] +mnemonic = "hold excess usual excess ring elephant install account glad dry fragile donkey gaze humble truck breeze nation gasp vacuum limb head keep delay hospital" +balance = 100_000_000_000_000 +# secret_key: 530d9f61984c888536871c6573073bdfc0058896dc1adfe9a6a10dfacadc209101 +# stx_address: ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG +# btc_address: muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG + +[accounts.wallet_3] +mnemonic = "cycle puppy glare enroll cost improve round trend wrist mushroom scorpion tower claim oppose clever elephant dinosaur eight problem before frozen dune wagon high" +balance = 100_000_000_000_000 +# secret_key: d655b2523bcd65e34889725c73064feb17ceb796831c0e111ba1a552b0f31b3901 +# stx_address: ST2JHG361ZXG51QTKY2NQCVBPPRRE2KZB1HR05NNC +# btc_address: mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7 + +[accounts.wallet_4] +mnemonic = "board list obtain sugar hour worth raven scout denial thunder horse logic fury scorpion fold genuine phrase wealth news aim below celery when cabin" +balance = 100_000_000_000_000 +# secret_key: f9d7206a47f14d2870c163ebab4bf3e70d18f5d14ce1031f3902fbbc894fe4c701 +# stx_address: ST2NEB84ASENDXKYGJPQW86YXQCEFEX2ZQPG87ND +# btc_address: mg1C76bNTutiCDV3t9nWhZs3Dc8LzUufj8 + +[accounts.wallet_5] +mnemonic = "hurry aunt blame peanut heavy update captain human rice crime juice adult scale device promote vast project quiz unit note reform update climb purchase" +balance = 100_000_000_000_000 +# secret_key: 3eccc5dac8056590432db6a35d52b9896876a3d5cbdea53b72400bc9c2099fe801 +# stx_address: ST2REHHS5J3CERCRBEPMGH7921Q6PYKAADT7JP2VB +# btc_address: mweN5WVqadScHdA81aATSdcVr4B6dNokqx + +[accounts.wallet_6] +mnemonic = "area desk dutch sign gold cricket dawn toward giggle vibrant indoor bench warfare wagon number tiny universe sand talk dilemma pottery bone trap buddy" +balance = 100_000_000_000_000 +# secret_key: 7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01 +# stx_address: ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0 +# btc_address: mzxXgV6e4BZSsz8zVHm3TmqbECt7mbuErt + +[accounts.wallet_7] +mnemonic = "prevent gallery kind limb income control noise together echo rival record wedding sense uncover school version force bleak nuclear include danger skirt enact arrow" +balance = 100_000_000_000_000 +# secret_key: b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401 +# stx_address: ST3PF13W7Z0RRM42A8VZRVFQ75SV1K26RXEP8YGKJ +# btc_address: n37mwmru2oaVosgfuvzBwgV2ysCQRrLko7 + +[accounts.wallet_8] +mnemonic = "female adjust gallery certain visit token during great side clown fitness like hurt clip knife warm bench start reunion globe detail dream depend fortune" +balance = 100_000_000_000_000 +# secret_key: 6a1a754ba863d7bab14adbbc3f8ebb090af9e871ace621d3e5ab634e1422885e01 +# stx_address: ST3NBRSFKX28FQ2ZJ1MAKX58HKHSDGNV5N7R21XCP +# btc_address: n2v875jbJ4RjBnTjgbfikDfnwsDV5iUByw + +[accounts.wallet_9] +mnemonic = "shadow private easily thought say logic fault paddle word top book during ignore notable orange flight clock image wealth health outside kitten belt reform" +balance = 100_000_000_000_000 +# secret_key: de433bdfa14ec43aa1098d5be594c8ffb20a31485ff9de2923b2689471c401b801 +# stx_address: STNHKEPYEPJ8ET55ZZ0M5A34J0R3N5FM2CMMMAZ6 +# btc_address: mjSrB3wS4xab3kYqFktwBzfTdPg367ZJ2d + +[devnet] +disable_bitcoin_explorer = true +# disable_stacks_explorer = true +# disable_stacks_api = true +# working_dir = "tmp/devnet" +# stacks_node_events_observers = ["host.docker.internal:8002"] +# miner_mnemonic = "twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw" +# miner_derivation_path = "m/44'/5757'/0'/0/0" +# orchestrator_port = 20445 +# bitcoin_node_p2p_port = 18444 +# bitcoin_node_rpc_port = 18443 +# bitcoin_node_username = "devnet" +# bitcoin_node_password = "devnet" +# bitcoin_controller_port = 18442 +# bitcoin_controller_block_time = 30_000 +# stacks_node_rpc_port = 20443 +# stacks_node_p2p_port = 20444 +# stacks_api_port = 3999 +# stacks_api_events_port = 3700 +# bitcoin_explorer_port = 8001 +# stacks_explorer_port = 8000 +# postgres_port = 5432 +# postgres_username = "postgres" +# postgres_password = "postgres" +# postgres_database = "postgres" +# bitcoin_node_image_url = "quay.io/hirosystems/bitcoind:devnet" +# stacks_node_image_url = "localhost:5000/stacks-node:devnet" +# stacks_api_image_url = "blockstack/stacks-blockchain-api:latest" +# stacks_explorer_image_url = "blockstack/explorer:latest" +# bitcoin_explorer_image_url = "quay.io/hirosystems/bitcoin-explorer:devnet" +# postgres_image_url = "postgres:alpine" + +# Send some stacking orders +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_1" +slots = 2 +btc_address = "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_2" +slots = 1 +btc_address = "muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 3 +duration = 12 +wallet = "wallet_3" +slots = 1 +btc_address = "mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7" diff --git a/reality-experience/tests/augmented_test.ts b/reality-experience/tests/augmented_test.ts new file mode 100644 index 0000000..9a18ae0 --- /dev/null +++ b/reality-experience/tests/augmented_test.ts @@ -0,0 +1,26 @@ + +import { Clarinet, Tx, Chain, Account, types } from 'https://deno.land/x/clarinet@v0.14.0/index.ts'; +import { assertEquals } from 'https://deno.land/std@0.90.0/testing/asserts.ts'; + +Clarinet.test({ + name: "Ensure that <...>", + async fn(chain: Chain, accounts: Map) { + let block = chain.mineBlock([ + /* + * Add transactions with: + * Tx.contractCall(...) + */ + ]); + assertEquals(block.receipts.length, 0); + assertEquals(block.height, 2); + + block = chain.mineBlock([ + /* + * Add transactions with: + * Tx.contractCall(...) + */ + ]); + assertEquals(block.receipts.length, 0); + assertEquals(block.height, 3); + }, +});