Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions contracts/quest_chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,14 @@ const MAX_LEADERBOARD_ENTRIES: u32 = 100;
//

const CHAIN_CREATED: Symbol = symbol_short!("chain_crt");
const CHAIN_STARTED: Symbol = symbol_short!("chn_start");
const QUEST_UNLOCKED: Symbol = symbol_short!("qst_unlck");
const QUEST_COMPLETED: Symbol = symbol_short!("qst_done");
const CHAIN_COMPLETED: Symbol = symbol_short!("chn_done");
const PROGRESS_CHECKPOINT: Symbol = symbol_short!("checkpt");
const CHAIN_RESET: Symbol = symbol_short!("chn_reset");
const REWARD_CLAIMED: Symbol = symbol_short!("rwrd_clmd");
const POOL_FUNDED: Symbol = symbol_short!("pool_fund");

//
// ──────────────────────────────────────────────────────────
Expand Down Expand Up @@ -303,6 +306,11 @@ impl QuestChainContract {
env.storage()
.persistent()
.set(&DataKey::PlayerProgress(player.clone(), chain_id), &progress);

env.events().publish(
(CHAIN_STARTED, player.clone(), chain_id),
(progress.start_time,),
);
}

/// Complete a quest in a chain
Expand Down Expand Up @@ -376,8 +384,8 @@ impl QuestChainContract {
if quest.checkpoint {
progress.checkpoint_quest = Some(quest_id);
env.events().publish(
(PROGRESS_CHECKPOINT, player.clone()),
(chain_id, quest_id),
(PROGRESS_CHECKPOINT, player.clone(), chain_id),
(quest_id,),
);
}

Expand All @@ -404,8 +412,8 @@ impl QuestChainContract {
.set(&DataKey::ChainCompletions(chain_id), &completions);

env.events().publish(
(CHAIN_COMPLETED, player.clone()),
(chain_id, duration, progress.total_reward_earned),
(CHAIN_COMPLETED, player.clone(), chain_id),
(duration, progress.total_reward_earned),
);
}

Expand All @@ -414,8 +422,8 @@ impl QuestChainContract {
.set(&DataKey::PlayerProgress(player.clone(), chain_id), &progress);

env.events().publish(
(QUEST_COMPLETED, player.clone()),
(chain_id, quest_id, quest.reward),
(QUEST_COMPLETED, player.clone(), chain_id),
(quest_id, quest.reward),
);
}

Expand Down Expand Up @@ -503,8 +511,8 @@ impl QuestChainContract {
.set(&DataKey::PlayerProgress(player.clone(), chain_id), &progress);

env.events().publish(
(CHAIN_RESET, player.clone()),
(chain_id, checkpoint_id),
(CHAIN_RESET, player.clone(), chain_id),
(checkpoint_id,),
);
}

Expand Down Expand Up @@ -539,7 +547,7 @@ impl QuestChainContract {
.remove(&DataKey::PendingRewards(player.clone(), chain_id));
}

env.events().publish((CHAIN_RESET, player.clone()), (chain_id, 0u32));
env.events().publish((CHAIN_RESET, player.clone(), chain_id), (0u32,));
}

