diff --git a/src/base/constants/errors.cairo b/src/base/constants/errors.cairo index ed767e5..c1689d7 100644 --- a/src/base/constants/errors.cairo +++ b/src/base/constants/errors.cairo @@ -25,4 +25,6 @@ pub mod Errors { pub const NOT_MEMBER: felt252 = 'Karst: Not a Community Member'; pub const NOT_TOKEN_OWNER: felt252 = 'Karst: Not a Token Owner'; pub const TOKEN_DOES_NOT_EXIST: felt252 = 'Karst: Token does not exist'; + pub const TOKEN_DOES_NOT_EXIST: felt252 = 'Karst: token_id does not exist!'; + } diff --git a/src/base/constants/types.cairo b/src/base/constants/types.cairo index 1b397f9..102e238 100644 --- a/src/base/constants/types.cairo +++ b/src/base/constants/types.cairo @@ -63,6 +63,9 @@ pub struct Publication { pub root_pub_id: u256, pub upvote: u256, pub downvote: u256, + pub channel_id: felt252, + pub collect_nft: ContractAddress, + pub tipped_amount: u256 } // /** @@ -92,6 +95,7 @@ pub enum PublicationType { pub struct PostParams { pub content_URI: ByteArray, pub profile_address: ContractAddress, + pub channel_id: felt252 } // /** @@ -153,35 +157,7 @@ pub struct QuoteParams { pub reference_pub_type: PublicationType } -#[derive(Debug, Drop, Serde, starknet::Store, Clone)] -pub struct Upvote { - pub publication_id: u256, - pub transaction_executor: ContractAddress, - pub block_timestamp: u64, -} -#[derive(Debug, Drop, Serde, starknet::Store, Clone)] -pub struct Downvote { - pub publication_id: u256, - pub transaction_executor: ContractAddress, - pub block_timestamp: u64, -} - -// /** -// * @notice A struct containing the parameters required for the `create_community()` function. -// * -// * @param community_owner The address of the profile to the create the community. -// * @param community_metadata_uri The URI to set for this new community. -// * @param community_nft_address The nft address of the community. -// * @param community_premium_status The community is premium or not . -// */ -// #[derive(Debug, Drop, Serde, starknet::Store, Clone)] -// pub struct CommunityParams { -// community_id: u256, -// community_owner: ContractAddress, -// community_nft_address: ContractAddress, -// community_premium_status: bool -// } #[derive(Debug, Drop, Serde, starknet::Store, Clone)] pub struct CommunityDetails { @@ -211,12 +187,7 @@ pub struct CommunityMember { ban_status: bool, } -// #[derive(Debug, Drop, Serde, starknet::Store, Clone)] -// pub struct CommunityMod { -// community_id: u256, -// transaction_executor: ContractAddress, -// mod_address: ContractAddress, -// } + #[derive(Debug, Drop, Serde, starknet::Store, Clone)] pub struct CommunityGateKeepDetails { diff --git a/src/collectnft.cairo b/src/collectnft.cairo new file mode 100644 index 0000000..a2daad5 --- /dev/null +++ b/src/collectnft.cairo @@ -0,0 +1 @@ +pub mod collectnft; diff --git a/src/collectnft/collectnft.cairo b/src/collectnft/collectnft.cairo new file mode 100644 index 0000000..56adc51 --- /dev/null +++ b/src/collectnft/collectnft.cairo @@ -0,0 +1,172 @@ +#[starknet::contract] +pub mod CollectNFT { + // ************************************************************************* + // IMPORTS + // ************************************************************************* + use core::array::ArrayTrait; + use core::traits::Into; + use core::option::OptionTrait; + use core::traits::TryInto; + use starknet::{ContractAddress, get_block_timestamp}; + use core::num::traits::zero::Zero; + use karst::interfaces::ICollectNFT::ICollectNFT; + use karst::interfaces::IHub::{IHubDispatcher, IHubDispatcherTrait}; + use karst::base::{ + constants::errors::Errors::{ALREADY_MINTED, TOKEN_DOES_NOT_EXIST}, + utils::base64_extended::convert_into_byteArray + }; + use starknet::storage::{ + Map, StoragePointerWriteAccess, StoragePointerReadAccess, StorageMapReadAccess, + StorageMapWriteAccess + }; + use openzeppelin::{ + access::ownable::OwnableComponent, token::erc721::{ERC721Component, ERC721HooksEmptyImpl}, + introspection::{src5::SRC5Component} + }; + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + + // allow to check what interface is supported + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + impl SRC5InternalImpl = SRC5Component::InternalImpl; + + // make it a NFT + #[abi(embed_v0)] + impl ERC721Impl = ERC721Component::ERC721Impl; + #[abi(embed_v0)] + impl ERC721CamelOnlyImpl = ERC721Component::ERC721CamelOnlyImpl; + impl ERC721InternalImpl = ERC721Component::InternalImpl; + + // add an owner + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + + // ************************************************************************* + // STORAGE + // ************************************************************************* + #[storage] + struct Storage { + #[substorage(v0)] + erc721: ERC721Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + karst_hub: ContractAddress, + last_minted_id: u256, + mint_timestamp: Map, + user_token_id: Map, + profile_address: ContractAddress, + pub_id: u256, + } + + // ************************************************************************* + // EVENTS + // ************************************************************************* + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + } + + // ************************************************************************* + // CONSTRUCTOR + // ************************************************************************* + #[constructor] + fn constructor( + ref self: ContractState, + karst_hub: ContractAddress, + profile_address: ContractAddress, + pub_id: u256, + ) { + self.karst_hub.write(karst_hub); + self.profile_address.write(profile_address); + self.pub_id.write(pub_id); + } + + #[abi(embed_v0)] + impl CollectNFTImpl of ICollectNFT { + // ************************************************************************* + // EXTERNAL + // ************************************************************************* + /// @notice mints the collect NFT + /// @param address address of user trying to mint the collect NFT + fn mint_nft(ref self: ContractState, address: ContractAddress) -> u256 { + let balance = self.erc721.balance_of(address); + assert(balance.is_zero(), ALREADY_MINTED); + + let mut token_id = self.last_minted_id.read() + 1; + self.erc721.mint(address, token_id); + let timestamp: u64 = get_block_timestamp(); + self.user_token_id.write(address, token_id); + self.last_minted_id.write(token_id); + self.mint_timestamp.write(token_id, timestamp); + self.last_minted_id.read() + } + + // ************************************************************************* + // GETTERS + // ************************************************************************* + /// @notice gets the token ID for a user address + /// @param user address of user to retrieve token ID for + fn get_user_token_id(self: @ContractState, user: ContractAddress) -> u256 { + self.user_token_id.read(user) + } + + fn get_token_mint_timestamp(self: @ContractState, token_id: u256) -> u64 { + self.mint_timestamp.read(token_id) + } + + /// @notice gets the last minted NFT + fn get_last_minted_id(self: @ContractState) -> u256 { + self.last_minted_id.read() + } + ///@notice get source publication pointer + /// + fn get_source_publication_pointer(self: @ContractState) -> (ContractAddress, u256) { + let profile_address = self.profile_address.read(); + let pub_id = self.pub_id.read(); + (profile_address, pub_id) + } + // ************************************************************************* + // METADATA + // ************************************************************************* + /// @notice returns the collection name + fn name(self: @ContractState) -> ByteArray { + let mut collection_name = ArrayTrait::::new(); + let profile_address_felt252: felt252 = self.profile_address.read().into(); + let pub_id_felt252: felt252 = self.pub_id.read().try_into().unwrap(); + collection_name.append('Karst Collect | Profile #'); + collection_name.append(profile_address_felt252); + collection_name.append('- Publication #'); + collection_name.append(pub_id_felt252); + let collection_name_byte = convert_into_byteArray(ref collection_name); + collection_name_byte + } + + /// @notice returns the collection symbol + fn symbol(self: @ContractState) -> ByteArray { + return "KARST:COLLECT"; + } + + /// @notice returns the token_uri for a particular token_id + fn token_uri(self: @ContractState, token_id: u256) -> ByteArray { + assert(self.erc721.exists(token_id), TOKEN_DOES_NOT_EXIST); + let profile_address = self.profile_address.read(); + let pub_id = self.pub_id.read(); + let karst_hub = self.karst_hub.read(); + let token_uri = IHubDispatcher { contract_address: karst_hub } + .get_publication_content_uri(profile_address, pub_id); + token_uri + } + } +} diff --git a/src/interfaces.cairo b/src/interfaces.cairo index d1ae9ad..c656948 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -8,3 +8,5 @@ pub mod IHandle; pub mod IHandleRegistry; pub mod IHub; pub mod ICommunity; +pub mod ICollectNFT; + diff --git a/src/interfaces/ICollectNFT.cairo b/src/interfaces/ICollectNFT.cairo new file mode 100644 index 0000000..0148d0e --- /dev/null +++ b/src/interfaces/ICollectNFT.cairo @@ -0,0 +1,21 @@ +use starknet::ContractAddress; +#[starknet::interface] +pub trait ICollectNFT { + // ************************************************************************* + // EXTERNALS + // ************************************************************************* + fn mint_nft(ref self: TState, address: ContractAddress) -> u256; + // ************************************************************************* + // GETTERS + // ************************************************************************* + fn get_last_minted_id(self: @TState) -> u256; + fn get_user_token_id(self: @TState, user: ContractAddress) -> u256; + fn get_token_mint_timestamp(self: @TState, token_id: u256) -> u64; + fn get_source_publication_pointer(self: @TState) -> (ContractAddress, u256); + // ************************************************************************* + // METADATA + // ************************************************************************* + fn name(self: @TState) -> ByteArray; + fn symbol(self: @TState) -> ByteArray; + fn token_uri(self: @TState, token_id: u256) -> ByteArray; +} diff --git a/src/interfaces/IPublication.cairo b/src/interfaces/IPublication.cairo index d730ac5..8c94e18 100644 --- a/src/interfaces/IPublication.cairo +++ b/src/interfaces/IPublication.cairo @@ -16,8 +16,15 @@ pub trait IKarstPublications { fn repost(ref self: TState, repost_params: RepostParams) -> u256; fn upvote(ref self: TState, profile_address: ContractAddress, pub_id: u256); fn downvote(ref self: TState, profile_address: ContractAddress, pub_id: u256); - fn collect(ref self: TState, pub_id: u256) -> bool; - + fn tip(ref self: TState, profile_address: ContractAddress, pub_id: u256, amount: u256); + fn collect( + ref self: TState, + karst_hub: ContractAddress, + profile_address: ContractAddress, + pub_id: u256, + collect_nft_impl_class_hash: felt252, + salt: felt252 + ) -> u256; // ************************************************************************* // GETTERS // ************************************************************************* @@ -30,7 +37,7 @@ pub trait IKarstPublications { fn get_publication_content_uri( self: @TState, profile_address: ContractAddress, pub_id: u256 ) -> ByteArray; - fn has_user_voted(self: @TState, profile_address: ContractAddress, pub_id: u256) -> bool; fn get_upvote_count(self: @TState, profile_address: ContractAddress, pub_id: u256) -> u256; fn get_downvote_count(self: @TState, profile_address: ContractAddress, pub_id: u256) -> u256; + fn get_tipped_amount(self: @TState, profile_address: ContractAddress, pub_id: u256) -> u256; } diff --git a/src/lib.cairo b/src/lib.cairo index c3f5301..66136a0 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -8,3 +8,4 @@ pub mod publication; pub mod namespaces; pub mod presets; pub mod hub; +pub mod collectnft; diff --git a/src/mocks/interfaces/IComposable.cairo b/src/mocks/interfaces/IComposable.cairo index af8ec4d..2ca8043 100644 --- a/src/mocks/interfaces/IComposable.cairo +++ b/src/mocks/interfaces/IComposable.cairo @@ -46,7 +46,16 @@ pub trait IComposable { fn repost(ref self: TState, mirror_params: RepostParams) -> u256; fn upvote(ref self: TState, profile_address: ContractAddress, pub_id: u256); fn downvote(ref self: TState, profile_address: ContractAddress, pub_id: u256); - fn collect(ref self: TState, pub_id: u256) -> bool; + fn tip(ref self: TState, profile_address: ContractAddress, pub_id: u256, amount: u256); + fn collect( + ref self: TState, + karst_hub: ContractAddress, + profile_address: ContractAddress, + pub_id: u256, + collect_nft_impl_class_hash: felt252, + salt: felt252 + ) -> u256; + // ************************************************************************* // GETTERS // ************************************************************************* @@ -62,4 +71,5 @@ pub trait IComposable { fn has_user_voted(self: @TState, profile_address: ContractAddress, pub_id: u256) -> bool; fn get_upvote_count(self: @TState, profile_address: ContractAddress, pub_id: u256) -> u256; fn get_downvote_count(self: @TState, profile_address: ContractAddress, pub_id: u256) -> u256; + fn get_tipped_amount(self: @TState, profile_address: ContractAddress, pub_id: u256) -> u256; } diff --git a/src/profile/profile.cairo b/src/profile/profile.cairo index f2fe6de..6b2e8b5 100644 --- a/src/profile/profile.cairo +++ b/src/profile/profile.cairo @@ -188,13 +188,7 @@ pub mod ProfileComponent { ) -> u256 { let mut profile: Profile = self.profile.read(profile_address); let new_pub_count = profile.pub_count + 1; - let updated_profile = Profile { - profile_address: profile.profile_address, - profile_owner: profile.profile_owner, - pub_count: new_pub_count, - metadata_URI: profile.metadata_URI, - follow_nft: profile.follow_nft - }; + let updated_profile = Profile { pub_count: new_pub_count, ..profile }; self.profile.write(profile_address, updated_profile); new_pub_count diff --git a/src/publication/publication.cairo b/src/publication/publication.cairo index f49bd57..dcf7a78 100644 --- a/src/publication/publication.cairo +++ b/src/publication/publication.cairo @@ -5,12 +5,15 @@ pub mod PublicationComponent { // ************************************************************************* use core::traits::TryInto; use karst::interfaces::IProfile::IProfile; + use core::num::traits::zero::Zero; use core::option::OptionTrait; use starknet::{ - ContractAddress, get_caller_address, get_block_timestamp, + ContractAddress, get_caller_address, get_block_timestamp, syscalls::{deploy_syscall}, + class_hash::ClassHash, SyscallResultTrait, storage::{Map, StorageMapReadAccess, StorageMapWriteAccess} }; use karst::interfaces::IPublication::IKarstPublications; + use karst::interfaces::ICollectNFT::{ICollectNFTDispatcher, ICollectNFTDispatcherTrait}; use karst::base::{ constants::errors::Errors::{NOT_PROFILE_OWNER, UNSUPPORTED_PUB_TYPE, ALREADY_REACTED}, constants::types::{PostParams, Publication, PublicationType, CommentParams, RepostParams} @@ -39,7 +42,9 @@ pub mod PublicationComponent { CommentCreated: CommentCreated, RepostCreated: RepostCreated, Upvoted: Upvoted, - Downvoted: Downvoted + Downvoted: Downvoted, + CollectedNFT: CollectedNFT, + DeployedCollectNFT: DeployedCollectNFT } #[derive(Drop, starknet::Event)] @@ -80,6 +85,23 @@ pub mod PublicationComponent { pub block_timestamp: u64, } + #[derive(Drop, starknet::Event)] + pub struct CollectedNFT { + publication_id: u256, + transaction_executor: ContractAddress, + token_id: u256, + block_timestamp: u64, + } + + #[derive(Drop, starknet::Event)] + pub struct DeployedCollectNFT { + publication_id: u256, + profile_address: ContractAddress, + collect_nft: ContractAddress, + block_timestamp: u64, + } + + // ************************************************************************* // EXTERNAL FUNCTIONS // ************************************************************************* @@ -115,7 +137,10 @@ pub mod PublicationComponent { root_profile_address: 0.try_into().unwrap(), root_pub_id: 0, upvote: 0, - downvote: 0 + downvote: 0, + channel_id: 0, + collect_nft: 0.try_into().unwrap(), + tipped_amount: 0 }; self.publication.write((post_params.profile_address, pub_id_assigned), new_post); @@ -217,16 +242,7 @@ pub mod PublicationComponent { let has_voted = self.vote_status.read((caller, pub_id)); let upvote_current_count = publication.upvote + 1; assert(has_voted == false, ALREADY_REACTED); - let updated_publication = Publication { - pointed_profile_address: publication.pointed_profile_address, - pointed_pub_id: publication.pointed_pub_id, - content_URI: publication.content_URI, - pub_Type: publication.pub_Type, - root_profile_address: publication.root_profile_address, - root_pub_id: publication.root_pub_id, - upvote: upvote_current_count, - downvote: publication.downvote - }; + let updated_publication = Publication { upvote: upvote_current_count, ..publication }; self.vote_status.write((caller, pub_id), true); self.publication.write((profile_address, pub_id), updated_publication); @@ -253,14 +269,7 @@ pub mod PublicationComponent { let downvote_current_count = publication.downvote + 1; assert(has_voted == false, ALREADY_REACTED); let updated_publication = Publication { - pointed_profile_address: publication.pointed_profile_address, - pointed_pub_id: publication.pointed_pub_id, - content_URI: publication.content_URI, - pub_Type: publication.pub_Type, - root_profile_address: publication.root_profile_address, - root_pub_id: publication.root_pub_id, - upvote: publication.upvote, - downvote: downvote_current_count, + downvote: downvote_current_count, ..publication }; self.publication.write((profile_address, pub_id), updated_publication); self.vote_status.write((caller, pub_id), true); @@ -273,8 +282,48 @@ pub mod PublicationComponent { } ) } - fn collect(ref self: ComponentState, pub_id: u256) -> bool { - true + //@ tip a user + //@param profile_address: + //@param pub_id: publication_id of publication to be tipped + // @param amount: amount to tip a publication + fn tip( + ref self: ComponentState, + profile_address: ContractAddress, + pub_id: u256, + amount: u256 + ) { + let mut publication = self.get_publication(profile_address, pub_id); + let current_tip_amount = publication.tipped_amount; + let updated_publication = Publication { + tipped_amount: current_tip_amount + amount, ..publication + }; + self.publication.write((profile_address, pub_id), updated_publication) + } + // @notice collect nft for a publication + fn collect( + ref self: ComponentState, + karst_hub: ContractAddress, + profile_address: ContractAddress, + pub_id: u256, + collect_nft_impl_class_hash: felt252, + salt: felt252 + ) -> u256 { + let collect_nft_address = self + ._get_or_deploy_collect_nft( + karst_hub, profile_address, pub_id, collect_nft_impl_class_hash, salt + ); + let token_id = self._mint_collect_nft(collect_nft_address); + + self + .emit( + CollectedNFT { + publication_id: pub_id, + transaction_executor: get_caller_address(), + token_id: token_id, + block_timestamp: get_block_timestamp() + } + ); + token_id } // ************************************************************************* @@ -312,14 +361,6 @@ pub mod PublicationComponent { self._get_publication_type(profile_address, pub_id_assigned) } - /// @notice retrieves a post vote_status - /// @param pub_id the ID of the publication whose count is to be retrieved - fn has_user_voted( - self: @ComponentState, profile_address: ContractAddress, pub_id: u256 - ) -> bool { - let status = self.vote_status.read((profile_address, pub_id)); - status - } /// @notice retrieves the upvote count /// @param profile_address the the profile address to be queried @@ -339,6 +380,15 @@ pub mod PublicationComponent { let downvote_count = self.get_publication(profile_address, pub_id).downvote; downvote_count } + /// @notice retrieves tip amount + /// @param profile_address the the profile address to be queried + /// @param pub_id the ID of the publication + fn get_tipped_amount( + self: @ComponentState, profile_address: ContractAddress, pub_id: u256 + ) -> u256 { + let tipped_amount = self.get_publication(profile_address, pub_id).tipped_amount; + tipped_amount + } } @@ -402,7 +452,10 @@ pub mod PublicationComponent { root_pub_id: root_pub_id, root_profile_address: root_profile_address, upvote: 0, - downvote: 0 + downvote: 0, + channel_id: pointed_pub.channel_id, + collect_nft: 0.try_into().unwrap(), + tipped_amount: 0 }; self.publication.write((profile_address, pub_id_assigned), updated_reference); @@ -499,6 +552,79 @@ pub mod PublicationComponent { content_uri } } + + /// @notice retrieves a post vote_status + /// @param pub_id the ID of the publication whose count is to be retrieved + fn _has_user_voted( + self: @ComponentState, profile_address: ContractAddress, pub_id: u256 + ) -> bool { + let status = self.vote_status.read((profile_address, pub_id)); + status + } + + fn _deploy_collect_nft( + ref self: ComponentState, + karst_hub: ContractAddress, + profile_address: ContractAddress, + pub_id: u256, + collect_nft_impl_class_hash: felt252, + salt: felt252 + ) -> ContractAddress { + let mut constructor_calldata: Array = array![ + karst_hub.into(), profile_address.into(), pub_id.low.into(), pub_id.high.into() + ]; + let class_hash: ClassHash = collect_nft_impl_class_hash.try_into().unwrap(); + let result = deploy_syscall(class_hash, salt, constructor_calldata.span(), true); + let (account_address, _) = result.unwrap_syscall(); + + self + .emit( + DeployedCollectNFT { + publication_id: pub_id, + profile_address: profile_address, + collect_nft: account_address, + block_timestamp: get_block_timestamp() + } + ); + account_address + } + fn _get_or_deploy_collect_nft( + ref self: ComponentState, + karst_hub: ContractAddress, + profile_address: ContractAddress, + pub_id: u256, + collect_nft_impl_class_hash: felt252, + salt: felt252 + ) -> ContractAddress { + let mut publication = self.get_publication(profile_address, pub_id); + let collect_nft = publication.collect_nft; + if collect_nft.is_zero() { + // Deploy a new Collect NFT contract + let deployed_collect_nft_address = self + ._deploy_collect_nft( + karst_hub, profile_address, pub_id, collect_nft_impl_class_hash, salt + ); + + // Update the publication with the deployed Collect NFT address + let updated_publication = Publication { + pointed_profile_address: publication.pointed_profile_address, + collect_nft: deployed_collect_nft_address, + ..publication + }; + + // Write the updated publication with the new Collect NFT address + self.publication.write((profile_address, pub_id), updated_publication); + } + let collect_nft_address = self.get_publication(profile_address, pub_id).collect_nft; + collect_nft_address + } + fn _mint_collect_nft( + ref self: ComponentState, collect_nft: ContractAddress + ) -> u256 { + let caller: ContractAddress = get_caller_address(); + let token_id = ICollectNFTDispatcher { contract_address: collect_nft }.mint_nft(caller); + token_id + } } } diff --git a/tests/test_publication.cairo b/tests/test_publication.cairo index 4b145dd..63ca18b 100644 --- a/tests/test_publication.cairo +++ b/tests/test_publication.cairo @@ -5,28 +5,19 @@ 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 starknet::{ContractAddress, 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 + declare, start_cheat_caller_address, stop_cheat_caller_address, spy_events, + EventSpyAssertionsTrait, ContractClassTrait, DeclareResultTrait, EventSpy }; use karst::publication::publication::PublicationComponent::{ Event as PublicationEvent, Post, CommentCreated, RepostCreated, Upvoted, Downvoted }; -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::publication::KarstPublication; -use karst::follownft::follownft::Follow; use karst::mocks::interfaces::IComposable::{IComposableDispatcher, IComposableDispatcherTrait}; -use karst::base::constants::types::{ - PostParams, RepostParams, CommentParams, PublicationType, QuoteParams -}; +use karst::base::constants::types::{PostParams, RepostParams, CommentParams, PublicationType,}; +use karst::interfaces::ICollectNFT::{ICollectNFTDispatcher, ICollectNFTDispatcherTrait}; const HUB_ADDRESS: felt252 = 'HUB'; const USER_ONE: felt252 = 'BOB'; @@ -49,7 +40,8 @@ fn __setup__() -> ( ContractAddress, ContractAddress, u256, - EventSpy + EventSpy, + felt252 ) { // deploy NFT let nft_contract = declare("KarstNFT").unwrap().contract_class(); @@ -73,6 +65,9 @@ fn __setup__() -> ( // declare follownft let follow_nft_classhash = declare("Follow").unwrap().contract_class(); + //declare collectnft + let collect_nft_classhash = declare("CollectNFT").unwrap().contract_class(); + // create dispatcher, initialize profile contract let dispatcher = IComposableDispatcher { contract_address: publication_contract_address }; dispatcher @@ -94,7 +89,11 @@ fn __setup__() -> ( let content_URI: ByteArray = "ipfs://helloworld"; let mut spy = spy_events(); let user_one_first_post_pointed_pub_id = dispatcher - .post(PostParams { content_URI: content_URI, profile_address: user_one_profile_address, }); + .post( + PostParams { + content_URI: content_URI, profile_address: user_one_profile_address, channel_id: 0 + } + ); stop_cheat_caller_address(publication_contract_address); // deploying karst Profile for USER 2 @@ -108,7 +107,11 @@ fn __setup__() -> ( ); let content_URI: ByteArray = "ipfs://helloworld"; dispatcher - .post(PostParams { content_URI: content_URI, profile_address: user_two_profile_address, }); + .post( + PostParams { + content_URI: content_URI, profile_address: user_two_profile_address, channel_id: 0 + } + ); stop_cheat_caller_address(publication_contract_address); // deploying karst Profile for USER 3 @@ -124,7 +127,9 @@ fn __setup__() -> ( let content_URI: ByteArray = "ipfs://helloworld"; dispatcher .post( - PostParams { content_URI: content_URI, profile_address: user_three_profile_address, } + PostParams { + content_URI: content_URI, profile_address: user_three_profile_address, channel_id: 0 + } ); stop_cheat_caller_address(publication_contract_address); @@ -138,7 +143,8 @@ fn __setup__() -> ( user_two_profile_address, user_three_profile_address, user_one_first_post_pointed_pub_id, - spy + spy, + (*collect_nft_classhash.class_hash).into() ); } @@ -158,6 +164,7 @@ fn test_post() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -185,6 +192,7 @@ fn test_upvote() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -194,12 +202,9 @@ fn test_upvote() { dispatcher.upvote(user_one_profile_address, user_one_first_post_pointed_pub_id); stop_cheat_caller_address(publication_contract_address); - start_cheat_caller_address(publication_contract_address, USER_THREE.try_into().unwrap()); - dispatcher.upvote(user_one_profile_address, user_one_first_post_pointed_pub_id); - stop_cheat_caller_address(publication_contract_address); let upvote_count = dispatcher .get_upvote_count(user_one_profile_address, user_one_first_post_pointed_pub_id); - assert(upvote_count == 2, 'invalid upvote count'); + assert(upvote_count == 1, 'invalid upvote count'); } #[test] @@ -214,21 +219,17 @@ fn test_downvote() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); let dispatcher = IComposableDispatcher { contract_address: publication_contract_address }; - // downvote start_cheat_caller_address(publication_contract_address, USER_FOUR.try_into().unwrap()); dispatcher.downvote(user_one_profile_address, user_one_first_post_pointed_pub_id); stop_cheat_caller_address(publication_contract_address); - - start_cheat_caller_address(publication_contract_address, USER_FIVE.try_into().unwrap()); - dispatcher.downvote(user_one_profile_address, user_one_first_post_pointed_pub_id); - stop_cheat_caller_address(publication_contract_address); let downvote_count = dispatcher .get_downvote_count(user_one_profile_address, user_one_first_post_pointed_pub_id); - assert(downvote_count == 2, 'invalid downvote count'); + assert(downvote_count == 1, 'invalid downvote count'); stop_cheat_caller_address(publication_contract_address); } @@ -244,7 +245,8 @@ fn test_upvote_event_emission() { _, _, user_one_first_post_pointed_pub_id, - spy + spy, + _ ) = __setup__(); @@ -276,7 +278,8 @@ fn test_downvote_event_emission() { _, _, user_one_first_post_pointed_pub_id, - spy + spy, + _ ) = __setup__(); @@ -296,6 +299,56 @@ fn test_downvote_event_emission() { stop_cheat_caller_address(publication_contract_address); } +#[test] +#[should_panic(expected: ('Karst: already react to post!',))] +fn test_upvote_should_fail_if_user_already_upvoted() { + let ( + _, + _, + publication_contract_address, + _, + _, + user_one_profile_address, + _, + _, + user_one_first_post_pointed_pub_id, + _, + _ + ) = + __setup__(); + let dispatcher = IComposableDispatcher { contract_address: publication_contract_address }; + + start_cheat_caller_address(publication_contract_address, USER_TWO.try_into().unwrap()); + dispatcher.upvote(user_one_profile_address, user_one_first_post_pointed_pub_id); + dispatcher.upvote(user_one_profile_address, user_one_first_post_pointed_pub_id); + stop_cheat_caller_address(publication_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: already react to post!',))] +fn test_downvote_should_fail_if_user_already_downvoted() { + let ( + _, + _, + publication_contract_address, + _, + _, + user_one_profile_address, + _, + _, + user_one_first_post_pointed_pub_id, + _, + _ + ) = + __setup__(); + let dispatcher = IComposableDispatcher { contract_address: publication_contract_address }; + + start_cheat_caller_address(publication_contract_address, USER_FOUR.try_into().unwrap()); + dispatcher.downvote(user_one_profile_address, user_one_first_post_pointed_pub_id); + dispatcher.downvote(user_one_profile_address, user_one_first_post_pointed_pub_id); + stop_cheat_caller_address(publication_contract_address); +} + #[test] fn test_post_event_emission() { let ( @@ -308,7 +361,8 @@ fn test_post_event_emission() { _, _, user_one_first_post_pointed_pub_id, - spy + spy, + _ ) = __setup__(); @@ -317,7 +371,9 @@ fn test_post_event_emission() { let expected_event = PublicationEvent::Post( Post { post: PostParams { - content_URI: "ipfs://helloworld", profile_address: user_one_profile_address, + content_URI: "ipfs://helloworld", + profile_address: user_one_profile_address, + channel_id: 0 }, publication_id: user_one_first_post_pointed_pub_id, transaction_executor: user_one_profile_address, @@ -331,14 +387,18 @@ fn test_post_event_emission() { #[test] #[should_panic(expected: ('Karst: not profile owner!',))] fn test_posting_should_fail_if_not_profile_owner() { - let (_, _, publication_contract_address, _, _, user_one_profile_address, _, _, _, _) = + let (_, _, publication_contract_address, _, _, user_one_profile_address, _, _, _, _, _) = __setup__(); let dispatcher = IComposableDispatcher { contract_address: publication_contract_address }; start_cheat_caller_address(publication_contract_address, USER_TWO.try_into().unwrap()); let content_URI = "ipfs://QmSkDCsS32eLpcymxtn1cEn7Rc5hfefLBgfvZyjaryrga/"; dispatcher - .post(PostParams { content_URI: content_URI, profile_address: user_one_profile_address, }); + .post( + PostParams { + content_URI: content_URI, profile_address: user_one_profile_address, channel_id: 0 + } + ); stop_cheat_caller_address(publication_contract_address); } @@ -354,6 +414,7 @@ fn test_comment() { user_two_profile_address, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -453,7 +514,8 @@ fn test_comment_event_emission() { user_two_profile_address, _, user_one_first_post_pointed_pub_id, - spy + spy, + _ ) = __setup__(); @@ -506,6 +568,7 @@ fn test_nested_comments() { user_two_profile_address, user_three_profile_address, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -645,6 +708,7 @@ fn test_commenting_should_fail_if_not_profile_owner() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -678,6 +742,7 @@ fn test_as_reference_pub_params_should_fail_on_wrong_pub_type() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -713,6 +778,7 @@ fn test_repost() { user_two_profile_address, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -761,7 +827,8 @@ fn test_repost_event_emission() { user_two_profile_address, _, user_one_first_post_pointed_pub_id, - spy + spy, + _ ) = __setup__(); @@ -809,6 +876,7 @@ fn test_reposting_should_fail_if_not_profile_owner() { user_two_profile_address, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -836,6 +904,7 @@ fn test_get_publication_content_uri() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -858,6 +927,7 @@ fn test_get_publication_type() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -868,3 +938,83 @@ fn test_get_publication_type() { assert(pub_type == PublicationType::Post, 'invalid pub type'); } +#[test] +fn test_tip() { + let ( + _, + _, + publication_contract_address, + _, + _, + user_one_profile_address, + _, + _, + user_one_first_post_pointed_pub_id, + _, + _ + ) = + __setup__(); + let dispatcher = IComposableDispatcher { contract_address: publication_contract_address }; + start_cheat_caller_address(publication_contract_address, USER_TWO.try_into().unwrap()); + dispatcher.tip(user_one_profile_address, user_one_first_post_pointed_pub_id, 100); + let tipped_amount = dispatcher + .get_tipped_amount(user_one_profile_address, user_one_first_post_pointed_pub_id); + assert(tipped_amount == 100, 'invalid amount'); + stop_cheat_caller_address(publication_contract_address); +} + +#[test] +fn test_collect() { + let ( + _, + _, + publication_contract_address, + _, + _, + user_one_profile_address, + user_two_profile_address, + user_three_profile_address, + user_one_first_post_pointed_pub_id, + _, + collect_nft_classhash + ) = + __setup__(); + let dispatcher = IComposableDispatcher { contract_address: publication_contract_address }; + start_cheat_caller_address(publication_contract_address, user_two_profile_address); + // Case 1: First collection, expecting new deployment + let token_id = dispatcher + .collect( + HUB_ADDRESS.try_into().unwrap(), + user_one_profile_address, + user_one_first_post_pointed_pub_id, + collect_nft_classhash, + 23465 + ); + let collect_nft1 = dispatcher + .get_publication(user_one_profile_address, user_one_first_post_pointed_pub_id) + .collect_nft; + let collect_dispatcher = ICollectNFTDispatcher { contract_address: collect_nft1 }; + let user2_token_id = collect_dispatcher.get_user_token_id(user_two_profile_address); + + assert(token_id == user2_token_id, 'invalid token_id'); + stop_cheat_caller_address(publication_contract_address); + + start_cheat_caller_address(publication_contract_address, user_three_profile_address); + // Case 2: collect the same publication, expecting reuse of the existing contract + let token_id2 = dispatcher + .collect( + HUB_ADDRESS.try_into().unwrap(), + user_one_profile_address, + user_one_first_post_pointed_pub_id, + collect_nft_classhash, + 234657 + ); + let collect_nft2 = dispatcher + .get_publication(user_one_profile_address, user_one_first_post_pointed_pub_id) + .collect_nft; + let collect_dispatcher = ICollectNFTDispatcher { contract_address: collect_nft2 }; + let user3_token_id = collect_dispatcher.get_user_token_id(user_three_profile_address); + assert(collect_nft1 == collect_nft2, 'invalid_ address'); + assert(token_id2 == user3_token_id, 'invalid token_id'); + stop_cheat_caller_address(publication_contract_address); +}