Skip to content

Commit

Permalink
Store CHIT balances per month (#6087)
Browse files Browse the repository at this point in the history
  • Loading branch information
hpeebles authored Jul 24, 2024
1 parent e0e306f commit 4d02fa2
Show file tree
Hide file tree
Showing 33 changed files with 454 additions and 143 deletions.
1 change: 1 addition & 0 deletions backend/canisters/user/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed

- Improve check for empty and dormant users ([#6073](https://github.com/open-chat-labs/open-chat/pull/6073))
- Store CHIT balances per month ([#6087](https://github.com/open-chat-labs/open-chat/pull/6087))

## [[2.0.1243](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.1243-user)] - 2024-07-17

Expand Down
3 changes: 3 additions & 0 deletions backend/canisters/user/api/can.did
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,7 @@ type InitialStateResponse = variant {
local_user_index_canister_id : CanisterId;
achievements : vec ChitEarned;
achievements_last_seen : TimestampMillis;
total_chit_earned : int32;
chit_balance : int32;
streak : nat16;
streak_ends : TimestampMillis;
Expand Down Expand Up @@ -940,6 +941,7 @@ type UpdatesResponse = variant {
suspended : opt bool;
achievements : vec ChitEarned;
achievements_last_seen : opt TimestampMillis;
total_chit_earned : int32;
chit_balance : int32;
streak : nat16;
streak_ends : TimestampMillis;
Expand Down Expand Up @@ -1152,6 +1154,7 @@ type LocalUserIndexResponse = variant {
type ChitEventsArgs = record {
from : opt TimestampMillis;
to : opt TimestampMillis;
skip : opt nat32;
max : nat32;
ascending : bool;
};
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user/api/src/queries/chit_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use types::{ChitEarned, TimestampMillis};
pub struct Args {
pub from: Option<TimestampMillis>,
pub to: Option<TimestampMillis>,
pub skip: Option<u32>,
pub max: u32,
pub ascending: bool,
}
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user/api/src/queries/initial_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub struct SuccessResult {
pub local_user_index_canister_id: CanisterId,
pub achievements: Vec<ChitEarned>,
pub achievements_last_seen: TimestampMillis,
pub total_chit_earned: i32,
pub chit_balance: i32,
pub streak: u16,
pub streak_ends: TimestampMillis,
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user/api/src/queries/updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub struct SuccessResult {
pub pin_number_settings: OptionUpdate<PinNumberSettings>,
pub achievements: Vec<ChitEarned>,
pub achievements_last_seen: Option<TimestampMillis>,
pub total_chit_earned: i32,
pub chit_balance: i32,
pub streak: u16,
pub streak_ends: TimestampMillis,
Expand Down
7 changes: 2 additions & 5 deletions backend/canisters/user/impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ impl RuntimeState {
escrow: self.data.escrow_canister_id,
icp_ledger: Cryptocurrency::InternetComputer.ledger_canister_id().unwrap(),
},
chit_balance: self.data.chit_balance.value,
chit_balance: self.data.chit_events.balance_for_month_by_timestamp(now),
streak: self.data.streak.days(now),
streak_ends: self.data.streak.ends(),
next_daily_claim: if self.data.streak.can_claim(now) { today(now) } else { tomorrow(now) },
Expand Down Expand Up @@ -222,7 +222,6 @@ struct Data {
pub pin_number: PinNumber,
pub btc_address: Option<String>,
pub chit_events: ChitEarnedEvents,
pub chit_balance: Timestamped<i32>,
pub streak: Streak,
pub achievements: HashSet<Achievement>,
pub achievements_last_seen: TimestampMillis,
Expand Down Expand Up @@ -285,7 +284,6 @@ impl Data {
pin_number: PinNumber::default(),
btc_address: None,
chit_events: ChitEarnedEvents::default(),
chit_balance: Timestamped::default(),
streak: Streak::default(),
achievements: HashSet::new(),
achievements_last_seen: 0,
Expand Down Expand Up @@ -360,7 +358,6 @@ impl Data {
timestamp: now,
reason: ChitEarnedReason::Achievement(achievement),
});
self.chit_balance = Timestamped::new(self.chit_balance.value + amount, now);
true
} else {
false
Expand All @@ -370,7 +367,7 @@ impl Data {
pub fn notify_user_index_of_chit(&self, now: TimestampMillis) {
let args = user_index_canister::c2c_notify_chit::Args {
timestamp: now,
chit_balance: self.chit_balance.value,
chit_balance: self.chit_events.balance_for_month_by_timestamp(now),
streak: self.streak.days(now),
streak_ends: self.streak.ends(),
};
Expand Down
2 changes: 1 addition & 1 deletion backend/canisters/user/impl/src/lifecycle/post_upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fn post_upgrade(args: Args) {
&& state.data.unique_person_proof.is_none()
{
let now = state.env.now();
if state.data.user_created + SIX_MONTHS < now && state.data.chit_balance.timestamp + SIX_MONTHS < now {
if state.data.user_created + SIX_MONTHS < now && state.data.chit_events.last_updated() + SIX_MONTHS < now {
ic_cdk_timers::set_timer(Duration::ZERO, mark_user_canister_empty);
}
}
Expand Down
168 changes: 98 additions & 70 deletions backend/canisters/user/impl/src/model/chit.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
use serde::{Deserialize, Serialize};
use std::ops::Range;
use types::{ChitEarned, ChitEarnedReason, TimestampMillis};
use utils::time::MonthKey;

#[derive(Serialize, Deserialize, Default)]
#[serde(from = "ChitEarnedEventsPrevious")]
pub struct ChitEarnedEvents {
events: Vec<ChitEarned>,
total_chit_earned: i32,
}

#[derive(Serialize, Deserialize, Default)]
pub struct ChitEarnedEventsPrevious {
events: Vec<ChitEarned>,
}

impl ChitEarnedEvents {
Expand All @@ -16,6 +25,7 @@ impl ChitEarnedEvents {
}
}

self.total_chit_earned += event.amount;
self.events.push(event);

if sort {
Expand All @@ -27,29 +37,31 @@ impl ChitEarnedEvents {
&self,
from: Option<TimestampMillis>,
to: Option<TimestampMillis>,
max: u32,
skip: usize,
max: usize,
ascending: bool,
) -> (Vec<ChitEarned>, u32) {
let page = if ascending {
self.events
.iter()
.skip_while(|e| from.map_or(false, |ts| e.timestamp <= ts))
.take_while(|e| to.map_or(true, |ts| e.timestamp <= ts))
.take(max as usize)
.cloned()
.collect()
if ascending {
let range = self.range(from.unwrap_or_default()..to.unwrap_or(TimestampMillis::MAX));
(range.iter().skip(skip).take(max).cloned().collect(), range.len() as u32)
} else {
self.events
.iter()
.rev()
.skip_while(|e| from.map_or(false, |ts| e.timestamp >= ts))
.take_while(|e| to.map_or(true, |ts| e.timestamp >= ts))
.take(max as usize)
.cloned()
.collect()
};
let range = self.range(to.unwrap_or_default()..from.unwrap_or(TimestampMillis::MAX));
(range.iter().rev().skip(skip).take(max).cloned().collect(), range.len() as u32)
}
}

(page, self.events.len() as u32)
pub fn total_chit_earned(&self) -> i32 {
self.total_chit_earned
}

pub fn balance_for_month_by_timestamp(&self, ts: TimestampMillis) -> i32 {
self.balance_for_month(MonthKey::from_timestamp(ts))
}

pub fn balance_for_month(&self, month: MonthKey) -> i32 {
let timestamp_range = month.timestamp_range();
let range = self.range(timestamp_range);
range.iter().map(|e| e.amount).sum()
}

pub fn achievements(&self, since: Option<TimestampMillis>) -> Vec<ChitEarned> {
Expand All @@ -62,12 +74,26 @@ impl ChitEarnedEvents {
.collect()
}

pub fn has_achievements_since(&self, since: TimestampMillis) -> bool {
self.events
.iter()
.rev()
.take_while(|e| e.timestamp > since)
.any(|e| matches!(e.reason, ChitEarnedReason::Achievement(_)))
pub fn last_updated(&self) -> TimestampMillis {
self.events.last().map(|e| e.timestamp).unwrap_or_default()
}

fn range(&self, range: Range<TimestampMillis>) -> &[ChitEarned] {
let start = self.events.partition_point(|e| e.timestamp < range.start);
let end = self.events.partition_point(|e| e.timestamp <= range.end);

&self.events[start..end]
}
}

impl From<ChitEarnedEventsPrevious> for ChitEarnedEvents {
fn from(value: ChitEarnedEventsPrevious) -> Self {
let total_chit_earned = value.events.iter().map(|e| e.amount).sum();

ChitEarnedEvents {
events: value.events,
total_chit_earned,
}
}
}

Expand All @@ -81,7 +107,7 @@ mod tests {
fn first_page_matches_expected() {
let store = init_test_data();

let (events, total) = store.events(None, None, 3, true);
let (events, total) = store.events(None, None, 0, 3, true);

assert_eq!(total, 7);
assert_eq!(events.len(), 3);
Expand All @@ -93,8 +119,7 @@ mod tests {
fn next_page_matches_expected() {
let store = init_test_data();

let (events, _) = store.events(None, None, 3, true);
let (events, _) = store.events(Some(events[2].timestamp), None, 3, true);
let (events, _) = store.events(None, None, 3, 3, true);

assert_eq!(events.len(), 3);
assert_eq!(events[0].timestamp, 13);
Expand All @@ -105,7 +130,7 @@ mod tests {
fn first_page_desc_matches_expected() {
let store = init_test_data();

let (events, _) = store.events(None, None, 3, false);
let (events, _) = store.events(None, None, 0, 3, false);

assert_eq!(events.len(), 3);
assert_eq!(events[0].timestamp, 16);
Expand All @@ -116,8 +141,7 @@ mod tests {
fn next_page_desc_matches_expected() {
let store = init_test_data();

let (events, _) = store.events(None, None, 3, false);
let (events, _) = store.events(Some(events[2].timestamp), None, 3, false);
let (events, _) = store.events(None, None, 3, 3, false);

assert_eq!(events.len(), 3);
assert_eq!(events[0].timestamp, 13);
Expand All @@ -128,7 +152,7 @@ mod tests {
fn range_matches_expected() {
let store = init_test_data();

let (events, _) = store.events(Some(11), Some(15), 99, true);
let (events, _) = store.events(Some(12), Some(15), 0, 99, true);

assert_eq!(events.len(), 4);
assert_eq!(events[0].timestamp, 12);
Expand All @@ -139,52 +163,56 @@ mod tests {
fn range_desc_matches_expected() {
let store = init_test_data();

let (events, _) = store.events(Some(15), Some(11), 99, false);
let (events, _) = store.events(Some(14), Some(11), 0, 99, false);

assert_eq!(events.len(), 4);
assert_eq!(events[0].timestamp, 14);
assert_eq!(events[3].timestamp, 11);
}

fn init_test_data() -> ChitEarnedEvents {
let events = vec![
ChitEarned {
amount: 200,
timestamp: 10,
reason: ChitEarnedReason::DailyClaim,
},
ChitEarned {
amount: 200,
timestamp: 11,
reason: ChitEarnedReason::DailyClaim,
},
ChitEarned {
amount: 300,
timestamp: 12,
reason: ChitEarnedReason::DailyClaim,
},
ChitEarned {
amount: 500,
timestamp: 13,
reason: ChitEarnedReason::Achievement(Achievement::SetBio),
},
ChitEarned {
amount: 300,
timestamp: 14,
reason: ChitEarnedReason::DailyClaim,
},
ChitEarned {
amount: 500,
timestamp: 15,
reason: ChitEarnedReason::Achievement(Achievement::SetAvatar),
},
ChitEarned {
amount: 500,
timestamp: 16,
reason: ChitEarnedReason::Achievement(Achievement::SentDirectMessage),
},
];
let total_chit_earned = events.iter().map(|e| e.amount).sum();

ChitEarnedEvents {
events: vec![
ChitEarned {
amount: 200,
timestamp: 10,
reason: ChitEarnedReason::DailyClaim,
},
ChitEarned {
amount: 200,
timestamp: 11,
reason: ChitEarnedReason::DailyClaim,
},
ChitEarned {
amount: 300,
timestamp: 12,
reason: ChitEarnedReason::DailyClaim,
},
ChitEarned {
amount: 500,
timestamp: 13,
reason: ChitEarnedReason::Achievement(Achievement::SetBio),
},
ChitEarned {
amount: 300,
timestamp: 14,
reason: ChitEarnedReason::DailyClaim,
},
ChitEarned {
amount: 500,
timestamp: 15,
reason: ChitEarnedReason::Achievement(Achievement::SetAvatar),
},
ChitEarned {
amount: 500,
timestamp: 16,
reason: ChitEarnedReason::Achievement(Achievement::SentDirectMessage),
},
],
events,
total_chit_earned,
}
}
}
8 changes: 7 additions & 1 deletion backend/canisters/user/impl/src/queries/chit_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ fn chit_events(args: Args) -> Response {
}

fn chit_events_impl(args: Args, state: &RuntimeState) -> Response {
let (events, total) = state.data.chit_events.events(args.from, args.to, args.max, args.ascending);
let (events, total) = state.data.chit_events.events(
args.from,
args.to,
args.skip.unwrap_or_default() as usize,
args.max as usize,
args.ascending,
);

Response::Success(SuccessResult { events, total })
}
3 changes: 2 additions & 1 deletion backend/canisters/user/impl/src/queries/initial_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ fn initial_state_impl(state: &RuntimeState) -> Response {
local_user_index_canister_id: state.data.local_user_index_canister_id,
achievements: state.data.chit_events.achievements(None),
achievements_last_seen: state.data.achievements_last_seen,
chit_balance: state.data.chit_balance.value,
total_chit_earned: state.data.chit_events.total_chit_earned(),
chit_balance: state.data.chit_events.balance_for_month_by_timestamp(now),
streak: state.data.streak.days(now),
streak_ends: state.data.streak.ends(),
next_daily_claim: if state.data.streak.can_claim(now) { today(now) } else { tomorrow(now) },
Expand Down
Loading

0 comments on commit 4d02fa2

Please sign in to comment.