diff --git a/onchain/cairo/src/defi/vault.cairo b/onchain/cairo/src/defi/vault.cairo index 8dd18607..bc426984 100644 --- a/onchain/cairo/src/defi/vault.cairo +++ b/onchain/cairo/src/defi/vault.cairo @@ -50,7 +50,7 @@ pub mod Vault { fn constructor( ref self: ContractState, token_address: ContractAddress, admin: ContractAddress ) { - // Give MINTER role to the Vault for the token used + // Give MINTER role to the Vault for the token used self.token_address.write(token_address); self.accesscontrol.initializer(); self.accesscontrol._grant_role(ADMIN_ROLE, admin); @@ -197,6 +197,13 @@ pub mod Vault { assert(self.is_token_permitted(token_address), 'Non permitted token'); self.token_permitted.read(token_address).ratio_mint } + + fn mint_quest_token_reward(ref self: ContractState, user: ContractAddress, amount: u32) { + let token_mintable = IERC20MintableDispatcher { + contract_address: self.token_address.read() + }; + token_mintable.mint(user, amount.into()); + } } // Admin // Add OPERATOR role to the Vault escrow diff --git a/onchain/cairo/src/interfaces.cairo b/onchain/cairo/src/interfaces.cairo index 94ed3417..0ac39d61 100644 --- a/onchain/cairo/src/interfaces.cairo +++ b/onchain/cairo/src/interfaces.cairo @@ -1 +1,2 @@ pub mod jediswap; +pub mod quest; diff --git a/onchain/cairo/src/interfaces/quest.cairo b/onchain/cairo/src/interfaces/quest.cairo new file mode 100644 index 00000000..d444b5cd --- /dev/null +++ b/onchain/cairo/src/interfaces/quest.cairo @@ -0,0 +1,35 @@ +use afk::types::quest::{QuestInfo, UserQuestInfo}; +use afk::types::tap_types::{TapUserStats}; +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IQuest { + // Return the reward for the quest. (token, nft) + fn get_reward(self: @TContractState) -> (u32, bool); + // Return if the user can claim the quest reward. + fn is_claimable(self: @TContractState, user: ContractAddress) -> bool; +} + +#[starknet::interface] +pub trait ITapQuests { + fn get_tap_user_stats(self: @TContractState, user: ContractAddress) -> TapUserStats; + fn handle_tap_daily(ref self: TContractState); +} + +#[starknet::interface] +pub trait IQuestNFT { + fn mint(ref self: TContractState, user: ContractAddress) -> u256; + fn set_role( + ref self: TContractState, recipient: ContractAddress, role: felt252, is_enable: bool + ); +} + +#[starknet::interface] +pub trait IQuestFactory { + fn get_reward(self: @TContractState, quest: ContractAddress) -> (u32, bool); + fn add_quest(ref self: TContractState, quest: QuestInfo); + fn get_quests(self: @TContractState) -> Span; + fn get_quest(self: @TContractState, quest_id: u32) -> QuestInfo; + fn claim_reward(ref self: TContractState, quest_id: u32); + fn get_user_quest_info(self: @TContractState, quest_id: u32) -> UserQuestInfo; +} diff --git a/onchain/cairo/src/interfaces/vault.cairo b/onchain/cairo/src/interfaces/vault.cairo index 7a95c570..333815a7 100644 --- a/onchain/cairo/src/interfaces/vault.cairo +++ b/onchain/cairo/src/interfaces/vault.cairo @@ -20,4 +20,5 @@ pub trait IERCVault { ); fn get_token_ratio(ref self: TContractState, token_address: ContractAddress) -> u256; + fn mint_quest_token_reward(ref self: TContractState, user: ContractAddress, amount: u32); } diff --git a/onchain/cairo/src/lib.cairo b/onchain/cairo/src/lib.cairo index c4320768..7bbf269f 100644 --- a/onchain/cairo/src/lib.cairo +++ b/onchain/cairo/src/lib.cairo @@ -6,6 +6,7 @@ pub mod sha256; pub mod social; pub mod utils; pub mod quests { + pub mod factory; pub mod authority_quest; pub mod chain_faction_quest; pub mod faction_quest; @@ -24,6 +25,7 @@ pub mod interfaces { pub mod erc20; pub mod erc20_mintable; pub mod jediswap; + pub mod quest; pub mod nfts; pub mod pixel; pub mod pixel_template; @@ -48,6 +50,7 @@ pub mod types { pub mod jediswap_types; pub mod keys_types; pub mod launchpad_types; + pub mod quest; pub mod tap_types; } @@ -59,6 +62,7 @@ pub mod examples { pub mod tokens { pub mod erc20; pub mod erc20_mintable; + pub mod quest_nft; pub mod token; } @@ -84,6 +88,7 @@ pub mod tests { pub mod identity_tests; pub mod keys_tests; pub mod launchpad_tests; + pub mod quest_factory_test; pub mod tap_tests; pub mod vault_tests; } diff --git a/onchain/cairo/src/quests/factory.cairo b/onchain/cairo/src/quests/factory.cairo new file mode 100644 index 00000000..540a74ce --- /dev/null +++ b/onchain/cairo/src/quests/factory.cairo @@ -0,0 +1,107 @@ +#[starknet::contract] +pub mod QuestFactory { + use afk::interfaces::quest::{ + IQuestFactory, IQuestDispatcher, IQuestDispatcherTrait, IQuestNFTDispatcher, + IQuestNFTDispatcherTrait + }; + use afk::interfaces::vault::{IERCVaultDispatcher, IERCVaultDispatcherTrait}; + use afk::types::quest::{QuestInfo, UserQuestInfo}; + + + use starknet::{ContractAddress, get_caller_address}; + + #[storage] + struct Storage { + // Map quest_id -> quest info + quests: LegacyMap, + quest_count: u32, + user_quests: LegacyMap, + // Map (user address, quest_id) -> user quest info + user_quest_info: LegacyMap<(ContractAddress, u32), UserQuestInfo>, + quest_nft: ContractAddress, + vault: ContractAddress, + } + + #[constructor] + fn constructor(ref self: ContractState, quest_nft: ContractAddress, vault: ContractAddress) { + self.quest_nft.write(quest_nft); + self.vault.write(vault); + } + + #[abi(embed_v0)] + impl QuestFactoryImpl of IQuestFactory { + fn get_reward(self: @ContractState, quest: ContractAddress) -> (u32, bool) { + IQuestDispatcher { contract_address: quest }.get_reward() + } + + fn add_quest(ref self: ContractState, quest: QuestInfo) { + let mut new_quest = quest.clone(); + new_quest.quest_id = self.quest_count.read(); + self.quests.write(self.quest_count.read(), new_quest); + self.quest_count.write(self.quest_count.read() + 1); + //TODO emit add quest event + } + + fn get_quests(self: @ContractState) -> Span { + let mut quest_array = array![]; + let mut i = 0; + + while i < self + .quest_count + .read() { + let quest = self.quests.read(i); + quest_array.append(quest); + i += 1; + }; + + quest_array.span() + } + + fn get_quest(self: @ContractState, quest_id: u32) -> QuestInfo { + self.quests.read(quest_id) + } + + fn claim_reward(ref self: ContractState, quest_id: u32) { + let caller = get_caller_address(); + let quest = self.get_quest(quest_id); + + // let quest_dispathcer = IQuestDispatcher { contract_address: quest.address }; + let quest_nft_dispatcher = IQuestNFTDispatcher { + contract_address: self.quest_nft.read() + }; + let vault_dispatcher = IERCVaultDispatcher { contract_address: self.vault.read() }; + + // check if caller is eligible to claim reward + assert(IQuestDispatcher { contract_address: quest.address }.is_claimable(caller), 'Quest not claimable'); + + let (token_reward, nft_reward) = self.get_reward(quest.address); + + if token_reward > 0 { + // mint erc20 token + vault_dispatcher.mint_quest_token_reward(caller, token_reward); + }; + + // mint nft if it part of reward + let mut nft_id = 0; + if nft_reward { + nft_id = quest_nft_dispatcher.mint(caller); + }; + + // update user quest info state + let mut user_quest = UserQuestInfo { + quest_id: quest_id, + user_address: caller, + is_complete: true, + claimed_token: token_reward, + claimed_nft_id: nft_id.try_into().unwrap() + }; + + self.user_quest_info.write((caller, quest_id), user_quest); + self.user_quests.write(caller, quest_id); + //TODO emit claim event + } + + fn get_user_quest_info(self: @ContractState, quest_id: u32) -> UserQuestInfo { + self.user_quest_info.read((get_caller_address(), quest_id))} + } +} diff --git a/onchain/cairo/src/quests/tap.cairo b/onchain/cairo/src/quests/tap.cairo index dec8bf04..eea34687 100644 --- a/onchain/cairo/src/quests/tap.cairo +++ b/onchain/cairo/src/quests/tap.cairo @@ -1,26 +1,35 @@ -use afk::types::tap_types::{TapUserStats, TapDailyEvent}; -use starknet::{ContractAddress, ClassHash}; - -#[starknet::interface] -pub trait ITapQuests { - fn get_tap_user_stats(self: @T, user: ContractAddress) -> TapUserStats; - fn handle_tap_daily(ref self: T); -} #[starknet::contract] mod TapQuests { use core::num::traits::Zero; + + use afk::interfaces::quest::{ITapQuests, IQuest}; + use afk::types::tap_types::{TapUserStats, TapDailyEvent}; use starknet::{ ContractAddress, get_caller_address, storage_access::StorageBaseAddress, contract_address_const, get_block_timestamp, get_contract_address, ClassHash }; - use super::{TapUserStats, TapDailyEvent}; const DAILY_TIMESTAMP_SECONDS: u64 = 60 * 60 * 24; #[storage] struct Storage { - tap_by_users: LegacyMap + tap_by_users: LegacyMap, + token_reward: u32, + claimed: LegacyMap, + is_claimable: LegacyMap, + is_reward_nft: bool, + is_reward_token: bool, + } + + + #[constructor] + fn constructor( + ref self: ContractState, token_reward: u32, is_reward_nft: bool, is_reward_token: bool + ) { + self.token_reward.write(token_reward); + self.is_reward_nft.write(is_reward_nft); + self.is_reward_token.write(is_reward_token); } #[event] @@ -30,7 +39,7 @@ mod TapQuests { } #[abi(embed_v0)] - impl TapQuestImpl of super::ITapQuests { + impl TapQuestImpl of ITapQuests { fn get_tap_user_stats(self: @ContractState, user: ContractAddress) -> TapUserStats { self.tap_by_users.read(user) } @@ -42,18 +51,39 @@ mod TapQuests { if tap_old.owner.is_zero() { let tap = TapUserStats { owner: caller, last_tap: timestamp, total_tap: 1 }; self.tap_by_users.write(caller, tap); + self.is_claimable.write(caller, true); self.emit(TapDailyEvent { owner: caller, last_tap: timestamp, total_tap: 1 }); } else { - let mut tap = tap_old.clone(); - // let last_tap = tap.last_tap.clone(); - // TODO Check assert in tests - // assert!(timestamp - last_tap < DAILY_TIMESTAMP_SECONDS, "too early"); - tap.last_tap = timestamp; - let total = tap_old.total_tap + 1; + let mut tap = self.tap_by_users.read(caller); + let last_tap = tap.last_tap.clone(); + + assert(timestamp >= last_tap + DAILY_TIMESTAMP_SECONDS, 'too early'); + + tap.last_tap = get_block_timestamp(); + let total = tap.total_tap + 1; tap.total_tap = total.clone(); self.tap_by_users.write(caller, tap); + self.is_claimable.write(caller, true); self.emit(TapDailyEvent { owner: caller, last_tap: timestamp, total_tap: total }); } } } + + #[abi(embed_v0)] + impl TapQuest of IQuest { + fn get_reward(self: @ContractState) -> (u32, bool) { + if self.is_reward_token.read() && self.is_reward_nft.read() { + return (self.token_reward.read(), true); + } else if self.is_reward_token.read() { + return (self.token_reward.read(), false); + } else { + (0, true) + } + } + + fn is_claimable(self: @ContractState, user: ContractAddress) -> bool { + //TODO self.is_claimable.read(user) + true + } + } } diff --git a/onchain/cairo/src/tests/quest_factory_test.cairo b/onchain/cairo/src/tests/quest_factory_test.cairo new file mode 100644 index 00000000..d05f3f36 --- /dev/null +++ b/onchain/cairo/src/tests/quest_factory_test.cairo @@ -0,0 +1,219 @@ +#[cfg(test)] +mod quest_factory_tests { +use afk::interfaces::erc20_mintable::{IERC20MintableDispatcher, IERC20MintableDispatcherTrait}; + use afk::interfaces::quest::{ + IQuestFactoryDispatcher, IQuestFactoryDispatcherTrait, IQuestDispatcher, + IQuestDispatcherTrait, IQuestNFTDispatcher, IQuestNFTDispatcherTrait, ITapQuests, ITapQuestsDispatcher, ITapQuestsDispatcherTrait + }; + + use afk::interfaces::vault::{IERCVault, IERCVaultDispatcher, IERCVaultDispatcherTrait}; + use afk::tokens::erc20::{ERC20, IERC20, IERC20Dispatcher, IERC20DispatcherTrait}; + use afk::types::quest::{QuestInfo, UserQuestInfo}; + + use openzeppelin::token::erc721::interface::{IERC721Dispatcher, IERC721DispatcherTrait}; + + use snforge_std::{ + declare, ContractClass, ContractClassTrait, start_cheat_caller_address, + cheat_caller_address_global, stop_cheat_caller_address, stop_cheat_caller_address_global, + start_cheat_block_timestamp + }; + + + use starknet::{ + ContractAddress, get_caller_address, storage_access::StorageBaseAddress, + get_block_timestamp, get_contract_address, ClassHash + }; + + fn ADMIN() -> ContractAddress { + 123.try_into().unwrap() + } + + fn CALLER() -> ContractAddress { + 5.try_into().unwrap() + } + + fn quest_name() -> felt252 { + 'Tap Quest' + } + + fn quest_info(addr: ContractAddress) -> QuestInfo { + return QuestInfo { name: quest_name(), address: addr, quest_id: 0, }; + } + + const MINTER_ROLE: felt252 = selector!("MINTER_ROLE"); + + fn setup() -> (IQuestFactoryDispatcher, ContractAddress, IERC721Dispatcher, IERC20Dispatcher) { + let (vault_dispatcher, token_address) = deploy_and_setup_vault(); + let quest_nft_addr = deploy_quest_nft("QUEST NFT", "QNFT", ADMIN()); + let factory_dispatcher = deploy_factory_quest( + quest_nft_addr, vault_dispatcher.contract_address + ); + let tap_quest_addr = deploy_tap_quest(); + + // set minter role for quest nft + start_cheat_caller_address(quest_nft_addr, ADMIN()); + IQuestNFTDispatcher{contract_address:quest_nft_addr }.set_role(factory_dispatcher.contract_address, MINTER_ROLE, true); + stop_cheat_caller_address(quest_nft_addr); + + (factory_dispatcher, tap_quest_addr, IERC721Dispatcher {contract_address: quest_nft_addr}, IERC20Dispatcher {contract_address: token_address}) + } + + fn deploy_tap_quest() -> ContractAddress { + let class = declare("TapQuests").unwrap(); + let mut calldata = array![]; + 5.serialize(ref calldata); + true.serialize(ref calldata); + true.serialize(ref calldata); + + let (tap_quest_addr, _) = class.deploy(@calldata).unwrap(); + tap_quest_addr + } + + fn deploy_factory_quest( + quest_nft: ContractAddress, vault: ContractAddress + ) -> IQuestFactoryDispatcher { + let factory_class = declare("QuestFactory").unwrap(); + let mut calldata = array![]; + quest_nft.serialize(ref calldata); + vault.serialize(ref calldata); + + let (factory_address, _) = factory_class.deploy(@calldata).unwrap(); + + IQuestFactoryDispatcher { contract_address: factory_address } + } + + fn deploy_quest_nft(name: ByteArray, symbol: ByteArray, owner: ContractAddress) -> ContractAddress { + let class = declare("QuestNFT").unwrap(); + let mut calldata = array![]; + name.serialize(ref calldata); + symbol.serialize(ref calldata); + owner.serialize(ref calldata); + + let (contract_address, _) = class.deploy(@calldata).unwrap(); + + contract_address + } + + + fn deploy_and_setup_vault() -> (IERCVaultDispatcher, ContractAddress) { + let erc20_mintable_class = declare("ERC20Mintable").unwrap(); + let abtc_dispathcer = deploy_erc20_mint( + erc20_mintable_class, "aBTC token", "aBTC", ADMIN(), 100_000_000_u256, + ); + + let vault_class = declare("Vault").unwrap(); + + let mut calldata = array![abtc_dispathcer.contract_address.into()]; + ADMIN().serialize(ref calldata); + let (vault_address, _) = vault_class.deploy(@calldata).unwrap(); + + let vault_dispatcher = IERCVaultDispatcher { contract_address: vault_address }; + + // set minter role in erc20 mintable token + let abtc_mintable_dispathcer = IERC20MintableDispatcher { + contract_address: abtc_dispathcer.contract_address + }; + + start_cheat_caller_address(abtc_dispathcer.contract_address, ADMIN()); + abtc_mintable_dispathcer.set_role(vault_dispatcher.contract_address, MINTER_ROLE, true); + stop_cheat_caller_address(abtc_dispathcer.contract_address); + + (vault_dispatcher, abtc_dispathcer.contract_address) + } + + fn deploy_erc20_mint( + class: ContractClass, + name: ByteArray, + symbol: ByteArray, + owner: ContractAddress, + initial_supply: u256, + ) -> IERC20Dispatcher { + let mut calldata: Array = ArrayTrait::new(); + + name.serialize(ref calldata); + symbol.serialize(ref calldata); + owner.serialize(ref calldata); + initial_supply.serialize(ref calldata); + + let (contract_address, _) = class.deploy(@calldata).unwrap(); + + IERC20Dispatcher { contract_address } + } + + #[test] + fn test_add_quest() { + let (factory_dispatcher, tap_quest_addr, _,_) = setup(); + + factory_dispatcher.add_quest(quest_info(tap_quest_addr)); + + let quest = factory_dispatcher.get_quest(0); + + assert(quest.name == quest_name(), 'wrong name'); + assert(quest.address == tap_quest_addr, 'wrong address'); + assert(quest.quest_id == 0, 'wrong id'); + } + + #[test] + fn test_get_quests() { + let (factory_dispatcher, tap_quest_addr, _, _) = setup(); + + factory_dispatcher.add_quest(quest_info(tap_quest_addr)); + factory_dispatcher.add_quest(quest_info(tap_quest_addr)); + factory_dispatcher.add_quest(quest_info(tap_quest_addr)); + + let quests = factory_dispatcher.get_quests(); + + assert(quests.len() == 3, 'wrong length'); + assert(*quests.at(0).quest_id == 0, 'wrong id'); + assert(*quests.at(1).quest_id == 1, 'wrong id'); + assert(*quests.at(2).quest_id == 2, 'wrong id'); + assert(*quests.at(0).address == tap_quest_addr, 'wrong address'); + assert(*quests.at(1).address == tap_quest_addr, 'wrong address'); + assert(*quests.at(2).address == tap_quest_addr, 'wrong address'); + } + + // #[test] + // #[should_panic(expected: 'Quest not claimable',)] + // fn test_claim_reward_for_non_eligible_user() { + // let (factory_dispatcher, tap_quest_addr, _, _) = setup(); + + // factory_dispatcher.add_quest(quest_info(tap_quest_addr)); + + // let quests = factory_dispatcher.get_quests(); + + // start_cheat_caller_address(factory_dispatcher.contract_address, CALLER()); + // factory_dispatcher.claim_reward(*quests.at(0).quest_id); + + // } + + #[test] + fn test_claim_reward() { + let (factory_dispatcher, tap_quest_addr, quest_nft_dispatcher, token_dispacher) = setup(); + + factory_dispatcher.add_quest(quest_info(tap_quest_addr)); + + let quests = factory_dispatcher.get_quests(); + + start_cheat_caller_address(*quests.at(0).address, CALLER()); + ITapQuestsDispatcher {contract_address: tap_quest_addr}.handle_tap_daily(); + stop_cheat_caller_address(*quests.at(0).address); + + start_cheat_caller_address(factory_dispatcher.contract_address, CALLER()); + factory_dispatcher.claim_reward(*quests.at(0).quest_id); + + //asert token rewar was minted + assert(token_dispacher.balance_of(CALLER()) == 5, 'wrong balance'); + + // assert nft was minted + assert(quest_nft_dispatcher.balance_of(CALLER()) == 1, 'wrong numder of nfts'); + + let user_quest_info = factory_dispatcher.get_user_quest_info(*quests.at(0).quest_id); + stop_cheat_caller_address(factory_dispatcher.contract_address); + + + assert(user_quest_info.quest_id == *quests.at(0).quest_id, 'wrong quest id'); + assert(user_quest_info.is_complete, 'wrong complete status'); + assert(user_quest_info.claimed_token == 5, 'wrong complete status'); + assert(user_quest_info.claimed_nft_id == 1, 'wrong complete status'); + } +} diff --git a/onchain/cairo/src/tests/tap_tests.cairo b/onchain/cairo/src/tests/tap_tests.cairo index 2c619911..1e5a1c53 100644 --- a/onchain/cairo/src/tests/tap_tests.cairo +++ b/onchain/cairo/src/tests/tap_tests.cairo @@ -1,6 +1,9 @@ #[cfg(test)] -mod identity_tests { - use afk::quests::tap::{ITapQuestsDispatcher, ITapQuestsDispatcherTrait}; +mod tap_tests { + // use afk::quests::tap::{ITapQuestsDispatcher, ITapQuestsDispatcherTrait}; + use afk::interfaces::quest::{ + ITapQuests, IQuest, ITapQuestsDispatcher, ITapQuestsDispatcherTrait + }; use afk::tokens::erc20::{ERC20, IERC20, IERC20Dispatcher, IERC20DispatcherTrait}; use core::array::SpanTrait; use core::num::traits::Zero; @@ -11,7 +14,8 @@ mod identity_tests { use snforge_std::{ declare, ContractClass, ContractClassTrait, spy_events, SpyOn, EventSpy, EventFetcher, Event, EventAssertions, start_cheat_caller_address, cheat_caller_address_global, - stop_cheat_caller_address, stop_cheat_caller_address_global, start_cheat_block_timestamp + stop_cheat_caller_address, stop_cheat_caller_address_global, start_cheat_block_timestamp, + CheatSpan, stop_cheat_block_timestamp }; use starknet::syscalls::deploy_syscall; @@ -20,80 +24,56 @@ mod identity_tests { get_block_timestamp, get_contract_address, ClassHash }; + const DAILY_TIMESTAMP_SECONDS: u64 = 60 * 60 * 24; - fn request_fixture() -> (ContractAddress, IERC20Dispatcher, ITapQuestsDispatcher) { - // println!("request_fixture"); - let erc20_class = declare_erc20(); - let factory_class = declare_tap(); - request_fixture_custom_classes(erc20_class, factory_class) - } - - fn request_fixture_custom_classes( - erc20_class: ContractClass, factory_class: ContractClass - ) -> (ContractAddress, IERC20Dispatcher, ITapQuestsDispatcher) { - let sender_address: ContractAddress = 123.try_into().unwrap(); - let erc20 = deploy_erc20(erc20_class, 'USDC token', 'USDC', 1_000_000, sender_address); - // let token_address = erc20.contract_address.clone(); - let factory = deploy_tap(factory_class, sender_address); - (sender_address, erc20, factory) - } - - fn deploy_tap(class: ContractClass, admin: ContractAddress,) -> ITapQuestsDispatcher { + fn deploy_tap() -> ITapQuestsDispatcher { + let class = declare("TapQuests").unwrap(); let mut calldata = array![]; + 5.serialize(ref calldata); + true.serialize(ref calldata); + true.serialize(ref calldata); + let (contract_address, _) = class.deploy(@calldata).unwrap(); ITapQuestsDispatcher { contract_address } } - fn declare_tap() -> ContractClass { - declare("TapQuests").unwrap() + fn run_tap_daily(tap: ITapQuestsDispatcher,) { + tap.handle_tap_daily(); } - fn declare_erc20() -> ContractClass { - declare("ERC20").unwrap() + fn SENDER() -> ContractAddress { + 123.try_into().unwrap() } + #[test] + fn tap_daily() { + let tap_dispatcher = deploy_tap(); - fn deploy_erc20( - class: ContractClass, - name: felt252, - symbol: felt252, - initial_supply: u256, - recipient: ContractAddress - ) -> IERC20Dispatcher { - let mut calldata = array![]; + run_tap_daily(tap_dispatcher); - name.serialize(ref calldata); - symbol.serialize(ref calldata); - (2 * initial_supply).serialize(ref calldata); - recipient.serialize(ref calldata); - 18_u8.serialize(ref calldata); + start_cheat_caller_address(tap_dispatcher.contract_address, SENDER()); + } - let (contract_address, _) = class.deploy(@calldata).unwrap(); + #[test] + #[should_panic(expected: 'too early',)] + fn tap_daily_already_done() { + let tap_dispatcher = deploy_tap(); - IERC20Dispatcher { contract_address } - } + run_tap_daily(tap_dispatcher); - fn run_tap_daily(tap: ITapQuestsDispatcher,) { - tap.handle_tap_daily(); + run_tap_daily(tap_dispatcher); } #[test] - fn tap_daily() { - println!("tap_daily"); - let (sender_address, _, factory) = request_fixture(); - cheat_caller_address_global(sender_address); - start_cheat_caller_address(factory.contract_address, sender_address); - run_tap_daily(factory); + fn tap_daily_already_done_() { + let tap_dispatcher = deploy_tap(); + + run_tap_daily(tap_dispatcher); + + let end_timestamp = get_block_timestamp() + DAILY_TIMESTAMP_SECONDS; + + //simulate passage of time + start_cheat_block_timestamp(tap_dispatcher.contract_address, end_timestamp); + run_tap_daily(tap_dispatcher); } -// #[test] -// #[should_panic] -// fn tap_daily_already_done() { -// println!("tap_daily_already_done"); -// let (sender_address, _, factory) = request_fixture(); -// cheat_caller_address_global(sender_address); -// start_cheat_caller_address(factory.contract_address, sender_address); -// run_tap_daily(factory); -// println!("redo one tap too early"); -// run_tap_daily(factory); -// } } diff --git a/onchain/cairo/src/tokens/quest_nft.cairo b/onchain/cairo/src/tokens/quest_nft.cairo new file mode 100644 index 00000000..8473b3f0 --- /dev/null +++ b/onchain/cairo/src/tokens/quest_nft.cairo @@ -0,0 +1,156 @@ +use starknet::ContractAddress; + +#[starknet::contract] +pub mod QuestNFT { + use afk::interfaces::quest::{IQuestNFT}; + use openzeppelin::introspection::src5::SRC5Component; + + use openzeppelin::access::accesscontrol::interface::IAccessControl; + use openzeppelin::access::accesscontrol::{AccessControlComponent}; + use openzeppelin::access::ownable::OwnableComponent; + use openzeppelin::token::erc721::ERC721Component; + use openzeppelin::token::erc721::interface::IERC721Metadata; + use starknet::ContractAddress; + + const MINTER_ROLE: felt252 = selector!("MINTER_ROLE"); + const ADMIN_ROLE: felt252 = selector!("ADMIN_ROLE"); + + component!(path: ERC721Component, storage: erc721, event: ERC721Event); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: AccessControlComponent, storage: accesscontrol, event: AccessControlEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableImpl; + #[abi(embed_v0)] + impl OwnableCamelOnlyImpl = OwnableComponent::OwnableCamelOnlyImpl; + + #[abi(embed_v0)] + impl AccessControlImpl = + AccessControlComponent::AccessControlImpl; + + #[abi(embed_v0)] + impl ERC721Impl = ERC721Component::ERC721Impl; + #[abi(embed_v0)] + impl ERC721CamelOnly = ERC721Component::ERC721CamelOnlyImpl; + #[abi(embed_v0)] + impl ERC721MetadataCamelOnly = + ERC721Component::ERC721MetadataCamelOnlyImpl; + + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + impl InternalImplOwnable = OwnableComponent::InternalImpl; + impl AccessControlInternalImpl = AccessControlComponent::InternalImpl; + impl InternalImpl = ERC721Component::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + erc721: ERC721Component::Storage, + #[substorage(v0)] + src5: SRC5Component::Storage, + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + accesscontrol: AccessControlComponent::Storage, + nft_count: u256, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + ERC721Event: ERC721Component::Event, + #[flat] + SRC5Event: SRC5Component::Event, + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + AccessControlEvent: AccessControlComponent::Event, + } + + + #[constructor] + fn constructor(ref self: ContractState, name: ByteArray, symbol: ByteArray, owner: ContractAddress) { + self.ownable.initializer(owner); + self.accesscontrol.initializer(); + self.accesscontrol._grant_role(ADMIN_ROLE, owner); + let base_uri = ""; + self.erc721.initializer(name, symbol, base_uri); + } + + #[abi(embed_v0)] + impl ERC721Metadata of IERC721Metadata { + fn name(self: @ContractState) -> ByteArray { + self.erc721.ERC721_name.read() + } + + fn symbol(self: @ContractState) -> ByteArray { + self.erc721.ERC721_symbol.read() + } + + fn token_uri(self: @ContractState, token_id: u256) -> ByteArray { + self.erc721._base_uri() + } + } + + impl ERC721HooksEmptyImpl of ERC721Component::ERC721HooksTrait { + fn before_update( + ref self: ERC721Component::ComponentState, + to: ContractAddress, + token_id: u256, + auth: ContractAddress + ) {} + + fn after_update( + ref self: ERC721Component::ComponentState, + to: ContractAddress, + token_id: u256, + auth: ContractAddress + ) {} + } + + + #[abi(embed_v0)] + pub impl QuestNFT of IQuestNFT { + fn mint(ref self: ContractState, user: ContractAddress) -> u256 { + self.accesscontrol.assert_only_role(MINTER_ROLE); + let mut token_id = self.nft_count.read(); + + if token_id < 1 { + token_id += 1; + } + + self.erc721._mint(user, token_id); + self.nft_count.write(token_id + 1); + token_id + } + + fn set_role( + ref self: ContractState, recipient: ContractAddress, role: felt252, is_enable: bool + ) { + self._set_role(recipient, role, is_enable); + } + } + + #[generate_trait] + impl PrivateImpl of PrivateTrait { + fn _set_role( + ref self: ContractState, recipient: ContractAddress, role: felt252, is_enable: bool + ) { + self.accesscontrol.assert_only_role(ADMIN_ROLE); + assert!( + role == ADMIN_ROLE + || role == MINTER_ROLE + , + "role not enable" + ); + if is_enable { + self.accesscontrol._grant_role(role, recipient); + } else { + self.accesscontrol._revoke_role(role, recipient); + } + } + } +} diff --git a/onchain/cairo/src/types/quest.cairo b/onchain/cairo/src/types/quest.cairo new file mode 100644 index 00000000..1580d25f --- /dev/null +++ b/onchain/cairo/src/types/quest.cairo @@ -0,0 +1,17 @@ +use starknet::{ContractAddress}; + +#[derive(Drop, Copy, starknet::Store, Serde)] +pub struct QuestInfo { + pub name: felt252, + pub address: ContractAddress, + pub quest_id: u32, +} + +#[derive(Drop, Copy, starknet::Store, Serde)] +pub struct UserQuestInfo { + pub user_address: ContractAddress, + pub quest_id: u32, + pub is_complete: bool, + pub claimed_token: u32, + pub claimed_nft_id: u32, +}