Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store CHIT balances per month #6087

Merged
merged 23 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 21 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
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
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
Loading