diff --git a/.gitignore b/.gitignore index 8ba66e8e..90ef8584 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ /frontend/node_modules /backend/env /backend/.env -.DS_Store \ No newline at end of file +.DS_Store + +.snfoundry_cache/ +target/ \ No newline at end of file diff --git a/land_registry/Scarb.lock b/land_registry/Scarb.lock index 3fd0e8b3..7be04a2b 100644 --- a/land_registry/Scarb.lock +++ b/land_registry/Scarb.lock @@ -4,3 +4,97 @@ version = 1 [[package]] name = "land_registry" version = "0.1.0" +dependencies = [ + "openzeppelin", +] + +[[package]] +name = "openzeppelin" +version = "0.16.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_governance", + "openzeppelin_introspection", + "openzeppelin_merkle_tree", + "openzeppelin_presets", + "openzeppelin_security", + "openzeppelin_token", + "openzeppelin_upgrades", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_access" +version = "0.16.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_account" +version = "0.16.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" +dependencies = [ + "openzeppelin_introspection", + "openzeppelin_utils", +] + +[[package]] +name = "openzeppelin_governance" +version = "0.16.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" +dependencies = [ + "openzeppelin_access", + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_introspection" +version = "0.16.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" + +[[package]] +name = "openzeppelin_merkle_tree" +version = "0.16.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" + +[[package]] +name = "openzeppelin_presets" +version = "0.16.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" +dependencies = [ + "openzeppelin_access", + "openzeppelin_account", + "openzeppelin_introspection", + "openzeppelin_token", + "openzeppelin_upgrades", +] + +[[package]] +name = "openzeppelin_security" +version = "0.16.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" + +[[package]] +name = "openzeppelin_token" +version = "0.16.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" +dependencies = [ + "openzeppelin_account", + "openzeppelin_governance", + "openzeppelin_introspection", +] + +[[package]] +name = "openzeppelin_upgrades" +version = "0.16.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" + +[[package]] +name = "openzeppelin_utils" +version = "0.16.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.16.0#ba00ce76a93dcf25c081ab2698da20690b5a1cfb" diff --git a/land_registry/Scarb.toml b/land_registry/Scarb.toml index fe50869a..fc41007b 100644 --- a/land_registry/Scarb.toml +++ b/land_registry/Scarb.toml @@ -8,6 +8,7 @@ cairo-version = "2.8.2" [dependencies] starknet = ">=2.8.0" +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.16.0" } [dev-dependencies] cairo_test = "2.8.0" diff --git a/land_registry/src/interface.cairo b/land_registry/src/interface.cairo index 3b6d2320..7be3e572 100644 --- a/land_registry/src/interface.cairo +++ b/land_registry/src/interface.cairo @@ -6,17 +6,11 @@ pub struct Land { location: felt252, area: u256, land_use: LandUse, + is_approved: bool, + inspector: Option, last_transaction_timestamp: u64, } -struct LandApprovalDetails { - ApprovedBy: felt252, - Agency: felt252, - land: Land, - land_id: u256, - timestamp: u64, -} - #[derive(Debug, Drop, Copy, Clone, Serde, starknet::Store, PartialEq)] pub enum LandUse { Residential, @@ -43,13 +37,10 @@ pub trait ILandRegistry { ) -> u256; fn transfer_land(ref self: TContractState, land_id: u256, new_owner: ContractAddress); fn get_land(self: @TContractState, land_id: u256) -> Land; - //fn get_owner_lands(self: @TContractState, owner_lands: ContractAddress) -> Array; - //fn get_lands(self: @TContractState, owner: ContractAddress, location: felt252, land_use: - //felt252) -> Array; - fn update_land(ref self: TContractState, land_id: u256, area: u256, land_use: LandUse); - // fn get_approved_lands - - //fn approve_land -//fn reject_land + fn approve_land(ref self: TContractState, land_id: u256); + fn reject_land(ref self: TContractState, land_id: u256); + fn is_inspector(self: @TContractState, address: ContractAddress) -> bool; + fn add_inspector(ref self: TContractState, inspector: ContractAddress); + fn remove_inspector(ref self: TContractState, inspector: ContractAddress); } diff --git a/land_registry/src/land_nft.cairo b/land_registry/src/land_nft.cairo index dfa1f53c..4d6f9461 100644 --- a/land_registry/src/land_nft.cairo +++ b/land_registry/src/land_nft.cairo @@ -1,29 +1,39 @@ +use starknet::ContractAddress; +use openzeppelin::token::erc721::ERC721Component; +use openzeppelin::introspection::src5::SRC5Component; + #[starknet::interface] -pub trait ILandNFT { - fn mint(ref self: ContractState, to: ContractAddress, token_id: u256); - fn transfer(ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256); - use starknet::ContractAddress; +pub trait ILandNFT { + fn mint(ref self: TContractState, to: ContractAddress, token_id: u256); + fn transfer( + ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ); } #[starknet::contract] pub mod LandNFT { + use super::ILandNFT; use starknet::ContractAddress; - use openzeppelin::token::erc20::ERC20Component; - use starknet::storage::{Map, StorageMapWriteAccess, StorageMapReadAccess}; + use openzeppelin::token::erc721::{ERC721Component, ERC721Component::InternalTrait}; + use openzeppelin::introspection::src5::SRC5Component; + use openzeppelin::token::erc721::ERC721HooksEmptyImpl; - component!(path: ERC20Component, storage: erc20, event: ERC20Event); + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); #[abi(embed_v0)] - impl ERC20Impl = ERC20Component::ERC20Impl; + impl ERC721Impl = ERC721Component::ERC721Impl; #[abi(embed_v0)] - impl ERC20CamelOnlyImpl = ERC20Component::ERC20CamelOnlyImpl; - impl ERC20InternalImpl = ERC20Component::InternalImpl; + impl ERC721CamelOnlyImpl = ERC721Component::ERC721CamelOnlyImpl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; #[storage] struct Storage { #[substorage(v0)] - erc20: ERC20Component::Storage, + erc721: ERC721Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, land_registry: ContractAddress, } @@ -31,26 +41,37 @@ pub mod LandNFT { #[derive(Drop, starknet::Event)] enum Event { #[flat] - ERC20Event: ERC20Component::Event + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, } #[constructor] fn constructor(ref self: ContractState, land_registry: ContractAddress) { - self.erc20.initializer('Land NFT', 'LAND'); + self.erc721.initializer("Land NFT", "LAND", format!("")); self.land_registry.write(land_registry); } - #[external(v0)] - fn mint(ref self: ContractState, to: ContractAddress, token_id: u256) { - // Only the land registry contract can mint NFTs - assert(starknet::get_caller_address() == self.land_registry.read(), 'Only land registry can mint'); - self.erc20._mint(to, token_id); - } + #[abi(embed_v0)] + impl LandNFTImpl of ILandNFT { + fn mint(ref self: ContractState, to: ContractAddress, token_id: u256) { + // Only the land registry contract can mint NFTs + assert( + starknet::get_caller_address() == self.land_registry.read(), + 'Only land registry can mint' + ); + self.erc721.mint(to, token_id); + } - #[external(v0)] - fn transfer_land_nft(ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256) { - // Only the land registry contract can transfer NFTs - assert(starknet::get_caller_address() == self.land_registry.read(), 'Only land registry can transfer'); - self.erc20._transfer(from, to, token_id); + fn transfer( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ) { + // Only the land registry contract can transfer NFTs + assert( + starknet::get_caller_address() == self.land_registry.read(), + 'Only land registry can transfer' + ); + self.erc721.transfer(from, to, token_id); + } } } diff --git a/land_registry/src/land_register.cairo b/land_registry/src/land_register.cairo index 48ba7f31..4e363232 100644 --- a/land_registry/src/land_register.cairo +++ b/land_registry/src/land_register.cairo @@ -2,16 +2,20 @@ pub mod LandRegistryContract { use starknet::{get_caller_address, get_block_timestamp, ContractAddress}; use land_registry::interface::{ILandRegistry, Land, LandUse}; + use land_registry::land_nft::{ILandNFTDispatcher, ILandNFTDispatcherTrait, LandNFT}; use core::array::ArrayTrait; - use core::array::SpanTrait; - use starknet::storage::{Map, StorageMapWriteAccess}; + use starknet::storage::{Map, StorageMapWriteAccess, StorageMapReadAccess}; #[storage] struct Storage { lands: Map::, - owner_lands: Map::, + owner_lands: Map::<(ContractAddress, u256), u256>, + owner_land_count: Map::, + land_inspectors: Map::, + approved_lands: Map::, land_count: u256, + nft_contract: ContractAddress, } #[event] @@ -43,6 +47,7 @@ pub mod LandRegistryContract { struct LandVerified { land_id: u256, } + #[derive(Drop, Copy, starknet::Event)] struct LandUpdated { land_id: u256, @@ -50,6 +55,11 @@ pub mod LandRegistryContract { area: u256 } + #[constructor] + fn constructor(ref self: ContractState, nft_contract: ContractAddress) { + self.nft_contract.write(nft_contract); + } + #[abi(embed_v0)] impl LandRegistry of ILandRegistry { fn register_land( @@ -57,22 +67,24 @@ pub mod LandRegistryContract { ) -> u256 { let caller = get_caller_address(); let timestamp = get_block_timestamp(); - let land_id = timestamp.into() + 1; + let land_id = self.land_count.read() + 1; let new_land = Land { owner: caller, location: location, area: area, land_use: land_use, + is_approved: false, + inspector: Option::None, last_transaction_timestamp: timestamp, }; self.lands.write(land_id, new_land); self.land_count.write(land_id); - self.owner_lands.write(caller, land_id); - - //Write new land to a specific owner.. + let owner_land_count = self.owner_land_count.read(caller); + self.owner_lands.write((caller, owner_land_count), land_id); + self.owner_land_count.write(caller, owner_land_count + 1); self .emit( @@ -93,46 +105,111 @@ pub mod LandRegistryContract { } fn update_land(ref self: ContractState, land_id: u256, area: u256, land_use: LandUse) { - self.lands.write(land_id, Land { area, land_use, ..self.lands.read(land_id) }); + assert(InternalFunctions::only_owner(@self, land_id), 'Only owner can update land'); + let mut land = self.lands.read(land_id); + land.area = area; + land.land_use = land_use; + self.lands.write(land_id, land); self.emit(LandUpdated { land_id: land_id, area: area, land_use: land_use.into(), }); } fn transfer_land(ref self: ContractState, land_id: u256, new_owner: ContractAddress) { + assert(InternalFunctions::only_owner(@self, land_id), 'Only owner can transfer'); + assert(self.approved_lands.read(land_id), 'Land must be approved'); + + let mut land = self.lands.read(land_id); + let old_owner = land.owner; + land.owner = new_owner; + self.lands.write(land_id, land); + + // Update owner_lands for old owner + let mut old_owner_land_count = self.owner_land_count.read(old_owner); + let mut index_to_remove = old_owner_land_count; + let mut i: u256 = 0; + loop { + if i >= old_owner_land_count { + break; + } + if self.owner_lands.read((old_owner, i)) == land_id { + index_to_remove = i; + break; + } + i += 1; + }; + + assert(index_to_remove < old_owner_land_count, 'Land not found'); + + if index_to_remove < old_owner_land_count - 1 { + let last_land = self.owner_lands.read((old_owner, old_owner_land_count - 1)); + self.owner_lands.write((old_owner, index_to_remove), last_land); + } + self.owner_land_count.write(old_owner, old_owner_land_count - 1); + + // Update owner_lands for new owner + let new_owner_land_count = self.owner_land_count.read(new_owner); + self.owner_lands.write((new_owner, new_owner_land_count), land_id); + self.owner_land_count.write(new_owner, new_owner_land_count + 1); + + // Transfer NFT + let nft_contract = self.nft_contract.read(); + let nft_dispatcher = ILandNFTDispatcher { contract_address: nft_contract }; + nft_dispatcher.transfer(old_owner, new_owner, land_id); + + self + .emit( + LandTransferred { + land_id: land_id, from_owner: old_owner, to_owner: new_owner, + } + ); + } + + fn approve_land(ref self: ContractState, land_id: u256) { + assert(InternalFunctions::only_inspector(@self), 'Only inspector can approve'); + self.approved_lands.write(land_id, true); + + // Mint NFT let land = self.lands.read(land_id); - self.lands.write(land_id, Land { owner: new_owner, ..land }); + let nft_contract = self.nft_contract.read(); + let nft_dispatcher = ILandNFTDispatcher { contract_address: nft_contract }; + nft_dispatcher.mint(land.owner, land_id); + + self.emit(LandVerified { land_id: land_id }); + } + + fn reject_land(ref self: ContractState, land_id: u256) { + assert(InternalFunctions::only_inspector(@self), 'Only inspector can reject'); + let mut land = self.lands.read(land_id); + land.is_approved = false; + self.lands.write(land_id, land); + self.emit(LandVerified { land_id: land_id }); + } + + fn is_inspector(self: @ContractState, address: ContractAddress) -> bool { + self.land_inspectors.read(address) + } + + fn add_inspector(ref self: ContractState, inspector: ContractAddress) { + // Todo: Add logic to ensure only authorized entities can add inspectors + self.land_inspectors.write(inspector, true); + } + + fn remove_inspector(ref self: ContractState, inspector: ContractAddress) { + // Todo: Add logic to ensure only authorized entities can remove inspectors + self.land_inspectors.write(inspector, false); } } -} -// #[cfg(test)] -// mod tests { -// use starknet::{get_caller_address, get_block_timestamp, ContractAddress}; - -// // Import the interface and dispatcher to be able to interact with the contract. -// use super::{SimpleContract, IContractDispatcher, ISimpleContractDispatcherTrait}; - -// // Import the deploy syscall to be able to deploy the contract. -// use starknet::{SyscallResultTrait, syscalls::deploy_syscall}; -// use starknet::{get_contract_address, contract_address_const}; - -// // Use starknet test utils to fake the contract_address -// use starknet::testing::set_contract_address; - -// // Deploy the contract and return its dispatcher. -// fn deploy(initial_value: u32) -> ISimpleContractDispatcher { -// // Declare and deploy -// let (contract_address, _) = deploy_syscall( -// SimpleContract::TEST_CLASS_HASH.try_into().unwrap(), -// 0, -// array![initial_value.into()].span(), -// false -// ) -// .unwrap_syscall(); - -// // Return the dispatcher. -// // The dispatcher allows to interact with the contract based on its interface. -// ISimpleContractDispatcher { contract_address } -// } - -// } + #[generate_trait] + impl InternalFunctions of InternalFunctionsTrait { + fn only_owner(self: @ContractState, land_id: u256) -> bool { + let land = self.lands.read(land_id); + land.owner == get_caller_address() + } + + fn only_inspector(self: @ContractState) -> bool { + let caller = get_caller_address(); + self.land_inspectors.read(caller) + } + } +} diff --git a/land_registry/src/lib.cairo b/land_registry/src/lib.cairo index 1e71d9ec..bd7c6cf0 100644 --- a/land_registry/src/lib.cairo +++ b/land_registry/src/lib.cairo @@ -2,3 +2,4 @@ pub mod interface; pub mod land_register; pub mod utils; pub mod tests; +pub mod land_nft; diff --git a/land_registry/src/tests.cairo b/land_registry/src/tests.cairo new file mode 100644 index 00000000..b17f2df1 --- /dev/null +++ b/land_registry/src/tests.cairo @@ -0,0 +1 @@ +mod test_land_register; diff --git a/land_registry/src/tests/test_land_register.cairo b/land_registry/src/tests/test_land_register.cairo index a1dea386..42e7d954 100644 --- a/land_registry/src/tests/test_land_register.cairo +++ b/land_registry/src/tests/test_land_register.cairo @@ -1,9 +1,63 @@ // ************************************************************************* // Setup // ************************************************************************* -use starknet::ContractAddress; -// ************************************************************************* -// TESTS -// ************************************************************************* +use starknet::{ContractAddress, contract_address_const}; +use land_registry::interface::{ + ILandRegistryDispatcher, ILandRegistryDispatcherTrait, Land, LandUse +}; +use land_registry::land_register::LandRegistryContract; +use land_registry::land_nft::{ILandNFTDispatcher, ILandNFTDispatcherTrait}; + +// Mock function contract deployment +fn deploy_land_registry() -> ILandRegistryDispatcher { + let contract_address: ContractAddress = contract_address_const::<0x123>(); + let nft_address: ContractAddress = contract_address_const::<0x456>(); + ILandRegistryDispatcher { contract_address } +} + +// Mock function NFT contract deployment +fn deploy_land_nft(land_registry: ContractAddress) -> ILandNFTDispatcher { + let nft_address: ContractAddress = contract_address_const::<0x456>(); + ILandNFTDispatcher { contract_address: nft_address } +} + +// Main test function +fn test_land_registry() { + // Test register land + let dispatcher = deploy_land_registry(); + let land_id = dispatcher.register_land(123, 1000, LandUse::Commercial); + assert(land_id == 1, 'Incorrect land ID'); + + let land = dispatcher.get_land(land_id); + assert(land.location == 123, 'Incorrect location'); + assert(land.area == 1000, 'Incorrect area'); + assert(land.land_use == LandUse::Commercial, 'Incorrect land use'); + assert(!land.is_approved, 'Land not approved'); + + // Test approve land + dispatcher.add_inspector(starknet::get_caller_address()); + dispatcher.approve_land(land_id); + let land = dispatcher.get_land(land_id); + assert(land.is_approved, 'Land should be approved'); + + // Test transfer land + let new_owner = contract_address_const::<0x456>(); + dispatcher.transfer_land(land_id, new_owner); + let land = dispatcher.get_land(land_id); + assert(land.owner == new_owner, 'Land ownership not transferred'); + // Test register and approve land with NFT + let land_registry = deploy_land_registry(); + let nft_contract = deploy_land_nft(land_registry.contract_address); + let land_id = land_registry.register_land(456, 2000, LandUse::Residential); + land_registry.add_inspector(starknet::get_caller_address()); + land_registry.approve_land(land_id); + let land = land_registry.get_land(land_id); + assert(land.is_approved, 'Land should be approved'); + // Test transfer land with NFT + let new_owner = contract_address_const::<0x789>(); + land_registry.transfer_land(land_id, new_owner); + let land = land_registry.get_land(land_id); + assert(land.owner == new_owner, 'Land ownership not transferred'); +}