diff --git a/Clarinet.toml b/Clarinet.toml index 2570733..c6c4daa 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -1,21 +1,19 @@ [project] -name = "BitEstates" -description = "" +name = 'BitEstates' +description = '' authors = [] telemetry = true -cache_dir = "./.cache" - -# [contracts.counter] -# path = "contracts/counter.clar" - +cache_dir = './.cache' +requirements = [] +[contracts.property-registry] +path = 'contracts/property-registry.clar' +clarity_version = 3 +epoch = 3.1 [repl.analysis] -passes = ["check_checker"] -check_checker = { trusted_sender = false, trusted_caller = false, callee_filter = false } +passes = ['check_checker'] -# Check-checker settings: -# trusted_sender: if true, inputs are trusted after tx_sender has been checked. -# trusted_caller: if true, inputs are trusted after contract-caller has been checked. -# callee_filter: if true, untrusted data may be passed into a private function without a -# warning, if it gets checked inside. This check will also propagate up to the -# caller. -# More informations: https://www.hiro.so/blog/new-safety-checks-in-clarinet +[repl.analysis.check_checker] +strict = false +trusted_sender = false +trusted_caller = false +callee_filter = false diff --git a/README.md b/README.md new file mode 100644 index 0000000..d8908bc --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# BitEstates - Decentralized Property Registry + +A blockchain-based real estate documentation system leveraging Stacks Layer 2 infrastructure for secure title management and Bitcoin-compliant record keeping. + +## Overview + +BitEstates revolutionizes property registration through a decentralized smart contract system that combines Bitcoin's security with Stacks L2 efficiency. This solution enables immutable audit trails, standardized metadata management, and granular access controls for real estate assets. + +## Key Features + +- **Immutable Title Records**: Cryptographic proof of ownership anchored to Bitcoin +- **L2 Efficiency**: Low-cost transactions with Bitcoin finality through Stacks +- **Document Versioning**: Maintain historical records of property modifications +- **Standardized Metadata**: + - 64-character title strings + - 128-character descriptions + - 10-tag classification system +- **Granular Access Control**: Permission-based document viewing system +- **Automatic Audit Trails**: Block-height timestamped registrations + +## Technical Specifications + +### Data Structures + +#### Estate Registry Entry + +```clarity +{ + title: (string-ascii 64), + owner: principal, + file-size: uint, + registration-block: uint, + description: (string-ascii 128), + tags: (list 10 (string-ascii 32)) +} +``` + +#### Viewing Permissions + +```clarity +{ + entry-id: uint, + viewer: principal +} => { + access-allowed: bool +} +``` + +### System Constants + +| Constant | Value | Description | +| -------------------- | --------- | --------------------------- | +| registry-admin | tx-sender | Contract deployer | +| registry-entry-count | uint | Total registered properties | + +## Core Functions + +### 1. Property Registration + +**Function:** `register-property` + +Registers new property with system-enforced metadata standards: + +- Title: 1-64 ASCII characters +- Document Size: 1 byte - 1GB +- Description: 1-128 ASCII characters +- Tags: 1-10 tags (32 chars max each) + +```clarity +(register-property + "123 Main St" + 2048 + "Residential 3BR/2BA" + ["residential", "single-family"]) +``` + +### 2. Property Updates + +**Function:** `update-property` + +Owner-only modification of property details: + +```clarity +(update-property + 123 + "123 Main St - Renovated" + 4096 + "Updated 4BR/3BA layout" + ["renovated", "modern"]) +``` + +### 3. Ownership Transfer + +**Function:** `transfer-property` + +Secure ownership reassignment: + +```clarity +(transfer-property 123 'SP3NEWOWNER000000000000000000000000000) +``` + +### 4. Property Deletion + +**Function:** `delete-property` + +Owner-initiated registry removal: + +```clarity +(delete-property 123) +``` + +## Access Control + +| Action | Permission Required | +| -------- | ------------------------ | +| Register | None (open registration) | +| Update | Current Owner | +| Transfer | Current Owner | +| Delete | Current Owner | + +## Error Codes + +| Code | Description | Trigger Conditions | +| ---- | ----------------- | --------------------------- | +| u300 | Admin restriction | Unauthorized admin action | +| u301 | Missing entry | Invalid property ID | +| u302 | Duplicate entry | ID collision | +| u303 | Invalid metadata | Title/desc format violation | +| u304 | File size error | Document >1GB or 0-size | +| u305 | Access denied | Unauthorized modification | +| u306 | Ownership error | Non-owner action attempt | +| u307 | Visibility error | Restricted document access | +| u308 | Tag error | Invalid tag format/quantity | + +## Governance + +- **Immutable Admin**: Contract deployer set as permanent `registry-admin` +- **Metadata Standards**: Enforced through runtime validation +- **Upgrade Path**: Future versions via new contract deployment + +## Contributing + +1. Fork repository +2. Create feature branch (`git checkout -b feature/improvement`) +3. Commit changes +4. Push to branch +5. Open Pull Request diff --git a/contracts/property-registry.clar b/contracts/property-registry.clar new file mode 100644 index 0000000..6d3438c --- /dev/null +++ b/contracts/property-registry.clar @@ -0,0 +1,208 @@ +;; BitEstates: Decentralized Property Registry on Stacks L2 +;; +;; Summary: Blockchain-based real estate documentation system offering +;; secure title management, immutable audit trails, and Bitcoin-compliant +;; record keeping through Stacks Layer 2 infrastructure. +;; +;; Description: A next-generation property registry solution leveraging +;; Bitcoin's security model and Stacks L2 smart contracts for decentralized +;; title management. Implements granular access controls, document versioning, +;; and standardized metadata schemas while maintaining compliance with +;; blockchain-based property recording standards. + +;; System Governance Constants + +(define-constant registry-admin tx-sender) +(define-constant err-admin-only (err u300)) +(define-constant err-item-not-found (err u301)) +(define-constant err-item-already-exists (err u302)) +(define-constant err-invalid-title (err u303)) +(define-constant err-invalid-document-size (err u304)) +(define-constant err-unauthorized-access (err u305)) +(define-constant err-not-property-owner (err u306)) +(define-constant err-visibility-restricted (err u307)) +(define-constant err-tag-format-invalid (err u308)) + +;; Track total registry entries +(define-data-var registry-entry-count uint u0) + +;; Core Registry Storage + +;; Primary data store for property documents +(define-map estate-registry + { entry-id: uint } + { + title: (string-ascii 64), + owner: principal, + file-size: uint, + registration-block: uint, + description: (string-ascii 128), + tags: (list 10 (string-ascii 32)) + } +) + +;; Permission management for document access +(define-map viewing-permissions + { entry-id: uint, viewer: principal } + { access-allowed: bool } +) + +;; Utility & Validation Functions + +;; Checks if entry with given ID exists +(define-private (entry-exists (entry-id uint)) + (is-some (map-get? estate-registry { entry-id: entry-id })) +) + +;; Validates property tag format requirements +(define-private (valid-tag (tag (string-ascii 32))) + (and + (> (len tag) u0) + (< (len tag) u33) + ) +) + +;; Validates the entire set of property tags +(define-private (validate-tags (tag-list (list 10 (string-ascii 32)))) + (and + (> (len tag-list) u0) + (<= (len tag-list) u10) + (is-eq (len (filter valid-tag tag-list)) (len tag-list)) + ) +) + +;; Checks if principal is the owner of a property record +(define-private (owns-entry (entry-id uint) (viewer principal)) + (match (map-get? estate-registry { entry-id: entry-id }) + entry-data (is-eq (get owner entry-data) viewer) + false + ) +) + +;; Gets document size for specified entry +(define-private (get-document-size (entry-id uint)) + (default-to u0 + (get file-size + (map-get? estate-registry { entry-id: entry-id }) + ) + ) +) + +;; Registry Management Functions + +;; Registers a new property document in the system +(define-public (register-property + (property-title (string-ascii 64)) + (document-size uint) + (property-details (string-ascii 128)) + (property-tags (list 10 (string-ascii 32))) +) + (let + ( + (next-id (+ (var-get registry-entry-count) u1)) + ) + ;; Input validation checks + (asserts! (> (len property-title) u0) err-invalid-title) + (asserts! (< (len property-title) u65) err-invalid-title) + (asserts! (> document-size u0) err-invalid-document-size) + (asserts! (< document-size u1000000000) err-invalid-document-size) + (asserts! (> (len property-details) u0) err-invalid-title) + (asserts! (< (len property-details) u129) err-invalid-title) + (asserts! (validate-tags property-tags) err-tag-format-invalid) + + ;; Create the property record + (map-insert estate-registry + { entry-id: next-id } + { + title: property-title, + owner: tx-sender, + file-size: document-size, + registration-block: stacks-block-height, + description: property-details, + tags: property-tags + } + ) + + ;; Set initial access permissions + (map-insert viewing-permissions + { entry-id: next-id, viewer: tx-sender } + { access-allowed: true } + ) + + ;; Update counter and return success + (var-set registry-entry-count next-id) + (ok next-id) + ) +) + +;; Updates property information for an existing entry +(define-public (update-property + (entry-id uint) + (new-title (string-ascii 64)) + (new-size uint) + (new-details (string-ascii 128)) + (new-tags (list 10 (string-ascii 32))) +) + (let + ( + (entry-data (unwrap! (map-get? estate-registry { entry-id: entry-id }) err-item-not-found)) + ) + ;; Validate ownership and input parameters + (asserts! (entry-exists entry-id) err-item-not-found) + (asserts! (is-eq (get owner entry-data) tx-sender) err-not-property-owner) + (asserts! (> (len new-title) u0) err-invalid-title) + (asserts! (< (len new-title) u65) err-invalid-title) + (asserts! (> new-size u0) err-invalid-document-size) + (asserts! (< new-size u1000000000) err-invalid-document-size) + (asserts! (> (len new-details) u0) err-invalid-title) + (asserts! (< (len new-details) u129) err-invalid-title) + (asserts! (validate-tags new-tags) err-tag-format-invalid) + + ;; Update the property record with new information + (map-set estate-registry + { entry-id: entry-id } + (merge entry-data { + title: new-title, + file-size: new-size, + description: new-details, + tags: new-tags + }) + ) + (ok true) + ) +) + +;; Transfers property ownership to another principal +(define-public (transfer-property (entry-id uint) (new-owner principal)) + (let + ( + (entry-data (unwrap! (map-get? estate-registry { entry-id: entry-id }) err-item-not-found)) + ) + ;; Verify caller is the current owner + (asserts! (entry-exists entry-id) err-item-not-found) + (asserts! (is-eq (get owner entry-data) tx-sender) err-not-property-owner) + + ;; Update ownership + (map-set estate-registry + { entry-id: entry-id } + (merge entry-data { owner: new-owner }) + ) + (ok true) + ) +) + +;; Removes a property record from the registry +(define-public (delete-property (entry-id uint)) + (let + ( + (entry-data (unwrap! (map-get? estate-registry { entry-id: entry-id }) err-item-not-found)) + ) + ;; Ownership verification + (asserts! (entry-exists entry-id) err-item-not-found) + (asserts! (is-eq (get owner entry-data) tx-sender) err-not-property-owner) + + ;; Remove the property record + (map-delete estate-registry { entry-id: entry-id }) + (ok true) + ) +) \ No newline at end of file diff --git a/tests/property-registry.test.ts b/tests/property-registry.test.ts new file mode 100644 index 0000000..4bb9cf3 --- /dev/null +++ b/tests/property-registry.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); + // }); +});