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
8 changes: 5 additions & 3 deletions leaderboard/src/admin_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ fn test_reset_player_zeroes_points_but_preserves_bet_history() {

let after = client.get_stats(&alice);
assert_eq!(after.points, 0); // zeroed
// Win/loss/bonus history preserved.
// Win/loss/bonus history preserved.
assert_eq!(after.won_bets, 1);
assert_eq!(after.lost_bets, 1);
assert_eq!(after.total_bets, 3);
Expand Down Expand Up @@ -526,7 +526,9 @@ fn test_banned_player_cannot_reenter_leaderboard() {
// Direct storage poke: force the ban flag away to prove accrual re-checks
// the flag every call (belt and suspenders — not part of the public API).
env.as_contract(&client.address, || {
env.storage().persistent().set(&DataKey::BannedPlayer(alice.clone()), &false);
env.storage()
.persistent()
.set(&DataKey::BannedPlayer(alice.clone()), &false);
});
// No longer banned, so the player may accrue again.
client.add_pts(&market, &alice, &50_u64, &true);
Expand Down Expand Up @@ -622,7 +624,7 @@ fn test_repair_top_index_survives_many_sequential_removals() {
assert_eq!(client.get_rank(&user), UNRANKED_RANK);
// After the 50th removal the list is empty and the min cache is reset.
if idx < 49 {
assert_eq!(client.get_top_player_count(), 49 - idx as u32);
assert_eq!(client.get_top_player_count(), 49 - idx);
}
}
assert_eq!(client.get_top_player_count(), 0);
Expand Down
174 changes: 68 additions & 106 deletions leaderboard/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,9 @@ impl LeaderboardContract {
env.storage()
.instance()
.set(&DataKey::ReferralContract, &referral_contract);
env.storage().instance().set(&DataKey::TopPlayerCount, &0_u32);
env.storage()
.instance()
.set(&DataKey::TopPlayerCount, &0_u32);
env.storage().instance().set(&DataKey::MinPoints, &0_u64);
env.storage().instance().set(&DataKey::MinSlot, &0_u32);
env.storage().instance().extend_ttl(TTL_BUMP, TTL_HIGH);
Expand All @@ -186,11 +188,7 @@ impl LeaderboardContract {

/// Set the PULSE token contract used by reward()/reward_bonus() for
/// internal minting. Admin only. `set_token` is the pre-#23 alias.
pub fn set_token(
env: Env,
admin: Address,
token: Address,
) -> Result<(), LeaderboardError> {
pub fn set_token(env: Env, admin: Address, token: Address) -> Result<(), LeaderboardError> {
Self::write_token_contract(&env, &admin, &token)
}

Expand All @@ -211,15 +209,17 @@ impl LeaderboardContract {
pub fn pause(env: Env, admin: Address) -> Result<(), LeaderboardError> {
Self::require_admin(&env, &admin)?;
env.storage().instance().set(&DataKey::Paused, &true);
env.events().publish((Symbol::new(&env, "paused"), admin), true);
env.events()
.publish((Symbol::new(&env, "paused"), admin), true);
Ok(())
}

/// Resume point/reward accrual. Admin only.
pub fn unpause(env: Env, admin: Address) -> Result<(), LeaderboardError> {
Self::require_admin(&env, &admin)?;
env.storage().instance().set(&DataKey::Paused, &false);
env.events().publish((Symbol::new(&env, "unpaused"), admin), true);
env.events()
.publish((Symbol::new(&env, "unpaused"), admin), true);
Ok(())
}

Expand Down Expand Up @@ -358,7 +358,9 @@ impl LeaderboardContract {
}

pub fn get_pending_reward(env: Env, user: Address) -> Option<PendingReward> {
env.storage().persistent().get(&DataKey::PendingReward(user))
env.storage()
.persistent()
.get(&DataKey::PendingReward(user))
}

// ── reward_bonus / add_bonus_pts (referral path) ─────────────────────────
Expand Down Expand Up @@ -412,39 +414,6 @@ impl LeaderboardContract {
pub fn record_bet(env: Env, caller: Address, _user: Address) -> Result<(), LeaderboardError> {
Self::require_not_paused(&env)?;
Self::require_market_contract(&env, &caller)?;

let mut stats = Self::stats_for_update(&env, &user);

// ── Legacy write functions (kept for backward-compat) ─────────────────────

/// Deprecated: use `reward()` instead. This function always returns
/// `UnauthorizedCaller` and will be removed in a future version.
pub fn add_pts(
_env: Env,
_caller: Address,
_user: Address,
_pts: u64,
_is_won: bool,
) -> Result<(), LeaderboardError> {
Err(LeaderboardError::UnauthorizedCaller)
}

/// Legacy: called by the referral contract to award bonus points.
/// Prefer reward_bonus() for new integrations (adds token minting).
pub fn add_bonus_pts(
env: Env,
caller: Address,
user: Address,
pts: u64,
) -> Result<(), LeaderboardError> {
let referral: Address = env
.storage()
.instance()
.get(&DataKey::ReferralContract)
.ok_or(LeaderboardError::NotInitialized)?;
if caller != referral {
return Err(LeaderboardError::UnauthorizedCaller);
}
caller.require_auth();
Ok(())
}
Expand Down Expand Up @@ -513,7 +482,7 @@ impl LeaderboardContract {
}

// Selection sort, descending — bounded by MAX_TOP_PLAYERS.
let n = ranked.len() as u32;
let n = ranked.len();
for i in 0..n {
let mut max_idx = i;
for j in (i + 1)..n {
Expand Down Expand Up @@ -551,10 +520,7 @@ impl LeaderboardContract {
}

pub fn get_min_slot(env: Env) -> u32 {
env.storage()
.instance()
.get(&DataKey::MinSlot)
.unwrap_or(0)
env.storage().instance().get(&DataKey::MinSlot).unwrap_or(0)
}

/// Rebuild `TopPlayerSlot` from live `TopPlayerAt` entries, compact holes
Expand All @@ -578,9 +544,11 @@ impl LeaderboardContract {
env.storage()
.persistent()
.extend_ttl(&DataKey::TopPlayerAt(slot), TTL_BUMP, TTL_HIGH);
env.storage()
.persistent()
.extend_ttl(&DataKey::TopPlayerSlot(user), TTL_BUMP, TTL_HIGH);
env.storage().persistent().extend_ttl(
&DataKey::TopPlayerSlot(user),
TTL_BUMP,
TTL_HIGH,
);
}
env.storage().instance().extend_ttl(TTL_BUMP, TTL_HIGH);
}
Expand All @@ -595,11 +563,7 @@ impl LeaderboardContract {
///
/// Returns `PlayerNotFound` when the address has no Stats record, no
/// pending reward, and is not in the top list.
pub fn remove_player(
env: Env,
admin: Address,
user: Address,
) -> Result<(), LeaderboardError> {
pub fn remove_player(env: Env, admin: Address, user: Address) -> Result<(), LeaderboardError> {
Self::require_admin(&env, &admin)?;

let stats_key = DataKey::Stats(user.clone());
Expand Down Expand Up @@ -632,10 +596,8 @@ impl LeaderboardContract {
}

env.storage().instance().extend_ttl(TTL_BUMP, TTL_HIGH);
env.events().publish(
(Symbol::new(&env, "player_removed"), admin),
user,
);
env.events()
.publish((Symbol::new(&env, "player_removed"), admin), user);
Ok(())
}

Expand All @@ -651,11 +613,7 @@ impl LeaderboardContract {
/// re-confirms the flag, re-removes any residual stats, and returns Ok.
/// Banning an unknown address simply records the ban (idempotent — no
/// error).
pub fn ban_player(
env: Env,
admin: Address,
user: Address,
) -> Result<(), LeaderboardError> {
pub fn ban_player(env: Env, admin: Address, user: Address) -> Result<(), LeaderboardError> {
Self::require_admin(&env, &admin)?;

// Erase any residual state (stats, pending rewards, top-list slot).
Expand Down Expand Up @@ -683,10 +641,8 @@ impl LeaderboardContract {
.extend_ttl(&ban_key, TTL_BUMP, TTL_HIGH);

env.storage().instance().extend_ttl(TTL_BUMP, TTL_HIGH);
env.events().publish(
(Symbol::new(&env, "player_banned"), admin),
user,
);
env.events()
.publish((Symbol::new(&env, "player_banned"), admin), user);
Ok(())
}

Expand All @@ -698,16 +654,11 @@ impl LeaderboardContract {
/// Works for an unranked player who has a Stats record (e.g. a low scorer
/// kept out of a full list). Returns `PlayerNotFound` when the address has
/// never accrued points and is not in the top list.
pub fn reset_player(
env: Env,
admin: Address,
user: Address,
) -> Result<(), LeaderboardError> {
pub fn reset_player(env: Env, admin: Address, user: Address) -> Result<(), LeaderboardError> {
Self::require_admin(&env, &admin)?;

let stats_key = DataKey::Stats(user.clone());
let stored_opt: Option<StoredStats> =
env.storage().persistent().get(&stats_key);
let stored_opt: Option<StoredStats> = env.storage().persistent().get(&stats_key);

let count = Self::top_count(&env);
let slot_opt = Self::resolved_slot(&env, &user, count);
Expand All @@ -720,7 +671,9 @@ impl LeaderboardContract {
let mut stored = stored_opt.unwrap_or_else(StoredStats::zero);
stored.points = 0;
env.storage().persistent().set(&stats_key, &stored);
env.storage().persistent().extend_ttl(&stats_key, TTL_BUMP, TTL_HIGH);
env.storage()
.persistent()
.extend_ttl(&stats_key, TTL_BUMP, TTL_HIGH);

// Update the epoch stamp so the zeroed score isn't accidentally decayed
// further from a stale baseline.
Expand All @@ -744,10 +697,8 @@ impl LeaderboardContract {
}

env.storage().instance().extend_ttl(TTL_BUMP, TTL_HIGH);
env.events().publish(
(Symbol::new(&env, "player_reset"), admin),
user,
);
env.events()
.publish((Symbol::new(&env, "player_reset"), admin), user);
Ok(())
}

Expand Down Expand Up @@ -788,7 +739,12 @@ impl LeaderboardContract {

#[inline]
fn require_not_paused(env: &Env) -> Result<(), LeaderboardError> {
if env.storage().instance().get(&DataKey::Paused).unwrap_or(false) {
if env
.storage()
.instance()
.get(&DataKey::Paused)
.unwrap_or(false)
{
return Err(LeaderboardError::ContractPaused);
}
Ok(())
Expand Down Expand Up @@ -834,9 +790,7 @@ impl LeaderboardContract {
return Err(LeaderboardError::NotAdmin);
}
admin.require_auth();
env.storage()
.instance()
.set(&DataKey::TokenContract, token);
env.storage().instance().set(&DataKey::TokenContract, token);
env.storage().instance().extend_ttl(TTL_BUMP, TTL_HIGH);
Ok(())
}
Expand Down Expand Up @@ -885,7 +839,9 @@ impl LeaderboardContract {
fn save_stored(env: &Env, user: &Address, s: &StoredStats) {
let key = DataKey::Stats(user.clone());
env.storage().persistent().set(&key, s);
env.storage().persistent().extend_ttl(&key, TTL_BUMP, TTL_HIGH);
env.storage()
.persistent()
.extend_ttl(&key, TTL_BUMP, TTL_HIGH);
}

/// A player's stats brought forward to the current epoch for a write. The
Expand Down Expand Up @@ -945,7 +901,7 @@ impl LeaderboardContract {
Self::update_top_players(env, user.clone(), s.points);
env.storage().instance().extend_ttl(TTL_BUMP, TTL_HIGH);
env.events().publish(
(Symbol::new(&env, "leaderboard_updated"), user.clone()),
(Symbol::new(env, "leaderboard_updated"), user.clone()),
(s.points, s.won_bets, s.lost_bets),
);
}
Expand All @@ -958,7 +914,7 @@ impl LeaderboardContract {
Self::update_top_players(env, user.clone(), s.points);
env.storage().instance().extend_ttl(TTL_BUMP, TTL_HIGH);
env.events().publish(
(Symbol::new(&env, "leaderboard_updated"), user.clone()),
(Symbol::new(env, "leaderboard_updated"), user.clone()),
(s.points, s.bonus_bets),
);
}
Expand All @@ -972,15 +928,17 @@ impl LeaderboardContract {
is_bonus: bool,
) {
let key = DataKey::PendingReward(user.clone());
let mut pending: PendingReward = env.storage().persistent().get(&key).unwrap_or(
PendingReward {
points: 0,
tokens: 0,
won_delta: 0,
lost_delta: 0,
bet_delta: 0,
},
);
let mut pending: PendingReward =
env.storage()
.persistent()
.get(&key)
.unwrap_or(PendingReward {
points: 0,
tokens: 0,
won_delta: 0,
lost_delta: 0,
bet_delta: 0,
});
pending.points += points;
pending.tokens += tokens;
pending.bet_delta += 1;
Expand All @@ -992,7 +950,9 @@ impl LeaderboardContract {
}
}
env.storage().persistent().set(&key, &pending);
env.storage().persistent().extend_ttl(&key, TTL_BUMP, TTL_HIGH);
env.storage()
.persistent()
.extend_ttl(&key, TTL_BUMP, TTL_HIGH);
}

// ── Internal: decay ───────────────────────────────────────────────────────
Expand Down Expand Up @@ -1226,21 +1186,23 @@ impl LeaderboardContract {
let mut steps = 0;
while slot > 0 && steps < MAX_BUBBLE_STEPS {
steps += 1;
let prev: Option<PlayerEntry> =
env.storage().persistent().get(&DataKey::TopPlayerAt(slot - 1));
let prev: Option<PlayerEntry> = env
.storage()
.persistent()
.get(&DataKey::TopPlayerAt(slot - 1));
match prev {
Some(prev)
if Self::entry_points_now(env, &prev) < Self::entry_points_now(env, entry) => {
if Self::entry_points_now(env, &prev) < Self::entry_points_now(env, entry) =>
{
env.storage()
.persistent()
.set(&DataKey::TopPlayerAt(slot - 1), entry);
env.storage()
.persistent()
.set(&DataKey::TopPlayerAt(slot), &prev);
env.storage().persistent().set(
&DataKey::TopPlayerSlot(entry.address.clone()),
&(slot - 1),
);
env.storage()
.persistent()
.set(&DataKey::TopPlayerSlot(entry.address.clone()), &(slot - 1));
env.storage()
.persistent()
.set(&DataKey::TopPlayerSlot(prev.address.clone()), &slot);
Expand Down Expand Up @@ -1314,11 +1276,11 @@ impl LeaderboardContract {
}
}

#[cfg(test)]
mod admin_tests;
#[cfg(test)]
mod decay_tests;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod ttl_tests;
#[cfg(test)]
mod admin_tests;
Loading