diff --git a/quest-verse-contract/.gitignore b/quest-verse-contract/.gitignore index cdbf0a3..93c6fd2 100644 --- a/quest-verse-contract/.gitignore +++ b/quest-verse-contract/.gitignore @@ -4,6 +4,8 @@ node_modules /.pnp .pnp.js +db.zo + # build /out diff --git a/quest-verse-contract/contract/src/external.rs b/quest-verse-contract/contract/src/keypom_ext.rs similarity index 97% rename from quest-verse-contract/contract/src/external.rs rename to quest-verse-contract/contract/src/keypom_ext.rs index a32f066..60531e1 100644 --- a/quest-verse-contract/contract/src/external.rs +++ b/quest-verse-contract/contract/src/keypom_ext.rs @@ -6,7 +6,7 @@ pub const NO_DEPOSIT: u128 = 0; pub const XCC_SUCCESS: u64 = 1; // Validator interface, for cross-contract calls -#[ext_contract(keypom_near)] +#[ext_contract(v2_keypom_near)] trait Keypom { fn create_drop( &mut self, diff --git a/quest-verse-contract/contract/src/lib.rs b/quest-verse-contract/contract/src/lib.rs index 59c3216..e3f6003 100644 --- a/quest-verse-contract/contract/src/lib.rs +++ b/quest-verse-contract/contract/src/lib.rs @@ -1,5 +1,6 @@ // Find all our documentation at https://docs.near.org use keypom_models::*; +use types::*; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::{LazyOption, LookupMap, LookupSet, UnorderedMap, UnorderedSet}; @@ -11,109 +12,17 @@ use near_sdk::{ PromiseResult, PublicKey, }; use std::convert::TryFrom; -pub mod external; -pub use crate::external::*; -mod keypom_models; -mod utils; - -#[allow(non_camel_case_types)] -#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize, Clone)] -#[serde(crate = "near_sdk::serde")] -pub struct Quest { - quest_id: U128, - quest_type: RewardType, - start_time: u64, - end_time: u64, - reward_config: Option, - reward_amount: U128, - total_participants_allowed: u64, - indexer_config_id: String, -} - -impl TryFrom for Quest { - type Error = &'static str; - - fn try_from(keypom_drop_info: JsonDrop) -> Result { - if let Some(config) = keypom_drop_info.config { - if let Some(time) = config.time { - if let Some(start_time) = time.start { - if let Some(end_time) = time.end { - if let Some(uses_per_key) = config.uses_per_key { - return Ok(Quest { - quest_id: keypom_drop_info.drop_id, - start_time, - end_time, - reward_config: None, - reward_amount: keypom_drop_info.deposit_per_use, - total_participants_allowed: uses_per_key, - indexer_config_id: String::new(), - quest_type: RewardType::Native, - }); - } - } - } - } - } - Err("Conversion from JsonDrop to Quest failed.") - } -} +pub mod keypom_ext; +pub use crate::keypom_ext::*; -#[allow(non_camel_case_types)] -#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize, Clone)] -#[serde(crate = "near_sdk::serde")] -pub struct QueryApiIndexerConfig { - indexer_id: String, - function_name: String, - account_id: AccountId, - version: BlockHeight, -} - -#[allow(non_camel_case_types)] -#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize, Clone)] -#[serde(crate = "near_sdk::serde")] -pub struct QuestRewardConfig { - reward_token_address: String, - reward_amount: U128, -} - -#[derive(BorshSerialize, BorshDeserialize)] -pub struct QuestUsage { - total_redemptions: U128, - redemptions_account_list: UnorderedSet, -} +mod keypom_models; +mod types; +mod utils; const KEYPOM_CONTRACT: &str = "v2.keypom.testnet"; -const QUESTS_PROTOCOL_PUBLIC_KEY_STR: &str = "ed25519:Dru47TDn3vaN2PMXwoq8cY6o2ERzqcidxFj6NTdxUgHh"; -pub const MIN_DROP_ID_PASSED_IN: u128 = 1_000_000_000; - -#[allow(non_camel_case_types)] -#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize, Clone)] -#[serde(crate = "near_sdk::serde")] -pub enum RewardType { - // In $NEAR - Native, - // Any Fungible Token NEP-141 - FT, - // NEP-177 tokens - NFT, -} - -#[derive(BorshSerialize, BorshDeserialize, Serialize, Clone)] -#[serde(crate = "near_sdk::serde")] -pub enum ClaimStatus { - Claimed, - NotClaimed, -} - -pub type BlockHeight = u64; - -pub struct QuestData { - quest_details: Quest, - usage: QuestUsage, -} - -pub type QuestId = U128; +const QUESTS_PROTOCOL_DEFAULT_PUBLIC_KEY_STR: &str = + "ed25519:Dru47TDn3vaN2PMXwoq8cY6o2ERzqcidxFj6NTdxUgHh"; #[derive(BorshStorageKey, BorshSerialize)] pub enum StorageKeys { @@ -128,10 +37,18 @@ pub type FunctionName = String; // pub type IndexerConfigById = ; // pub type QuestIdsByDeployer = UnorderedMap>; // Define the contract structure + +#[derive(BorshSerialize, BorshDeserialize)] +pub struct QuestUsage { + total_redemptions: U128, + redemptions_account_list: UnorderedSet, +} + #[near_bindgen] #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] pub struct QuestProtocol { contract_owner_id: AccountId, + keypom_account_id: AccountId, // indexer configurations. At this moment only editable by the owner indexer_configs_by_id: LookupMap, // quests by a quest_id @@ -150,14 +67,21 @@ pub struct QuestProtocol { #[near_bindgen] impl QuestProtocol { #[init] - pub fn new(owner_id: AccountId, claim_signer_public_key: String) -> Self { + pub fn new( + owner_id: AccountId, + keypom_account_id: AccountId, + default_claim_signer_public_key: String, + ) -> Self { Self { contract_owner_id: owner_id, + keypom_account_id: keypom_account_id + .unwrap_or_else(|| KEYPOM_CONTRACT.parse().unwrap()), quest_by_id: UnorderedMap::new(StorageKeys::QuestById), quest_ids_by_deployer: UnorderedMap::new(StorageKeys::QuestIdsByDeployer), indexer_configs_by_id: LookupMap::new(StorageKeys::IndexerConfigsById), whitelisted_tokens: UnorderedSet::new(StorageKeys::Whitelist), - claim_signer_public_key: claim_signer_public_key.parse().unwrap(), + claim_signer_public_key: default_claim_signer_public_key + .unwrap_or_else(|| QUESTS_PROTOCOL_DEFAULT_PUBLIC_KEY_STR.parse().unwrap()), global_freeze: false, } } @@ -172,8 +96,8 @@ impl QuestProtocol { reward_amount: U128, reward_type: RewardType, indexer_config_id: String, + quest_validator_public_key: Option, ) -> Promise { - // //TODO check for drop ID being above the minimum let contract_drop_public_key: PublicKey = QUESTS_PROTOCOL_PUBLIC_KEY_STR.parse().unwrap(); // Ensuring start_time is before end_time require!(start_time < end_time, "Start time must be before end time."); @@ -211,7 +135,7 @@ impl QuestProtocol { }; let near_attached = env::attached_deposit(); - let promise = keypom_near::ext(KEYPOM_CONTRACT.parse().unwrap()) + let promise = v2_keypom_near::ext(KEYPOM_CONTRACT.parse().unwrap()) .with_attached_deposit(near_attached) .with_static_gas(Gas(5 * TGAS)) .create_drop( @@ -253,7 +177,7 @@ impl QuestProtocol { // Deserialize the returned object. let drop_id: U128 = serde_json::from_str(&call_result.unwrap()).unwrap(); - let promise = keypom_near::ext(KEYPOM_CONTRACT.parse().unwrap()) + let promise = v2_keypom_near::ext(KEYPOM_CONTRACT.parse().unwrap()) .with_attached_deposit(near_attached) .with_static_gas(Gas(5 * TGAS)) .get_drop_information(Some(drop_id), None); @@ -327,13 +251,95 @@ impl QuestProtocol { unimplemented!() } + #[private] + fn is_receipt_valid(message: RewardReceiptDetails, receipt: String) -> bool { + true + // unimplemented!() + // recreated_receipt = hash(message) + // assert!(receipt, recreated_receipt) + } + #[payable] - pub fn claim_reward(&mut self, quest_id: String, signed_claim_receipt: String) { + pub fn claim_reward(&mut self, quest_id: String, receipt: String) { + let account_id = env::current_account_id(); + // check if user already claimed their reward or not. + self.quest_by_id.get(&quest_id); + + assert_eq!( + env::predecessor_account_id(), + env::current_account_id(), + "Claim only can come by the user who completed the quest" + ); + assert!( + env::is_valid_account_id(account_id.as_bytes()), + "Invalid AccountId passed in" + ); + let reward_receipt_details = RewardReceiptDetails { + quest_id, + account_id, + completed: true, + }; + // + // + // TODO: Check if enough gas is attached + // Check if enough gas is attached. For callback + this function to go through + // + let is_valid = self.is_receipt_valid(reward_receipt_details, receipt); + assert_eq!(is_valid, true, "Receipt was not valid"); + // Check if receipt was signed by claim_signer_public_key // Call Keypom.claim() - unimplemented!() + let near_attached = env::attached_deposit(); + let promise = v2_keypom_near::ext(self.keypom_account_id.parse().unwrap()) + .with_attached_deposit(near_attached) + .with_static_gas(Gas(5 * TGAS)) + .claim(quest_id); + + promise.then( + Self::ext(env::current_account_id()) + .with_attached_deposit(near_attached) + .with_static_gas(Gas(5 * TGAS)) + .claim_successful_callback(), + ) } + #[private] + pub fn claim_successful_callback( + &mut self, + quest_id: U128, + #[callback_result] call_result: Result, + ) -> bool { + // Add information about the drop being succesfful + log!("quest_id, {}", u128::from(quest_id)); + let account_id = env::signer_account_id(); + + if call_result.is_err() { + log!("There was an error redeeming your rewards"); + return false; + } + + // let drop_info: JsonDrop = serde_json::from_str(&call_result.unwrap()).unwrap(); + // let quest: Result = drop_info.try_into(); + // + // match quest { + // Ok(quest) => { + // let mut quest_ids_set = self + // .quest_ids_by_deployer + // .get(&account_id) + // .unwrap_or_else(|| UnorderedSet::new(StorageKeys::QuestSet)); + // quest_ids_set.insert(&quest.quest_id); + // + // self.quest_ids_by_deployer + // .insert(&account_id, &quest_ids_set); + // self.quest_by_id.insert(&quest.quest_id, &quest); + // true + // } + // Err(e) => { + // log!("There was an error creating your quest. {}", e); + // false + // } + // } + } pub fn set_global_freeze(&mut self, freeze: bool) { self.assert_owner_calling(); self.global_freeze = freeze; @@ -402,6 +408,46 @@ impl QuestProtocol { mod tests { use super::*; + // #[test] + // fn list_account_roles() { + // let admins = vec![ + // AccountRole { + // account_id: AccountId::new_unchecked("bob.near".to_string()), + // role: Role::Owner, + // }, + // AccountRole { + // account_id: AccountId::new_unchecked("flatirons.near".to_string()), + // role: Role::User, + // }, + // ]; + // let contract = Contract { + // registry: IndexersByAccount::new(StorageKeys::Registry), + // account_roles: admins.clone(), + // }; + // assert_eq!(contract.list_account_roles(), admins); + // } + #[test] + fn quest_creation_works() { + let contract = QuestProtocol::new( + "roshaan.near".to_string(), + "v2.keypom.near".to_string(), + QUESTS_PROTOCOL_PUBLIC_KEY_STR.to_string(), + ); + + let quest = Quest { + quest_id: U128(23902390239), + quest_type: RewardType::Native, + start_time: 5858585583, + end_time: 384484848484, + reward_config: None, + reward_amount: U128::from("239293092").unwrap(), + total_participants_allowed: 50, + indexer_config_id: "creator_quest".to_string(), + }; + + // contract.create_near_quest(quest.quest_id, quest.end_time, quest.start_time, total_participants_allowed, reward_amount, reward_type, indexer_config_id, quest_validator_public_key) + // } + } // Only the owner of the quest can delete the quest // // Unable to interact with contract when global freeze is on diff --git a/quest-verse-contract/contract/src/types.rs b/quest-verse-contract/contract/src/types.rs new file mode 100644 index 0000000..dc3d5ed --- /dev/null +++ b/quest-verse-contract/contract/src/types.rs @@ -0,0 +1,98 @@ +use crate::*; + +#[allow(non_camel_case_types)] +#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct Quest { + pub quest_id: U128, + quest_type: RewardType, + start_time: u64, + end_time: u64, + reward_config: Option, + reward_amount: U128, + total_participants_allowed: u64, + indexer_config_id: String, +} + +impl TryFrom for Quest { + type Error = &'static str; + + fn try_from(keypom_drop_info: JsonDrop) -> Result { + if let Some(config) = keypom_drop_info.config { + if let Some(time) = config.time { + if let Some(start_time) = time.start { + if let Some(end_time) = time.end { + if let Some(uses_per_key) = config.uses_per_key { + return Ok(Quest { + quest_id: keypom_drop_info.drop_id, + start_time, + end_time, + reward_config: None, + reward_amount: keypom_drop_info.deposit_per_use, + total_participants_allowed: uses_per_key, + indexer_config_id: String::new(), + quest_type: RewardType::Native, + }); + } + } + } + } + } + + Err("Conversion from JsonDrop to Quest failed.") + } +} + +#[allow(non_camel_case_types)] +#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct QueryApiIndexerConfig { + indexer_id: String, + function_name: String, + account_id: AccountId, + version: BlockHeight, +} + +#[allow(non_camel_case_types)] +#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct QuestRewardConfig { + reward_token_address: String, + reward_amount: U128, +} + +pub const MIN_DROP_ID_PASSED_IN: u128 = 1_000_000_000; + +#[allow(non_camel_case_types)] +#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub enum RewardType { + // In $NEAR + Native, + // Any Fungible Token NEP-141 + FT, + // NEP-177 tokens + NFT, +} + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub enum ClaimStatus { + Claimed, + NotClaimed, +} + +pub type BlockHeight = u64; + +pub struct QuestData { + quest_details: Quest, + usage: QuestUsage, +} + +pub type QuestId = U128; + +pub struct RewardReceiptDetails { + pub quest_id: String, + pub account_id: AccountId, + pub completed: Bool, +} diff --git a/quest-verse-contract/integration-tests/src/quest-creation/quest-creation.ava.ts b/quest-verse-contract/integration-tests/src/quest-creation/quest-creation.ava.ts index 098d9d6..dc55915 100644 --- a/quest-verse-contract/integration-tests/src/quest-creation/quest-creation.ava.ts +++ b/quest-verse-contract/integration-tests/src/quest-creation/quest-creation.ava.ts @@ -17,6 +17,7 @@ test.beforeEach(async (t) => { // Deploy contract const root = worker.rootAccount; + console.log("starting") // Deploy the keypom contract. const keypom = await root.devDeploy(path.join(__dirname, '../ext-wasm/keypom.wasm')); console.log("deployed keypom!") @@ -48,3 +49,16 @@ test('create a simple near quest', async (t) => { t.is("hi", "hi") }); +test('can redeem aaa quest', async (t) => { + const { root, keypom, questVerse, rosh, morgs } = t.context.accounts; + + // Create a quest + // + // + // Check Quest Creation + // questVerse.get(quest_id) + // + // + + t.is("hi", "hi") +});