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
1 change: 1 addition & 0 deletions backend/canisters/community/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed

- Add `duration_ms` and `samples` to `AudioContent` ([#8669](https://github.com/open-chat-labs/open-chat/pull/8669))
- Apply user re-authentication check when claiming prizes ([#8696](https://github.com/open-chat-labs/open-chat/pull/8696))

### Removed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct Args {
pub total_chit_earned: u32,
pub streak: u16,
pub streak_ends: TimestampMillis,
pub user_reauthenticated: bool,
}

// this is needed so that the generate_c2c_call macro doesn't complain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ fn prepare(args: &Args, state: &mut RuntimeState) -> OCResult<PrepareResult> {
args.total_chit_earned,
args.streak,
args.streak_ends,
args.user_reauthenticated,
)?;

// Hack to ensure 2 prizes claimed by the same user in the same block don't result in "duplicate transaction" errors.
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/group/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed

- Add `duration_ms` and `samples` to `AudioContent` ([#8669](https://github.com/open-chat-labs/open-chat/pull/8669))
- Apply user re-authentication check when claiming prizes ([#8696](https://github.com/open-chat-labs/open-chat/pull/8696))

### Removed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub struct Args {
pub total_chit_earned: u32,
pub streak: u16,
pub streak_ends: TimestampMillis,
pub user_reauthenticated: bool,
}

// this is needed so that the generate_c2c_call macro doesn't complain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ fn prepare(args: &Args, state: &mut RuntimeState) -> OCResult<PrepareResult> {
args.total_chit_earned,
args.streak,
args.streak_ends,
args.user_reauthenticated,
)?;

// Hack to ensure 2 prizes claimed by the same user in the same block don't result in "duplicate transaction" errors.
Expand Down
4 changes: 4 additions & 0 deletions backend/canisters/local_user_index/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [unreleased]

### Changed

- Apply user re-authentication check when claiming prizes ([#8696](https://github.com/open-chat-labs/open-chat/pull/8696))

### Removed

- Remove `c2c_diamond_membership_expiry_dates` endpoint ([#8691](https://github.com/open-chat-labs/open-chat/pull/8691))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ use candid::CandidType;
use oc_error_codes::OCError;
use serde::{Deserialize, Serialize};
use ts_export::ts_export;
use types::{CompletedCryptoTransaction, FailedCryptoTransaction, MessageId, MultiUserChat};
use types::{CompletedCryptoTransaction, FailedCryptoTransaction, MessageId, MultiUserChat, SignedDelegation};

#[ts_export(local_user_index, claim_prize)]
#[derive(CandidType, Serialize, Deserialize, Debug)]
pub struct Args {
pub chat_id: MultiUserChat,
pub message_id: MessageId,
pub delegation: Option<SignedDelegation>,
}

#[ts_export(local_user_index, claim_prize)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::{guards::caller_is_openchat_user, read_state};
use canister_api_macros::update;
use canister_tracing_macros::trace;
use constants::LIFETIME_DIAMOND_TIMESTAMP;
use constants::{LIFETIME_DIAMOND_TIMESTAMP, MINUTE_IN_MS};
use identity_utils::verify_signature;
use local_user_index_canister::{GlobalUser, claim_prize::*};
use types::{
DiamondMembershipStatus, MultiUserChat,
Expand All @@ -19,8 +20,24 @@ async fn claim_prize(args: Args) -> PrizeClaimResponse {
unique_person_proof,
..
},
user_reauthenticated,
now,
) = read_state(|state| (state.calling_user(), state.env.now()));
) = read_state(|state| {
let user = state.calling_user();
let user_reauthenticated = args.delegation.is_some_and(|d| {
verify_signature(
&d.signature,
state.data.identity_canister_id,
5 * MINUTE_IN_MS,
&state.data.ic_root_key,
state.env.now(),
)
.is_ok()
});
let now = state.env.now();

(user, user_reauthenticated, now)
});

let is_unique_person = unique_person_proof.is_some();
let diamond_status = match diamond_membership_expires_at {
Expand All @@ -41,6 +58,7 @@ async fn claim_prize(args: Args) -> PrizeClaimResponse {
total_chit_earned,
streak: chit.streak,
streak_ends: chit.streak_ends,
user_reauthenticated,
};
group_canister_c2c_client::c2c_claim_prize(chat_id.into(), &c2c_args).await
}
Expand All @@ -54,6 +72,7 @@ async fn claim_prize(args: Args) -> PrizeClaimResponse {
total_chit_earned,
streak: chit.streak,
streak_ends: chit.streak_ends,
user_reauthenticated,
};
community_canister_c2c_client::c2c_claim_prize(community_id.into(), &c2c_args).await
}
Expand Down
9 changes: 7 additions & 2 deletions backend/integration_tests/src/client/local_user_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub mod happy_path {
use pocket_ic::PocketIc;
use types::{
BotInstallationLocation, BotPermissions, CanisterId, ChannelId, ChatId, CommunityCanisterCommunitySummary, CommunityId,
Empty, MessageId, MultiUserChat, UserId,
Empty, MessageId, MultiUserChat, SignedDelegation, UserId,
};

pub fn register_user(env: &mut PocketIc, principal: Principal, canister_id: CanisterId, public_key: Vec<u8>) -> User {
Expand Down Expand Up @@ -338,12 +338,17 @@ pub mod happy_path {
local_user_index: CanisterId,
chat_id: MultiUserChat,
message_id: MessageId,
delegation: Option<SignedDelegation>,
) {
let response = super::claim_prize(
env,
sender,
local_user_index,
&local_user_index_canister::claim_prize::Args { chat_id, message_id },
&local_user_index_canister::claim_prize::Args {
chat_id,
message_id,
delegation,
},
);

match response {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,13 +252,15 @@ fn pending_prizes_transferred_to_community() {
local_user_index,
MultiUserChat::Channel(community_id, channel_id),
message_id,
None,
);
client::local_user_index::happy_path::claim_prize(
env,
user3.principal,
local_user_index,
MultiUserChat::Channel(community_id, channel_id),
message_id,
None,
);

let community_balance = client::ledger::happy_path::balance_of(env, canister_ids.icp_ledger, Principal::from(community_id));
Expand Down
99 changes: 99 additions & 0 deletions backend/integration_tests/src/prize_message_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::env::ENV;
use crate::utils::{now_millis, now_nanos, tick_many};
use crate::{TestEnv, client};
use constants::{HOUR_IN_MS, ICP_SYMBOL, ICP_TRANSFER_FEE, MINUTE_IN_MS, PRIZE_FEE_PERCENT};
use oc_error_codes::OCErrorCode;
use std::ops::Deref;
use std::time::Duration;
use test_case::test_case;
Expand Down Expand Up @@ -83,6 +84,7 @@ fn prize_messages_can_be_claimed_successfully() {
local_user_index,
MultiUserChat::Group(group_id),
message_id,
None,
);
let user2_balance = client::ledger::happy_path::balance_of(env, canister_ids.icp_ledger, user2.user_id);
assert_eq!(user2_balance, 200000);
Expand All @@ -93,6 +95,7 @@ fn prize_messages_can_be_claimed_successfully() {
local_user_index,
MultiUserChat::Group(group_id),
message_id,
None,
);
let user3_balance = client::ledger::happy_path::balance_of(env, canister_ids.icp_ledger, user3.user_id);
assert_eq!(user3_balance, 100000);
Expand All @@ -119,6 +122,101 @@ fn prize_messages_can_be_claimed_successfully() {
}
}

#[test]
fn prize_message_requiring_reauthentication() {
let mut wrapper = ENV.deref().get();
let TestEnv {
env,
canister_ids,
controller,
..
} = wrapper.env();

let user1 = client::register_diamond_user(env, canister_ids, *controller);
let (user2, user2_auth) = client::register_user_and_include_auth(env, canister_ids);
let group_id = client::user::happy_path::create_group(env, &user1, random_string().as_str(), true, true);
client::group::happy_path::join_group(env, user2.principal, group_id);

// Send user1 some ICP
client::ledger::happy_path::transfer(env, *controller, canister_ids.icp_ledger, user1.user_id, 1_000_000_000);

let prizes = [100000, 200000];
let fee = ICP_TRANSFER_FEE;
let message_id = random_from_u128();

let send_message_response = client::user::send_message_with_transfer_to_group(
env,
user1.principal,
user1.user_id.into(),
&user_canister::send_message_with_transfer_to_group::Args {
group_id,
thread_root_message_index: None,
message_id,
content: MessageContentInitial::Prize(PrizeContentInitial {
prizes_v2: prizes.into_iter().map(u128::from).collect(),
transfer: CryptoTransaction::Pending(PendingCryptoTransaction::ICRC1(icrc1::PendingCryptoTransaction {
ledger: canister_ids.icp_ledger,
token_symbol: ICP_SYMBOL.to_string(),
amount: prizes.iter().sum::<u64>() as u128 + fee * prizes.len() as u128,
to: group_id.into(),
fee,
memo: None,
created: now_nanos(env),
})),
end_date: now_millis(env) + HOUR_IN_MS,
caption: None,
diamond_only: false,
lifetime_diamond_only: false,
unique_person_only: false,
streak_only: 0,
requires_captcha: true,
min_chit_earned: 0,
}),
sender_name: user1.username(),
sender_display_name: None,
replies_to: None,
mentioned: Vec::new(),
block_level_markdown: false,
rules_accepted: None,
message_filter_failed: None,
pin: None,
},
);

assert!(matches!(
send_message_response,
user_canister::send_message_with_transfer_to_group::Response::Success(_)
));

let local_user_index = canister_ids.local_user_index(env, group_id);

let response = client::local_user_index::claim_prize(
env,
user2.principal,
local_user_index,
&local_user_index_canister::claim_prize::Args {
chat_id: MultiUserChat::Group(group_id),
message_id,
delegation: None,
},
);

assert!(
matches!(response, local_user_index_canister::claim_prize::Response::Error(e) if e.matches_code(OCErrorCode::PrizeUserNotElligible))
);

client::local_user_index::happy_path::claim_prize(
env,
user2.principal,
local_user_index,
MultiUserChat::Group(group_id),
message_id,
Some(user2_auth.auth_delegation),
);
let user2_balance = client::ledger::happy_path::balance_of(env, canister_ids.icp_ledger, user2.user_id);
assert_eq!(user2_balance, 200000);
}

#[test_case(1; "Prize expires")]
#[test_case(2; "Message deleted")]
#[test_case(3; "Message removed due to disappearing messages")]
Expand Down Expand Up @@ -205,6 +303,7 @@ fn unclaimed_prizes_get_refunded(case: u32) {
local_user_index,
MultiUserChat::Group(group_id),
message_id,
None,
);

let interval = match case {
Expand Down
7 changes: 7 additions & 0 deletions backend/libraries/chat_events/src/chat_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,7 @@ impl ChatEvents {
total_chit_earned: u32,
streak: u16,
streak_ends: TimestampMillis,
user_reauthenticated: bool,
) -> OCResult<ReservePrizeSuccess> {
match self.update_message(
None,
Expand All @@ -1034,6 +1035,7 @@ impl ChatEvents {
total_chit_earned,
streak,
streak_ends,
user_reauthenticated,
)
},
) {
Expand All @@ -1053,6 +1055,7 @@ impl ChatEvents {
total_chit_earned: u32,
streak: u16,
streak_ends: TimestampMillis,
user_reauthenticated: bool,
) -> Result<ReservePrizeSuccess, UpdateEventError<OCErrorCode>> {
let MessageContentInternal::Prize(content) = &mut message.content else {
return Err(UpdateEventError::NotFound);
Expand All @@ -1078,6 +1081,10 @@ impl ChatEvents {
return Err(UpdateEventError::NoChange(OCErrorCode::PrizeUserNotElligible));
}

if content.requires_captcha && !user_reauthenticated {
return Err(UpdateEventError::NoChange(OCErrorCode::PrizeUserNotElligible));
}

if content.end_date < now {
return Err(UpdateEventError::NoChange(OCErrorCode::PrizeEnded));
}
Expand Down
2 changes: 2 additions & 0 deletions backend/libraries/group_chat_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1753,6 +1753,7 @@ impl GroupChatCore {
total_chit_earned: u32,
streak: u16,
streak_ends: TimestampMillis,
user_reauthenticated: bool,
) -> OCResult<ReservePrizeSuccess> {
let member = self.members.get_verified_member(user_id)?;
let min_visible_event_index = member.min_visible_event_index();
Expand All @@ -1767,6 +1768,7 @@ impl GroupChatCore {
total_chit_earned,
streak,
streak_ends,
user_reauthenticated,
)
}

Expand Down
Loading
Loading