// ───────────── VIEW FUNCTIONS ─────────────
Expand Down Expand Up @@ -660,8 +668,8 @@ impl QuestChainContract {
.remove(&DataKey::PendingRewards(player.clone(), chain_id));

env.events().publish(
(symbol_short!("rwrd_clmd"), player.clone()),
(chain_id, pending),
(REWARD_CLAIMED, player.clone(), chain_id),
(pending,),
);

pending
Expand Down Expand Up @@ -758,8 +766,8 @@ impl QuestChainContract {
.set(&DataKey::RewardPool(chain_id), &(current_pool + amount));

env.events().publish(
(symbol_short!("pool_fund"), admin),
(chain_id, amount, current_pool + amount),
(POOL_FUNDED, admin, chain_id),
(amount, current_pool + amount),
);
}

Expand Down
197 changes: 194 additions & 3 deletions contracts/quest_chain/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

use super::*;
use soroban_sdk::{
testutils::{Address as _, Ledger},
Address, Env, Symbol,
testutils::{Address as _, Events, Ledger},
Address, Env, Symbol, TryFromVal,
};

fn setup_contract(env: &Env) -> (QuestChainContractClient, Address) {
Expand Down Expand Up @@ -119,7 +119,7 @@ fn test_double_initialization() {
env.mock_all_auths();

let (client, admin) = setup_contract(&env);
client.initialize(&admin);
client.initialize(&admin, &None);
}

#[test]
Expand Down Expand Up @@ -811,6 +811,197 @@ fn test_reward_token_configuration() {
assert_eq!(config.reward_token, Some(reward_token));
}

// ──────────────────────────────────────────────────────────
// EVENT TESTS
// ──────────────────────────────────────────────────────────

fn find_event_by_name(
env: &Env,
events: &soroban_sdk::Vec<(Address, soroban_sdk::Vec<soroban_sdk::Val>, soroban_sdk::Val)>,
name: &Symbol,
) -> Option<(Address, soroban_sdk::Vec<soroban_sdk::Val>, soroban_sdk::Val)> {
for event in events.iter() {
let topics = &event.1;
if let Some(first) = topics.get(0) {
if let Ok(sym) = Symbol::try_from_val(env, &first) {
if sym == *name {
return Some(event.clone());
}
}
}
}
None
}

#[test]
fn test_event_chain_created() {
let env = Env::default();
env.mock_all_auths();
env.ledger().set_timestamp(1000);

let (client, admin) = setup_contract(&env);
let quests = create_test_quests(&env);

let chain_id = client.create_chain(
&admin,
&Symbol::new(&env, "TestChain"),
&Symbol::new(&env, "Desc"),
&quests,
&None,
&None,
);

let events = env.events().all();
let event = find_event_by_name(&env, &events, &CHAIN_CREATED);
assert!(event.is_some(), "CHAIN_CREATED event not found");
let (_, topics, _) = event.unwrap();
assert_eq!(Symbol::try_from_val(&env, &topics.get(0).unwrap()).unwrap(), CHAIN_CREATED);
assert_eq!(u32::try_from_val(&env, &topics.get(1).unwrap()).unwrap(), chain_id);
}

#[test]
fn test_event_chain_started() {
let env = Env::default();
env.mock_all_auths();
env.ledger().set_timestamp(1000);

let (client, admin) = setup_contract(&env);
let quests = create_test_quests(&env);
let chain_id = client.create_chain(
&admin,
&Symbol::new(&env, "TestChain"),
&Symbol::new(&env, "Desc"),
&quests,
&None,
&None,
);

let player = Address::generate(&env);
client.start_chain(&player, &chain_id);

let events = env.events().all();
let event = find_event_by_name(&env, &events, &CHAIN_STARTED);
assert!(event.is_some(), "CHAIN_STARTED event not found");
let (_, topics, _) = event.unwrap();
assert_eq!(Symbol::try_from_val(&env, &topics.get(0).unwrap()).unwrap(), CHAIN_STARTED);
assert_eq!(Address::try_from_val(&env, &topics.get(1).unwrap()).unwrap(), player);
assert_eq!(u32::try_from_val(&env, &topics.get(2).unwrap()).unwrap(), chain_id);
}

#[test]
fn test_event_quest_completed() {
let env = Env::default();
env.mock_all_auths();
env.ledger().set_timestamp(1000);

let (client, admin) = setup_contract(&env);
let quests = create_test_quests(&env);
let chain_id = client.create_chain(
&admin,
&Symbol::new(&env, "TestChain"),
&Symbol::new(&env, "Desc"),
&quests,
&None,
&None,
);

let player = Address::generate(&env);
client.start_chain(&player, &chain_id);
client.complete_quest(&player, &chain_id, &1);

let events = env.events().all();
let event = find_event_by_name(&env, &events, &QUEST_COMPLETED);
assert!(event.is_some(), "QUEST_COMPLETED event not found");
let (_, topics, _) = event.unwrap();
assert_eq!(Symbol::try_from_val(&env, &topics.get(0).unwrap()).unwrap(), QUEST_COMPLETED);
assert_eq!(Address::try_from_val(&env, &topics.get(1).unwrap()).unwrap(), player);
assert_eq!(u32::try_from_val(&env, &topics.get(2).unwrap()).unwrap(), chain_id);
}

#[test]
fn test_event_chain_completed() {
let env = Env::default();
env.mock_all_auths();
env.ledger().set_timestamp(1000);

let (client, admin) = setup_contract(&env);
let quests = create_test_quests(&env);
let chain_id = client.create_chain(
&admin,
&Symbol::new(&env, "TestChain"),
&Symbol::new(&env, "Desc"),
&quests,
&None,
&None,
);

let player = Address::generate(&env);
client.start_chain(&player, &chain_id);
client.complete_quest(&player, &chain_id, &1);
client.complete_quest(&player, &chain_id, &2);
client.complete_quest(&player, &chain_id, &3);
client.complete_quest(&player, &chain_id, &4);
env.ledger().set_timestamp(2000);
client.complete_quest(&player, &chain_id, &5);

let events = env.events().all();
let event = find_event_by_name(&env, &events, &CHAIN_COMPLETED);
assert!(event.is_some(), "CHAIN_COMPLETED event not found");
let (_, topics, _) = event.unwrap();
assert_eq!(Symbol::try_from_val(&env, &topics.get(0).unwrap()).unwrap(), CHAIN_COMPLETED);
assert_eq!(Address::try_from_val(&env, &topics.get(1).unwrap()).unwrap(), player);
assert_eq!(u32::try_from_val(&env, &topics.get(2).unwrap()).unwrap(), chain_id);
}

#[test]
fn test_event_reward_claimed() {
let env = Env::default();
env.mock_all_auths();
env.ledger().set_timestamp(1000);

let admin = Address::generate(&env);
let sac = env.register_stellar_asset_contract_v2(admin.clone());
let reward_token = sac.address();
let sac_client = soroban_sdk::token::StellarAssetClient::new(&env, &reward_token);

let contract_id = env.register_contract(None, QuestChainContract);
let client = QuestChainContractClient::new(&env, &contract_id);
client.initialize(&admin, &Some(reward_token.clone()));

let quests = create_test_quests(&env);
let chain_id = client.create_chain(
&admin,
&Symbol::new(&env, "TestChain"),
&Symbol::new(&env, "Desc"),
&quests,
&None,
&None,
);

// Mint tokens to the quest chain contract so it can pay out rewards
sac_client.mint(&contract_id, &10000i128);

// Seed reward pool directly in storage
env.as_contract(&contract_id, || {
env.storage()
.persistent()
.set(&DataKey::RewardPool(chain_id), &10000i128);
});

let player = Address::generate(&env);
client.start_chain(&player, &chain_id);
client.complete_quest(&player, &chain_id, &1);
client.claim_rewards(&player, &chain_id);

let events = env.events().all();
let event = find_event_by_name(&env, &events, &REWARD_CLAIMED);
assert!(event.is_some(), "REWARD_CLAIMED event not found");
let (_, topics, _) = event.unwrap();
assert_eq!(Symbol::try_from_val(&env, &topics.get(0).unwrap()).unwrap(), REWARD_CLAIMED);
assert_eq!(Address::try_from_val(&env, &topics.get(1).unwrap()).unwrap(), player);
assert_eq!(u32::try_from_val(&env, &topics.get(2).unwrap()).unwrap(), chain_id);
}

#[test]
fn test_pending_rewards_tracking() {
let env = Env::default();
Expand Down