diff --git a/src/community/community.cairo b/src/community/community.cairo index fa7bb4f..41467dd 100644 --- a/src/community/community.cairo +++ b/src/community/community.cairo @@ -1,5 +1,5 @@ #[starknet::component] -mod CommunityComponent { +pub mod CommunityComponent { // ************************************************************************* // IMPORT // ************************************************************************* @@ -35,7 +35,7 @@ mod CommunityComponent { // STORAGE // ************************************************************************* #[storage] - struct Storage { + pub struct Storage { community_counter: u256, community_owner: Map, // map communities: Map, // map @@ -56,6 +56,8 @@ mod CommunityComponent { gate_keep_permissioned_addresses: Map< (u256, ContractAddress), bool >, // >, + hub_address: ContractAddress, + community_nft_classhash: felt252 } // ************************************************************************* @@ -63,7 +65,7 @@ mod CommunityComponent { // ************************************************************************* #[event] #[derive(Drop, starknet::Event)] - enum Event { + pub enum Event { CommunityCreated: CommunityCreated, CommunityModAdded: CommunityModAdded, CommunityBanStatusUpdated: CommunityBanStatusUpdated, @@ -132,14 +134,20 @@ mod CommunityComponent { // ************************************************************************* // EXTERNAL FUNCTIONS // ************************************************************************* - #[embeddable_as(Community)] + #[embeddable_as(KarstCommunity)] impl CommunityImpl< TContractState, +HasComponent > of ICommunity> { - fn initializer(ref self: ComponentState,) { + fn initializer( + ref self: ComponentState, + hub_address: ContractAddress, + community_nft_classhash: felt252 + ) { self.community_counter.write(0); + self.hub_address.write(hub_address); + self.community_nft_classhash.write(community_nft_classhash); } - fn create_comminuty(ref self: ComponentState,) -> u256 { + fn create_comminuty(ref self: ComponentState) -> u256 { let community_owner = get_caller_address(); let community_counter = self.community_counter.read(); let community_id = community_counter + 1; @@ -147,7 +155,7 @@ mod CommunityComponent { // deploy a new NFT and save the address in community_nft_address // let community_nft_address = self // ._get_or_deploy_community_nft( - // karst_hub, profile_address, pub_id, collect_nft_impl_class_hash, salt + // karst_hub, community_owner, community_id, collect_nft_impl_class_hash, salt // ); let community_details = CommunityDetails { community_id: community_id, @@ -183,10 +191,7 @@ mod CommunityComponent { let community_member = self .community_membership_status .read((community_id, member_address)); - assert(community_member == true, ALREADY_MEMBER); - - // let member_community_id = self.member_community_id.read(member_address); - // assert(member_community_id != community_id, "Already a member"); + assert(community_member != true, ALREADY_MEMBER); // community_token_id // a token is minted from the comunity token contract address @@ -218,7 +223,8 @@ mod CommunityComponent { let community_member = self .community_membership_status .read((community_id, member_address)); - assert(community_member != true, NOT_MEMBER); + println!("Before inside leave_community is member: {}", community_member); + assert(community_member == true, NOT_MEMBER); // remove the member_community_id self.community_membership_status.write((community_id, member_address), false); @@ -451,11 +457,7 @@ mod CommunityComponent { let is_community_member_id = self .community_membership_status .read((community_id, profile)); - if (is_community_member_id) { - (true, community_id) - } else { - (false, community_id) - } + (is_community_member_id, community_id) } fn get_total_members(self: @ComponentState, community_id: u256) -> u256 { let community = self.communities.read(community_id); @@ -499,14 +501,15 @@ mod CommunityComponent { // PRIVATE FUNCTIONS // ************************************************************************* + // #[generate_trait] #[generate_trait] - impl InternalImpl< + impl CommunityPrivateImpl< TContractState, +HasComponent, +Drop, - > of InternalTrait { + > of CommunityPrivateTrait { fn _get_or_deploy_community_nft( ref self: ComponentState, karst_hub: ContractAddress, - profile_address: ContractAddress, + community_owner_address: ContractAddress, community_id: u256, community_nft_impl_class_hash: felt252, salt: felt252 @@ -518,7 +521,7 @@ mod CommunityComponent { let deployed_collect_nft_address = self ._deploy_community_nft( karst_hub, - profile_address, + community_owner_address, community_id, community_nft_impl_class_hash, salt diff --git a/src/interfaces/ICommunity.cairo b/src/interfaces/ICommunity.cairo index 656cc00..c66a0a1 100644 --- a/src/interfaces/ICommunity.cairo +++ b/src/interfaces/ICommunity.cairo @@ -1,4 +1,4 @@ -use starknet::ContractAddress; +use starknet::{ContractAddress}; use karst::base::constants::types::{GateKeepType, CommunityType, CommunityDetails}; // ************************************************************************* @@ -10,7 +10,9 @@ pub trait ICommunity { // ************************************************************************* // EXTERNALS // ************************************************************************* - fn initializer(ref self: TState,); + fn initializer( + ref self: TState, hub_address: ContractAddress, community_nft_classhash: felt252 + ); fn create_comminuty(ref self: TState) -> u256; fn join_community(ref self: TState, community_id: u256); fn leave_community(ref self: TState, community_id: u256); diff --git a/src/presets.cairo b/src/presets.cairo index 46ac7a7..f6fee3f 100644 --- a/src/presets.cairo +++ b/src/presets.cairo @@ -1,2 +1,3 @@ pub mod profile; pub mod publication; +pub mod community; diff --git a/src/presets/community.cairo b/src/presets/community.cairo new file mode 100644 index 0000000..38046af --- /dev/null +++ b/src/presets/community.cairo @@ -0,0 +1,31 @@ +#[starknet::contract] +pub mod KarstCommunity { + use starknet::ContractAddress; + use karst::community::community::CommunityComponent; + use karst::interfaces::ICommunity::ICommunity; + + component!(path: CommunityComponent, storage: community, event: CommunityEvent); + + #[abi(embed_v0)] + impl communityImpl = CommunityComponent::KarstCommunity; + + #[storage] + struct Storage { + #[substorage(v0)] + community: CommunityComponent::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + CommunityEvent: CommunityComponent::Event + } + + #[constructor] + fn constructor( + ref self: ContractState, hub_address: ContractAddress, community_nft_classhash: felt252 + ) { + self.community.initializer(hub_address, community_nft_classhash); + } +} diff --git a/tests/test_community.cairo b/tests/test_community.cairo new file mode 100644 index 0000000..dd5ccaf --- /dev/null +++ b/tests/test_community.cairo @@ -0,0 +1,307 @@ +// ************************************************************************* +// COMMUNITY TEST +// ************************************************************************* +use core::option::OptionTrait; +use core::starknet::SyscallResultTrait; +use core::result::ResultTrait; +use core::traits::{TryInto, Into}; +use starknet::{ContractAddress, class_hash::ClassHash, contract_address_const, get_block_timestamp}; + +use snforge_std::{ + declare, start_cheat_caller_address, stop_cheat_caller_address, start_cheat_transaction_hash, + start_cheat_nonce, spy_events, EventSpyAssertionsTrait, ContractClass, ContractClassTrait, + DeclareResultTrait, start_cheat_block_timestamp, stop_cheat_block_timestamp, EventSpy +}; + +use token_bound_accounts::interfaces::IAccount::{IAccountDispatcher, IAccountDispatcherTrait}; +use token_bound_accounts::presets::account::Account; +use karst::mocks::registry::Registry; +use karst::interfaces::IRegistry::{IRegistryDispatcher, IRegistryDispatcherTrait}; +use karst::karstnft::karstnft::KarstNFT; +use karst::presets::community::KarstCommunity; + +use karst::base::constants::types::{ + CommunityDetails, GateKeepType, CommunityType, CommunityMember, CommunityGateKeepDetails +}; +use karst::interfaces::ICommunity::{ICommunityDispatcher, ICommunityDispatcherTrait}; + +const HUB_ADDRESS: felt252 = 'HUB'; +const USER_ONE: felt252 = 'BOB'; +const USER_TWO: felt252 = 'ALICE'; +const USER_THREE: felt252 = 'ROB'; +const USER_FOUR: felt252 = 'DAN'; +const USER_FIVE: felt252 = 'RANDY'; +const USER_SIX: felt252 = 'JOE'; + +// ************************************************************************* +// SETUP +// ************************************************************************* +fn __setup__() -> (ContractAddress, ContractAddress, ContractAddress, ContractAddress) { + // deploy Karst NFT + let nft_class_hash = declare("KarstNFT").unwrap().contract_class(); + let mut calldata: Array = array![USER_ONE]; + let (karst_nft_contract_address, _) = nft_class_hash.deploy(@calldata).unwrap_syscall(); + + // deploy handle contract + let handle_class_hash = declare("Handles").unwrap().contract_class(); + let mut calldata: Array = array![USER_ONE]; + let (handle_contract_address, _) = handle_class_hash.deploy(@calldata).unwrap_syscall(); + + // deploy handle registry contract + let handle_registry_class_hash = declare("HandleRegistry").unwrap().contract_class(); + let mut calldata: Array = array![handle_contract_address.into()]; + let (handle_registry_contract_address, _) = handle_registry_class_hash + .deploy(@calldata) + .unwrap_syscall(); + + // deploy community nft + let community_nft_class_hash = declare("CommunityNft").unwrap().contract_class(); + + // declare follownft + let follow_nft_classhash = declare("Follow").unwrap().contract_class(); + + // deploy hub contract + let hub_class_hash = declare("KarstHub").unwrap().contract_class(); + let mut calldata: Array = array![ + karst_nft_contract_address.into(), + handle_contract_address.into(), + handle_registry_contract_address.into(), + (*follow_nft_classhash.class_hash).into() + ]; + let (hub_contract_address, _) = hub_class_hash.deploy(@calldata).unwrap_syscall(); + + // deploy community preset contract + let community_contract = declare("KarstCommunity").unwrap().contract_class(); + let mut community_constructor_calldata: Array = array![ + hub_contract_address.into(), (*community_nft_class_hash.class_hash).into(), + ]; + let (community_contract_address, _) = community_contract + .deploy(@community_constructor_calldata) + .unwrap_syscall(); + + return ( + community_contract_address, + karst_nft_contract_address, + handle_contract_address, + handle_registry_contract_address, + ); +} + +// ************************************************************************* +// TESTS +// ************************************************************************* +#[test] +fn test_creation_community() { + let ( + community_contract_address, + karst_nft_contract_address, + handle_contract_address, + handle_registry_contract_address, + ) = + __setup__(); + let communityDispatcher = ICommunityDispatcher { contract_address: community_contract_address }; + + start_cheat_caller_address(community_contract_address, USER_ONE.try_into().unwrap()); + let community_id = communityDispatcher.create_comminuty(); + assert(community_id == 1, 'invalid community creation'); + stop_cheat_caller_address(community_contract_address); +} + + +#[test] +fn test_join_community() { + let ( + community_contract_address, + karst_nft_contract_address, + handle_contract_address, + handle_registry_contract_address, + ) = + __setup__(); + let communityDispatcher = ICommunityDispatcher { contract_address: community_contract_address }; + + start_cheat_caller_address(community_contract_address, USER_ONE.try_into().unwrap()); + //create the community + let community_id = communityDispatcher.create_comminuty(); + // join the community + communityDispatcher.join_community(community_id); + let (is_member, community) = communityDispatcher + .is_community_member(USER_ONE.try_into().unwrap(), community_id); + println!("is member: {}", is_member); + assert(is_member == true, 'Not Community Member'); + stop_cheat_caller_address(community_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: already a Member',))] +fn test_should_panic_join_one_community_twice() { + let ( + community_contract_address, + karst_nft_contract_address, + handle_contract_address, + handle_registry_contract_address, + ) = + __setup__(); + let communityDispatcher = ICommunityDispatcher { contract_address: community_contract_address }; + + start_cheat_caller_address(community_contract_address, USER_ONE.try_into().unwrap()); + + let community_id = communityDispatcher.create_comminuty(); + + communityDispatcher.join_community(community_id); + communityDispatcher.join_community(community_id); +} + + +#[test] +fn test_leave_community() { + let ( + community_contract_address, + karst_nft_contract_address, + handle_contract_address, + handle_registry_contract_address, + ) = + __setup__(); + let communityDispatcher = ICommunityDispatcher { contract_address: community_contract_address }; + + start_cheat_caller_address(community_contract_address, USER_ONE.try_into().unwrap()); + //create the community + let community_id = communityDispatcher.create_comminuty(); + // join the community + communityDispatcher.join_community(community_id); + + stop_cheat_caller_address(community_contract_address); + + start_cheat_caller_address(community_contract_address, USER_TWO.try_into().unwrap()); + communityDispatcher.join_community(community_id); + + stop_cheat_caller_address(community_contract_address); + + // leave community + start_cheat_caller_address(community_contract_address, USER_TWO.try_into().unwrap()); + communityDispatcher.leave_community(community_id); + + let (is_member, community) = communityDispatcher + .is_community_member(USER_TWO.try_into().unwrap(), community_id); + println!("is member: {}", is_member); + assert(is_member != true, 'A Community Member'); + + stop_cheat_caller_address(community_contract_address); +} + + +#[test] +#[should_panic(expected: ('Karst: Not a Community Member',))] +fn test_should_panic_not_member() { + let ( + community_contract_address, + karst_nft_contract_address, + handle_contract_address, + handle_registry_contract_address, + ) = + __setup__(); + let communityDispatcher = ICommunityDispatcher { contract_address: community_contract_address }; + + start_cheat_caller_address(community_contract_address, USER_ONE.try_into().unwrap()); + //create the community + let community_id = communityDispatcher.create_comminuty(); + // join the community + communityDispatcher.join_community(community_id); + + stop_cheat_caller_address(community_contract_address); + + // leave community + start_cheat_caller_address(community_contract_address, USER_TWO.try_into().unwrap()); + communityDispatcher.leave_community(community_id); +} + + +#[test] +fn test_set_community_metadata_uri() { + let ( + community_contract_address, + karst_nft_contract_address, + handle_contract_address, + handle_registry_contract_address, + ) = + __setup__(); + let communityDispatcher = ICommunityDispatcher { contract_address: community_contract_address }; + + start_cheat_caller_address(community_contract_address, USER_ONE.try_into().unwrap()); + let community_id = communityDispatcher.create_comminuty(); + let metadata_uri = "ipfs://helloworld"; + communityDispatcher.set_community_metadata_uri(community_id, metadata_uri); + let result_meta_uri = communityDispatcher.get_community_metadata_uri(community_id); + assert(result_meta_uri == "ipfs://helloworld", 'invalid uri'); + stop_cheat_caller_address(community_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: Not Community owner',))] +fn test_should_panic_set_community_metadata_uri() { + let ( + community_contract_address, + karst_nft_contract_address, + handle_contract_address, + handle_registry_contract_address, + ) = + __setup__(); + let communityDispatcher = ICommunityDispatcher { contract_address: community_contract_address }; + + start_cheat_caller_address(community_contract_address, USER_ONE.try_into().unwrap()); + let community_id = communityDispatcher.create_comminuty(); + + stop_cheat_caller_address(community_contract_address); + + start_cheat_caller_address(community_contract_address, USER_THREE.try_into().unwrap()); + let metadata_uri = "ipfs://helloworld"; + communityDispatcher.set_community_metadata_uri(community_id, metadata_uri); +} + + +#[test] +fn test_add_community_mod() { + let ( + community_contract_address, + karst_nft_contract_address, + handle_contract_address, + handle_registry_contract_address, + ) = + __setup__(); + let communityDispatcher = ICommunityDispatcher { contract_address: community_contract_address }; + + start_cheat_caller_address(community_contract_address, USER_ONE.try_into().unwrap()); + let community_id = communityDispatcher.create_comminuty(); + // add a community mod + communityDispatcher.add_community_mods(community_id, USER_SIX.try_into().unwrap()); + + // check a community mod - is_community_mod + let is_community_mod = communityDispatcher + .is_community_mod(USER_SIX.try_into().unwrap(), community_id); + assert(is_community_mod == true, 'Not a Community Mod'); + stop_cheat_caller_address(community_contract_address); +} + + +#[test] +#[should_panic(expected: ('Karst: Not Community owner',))] +fn test_should_panic_add_community_mod() { + let ( + community_contract_address, + karst_nft_contract_address, + handle_contract_address, + handle_registry_contract_address, + ) = + __setup__(); + let communityDispatcher = ICommunityDispatcher { contract_address: community_contract_address }; + + start_cheat_caller_address(community_contract_address, USER_ONE.try_into().unwrap()); + let community_id = communityDispatcher.create_comminuty(); + + stop_cheat_caller_address(community_contract_address); + + // when a wrong community owner try to add a MOD + start_cheat_caller_address(community_contract_address, USER_TWO.try_into().unwrap()); + + // add a community mod + communityDispatcher.add_community_mods(community_id, USER_SIX.try_into().unwrap()); +}