From 2ca5fe060639493bb523fce0dfa485b7985ac17f Mon Sep 17 00:00:00 2001 From: bukas898 Date: Thu, 20 Feb 2025 13:36:21 +0100 Subject: [PATCH 1/4] Implements core functionality for course registration and enrollment --- blocklearn-dir/.gitattributes | 3 + blocklearn-dir/.gitignore | 13 ++ blocklearn-dir/.vscode/settings.json | 4 + blocklearn-dir/.vscode/tasks.json | 19 +++ blocklearn-dir/Clarinet.toml | 19 +++ blocklearn-dir/contracts/bl_contract.clar | 88 ++++++++++++ blocklearn-dir/package.json | 24 ++++ blocklearn-dir/settings/Devnet.toml | 157 ++++++++++++++++++++++ blocklearn-dir/tests/bl_contract.test.ts | 21 +++ blocklearn-dir/tsconfig.json | 26 ++++ blocklearn-dir/vitest.config.js | 42 ++++++ 11 files changed, 416 insertions(+) create mode 100644 blocklearn-dir/.gitattributes create mode 100644 blocklearn-dir/.gitignore create mode 100644 blocklearn-dir/.vscode/settings.json create mode 100644 blocklearn-dir/.vscode/tasks.json create mode 100644 blocklearn-dir/Clarinet.toml create mode 100644 blocklearn-dir/contracts/bl_contract.clar create mode 100644 blocklearn-dir/package.json create mode 100644 blocklearn-dir/settings/Devnet.toml create mode 100644 blocklearn-dir/tests/bl_contract.test.ts create mode 100644 blocklearn-dir/tsconfig.json create mode 100644 blocklearn-dir/vitest.config.js diff --git a/blocklearn-dir/.gitattributes b/blocklearn-dir/.gitattributes new file mode 100644 index 0000000..da6a065 --- /dev/null +++ b/blocklearn-dir/.gitattributes @@ -0,0 +1,3 @@ +tests/** linguist-vendored +vitest.config.js linguist-vendored +* text=lf diff --git a/blocklearn-dir/.gitignore b/blocklearn-dir/.gitignore new file mode 100644 index 0000000..76c2842 --- /dev/null +++ b/blocklearn-dir/.gitignore @@ -0,0 +1,13 @@ + +**/settings/Mainnet.toml +**/settings/Testnet.toml +.cache/** +history.txt + +logs +*.log +npm-debug.log* +coverage +*.info +costs-reports.json +node_modules diff --git a/blocklearn-dir/.vscode/settings.json b/blocklearn-dir/.vscode/settings.json new file mode 100644 index 0000000..3062519 --- /dev/null +++ b/blocklearn-dir/.vscode/settings.json @@ -0,0 +1,4 @@ + +{ + "files.eol": "\n" +} diff --git a/blocklearn-dir/.vscode/tasks.json b/blocklearn-dir/.vscode/tasks.json new file mode 100644 index 0000000..4dec0ff --- /dev/null +++ b/blocklearn-dir/.vscode/tasks.json @@ -0,0 +1,19 @@ + +{ + "version": "2.0.0", + "tasks": [ + { + "label": "check contracts", + "group": "test", + "type": "shell", + "command": "clarinet check" + }, + { + "type": "npm", + "script": "test", + "group": "test", + "problemMatcher": [], + "label": "npm test" + } + ] +} diff --git a/blocklearn-dir/Clarinet.toml b/blocklearn-dir/Clarinet.toml new file mode 100644 index 0000000..bb612bb --- /dev/null +++ b/blocklearn-dir/Clarinet.toml @@ -0,0 +1,19 @@ +[project] +name = 'blocklearn-dir' +description = '' +authors = [] +telemetry = true +cache_dir = '.\.cache' +requirements = [] +[contracts.bl_contract] +path = 'contracts/bl_contract.clar' +clarity_version = 2 +epoch = 2.5 +[repl.analysis] +passes = ['check_checker'] + +[repl.analysis.check_checker] +strict = false +trusted_sender = false +trusted_caller = false +callee_filter = false diff --git a/blocklearn-dir/contracts/bl_contract.clar b/blocklearn-dir/contracts/bl_contract.clar new file mode 100644 index 0000000..091889f --- /dev/null +++ b/blocklearn-dir/contracts/bl_contract.clar @@ -0,0 +1,88 @@ +;; Edu Management System + +;; Initial version with basic features + +;; Error codes +(define-constant ERR-UNAUTHORIZED-ACCESS (err u1)) +(define-constant ERR-INVALID-PRICING (err u2)) +(define-constant ERR-DUPLICATE-ENROLLMENT (err u3)) +(define-constant ERR-COURSE-NOT-FOUND (err u4)) +(define-constant ERR-INSUFFICIENT-BALANCE (err u5)) +(define-constant ERR-INVALID-COURSE-ID (err u6)) + +;; Data maps +(define-map course-registry + { course-id: uint } + { + educator: principal, + course-price-stx: uint, + syllabus-uri: (string-utf8 256) + } +) + +(define-map student-enrollments + { student: principal, course-id: uint } + { + enrollment-timestamp: uint, + enrollment-active: bool + } +) + +;; Private functions +(define-private (execute-stx-transfer (amount uint) (recipient principal)) + (stx-transfer? amount tx-sender recipient) +) + +;; Public functions +(define-public (register-course (course-id uint) + (course-price-stx uint) + (syllabus-uri (string-utf8 256))) + (begin + (asserts! (> course-id u0) ERR-INVALID-COURSE-ID) + (asserts! (> course-price-stx u0) ERR-INVALID-PRICING) + + (map-set course-registry + { course-id: course-id } + { + educator: tx-sender, + course-price-stx: course-price-stx, + syllabus-uri: syllabus-uri + } + ) + (ok true) + ) +) + +(define-public (enroll-in-course (course-id uint)) + (let + ( + (course-details (unwrap! (map-get? course-registry { course-id: course-id }) ERR-COURSE-NOT-FOUND)) + (educator-address (get educator course-details)) + ) + + (asserts! (> course-id u0) ERR-INVALID-COURSE-ID) + (try! (execute-stx-transfer (get course-price-stx course-details) educator-address)) + + (map-set student-enrollments + { student: tx-sender, course-id: course-id } + { + enrollment-timestamp: block-height, + enrollment-active: true + } + ) + + (ok true) + ) +) + +;; Read-only functions +(define-read-only (get-course-info (course-id uint)) + (map-get? course-registry { course-id: course-id }) +) + +(define-read-only (get-enrollment-status (student principal) (course-id uint)) + (match (map-get? student-enrollments { student: student, course-id: course-id }) + enrollment-record (ok (get enrollment-active enrollment-record)) + ERR-COURSE-NOT-FOUND + ) +) diff --git a/blocklearn-dir/package.json b/blocklearn-dir/package.json new file mode 100644 index 0000000..644a6c2 --- /dev/null +++ b/blocklearn-dir/package.json @@ -0,0 +1,24 @@ + +{ + "name": "blocklearn-dir-tests", + "version": "1.0.0", + "description": "Run unit tests on this project.", + "type": "module", + "private": true, + "scripts": { + "test": "vitest run", + "test:report": "vitest run -- --coverage --costs", + "test:watch": "chokidar \"tests/**/*.ts\" \"contracts/**/*.clar\" -c \"npm run test:report\"" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@hirosystems/clarinet-sdk": "^2.3.2", + "@stacks/transactions": "^6.12.0", + "chokidar-cli": "^3.0.0", + "typescript": "^5.3.3", + "vite": "^5.1.4", + "vitest": "^1.3.1", + "vitest-environment-clarinet": "^2.0.0" + } +} diff --git a/blocklearn-dir/settings/Devnet.toml b/blocklearn-dir/settings/Devnet.toml new file mode 100644 index 0000000..54a2c35 --- /dev/null +++ b/blocklearn-dir/settings/Devnet.toml @@ -0,0 +1,157 @@ +[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.faucet] +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_stacks_explorer = false +disable_stacks_api = false +# disable_postgres = false +# disable_subnet_api = false +# disable_bitcoin_explorer = true +# working_dir = "tmp/devnet" +# stacks_node_events_observers = ["host.docker.internal:8002"] +# miner_mnemonic = "fragile loan twenty basic net assault jazz absorb diet talk art shock innocent float punch travel gadget embrace caught blossom hockey surround initial reduce" +# miner_derivation_path = "m/44'/5757'/0'/0/0" +# faucet_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" +# faucet_derivation_path = "m/44'/5757'/0'/0/0" +# stacker_mnemonic = "empty lens any direct brother then drop fury rule pole win claim scissors list rescue horn rent inform relief jump sword weekend half legend" +# stacker_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_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:26.0" +# stacks_node_image_url = "quay.io/hirosystems/stacks-node:devnet-3.0" +# stacks_signer_image_url = "quay.io/hirosystems/stacks-signer:devnet-3.0" +# stacks_api_image_url = "hirosystems/stacks-blockchain-api:master" +# stacks_explorer_image_url = "hirosystems/explorer:latest" +# bitcoin_explorer_image_url = "quay.io/hirosystems/bitcoin-explorer:devnet" +# postgres_image_url = "postgres:alpine" +# enable_subnet_node = true +# subnet_node_image_url = "hirosystems/stacks-subnets:0.8.1" +# subnet_leader_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" +# subnet_leader_derivation_path = "m/44'/5757'/0'/0/0" +# subnet_contract_id = "ST173JK7NZBA4BS05ZRATQH1K89YJMTGEH1Z5J52E.subnet-v3-0-1" +# subnet_node_rpc_port = 30443 +# subnet_node_p2p_port = 30444 +# subnet_events_ingestion_port = 30445 +# subnet_node_events_observers = ["host.docker.internal:8002"] +# subnet_api_image_url = "hirosystems/stacks-blockchain-api:master" +# subnet_api_postgres_database = "subnet_api" + +# epoch_2_0 = 100 +# epoch_2_05 = 100 +# epoch_2_1 = 101 +# epoch_2_2 = 102 +# epoch_2_3 = 103 +# epoch_2_4 = 104 +# epoch_2_5 = 108 +# epoch_3_0 = 142 + +# Send some stacking orders +[[devnet.pox_stacking_orders]] +start_at_cycle = 1 +duration = 10 +auto_extend = true +wallet = "wallet_1" +slots = 2 +btc_address = "mr1iPkD9N3RJZZxXRk7xF9d36gffa6exNC" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 1 +duration = 10 +auto_extend = true +wallet = "wallet_2" +slots = 2 +btc_address = "muYdXKmX9bByAueDe6KFfHd5Ff1gdN9ErG" + +[[devnet.pox_stacking_orders]] +start_at_cycle = 1 +duration = 10 +auto_extend = true +wallet = "wallet_3" +slots = 2 +btc_address = "mvZtbibDAAA3WLpY7zXXFqRa3T4XSknBX7" + diff --git a/blocklearn-dir/tests/bl_contract.test.ts b/blocklearn-dir/tests/bl_contract.test.ts new file mode 100644 index 0000000..4bb9cf3 --- /dev/null +++ b/blocklearn-dir/tests/bl_contract.test.ts @@ -0,0 +1,21 @@ + +import { describe, expect, it } from "vitest"; + +const accounts = simnet.getAccounts(); +const address1 = accounts.get("wallet_1")!; + +/* + The test below is an example. To learn more, read the testing documentation here: + https://docs.hiro.so/stacks/clarinet-js-sdk +*/ + +describe("example tests", () => { + it("ensures simnet is well initalised", () => { + expect(simnet.blockHeight).toBeDefined(); + }); + + // it("shows an example", () => { + // const { result } = simnet.callReadOnlyFn("counter", "get-counter", [], address1); + // expect(result).toBeUint(0); + // }); +}); diff --git a/blocklearn-dir/tsconfig.json b/blocklearn-dir/tsconfig.json new file mode 100644 index 0000000..1bdaf36 --- /dev/null +++ b/blocklearn-dir/tsconfig.json @@ -0,0 +1,26 @@ + +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext"], + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + "strict": true, + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "node_modules/@hirosystems/clarinet-sdk/vitest-helpers/src", + "tests" + ] +} diff --git a/blocklearn-dir/vitest.config.js b/blocklearn-dir/vitest.config.js new file mode 100644 index 0000000..c6a8506 --- /dev/null +++ b/blocklearn-dir/vitest.config.js @@ -0,0 +1,42 @@ + +/// + +import { defineConfig } from "vite"; +import { vitestSetupFilePath, getClarinetVitestsArgv } from "@hirosystems/clarinet-sdk/vitest"; + +/* + In this file, Vitest is configured so that it works seamlessly with Clarinet and the Simnet. + + The `vitest-environment-clarinet` will initialise the clarinet-sdk + and make the `simnet` object available globally in the test files. + + `vitestSetupFilePath` points to a file in the `@hirosystems/clarinet-sdk` package that does two things: + - run `before` hooks to initialize the simnet and `after` hooks to collect costs and coverage reports. + - load custom vitest matchers to work with Clarity values (such as `expect(...).toBeUint()`) + + The `getClarinetVitestsArgv()` will parse options passed to the command `vitest run --` + - vitest run -- --manifest ./Clarinet.toml # pass a custom path + - vitest run -- --coverage --costs # collect coverage and cost reports +*/ + +export default defineConfig({ + test: { + environment: "clarinet", // use vitest-environment-clarinet + pool: "forks", + poolOptions: { + threads: { singleThread: true }, + forks: { singleFork: true }, + }, + setupFiles: [ + vitestSetupFilePath, + // custom setup files can be added here + ], + environmentOptions: { + clarinet: { + ...getClarinetVitestsArgv(), + // add or override options + }, + }, + }, +}); + From 11244637e6f359cd0f0c991a1ffe4fc63e9c0417 Mon Sep 17 00:00:00 2001 From: bukas898 Date: Thu, 20 Feb 2025 13:38:29 +0100 Subject: [PATCH 2/4] Added revenue sharing, subscription features, and platform administration --- blocklearn-dir/contracts/bl_contract.clar | 109 ++++++++++++++++++---- 1 file changed, 91 insertions(+), 18 deletions(-) diff --git a/blocklearn-dir/contracts/bl_contract.clar b/blocklearn-dir/contracts/bl_contract.clar index 091889f..6b5b0c7 100644 --- a/blocklearn-dir/contracts/bl_contract.clar +++ b/blocklearn-dir/contracts/bl_contract.clar @@ -1,14 +1,20 @@ -;; Edu Management System +;; Intermediate Course Management System -;; Initial version with basic features +;; Enhanced version with monetization features ;; Error codes (define-constant ERR-UNAUTHORIZED-ACCESS (err u1)) -(define-constant ERR-INVALID-PRICING (err u2)) +(define-constant ERR-INVALID-PRICING-PARAMETERS (err u2)) (define-constant ERR-DUPLICATE-ENROLLMENT (err u3)) (define-constant ERR-COURSE-NOT-FOUND (err u4)) -(define-constant ERR-INSUFFICIENT-BALANCE (err u5)) -(define-constant ERR-INVALID-COURSE-ID (err u6)) +(define-constant ERR-INSUFFICIENT-STX-BALANCE (err u5)) +(define-constant ERR-ENROLLMENT-EXPIRED (err u6)) +(define-constant ERR-INVALID-COURSE-ID (err u7)) +(define-constant ERR-INVALID-SYLLABUS-URI (err u8)) + +;; Data variables +(define-data-var platform-administrator principal tx-sender) +(define-data-var platform-commission-rate uint u50) ;; 5% platform fee (base 1000) ;; Data maps (define-map course-registry @@ -16,7 +22,10 @@ { educator: principal, course-price-stx: uint, - syllabus-uri: (string-utf8 256) + educator-revenue-percentage: uint, + syllabus-uri: (string-utf8 256), + subscription-enabled: bool, + enrollment-period-blocks: uint } ) @@ -24,11 +33,29 @@ { student: principal, course-id: uint } { enrollment-timestamp: uint, - enrollment-active: bool + enrollment-end-block: uint, + enrollment-status-active: bool } ) +(define-map educator-earnings-ledger + { educator: principal } + { available-balance: uint } +) + ;; Private functions +(define-private (calculate-revenue-distribution (total-price uint)) + (let + ( + (platform-fee-amount (/ (* total-price (var-get platform-commission-rate)) u1000)) + ) + { + platform-commission: platform-fee-amount, + educator-earnings: (- total-price platform-fee-amount) + } + ) +) + (define-private (execute-stx-transfer (amount uint) (recipient principal)) (stx-transfer? amount tx-sender recipient) ) @@ -36,17 +63,24 @@ ;; Public functions (define-public (register-course (course-id uint) (course-price-stx uint) - (syllabus-uri (string-utf8 256))) + (educator-revenue-percentage uint) + (syllabus-uri (string-utf8 256)) + (subscription-enabled bool) + (enrollment-period-blocks uint)) (begin (asserts! (> course-id u0) ERR-INVALID-COURSE-ID) - (asserts! (> course-price-stx u0) ERR-INVALID-PRICING) + (asserts! (> course-price-stx u0) ERR-INVALID-PRICING-PARAMETERS) + (asserts! (and (>= educator-revenue-percentage u0) (<= educator-revenue-percentage u1000)) ERR-INVALID-PRICING-PARAMETERS) (map-set course-registry { course-id: course-id } { educator: tx-sender, course-price-stx: course-price-stx, - syllabus-uri: syllabus-uri + educator-revenue-percentage: educator-revenue-percentage, + syllabus-uri: syllabus-uri, + subscription-enabled: subscription-enabled, + enrollment-period-blocks: enrollment-period-blocks } ) (ok true) @@ -57,17 +91,30 @@ (let ( (course-details (unwrap! (map-get? course-registry { course-id: course-id }) ERR-COURSE-NOT-FOUND)) + (revenue-distribution (calculate-revenue-distribution (get course-price-stx course-details))) (educator-address (get educator course-details)) + (current-block-height block-height) ) - (asserts! (> course-id u0) ERR-INVALID-COURSE-ID) - (try! (execute-stx-transfer (get course-price-stx course-details) educator-address)) + (try! (execute-stx-transfer (get course-price-stx course-details) (as-contract tx-sender))) + + (map-set educator-earnings-ledger + { educator: educator-address } + { + available-balance: (+ (default-to u0 + (get available-balance (map-get? educator-earnings-ledger { educator: educator-address }))) + (get educator-earnings revenue-distribution)) + } + ) (map-set student-enrollments { student: tx-sender, course-id: course-id } { - enrollment-timestamp: block-height, - enrollment-active: true + enrollment-timestamp: current-block-height, + enrollment-end-block: (if (get subscription-enabled course-details) + (+ current-block-height (get enrollment-period-blocks course-details)) + u0), + enrollment-status-active: true } ) @@ -75,14 +122,40 @@ ) ) +(define-public (withdraw-educator-earnings) + (let + ( + (educator-earnings-record (unwrap! (map-get? educator-earnings-ledger { educator: tx-sender }) ERR-COURSE-NOT-FOUND)) + (withdrawal-amount (get available-balance educator-earnings-record)) + ) + + (asserts! (> withdrawal-amount u0) ERR-INSUFFICIENT-STX-BALANCE) + + (map-set educator-earnings-ledger + { educator: tx-sender } + { available-balance: u0 } + ) + + (try! (execute-stx-transfer withdrawal-amount tx-sender)) + (ok true) + ) +) + ;; Read-only functions (define-read-only (get-course-info (course-id uint)) (map-get? course-registry { course-id: course-id }) ) -(define-read-only (get-enrollment-status (student principal) (course-id uint)) - (match (map-get? student-enrollments { student: student, course-id: course-id }) - enrollment-record (ok (get enrollment-active enrollment-record)) - ERR-COURSE-NOT-FOUND +(define-read-only (get-educator-current-balance (educator principal)) + (default-to u0 (get available-balance (map-get? educator-earnings-ledger { educator: educator }))) +) + +;; Administrative functions +(define-public (update-platform-commission (new-commission-rate uint)) + (begin + (asserts! (is-eq tx-sender (var-get platform-administrator)) ERR-UNAUTHORIZED-ACCESS) + (asserts! (<= new-commission-rate u1000) ERR-INVALID-PRICING-PARAMETERS) + (var-set platform-commission-rate new-commission-rate) + (ok true) ) ) From 6f0c4d5dbb0bb80207f09db44abe372162a6a864 Mon Sep 17 00:00:00 2001 From: bukas898 Date: Thu, 20 Feb 2025 13:47:47 +0100 Subject: [PATCH 3/4] Complete feature set with enhanced security, subscription management, and administrative controls --- blocklearn-dir/contracts/bl_contract.clar | 71 +++++++++++++++++++++-- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/blocklearn-dir/contracts/bl_contract.clar b/blocklearn-dir/contracts/bl_contract.clar index 6b5b0c7..20236c1 100644 --- a/blocklearn-dir/contracts/bl_contract.clar +++ b/blocklearn-dir/contracts/bl_contract.clar @@ -1,6 +1,5 @@ -;; Intermediate Course Management System - -;; Enhanced version with monetization features +;; Final Course Management System +;; Production-ready version with full functionality ;; Error codes (define-constant ERR-UNAUTHORIZED-ACCESS (err u1)) @@ -9,8 +8,10 @@ (define-constant ERR-COURSE-NOT-FOUND (err u4)) (define-constant ERR-INSUFFICIENT-STX-BALANCE (err u5)) (define-constant ERR-ENROLLMENT-EXPIRED (err u6)) -(define-constant ERR-INVALID-COURSE-ID (err u7)) -(define-constant ERR-INVALID-SYLLABUS-URI (err u8)) +(define-constant ERR-INVALID-ENROLLMENT-DURATION (err u7)) +(define-constant ERR-INVALID-COURSE-ID (err u8)) +(define-constant ERR-INVALID-SYLLABUS-URI (err u9)) +(define-constant ERR-INVALID-ADMINISTRATOR (err u10)) ;; Data variables (define-data-var platform-administrator principal tx-sender) @@ -60,6 +61,16 @@ (stx-transfer? amount tx-sender recipient) ) +(define-private (verify-enrollment-status (student-address principal) (course-id uint)) + (match (map-get? student-enrollments { student: student-address, course-id: course-id }) + enrollment-record (and + (get enrollment-status-active enrollment-record) + (<= block-height (get enrollment-end-block enrollment-record)) + ) + false + ) +) + ;; Public functions (define-public (register-course (course-id uint) (course-price-stx uint) @@ -71,6 +82,8 @@ (asserts! (> course-id u0) ERR-INVALID-COURSE-ID) (asserts! (> course-price-stx u0) ERR-INVALID-PRICING-PARAMETERS) (asserts! (and (>= educator-revenue-percentage u0) (<= educator-revenue-percentage u1000)) ERR-INVALID-PRICING-PARAMETERS) + (asserts! (> (len syllabus-uri) u0) ERR-INVALID-SYLLABUS-URI) + (asserts! (or (not subscription-enabled) (> enrollment-period-blocks u0)) ERR-INVALID-ENROLLMENT-DURATION) (map-set course-registry { course-id: course-id } @@ -96,6 +109,9 @@ (current-block-height block-height) ) + (asserts! (> course-id u0) ERR-INVALID-COURSE-ID) + (asserts! (not (verify-enrollment-status tx-sender course-id)) ERR-DUPLICATE-ENROLLMENT) + (try! (execute-stx-transfer (get course-price-stx course-details) (as-contract tx-sender))) (map-set educator-earnings-ledger @@ -141,15 +157,51 @@ ) ) +(define-public (cancel-enrollment (course-id uint)) + (let + ( + (enrollment-record (unwrap! (map-get? student-enrollments + { student: tx-sender, course-id: course-id }) ERR-COURSE-NOT-FOUND)) + ) + + (asserts! (> course-id u0) ERR-INVALID-COURSE-ID) + (asserts! (get enrollment-status-active enrollment-record) ERR-COURSE-NOT-FOUND) + + (map-set student-enrollments + { student: tx-sender, course-id: course-id } + { + enrollment-timestamp: (get enrollment-timestamp enrollment-record), + enrollment-end-block: block-height, + enrollment-status-active: false + } + ) + (ok true) + ) +) + ;; Read-only functions (define-read-only (get-course-info (course-id uint)) (map-get? course-registry { course-id: course-id }) ) +(define-read-only (get-student-enrollment-info (student principal) (course-id uint)) + (map-get? student-enrollments { student: student, course-id: course-id }) +) + (define-read-only (get-educator-current-balance (educator principal)) (default-to u0 (get available-balance (map-get? educator-earnings-ledger { educator: educator }))) ) +(define-read-only (verify-course-access (student principal) (course-id uint)) + (begin + (asserts! (> course-id u0) ERR-INVALID-COURSE-ID) + (match (map-get? student-enrollments { student: student, course-id: course-id }) + enrollment-record (ok (verify-enrollment-status student course-id)) + ERR-COURSE-NOT-FOUND + ) + ) +) + ;; Administrative functions (define-public (update-platform-commission (new-commission-rate uint)) (begin @@ -159,3 +211,12 @@ (ok true) ) ) + +(define-public (transfer-platform-administration (new-administrator principal)) + (begin + (asserts! (is-eq tx-sender (var-get platform-administrator)) ERR-UNAUTHORIZED-ACCESS) + (asserts! (not (is-eq new-administrator 'SP000000000000000000002Q6VF78)) ERR-INVALID-ADMINISTRATOR) + (var-set platform-administrator new-administrator) + (ok true) + ) +) \ No newline at end of file From 465b8f77842cd974b9ea3e9ace3354763bdf4229 Mon Sep 17 00:00:00 2001 From: bukas898 Date: Thu, 20 Feb 2025 13:57:10 +0100 Subject: [PATCH 4/4] added a README file --- blocklearn-dir/README.md | 87 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 blocklearn-dir/README.md diff --git a/blocklearn-dir/README.md b/blocklearn-dir/README.md new file mode 100644 index 0000000..bf77b67 --- /dev/null +++ b/blocklearn-dir/README.md @@ -0,0 +1,87 @@ +# BlockLearn + +BlockLearn is a decentralized educational platform built on the Stacks blockchain that enables educators to create, monetize, and manage online courses while providing students with verifiable course enrollments and secure access to educational content. + +## Features + +- **Course Management** + - Educators can register courses with customizable pricing + - Support for both one-time purchases and subscription-based models + - Secure content access through blockchain verification + - Custom syllabus URI support for course materials + +- **Revenue Management** + - Automated revenue sharing between educators and platform + - Transparent commission structure + - Direct educator earnings withdrawal + - Real-time balance tracking + +- **Enrollment System** + - Secure student enrollment verification + - Subscription period management + - Enrollment status tracking + - Cancellation support + +- **Administrative Controls** + - Flexible platform commission adjustment + - Secure administrative transfer capability + - Built-in access control mechanisms + +## Smart Contract Functions + +### For Educators + +- `register-course`: Register a new course with pricing and enrollment parameters +- `withdraw-educator-earnings`: Withdraw accumulated teaching revenue +- `get-educator-current-balance`: Check current earnings balance + +### For Students + +- `enroll-in-course`: Enroll in a specific course +- `cancel-enrollment`: Cancel an active course enrollment +- `verify-course-access`: Verify access rights to course content + +### For Administrators + +- `update-platform-commission`: Update the platform's commission rate +- `transfer-platform-administration`: Transfer administrative rights + +## Error Codes + +| Code | Description | +|------|-------------| +| u1 | Unauthorized Access | +| u2 | Invalid Pricing Parameters | +| u3 | Duplicate Enrollment | +| u4 | Course Not Found | +| u5 | Insufficient STX Balance | +| u6 | Enrollment Expired | +| u7 | Invalid Enrollment Duration | +| u8 | Invalid Course ID | +| u9 | Invalid Syllabus URI | +| u10 | Invalid Administrator | + +## Technical Details + +- Built on Stacks blockchain +- Written in Clarity smart contract language +- Uses STX for payments and transactions +- Implements secure principal-based authentication + +## Getting Started + +1. Deploy the smart contract to the Stacks blockchain +2. Set up the initial platform administrator +3. Configure the platform commission rate +4. Begin registering courses and managing enrollments + +## Security Considerations + +- All financial transactions are secured by blockchain technology +- Access control is managed through principal-based authentication +- Revenue distribution is automated and transparent +- Administrative functions are protected by authorization checks + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request.