From 49d6212d3593c35cf9db799fa22fadb6fa83c4e4 Mon Sep 17 00:00:00 2001 From: Adegbite Ademola Kelvin Date: Wed, 2 Oct 2024 13:28:22 +0100 Subject: [PATCH 1/4] feat: add tip, collect features --- src/base/constants/errors.cairo | 1 + src/base/constants/types.cairo | 17 +-- src/collectnft.cairo | 1 + src/collectnft/collectnft.cairo | 172 ++++++++++++++++++++++++ src/interfaces.cairo | 1 + src/interfaces/ICollectNFT.cairo | 21 +++ src/interfaces/IPublication.cairo | 12 +- src/lib.cairo | 1 + src/mocks/interfaces/IComposable.cairo | 12 +- src/profile/profile.cairo | 8 +- src/publication/publication.cairo | 175 +++++++++++++++++++++---- tests/test_publication.cairo | 135 +++++++++++++++---- 12 files changed, 482 insertions(+), 74 deletions(-) create mode 100644 src/collectnft.cairo create mode 100644 src/collectnft/collectnft.cairo create mode 100644 src/interfaces/ICollectNFT.cairo diff --git a/src/base/constants/errors.cairo b/src/base/constants/errors.cairo index 221d357..465da0c 100644 --- a/src/base/constants/errors.cairo +++ b/src/base/constants/errors.cairo @@ -19,4 +19,5 @@ pub mod Errors { pub const INVALID_PROFILE_ADDRESS: felt252 = 'Karst: invalid profile address!'; pub const SELF_FOLLOWING: felt252 = 'Karst: self follow is forbidden'; pub const ALREADY_REACTED: felt252 = 'Karst: already react to post!'; + 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 8e1d6c8..1bfdf2c 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,16 +157,3 @@ 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, -} 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 088ac5a..8f396d4 100644 --- a/src/interfaces.cairo +++ b/src/interfaces.cairo @@ -7,3 +7,4 @@ pub mod IPublication; pub mod IHandle; pub mod IHandleRegistry; pub mod IHub; +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..efa46f3 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 // ************************************************************************* @@ -33,4 +40,5 @@ pub trait IKarstPublications { 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..51f3c05 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, profile_address); + + self + .emit( + CollectedNFT { + publication_id: pub_id, + transaction_executor: get_caller_address(), + token_id: token_id, + block_timestamp: get_block_timestamp() + } + ); + token_id } // ************************************************************************* @@ -339,6 +388,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 +460,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 +560,72 @@ pub mod PublicationComponent { content_uri } } + + 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, + profile_address: ContractAddress + ) -> u256 { + let token_id = ICollectNFTDispatcher { contract_address: collect_nft } + .mint_nft(profile_address); + token_id + } } } diff --git a/tests/test_publication.cairo b/tests/test_publication.cairo index 4b145dd..6f8a70a 100644 --- a/tests/test_publication.cairo +++ b/tests/test_publication.cairo @@ -5,28 +5,18 @@ 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,}; const HUB_ADDRESS: felt252 = 'HUB'; const USER_ONE: felt252 = 'BOB'; @@ -49,7 +39,8 @@ fn __setup__() -> ( ContractAddress, ContractAddress, u256, - EventSpy + EventSpy, + felt252 ) { // deploy NFT let nft_contract = declare("KarstNFT").unwrap().contract_class(); @@ -73,6 +64,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 +88,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 +106,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 +126,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 +142,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 +163,7 @@ fn test_post() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -185,6 +191,7 @@ fn test_upvote() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -214,6 +221,7 @@ fn test_downvote() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -244,7 +252,8 @@ fn test_upvote_event_emission() { _, _, user_one_first_post_pointed_pub_id, - spy + spy, + _ ) = __setup__(); @@ -276,7 +285,8 @@ fn test_downvote_event_emission() { _, _, user_one_first_post_pointed_pub_id, - spy + spy, + _ ) = __setup__(); @@ -308,7 +318,8 @@ fn test_post_event_emission() { _, _, user_one_first_post_pointed_pub_id, - spy + spy, + _ ) = __setup__(); @@ -317,7 +328,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 +344,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 +371,7 @@ fn test_comment() { user_two_profile_address, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -453,7 +471,8 @@ fn test_comment_event_emission() { user_two_profile_address, _, user_one_first_post_pointed_pub_id, - spy + spy, + _ ) = __setup__(); @@ -506,6 +525,7 @@ fn test_nested_comments() { user_two_profile_address, user_three_profile_address, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -645,6 +665,7 @@ fn test_commenting_should_fail_if_not_profile_owner() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -678,6 +699,7 @@ fn test_as_reference_pub_params_should_fail_on_wrong_pub_type() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -713,6 +735,7 @@ fn test_repost() { user_two_profile_address, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -761,7 +784,8 @@ fn test_repost_event_emission() { user_two_profile_address, _, user_one_first_post_pointed_pub_id, - spy + spy, + _ ) = __setup__(); @@ -809,6 +833,7 @@ fn test_reposting_should_fail_if_not_profile_owner() { user_two_profile_address, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -836,6 +861,7 @@ fn test_get_publication_content_uri() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -858,6 +884,7 @@ fn test_get_publication_type() { _, _, user_one_first_post_pointed_pub_id, + _, _ ) = __setup__(); @@ -868,3 +895,57 @@ 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_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.try_into().unwrap()); + 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 + ); + assert(token_id == 1, 'invalid token id'); + stop_cheat_caller_address(publication_contract_address); +} From f13c5ad292364d33eae8fada396994dad0dd1b73 Mon Sep 17 00:00:00 2001 From: Adegbite Ademola Kelvin Date: Thu, 3 Oct 2024 04:05:26 +0100 Subject: [PATCH 2/4] test: add more test cases --- src/interfaces/IPublication.cairo | 1 - src/publication/publication.cairo | 27 ++++---- tests/test_publication.cairo | 111 ++++++++++++++++++++++++------ 3 files changed, 104 insertions(+), 35 deletions(-) diff --git a/src/interfaces/IPublication.cairo b/src/interfaces/IPublication.cairo index efa46f3..8c94e18 100644 --- a/src/interfaces/IPublication.cairo +++ b/src/interfaces/IPublication.cairo @@ -37,7 +37,6 @@ 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/publication/publication.cairo b/src/publication/publication.cairo index 51f3c05..70cac84 100644 --- a/src/publication/publication.cairo +++ b/src/publication/publication.cairo @@ -312,7 +312,7 @@ pub mod PublicationComponent { ._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, profile_address); + let token_id = self._mint_collect_nft(collect_nft_address); self .emit( @@ -361,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 @@ -561,6 +553,15 @@ pub mod PublicationComponent { } } + /// @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, @@ -618,12 +619,10 @@ pub mod PublicationComponent { collect_nft_address } fn _mint_collect_nft( - ref self: ComponentState, - collect_nft: ContractAddress, - profile_address: ContractAddress + ref self: ComponentState, collect_nft: ContractAddress, ) -> u256 { - let token_id = ICollectNFTDispatcher { contract_address: collect_nft } - .mint_nft(profile_address); + 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 6f8a70a..ed6653d 100644 --- a/tests/test_publication.cairo +++ b/tests/test_publication.cairo @@ -17,6 +17,7 @@ use karst::publication::publication::PublicationComponent::{ use karst::mocks::interfaces::IComposable::{IComposableDispatcher, IComposableDispatcherTrait}; 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'; @@ -132,6 +133,16 @@ fn __setup__() -> ( ); stop_cheat_caller_address(publication_contract_address); + //@notice: initialise upvote and downvote state to test that users cannot vote twice + // upvote + 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); + stop_cheat_caller_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); + return ( nft_contract_address, registry_contract_address, @@ -197,16 +208,9 @@ fn test_upvote() { __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); - 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] @@ -226,17 +230,9 @@ fn test_downvote() { ) = __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); } @@ -306,6 +302,54 @@ 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); + 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); + stop_cheat_caller_address(publication_contract_address); +} + #[test] fn test_post_event_emission() { let ( @@ -929,8 +973,8 @@ fn test_collect() { _, _, user_one_profile_address, - _, - _, + user_two_profile_address, + user_three_profile_address, user_one_first_post_pointed_pub_id, _, collect_nft_classhash @@ -938,6 +982,7 @@ fn test_collect() { __setup__(); let dispatcher = IComposableDispatcher { contract_address: publication_contract_address }; start_cheat_caller_address(publication_contract_address, USER_TWO.try_into().unwrap()); + // Case 1: First collection, expecting new deployment let token_id = dispatcher .collect( HUB_ADDRESS.try_into().unwrap(), @@ -946,6 +991,32 @@ fn test_collect() { collect_nft_classhash, 23465 ); - assert(token_id == 1, 'invalid token id'); + 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.try_into().unwrap()); + + 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.try_into().unwrap()); + // 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.try_into().unwrap()); + + assert(collect_nft1 == collect_nft2, 'invalid_ address'); + assert(token_id2 == user3_token_id, 'invalid token_id'); stop_cheat_caller_address(publication_contract_address); } From 60653e87b1add52d4658d20d022ad76cce846168 Mon Sep 17 00:00:00 2001 From: Adegbite Ademola Kelvin Date: Thu, 3 Oct 2024 04:40:03 +0100 Subject: [PATCH 3/4] chore: fix caller_address in publication test --- src/publication/publication.cairo | 2 +- tests/test_publication.cairo | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/publication/publication.cairo b/src/publication/publication.cairo index 70cac84..dcf7a78 100644 --- a/src/publication/publication.cairo +++ b/src/publication/publication.cairo @@ -619,7 +619,7 @@ pub mod PublicationComponent { collect_nft_address } fn _mint_collect_nft( - ref self: ComponentState, collect_nft: ContractAddress, + ref self: ComponentState, collect_nft: ContractAddress ) -> u256 { let caller: ContractAddress = get_caller_address(); let token_id = ICollectNFTDispatcher { contract_address: collect_nft }.mint_nft(caller); diff --git a/tests/test_publication.cairo b/tests/test_publication.cairo index ed6653d..ed8993f 100644 --- a/tests/test_publication.cairo +++ b/tests/test_publication.cairo @@ -981,7 +981,7 @@ fn test_collect() { ) = __setup__(); let dispatcher = IComposableDispatcher { contract_address: publication_contract_address }; - start_cheat_caller_address(publication_contract_address, USER_TWO.try_into().unwrap()); + start_cheat_caller_address(publication_contract_address, user_two_profile_address); // Case 1: First collection, expecting new deployment let token_id = dispatcher .collect( @@ -995,12 +995,12 @@ fn test_collect() { .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.try_into().unwrap()); + 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.try_into().unwrap()); + 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( @@ -1014,8 +1014,7 @@ fn test_collect() { .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.try_into().unwrap()); - + 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); From 640c5a732afe900a880de17a02ba2fbedbd9af7f Mon Sep 17 00:00:00 2001 From: Adegbite Ademola Kelvin Date: Thu, 3 Oct 2024 09:49:12 +0100 Subject: [PATCH 4/4] chore: remove upvote & downvote from __setup__ --- tests/test_publication.cairo | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/test_publication.cairo b/tests/test_publication.cairo index ed8993f..63ca18b 100644 --- a/tests/test_publication.cairo +++ b/tests/test_publication.cairo @@ -133,16 +133,6 @@ fn __setup__() -> ( ); stop_cheat_caller_address(publication_contract_address); - //@notice: initialise upvote and downvote state to test that users cannot vote twice - // upvote - 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); - stop_cheat_caller_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); - return ( nft_contract_address, registry_contract_address, @@ -208,6 +198,10 @@ fn test_upvote() { __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); + 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 == 1, 'invalid upvote count'); @@ -230,6 +224,9 @@ fn test_downvote() { ) = __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); + 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 == 1, 'invalid downvote count'); @@ -323,6 +320,7 @@ fn test_upvote_should_fail_if_user_already_upvoted() { 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); } @@ -347,6 +345,7 @@ fn test_downvote_should_fail_if_user_already_downvoted() { 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); }