diff --git a/src/base/errors.cairo b/src/base/errors.cairo index 82a4d7a..648be14 100644 --- a/src/base/errors.cairo +++ b/src/base/errors.cairo @@ -7,4 +7,5 @@ pub mod Errors { pub const HUB_RESTRICTED: felt252 = 'CALLER_IS_NOT_HUB'; pub const FOLLOWING: felt252 = 'USER_ALREADY_FOLLOWING'; pub const NOT_FOLLOWING: felt252 = 'USER_NOT_FOLLOWING'; + pub const BLOCKED_STATUS: felt252 = 'BLOCKED'; } diff --git a/src/base/types.cairo b/src/base/types.cairo index 82d4654..e1319d0 100644 --- a/src/base/types.cairo +++ b/src/base/types.cairo @@ -8,3 +8,77 @@ pub struct FollowData { follower_profile_address: ContractAddress, follow_timestamp: u64 } + +#[derive(Drop, Serde, starknet::Store)] +pub struct PostParams { + profileId: ContractAddress, + contentURI: ByteArray, + profile_address: ContractAddress, +//actionModule, +//actionModulesInitDatas, +//referenceModule +//referenceModuleInitData + +} + +#[derive(Drop, Serde, starknet::Store)] +pub struct Profile { + pubCount: u256, + metadataURI: ByteArray, + profile_address: ContractAddress, + profile_owner: ContractAddress +} + + +#[derive(Drop, Serde, starknet::Store)] +pub struct Publication { + pointed_profile_address: ContractAddress, + pointedPubId: u256, + contentURI: ByteArray, + pubType: PublicationType, + root_profile_address: ContractAddress, + rootPubId: u256 +} + + +#[derive(Drop, Serde, starknet::Store, PartialEq)] +enum PublicationType { + Nonexistent, + Post, + Comment, + Mirror, + Quote +} + + + +#[derive(Drop, Serde, starknet::Store)] +struct ReferencePubParams { + profile_address: ContractAddress, + contentURI: ByteArray, + pointedProfile_address: ContractAddress, + pointedPubId: u256 +// uint256[] referrerProfileIds; +// uint256[] referrerPubIds; +// bytes referenceModuleData; +// address[] actionModules; +// bytes[] actionModulesInitDatas; +// address referenceModule; +// bytes referenceModuleInitData; +} + + +#[derive(Drop, Serde, starknet::Store)] +struct CommentParams { + profile_address: ContractAddress, + contentURI: ByteArray, + pointedProfile_address: ContractAddress, + pointedPubId: u256, +// uint256[] referrerProfileIds; +// uint256[] referrerPubIds; +// bytes referenceModuleData; +// address[] actionModules; +// bytes[] actionModulesInitDatas; +// address referenceModule; +// bytes referenceModuleInitData; +} diff --git a/src/interfaces/IProfile.cairo b/src/interfaces/IProfile.cairo index e562d43..825e8e9 100644 --- a/src/interfaces/IProfile.cairo +++ b/src/interfaces/IProfile.cairo @@ -1,4 +1,5 @@ use starknet::ContractAddress; +use karst::base::types::Profile; // ************************************************************************* // INTERFACE of KARST PROFILE // ************************************************************************* @@ -19,6 +20,10 @@ pub trait IKarstProfile { // GETTERS // ************************************************************************* fn get_user_profile_address(self: @TState, user: ContractAddress) -> ContractAddress; - fn get_profile(self: @TState, profile_id: ContractAddress) -> ByteArray; - fn get_profile_owner_by_id(self: @TState, profile_id: ContractAddress) -> ContractAddress; + fn get_profile_metadata(self: @TState, user: ContractAddress) -> ByteArray; + fn get_profile_owner(self: @TState, user: ContractAddress) -> ContractAddress; + fn get_profile_details( + self: @TState, profile_address: ContractAddress + ) -> (u256, ByteArray, ContractAddress, ContractAddress); + fn get_profile(self: @TState, profile_address: ContractAddress) -> Profile; } diff --git a/src/lib.cairo b/src/lib.cairo index a8db022..e2460a0 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -4,3 +4,5 @@ pub mod profile; pub mod base; pub mod follownft; pub mod mocks; +pub mod publication; + diff --git a/src/profile/profile.cairo b/src/profile/profile.cairo index 8d75b53..3365574 100644 --- a/src/profile/profile.cairo +++ b/src/profile/profile.cairo @@ -14,21 +14,14 @@ mod KarstProfile { use karst::interfaces::IERC721::{IERC721Dispatcher, IERC721DispatcherTrait}; use karst::interfaces::IProfile::IKarstProfile; use karst::base::errors::Errors::{NOT_PROFILE_OWNER}; + use karst::base::types::Profile; // ************************************************************************* // STORAGE // ************************************************************************* #[storage] struct Storage { - profile_address: LegacyMap< - ContractAddress, ContractAddress - >, // mapping of user => profile_address - profile_metadata_uri: LegacyMap< - ContractAddress, ByteArray - >, //mapping of profile_id => metadata_uri - profile_owner: LegacyMap< - ContractAddress, ContractAddress - > // mapping of profile_address => user + profile: LegacyMap //maps user => Profile } // ************************************************************************* @@ -80,44 +73,59 @@ mod KarstProfile { class_hash: registry_hash.try_into().unwrap() } .create_account(implementation_hash, karstnft_contract_address, token_id, salt); - self.profile_address.write(caller, profile_address); - let profile_id = self.profile_address.read(caller); - self.profile_owner.write(profile_id, caller); - + let new_profile = Profile { + pubCount: 0, + metadataURI: "", + profile_address: profile_address, + profile_owner: caller + }; + self.profile.write(caller, new_profile); self.emit(CreateProfile { user: caller, token_id, profile_address }) } /// @notice set profile metadata_uri (`banner_image, description, profile_image` to be uploaded to arweave or ipfs) /// @params metadata_uri the profile CID fn set_profile_metadata_uri(ref self: ContractState, metadata_uri: ByteArray) { let caller = get_caller_address(); - let profile_id = self.profile_address.read(caller); - let profile_owner = self.profile_owner.read(profile_id); + let mut profile = self.profile.read(caller); // assert that caller is the owner of the profile to be updated. - assert(caller == profile_owner, NOT_PROFILE_OWNER); - self.profile_metadata_uri.write(profile_id, metadata_uri); + assert(caller == profile.profile_owner, NOT_PROFILE_OWNER); + profile.metadataURI = metadata_uri; + self.profile.write(caller, profile); } + // ************************************************************************* // GETTERS // ************************************************************************* - /// @notice returns user profile_id + /// @notice returns user profile_address /// @params user ContractAddress of user fn get_user_profile_address( self: @ContractState, user: ContractAddress ) -> ContractAddress { - self.profile_address.read(user) + self.profile.read(user).profile_address } /// @notice returns user metadata - /// @params profile_id profile_id of user - fn get_profile(self: @ContractState, profile_id: ContractAddress) -> ByteArray { - self.profile_metadata_uri.read(profile_id) + /// @params user + fn get_profile_metadata(self: @ContractState, user: ContractAddress) -> ByteArray { + self.profile.read(user).metadataURI } /// @notice returns owner of a profile - /// @params profile_id the profile_id_address to query for. - fn get_profile_owner_by_id( - self: @ContractState, profile_id: ContractAddress - ) -> ContractAddress { - self.profile_owner.read(profile_id) + /// @params user the user address to query for. + fn get_profile_owner(self: @ContractState, user: ContractAddress) -> ContractAddress { + self.profile.read(user).profile_owner + } + + /// @notice returns a profile + /// @params profile_address the profile_id_address to query for. + fn get_profile_details( + self: @ContractState, profile_address: ContractAddress + ) -> (u256, ByteArray, ContractAddress, ContractAddress) { + let profile = self.profile.read(profile_address); + (profile.pubCount, profile.metadataURI, profile.profile_address, profile.profile_owner) + } + + fn get_profile(self: @ContractState, profile_address: ContractAddress) -> Profile { + self.profile.read(profile_address) } } } diff --git a/src/publication.cairo b/src/publication.cairo new file mode 100644 index 0000000..60524f5 --- /dev/null +++ b/src/publication.cairo @@ -0,0 +1 @@ +pub mod Publication; diff --git a/src/publication/publication.cairo b/src/publication/publication.cairo new file mode 100644 index 0000000..135f5de --- /dev/null +++ b/src/publication/publication.cairo @@ -0,0 +1,259 @@ +//! Contract for Karst Publications + +use starknet::{ContractAddress, get_caller_address}; +use karst::base::types::{PostParams, PublicationType, CommentParams, ReferencePubParams}; +use karst::interfaces::IProfile::{IKarstProfileDispatcher, IKarstProfileDispatcherTrait}; + + +// ************************************************************************* +// INTERFACE of KARST PUBLICATIONS +// ************************************************************************* +#[starknet::interface] +pub trait IKarstPublications { + // ************************************************************************* + // PUBLISHING FUNCTIONS + // ************************************************************************* + + fn post( + ref self: T, post_params: PostParams, profile_contract_address: ContractAddress + ) -> u256; + fn comment( + ref self: T, referencePubParams: ReferencePubParams, profile_address: ContractAddress + ) -> u256; +// ************************************************************************* +// PROFILE INTERACTION FUNCTIONS +// ************************************************************************* +} + +#[starknet::contract] +pub mod Publications { + // ************************************************************************* + // IMPORTS + // ************************************************************************* + use starknet::{ContractAddress, get_contract_address, get_caller_address}; + use karst::base::types::{ + PostParams, Publication, PublicationType, ReferencePubParams, CommentParams + }; + use super::IKarstPublications; + use karst::interfaces::IProfile::{IKarstProfileDispatcher, IKarstProfileDispatcherTrait}; + use karst::base::errors::Errors::{NOT_PROFILE_OWNER, BLOCKED_STATUS}; + + // ************************************************************************* + // STORAGE + // ************************************************************************* + + #[storage] + struct Storage { + publication: LegacyMap, + blocked_profile_address: LegacyMap<(ContractAddress, ContractAddress), bool> + } + + // ************************************************************************* + // EVENTS + // ************************************************************************* + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + Post: Post, + // Comment: Comment, + // Mirror: Mirror, + // Quote: Quote, + // Tip: Tip, + } + + // ************************************************************************* + // STRUCTS + // ************************************************************************* + + #[derive(Drop, starknet::Event)] + pub struct Post { + pub post: PostParams, + pub publication_id: u256, + pub transaction_executor: ContractAddress, + pub block_timestamp: u256, + } + + + // ************************************************************************* + // CONSTRUCTOR + // ************************************************************************* + #[constructor] + fn constructor(ref self: ContractState,) {} + + // ************************************************************************* + // EXTERNAL FUNCTIONS + // ************************************************************************* + + #[abi(embed_v0)] + impl PublicationsImpl of IKarstPublications { + // ************************************************************************* + // PUBLISHING FUNCTIONS + // ************************************************************************* + + fn post( + ref self: ContractState, + post_params: PostParams, + profile_contract_address: ContractAddress + ) -> u256 { + let caller = get_caller_address(); + + let (_, _, _, profile_owner) = IKarstProfileDispatcher { + contract_address: profile_contract_address + } + .get_profile_details(caller); + assert(caller == profile_owner, NOT_PROFILE_OWNER); + let mut profile = IKarstProfileDispatcher { contract_address: profile_contract_address } + .get_profile(caller); + profile.pubCount += 1; + let mut post = self.publication.read(post_params.profile_address); + post.pointed_profile_address = post_params.profile_address; + post.pointedPubId = profile.pubCount; + post.contentURI = post_params.contentURI; + post.pubType = PublicationType::Post; + self.publication.write(post_params.profile_address, post); + + profile.pubCount + // self.emit(Post { post, publication_id, transaction_executor, block_timestamp, }); + } + + fn comment( + ref self: ContractState, + referencePubParams: ReferencePubParams, + profile_address: ContractAddress + ) -> u256 { + let pubIdAssigned = self + ._createReferencePublication( + ref referencePubParams, PublicationType::Comment, profile_address + ); + pubIdAssigned + } + // ************************************************************************* + } + + + // ************************************************************************* + // PRIVATE FUNCTIONS + // ************************************************************************* + #[generate_trait] + impl Private of PrivateTrait { + fn _fillRootOfPublicationInStorage( + ref self: ContractState, + pointedProfileId: ContractAddress, + pointedPubId: u256, + profile_contract_address: ContractAddress + ) -> ContractAddress { + let caller = get_caller_address(); + let (_, _, profile_address, _) = IKarstProfileDispatcher { + contract_address: profile_contract_address + } + .get_profile_details(caller); + let mut pubPointed = self.publication.read(profile_address); + let pubPointedType = pubPointed.pubType; + + if pubPointedType == PublicationType::Post { + pubPointed.rootPubId = pointedPubId; + return pubPointed.root_profile_address; + } else if pubPointedType == PublicationType::Comment + || pubPointedType == PublicationType::Quote { + pubPointed.rootPubId = pubPointed.rootPubId; + return pubPointed.root_profile_address; + }; + return 0.try_into().unwrap(); + } + + fn _fillRefeferencePublicationStorage( + ref self: ContractState, + ref referencePubParams: ReferencePubParams, + referencePubType: PublicationType, + profile_contract_address: ContractAddress + ) -> (u256, ContractAddress) { + let caller = get_caller_address(); + let mut profile = IKarstProfileDispatcher { contract_address: profile_contract_address } + .get_profile(caller); + profile.pubCount += 1; + let mut referencePub = self.publication.read(referencePubParams.profile_address); + referencePub.pointed_profile_address = referencePubParams.pointedProfile_address; + referencePub.pointedPubId = referencePubParams.pointedPubId; + referencePub.contentURI = referencePubParams.contentURI; + referencePub.pubType = referencePubType; + let rootProfileId = self + ._fillRootOfPublicationInStorage( + referencePubParams.pointedProfile_address, + referencePubParams.pointedPubId, + profile_contract_address + ); + (profile.pubCount, rootProfileId) + } + + fn _createReferencePublication( + ref self: ContractState, + ref referencePubParams: ReferencePubParams, + referencePubType: PublicationType, + profile_contract_address: ContractAddress + ) -> u256 { + self._validatePointedPub(referencePubParams.pointedProfile_address); + + let (pubIdAssigned, rootProfileId) = self + ._fillRefeferencePublicationStorage( + ref referencePubParams, referencePubType, profile_contract_address + ); + + if (rootProfileId != referencePubParams.pointedProfile_address) { + self + .validateNotBlocked( + referencePubParams.profile_address, + referencePubParams.pointedProfile_address, + false + ); + } + pubIdAssigned + } + + fn _blockedStatus( + ref self: ContractState, + profile_address: ContractAddress, + byProfile_address: ContractAddress + ) -> bool { + self.blocked_profile_address.read((profile_address, byProfile_address)) + } + + + fn validateNotBlocked( + ref self: ContractState, + profile_address: ContractAddress, + byProfile_address: ContractAddress, + unidirectionalCheck: bool, + ) { + if (profile_address != byProfile_address + && (self._blockedStatus(profile_address, byProfile_address) + || (!unidirectionalCheck + && self._blockedStatus(byProfile_address, profile_address)))) { + return; + } + } + + fn _validatePointedPub(ref self: ContractState, profile_address: ContractAddress) { + // If it is pointing to itself it will fail because it will return a non-existent type. + let pointedPubType = self._getPublicationType(profile_address); + if pointedPubType == PublicationType::Nonexistent + || pointedPubType == PublicationType::Mirror { + return; + } + } + + fn _getPublicationType( + ref self: ContractState, profile_address: ContractAddress + ) -> PublicationType { + let pubType = self.publication.read(profile_address).pubType; + match pubType { + PublicationType::Nonexistent => PublicationType::Nonexistent, + PublicationType::Post => PublicationType::Post, + PublicationType::Comment => PublicationType::Comment, + PublicationType::Mirror => PublicationType::Mirror, + PublicationType::Quote => PublicationType::Quote, + } + } + + } +} diff --git a/tests/test_karstnft.cairo b/tests/test_karstnft.cairo index ef0e1c7..3552841 100644 --- a/tests/test_karstnft.cairo +++ b/tests/test_karstnft.cairo @@ -78,7 +78,7 @@ fn test_token_mint() { let profile_contract_address = deploy_profile(); let acct_class_hash = declare("Account").unwrap_syscall().class_hash; let karstDispatcher = IKarstNFTDispatcher { contract_address }; - let erc721Dispatcher = IERC721Dispatcher { contract_address }; + let _erc721Dispatcher = IERC721Dispatcher { contract_address }; //user 1 create profile start_prank( @@ -87,58 +87,31 @@ fn test_token_mint() { ); let dispatcher = IKarstProfileDispatcher { contract_address: profile_contract_address }; dispatcher.create_profile(contract_address, registry_class_hash, acct_class_hash.into(), 2456); - let token_id = karstDispatcher.get_user_token_id(user1.try_into().unwrap()); - let _owner = erc721Dispatcher.owner_of(token_id); let current_token_id = karstDispatcher.get_current_token_id(); - let _token_user1_uri = erc721Dispatcher.token_uri(token_id); dispatcher.set_profile_metadata_uri("ipfs://QmSkDCsS32eLpcymxtn1cEn7Rc5hfefLBgfvZyjaYXr4gQ/"); - let user1_profile_id = dispatcher.get_user_profile_address(user1.try_into().unwrap()); - let user1_profile_uri = dispatcher.get_profile(user1_profile_id); + let user1_profile_uri = dispatcher.get_profile_metadata(user1.try_into().unwrap()); assert( user1_profile_uri == "ipfs://QmSkDCsS32eLpcymxtn1cEn7Rc5hfefLBgfvZyjaYXr4gQ/", 'invalid' ); + assert(current_token_id == 1, 'invalid'); stop_prank(CheatTarget::Multiple(array![profile_contract_address, contract_address])); - //user2 create profile + //user 2 create profile start_prank( CheatTarget::Multiple(array![profile_contract_address, contract_address]), user2.try_into().unwrap() ); - let karstDispatcher = IKarstNFTDispatcher { contract_address }; - karstDispatcher.mint_karstnft(user2.try_into().unwrap()); - let _user2_token_id = karstDispatcher.get_user_token_id(user2.try_into().unwrap()); - let current_token_id = karstDispatcher.get_current_token_id(); + let dispatcher = IKarstProfileDispatcher { contract_address: profile_contract_address }; dispatcher.create_profile(contract_address, registry_class_hash, acct_class_hash.into(), 2456); - assert(current_token_id == 2, 'invalid'); - stop_prank(CheatTarget::Multiple(array![profile_contract_address, contract_address])); - - //user3 create profile - start_prank( - CheatTarget::Multiple(array![profile_contract_address, contract_address]), - user3.try_into().unwrap() - ); - let karstDispatcher = IKarstNFTDispatcher { contract_address }; - karstDispatcher.mint_karstnft(user3.try_into().unwrap()); - let user3_token_id = karstDispatcher.get_user_token_id(user3.try_into().unwrap()); - let _token_user3_uri = erc721Dispatcher.token_uri(user3_token_id); let current_token_id = karstDispatcher.get_current_token_id(); - dispatcher.create_profile(contract_address, registry_class_hash, acct_class_hash.into(), 2456); - assert(current_token_id == 3, 'invalid'); - stop_prank(CheatTarget::Multiple(array![profile_contract_address, contract_address])); - - //user4 create profile - start_prank( - CheatTarget::Multiple(array![profile_contract_address, contract_address]), - user4.try_into().unwrap() + dispatcher.set_profile_metadata_uri("ipfs://QmSkDCsS32eLpcymxtn1cEn7Rc5hfefLBgfvZyjaYXr4gQ/"); + let user2_profile_uri = dispatcher.get_profile_metadata(user2.try_into().unwrap()); + assert( + user2_profile_uri == "ipfs://QmSkDCsS32eLpcymxtn1cEn7Rc5hfefLBgfvZyjaYXr4gQ/", 'invalid' ); - let karstDispatcher = IKarstNFTDispatcher { contract_address }; - karstDispatcher.mint_karstnft(user4.try_into().unwrap()); - let user4_token_id = karstDispatcher.get_user_token_id(user4.try_into().unwrap()); - let _token_user4_uri = erc721Dispatcher.token_uri(user4_token_id); - let current_token_id = karstDispatcher.get_current_token_id(); - dispatcher.create_profile(contract_address, registry_class_hash, acct_class_hash.into(), 2456); - assert(current_token_id == 4, 'invalid'); + + assert(current_token_id == 2, 'invalid'); stop_prank(CheatTarget::Multiple(array![profile_contract_address, contract_address])); } @@ -146,7 +119,4 @@ fn test_token_mint() { fn to_address(name: felt252) -> ContractAddress { name.try_into().unwrap() } -// To do: -// - Test profile token balance -