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
16 changes: 16 additions & 0 deletions contracts/earn-quest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,22 @@ impl EarnQuestContract {
payout::execute_clawback(&env, &caller, &quest_id, &recipient)
}

/// Awards experience points (XP) to multiple users in a batch, accumulating
/// per-address reputation/XP deltas in memory to minimize storage operations.
///
/// # Arguments
///
/// * `env` - The environment.
/// * `grants` - Vector of (Address, u64) tuples representing target user and XP amount.
///
/// # Returns
///
/// `Ok(())` on success, or `Err(Error)` if index/storage operations fail.
pub fn award_xp_batch(env: Env, grants: Vec<(Address, u64)>) -> Result<(), Error> {
bump_instance_ttl(&env);
reputation::award_xp_batch(&env, &grants)
}

/// Returns the core statistics for a user (XP, level, quests completed).
///
/// # Arguments
Expand Down
62 changes: 61 additions & 1 deletion contracts/earn-quest/src/reputation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::errors::Error;
use crate::events;
use crate::storage;
use crate::types::{Badge, BadgeType, Role, UserCore};
use soroban_sdk::{symbol_short, Address, Env, String, Symbol};
use soroban_sdk::{symbol_short, Address, Env, Map, String, Symbol, Vec};

const LEVEL_2_XP: u64 = 300;
const LEVEL_3_XP: u64 = 600;
Expand Down Expand Up @@ -45,6 +45,66 @@ pub fn award_xp(env: &Env, user: &Address, xp_amount: u64) -> Result<UserCore, E
Ok(stats)
}

/// Awards experience points (XP) to multiple users in a batch, accumulating per-address deltas in memory.
///
/// Instead of performing individual storage read + write operations per entry, this function aggregates
/// per-address reputation/XP deltas and quest completions in memory during the batch and applies a single
/// storage read + write per unique affected address.
///
/// # Arguments
///
/// * `env` - The contract environment.
/// * `grants` - A vector of (Address, u64) tuples representing user addresses and XP amounts.
///
/// # Returns
///
/// * `Ok(())` on success.
/// * `Err(Error)` if index operations or storage calls fail.
pub fn award_xp_batch(env: &Env, grants: &Vec<(Address, u64)>) -> Result<(), Error> {
if grants.is_empty() {
return Ok(());
}

// Accumulate XP deltas and quest completions per unique address in memory
let mut deltas: Map<Address, (u64, u32)> = Map::new(env);

for i in 0u32..grants.len() {
let (user, xp_amount) = grants.get(i).ok_or(Error::IndexOutOfBounds)?;
if let Some((curr_xp, curr_quests)) = deltas.get(user.clone()) {
deltas.set(
user.clone(),
(curr_xp.saturating_add(xp_amount), curr_quests.saturating_add(1)),
);
} else {
deltas.set(user.clone(), (xp_amount, 1));
}
}

// Apply a single storage read + write per unique affected user address
for (user, (xp_delta, quests_delta)) in deltas.iter() {
let mut stats = storage::get_user_stats_or_default(env, &user);

let old_level = stats.level;
stats.xp = stats.xp.saturating_add(xp_delta);
stats.quests_completed = stats.quests_completed.saturating_add(quests_delta);

let new_level = calculate_level(stats.xp);
let level_up = new_level > old_level;
stats.level = new_level;

storage::set_user_stats(env, &user, &stats);

events::xp_awarded(env, user.clone(), xp_delta, stats.xp, stats.level);

if level_up {
events::level_up(env, user.clone(), stats.level);
}
}

Ok(())
}


/// Calculates the user level based on their current experience points (XP).
///
/// # Level Thresholds:
Expand Down
Loading
Loading