From 9a0d3e4996effb8329597d2e0f3d8dc0f0362329 Mon Sep 17 00:00:00 2001 From: pvsaint Date: Sun, 22 Feb 2026 08:21:02 +0100 Subject: [PATCH] Feat: Implement Rewards Event Module --- contracts/src/rewards/events.rs | 80 +++++++++++ contracts/src/rewards/mod.rs | 11 +- contracts/src/rewards/redemption.rs | 6 +- contracts/src/rewards/storage.rs | 43 ++---- ...entrypoint_increments_within_window.1.json | 46 +++++- ...treak_entrypoint_reset_after_window.1.json | 46 +++++- contracts/tests/rewards_test.rs | 134 ++++++++++++++++++ 7 files changed, 317 insertions(+), 49 deletions(-) create mode 100644 contracts/src/rewards/events.rs create mode 100644 contracts/tests/rewards_test.rs diff --git a/contracts/src/rewards/events.rs b/contracts/src/rewards/events.rs new file mode 100644 index 00000000..b80b3717 --- /dev/null +++ b/contracts/src/rewards/events.rs @@ -0,0 +1,80 @@ +//! Event definitions and helpers for the rewards module. +use soroban_sdk::{contracttype, symbol_short, Address, Env, Symbol}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PointsAwarded { + pub user: Address, + pub amount: u128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BonusAwarded { + pub user: Address, + pub amount: u128, + pub bonus_type: Symbol, // e.g., "streak", "lock", "goal" +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PointsRedeemed { + pub user: Address, + pub amount: u128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StreakUpdated { + pub user: Address, + pub streak: u32, +} + +/// Emits a PointsAwarded event. +pub fn emit_points_awarded(env: &Env, user: Address, amount: u128) { + let event = PointsAwarded { + user: user.clone(), + amount, + }; + env.events().publish( + (symbol_short!("rewards"), symbol_short!("awarded"), user), + event, + ); +} + +/// Emits a BonusAwarded event. +pub fn emit_bonus_awarded(env: &Env, user: Address, amount: u128, bonus_type: Symbol) { + let event = BonusAwarded { + user: user.clone(), + amount, + bonus_type, + }; + env.events().publish( + (symbol_short!("rewards"), symbol_short!("bonus"), user), + event, + ); +} + +/// Emits a PointsRedeemed event. +pub fn emit_points_redeemed(env: &Env, user: Address, amount: u128) { + let event = PointsRedeemed { + user: user.clone(), + amount, + }; + env.events().publish( + (symbol_short!("rewards"), symbol_short!("redeem"), user), + event, + ); +} + +/// Emits a StreakUpdated event. +pub fn emit_streak_updated(env: &Env, user: Address, streak: u32) { + let event = StreakUpdated { + user: user.clone(), + streak, + }; + env.events().publish( + (symbol_short!("rewards"), symbol_short!("streak"), user), + event, + ); +} diff --git a/contracts/src/rewards/mod.rs b/contracts/src/rewards/mod.rs index 41756f61..c929253f 100644 --- a/contracts/src/rewards/mod.rs +++ b/contracts/src/rewards/mod.rs @@ -1,4 +1,5 @@ pub mod config; +pub mod events; pub mod ranking; pub mod redemption; pub mod storage; @@ -6,13 +7,5 @@ pub mod storage_types; // Re-exporting these makes them accessible as crate::rewards::UserRewards pub use config::*; +pub use events::*; pub use storage_types::{RewardsDataKey, UserRewards}; // Optional: re-exports config functions - -use soroban_sdk::{contracttype, Address}; - -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PointsAwardedEvent { - pub user: Address, - pub amount: u128, -} diff --git a/contracts/src/rewards/redemption.rs b/contracts/src/rewards/redemption.rs index c3eeb4eb..9afc6f6d 100644 --- a/contracts/src/rewards/redemption.rs +++ b/contracts/src/rewards/redemption.rs @@ -1,8 +1,9 @@ //! Points redemption functionality for protocol benefits. use crate::errors::SavingsError; +use crate::rewards::events::emit_points_redeemed; use crate::rewards::storage::{get_user_rewards, save_user_rewards}; -use soroban_sdk::{symbol_short, Address, Env, Symbol}; +use soroban_sdk::{Address, Env}; /// Redeem points for protocol benefits (fee discounts, boost multiplier, etc.) /// @@ -43,8 +44,7 @@ pub fn redeem_points(env: &Env, user: Address, amount: u128) -> Result<(), Savin save_user_rewards(env, user.clone(), &rewards); // Emit redemption event - env.events() - .publish((Symbol::new(env, "PointsRedeemed"), user.clone()), amount); + emit_points_redeemed(env, user, amount); Ok(()) } diff --git a/contracts/src/rewards/storage.rs b/contracts/src/rewards/storage.rs index 2e04d17f..fe95f231 100644 --- a/contracts/src/rewards/storage.rs +++ b/contracts/src/rewards/storage.rs @@ -1,8 +1,8 @@ -//! Rewards storage, streak logic, and bonus point calculations. use super::storage_types::{RewardsDataKey, UserRewards}; use crate::errors::SavingsError; use crate::rewards::config::get_rewards_config; -use soroban_sdk::{symbol_short, Address, Env, Symbol}; +use crate::rewards::events::{emit_bonus_awarded, emit_points_awarded, emit_streak_updated}; +use soroban_sdk::{Address, Env, Symbol}; /// Duration threshold for long-lock bonus eligibility (in seconds). pub const LONG_LOCK_BONUS_THRESHOLD_SECS: u64 = 180 * 24 * 60 * 60; @@ -113,7 +113,8 @@ pub fn update_streak(env: &Env, user: Address) -> Result { } }; rewards.last_action_timestamp = now; - save_user_rewards(env, user, &rewards); + save_user_rewards(env, user.clone(), &rewards); + emit_streak_updated(env, user, rewards.current_streak); Ok(rewards.current_streak) } @@ -213,25 +214,11 @@ pub fn award_deposit_points(env: &Env, user: Address, amount: i128) -> Result<() // Track user for ranking leaderboard crate::rewards::ranking::track_user_for_ranking(env, user.clone()); - env.events().publish( - ( - symbol_short!("rewards"), - symbol_short!("awarded"), - user.clone(), - ), - capped_points, - ); + emit_points_awarded(env, user.clone(), capped_points); if streak_bonus_points > 0 && capped_points > base_points { let actual_bonus = capped_points.saturating_sub(base_points); - env.events().publish( - ( - Symbol::new(env, "BonusAwarded"), - user.clone(), - symbol_short!("streak"), - ), - actual_bonus, - ); + emit_bonus_awarded(env, user, actual_bonus, Symbol::new(env, "streak")); } Ok(()) @@ -270,14 +257,7 @@ pub fn award_long_lock_bonus( } add_points(env, user.clone(), bonus_points)?; - env.events().publish( - ( - Symbol::new(env, "BonusAwarded"), - user, - symbol_short!("lock"), - ), - bonus_points, - ); + emit_bonus_awarded(env, user, bonus_points, Symbol::new(env, "lock")); Ok(bonus_points) } @@ -294,14 +274,7 @@ pub fn award_goal_completion_bonus(env: &Env, user: Address) -> Result (Env, NesteraContractClient<'static>, Address, Address) { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(NesteraContract, ()); + let client = NesteraContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let admin_pk = BytesN::from_array(&env, &[0u8; 32]); + client.initialize(&admin, &admin_pk); + + let user = Address::generate(&env); + client.init_user(&user); + + (env, client, admin, user) +} + +fn setup_rewards_config(_env: &Env, client: &NesteraContractClient, admin: &Address) { + client.init_rewards_config( + admin, &10, // points_per_token + &2000, // streak_bonus_bps (20%) + &1000, // long_lock_bonus_bps (10%) + &100, // goal_completion_bonus + &true, // enabled + &0, // min_deposit_for_rewards + &0, // action_cooldown_seconds + &1_000_000, // max_daily_points + &10_000, // max_streak_multiplier (100%) + ); +} + +#[test] +fn test_points_awarded_event() { + let (env, client, admin, user) = create_test_env(); + setup_rewards_config(&env, &client, &admin); + + client.deposit_flexi(&user, &100); + + let events = env.events().all(); + let rewards_events: Vec<_> = events + .iter() + .filter(|e| { + e.0 == client.address + && e.1 + == ( + symbol_short!("rewards"), + symbol_short!("awarded"), + user.clone(), + ) + .into_val(&env) + }) + .collect(); + + assert!(rewards_events.len() > 0); + let last_event = rewards_events.get(rewards_events.len() - 1).unwrap(); + let event_data: PointsAwarded = last_event.2.clone().into_val(&env); + + assert_eq!(event_data.user, user); + assert_eq!(event_data.amount, 1000); // 100 * 10 +} + +#[test] +fn test_streak_updated_event() { + let (env, client, admin, user) = create_test_env(); + setup_rewards_config(&env, &client, &admin); + + client.deposit_flexi(&user, &100); + + let events = env.events().all(); + let streak_events: Vec<_> = events + .iter() + .filter(|e| { + e.0 == client.address + && e.1 + == ( + symbol_short!("rewards"), + symbol_short!("streak"), + user.clone(), + ) + .into_val(&env) + }) + .collect(); + + assert!(streak_events.len() > 0); + let event_data: StreakUpdated = streak_events.get(0).unwrap().2.clone().into_val(&env); + + assert_eq!(event_data.user, user); + assert_eq!(event_data.streak, 1); +} + +#[test] +fn test_bonus_awarded_streak_event() { + let (env, client, admin, user) = create_test_env(); + setup_rewards_config(&env, &client, &admin); + + // Initial streak + client.deposit_flexi(&user, &100); // streak 1 + client.deposit_flexi(&user, &100); // streak 2 + client.deposit_flexi(&user, &100); // streak 3 + + let events = env.events().all(); + let bonus_events: Vec<_> = events + .iter() + .filter(|e| { + e.0 == client.address + && e.1 + == ( + symbol_short!("rewards"), + symbol_short!("bonus"), + user.clone(), + ) + .into_val(&env) + }) + .collect(); + + assert!(bonus_events.len() > 0); + let event_data: BonusAwarded = bonus_events.get(0).unwrap().2.clone().into_val(&env); + + assert_eq!(event_data.user, user); + assert_eq!(event_data.bonus_type, Symbol::new(&env, "streak")); + assert_eq!(event_data.amount, 200); // 1000 base * 20% bonus = 200 +}