From 28e891a95f94ddd95616b94d7bb5b1f0a030cea7 Mon Sep 17 00:00:00 2001 From: megrogan Date: Mon, 2 Oct 2023 18:09:42 +0100 Subject: [PATCH] Refund any prize message balance once it has ended --- backend/canisters/community/CHANGELOG.md | 1 + .../community/impl/src/timer_job_types.rs | 63 ++++++++++++++++++- .../impl/src/updates/send_message.rs | 14 ++++- backend/canisters/group/CHANGELOG.md | 1 + .../group/impl/src/timer_job_types.rs | 59 ++++++++++++++++- .../group/impl/src/updates/send_message.rs | 14 ++++- .../libraries/chat_events/src/chat_events.rs | 44 +++++++++++-- 7 files changed, 182 insertions(+), 14 deletions(-) diff --git a/backend/canisters/community/CHANGELOG.md b/backend/canisters/community/CHANGELOG.md index 7082af3b6f..2e42d6a0a2 100644 --- a/backend/canisters/community/CHANGELOG.md +++ b/backend/canisters/community/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Notifications for custom messages should use the sub-type ([#4465](https://github.com/open-chat-labs/open-chat/pull/4465)) - Join all community members to channels that are made public ([#4469](https://github.com/open-chat-labs/open-chat/pull/4469)) - Support prize messages in any token by getting fee from original transfer ([#4470](https://github.com/open-chat-labs/open-chat/pull/4470)) +- Refund any prize message balance once it has ended ([#4476](https://github.com/open-chat-labs/open-chat/pull/4476)) ## [[2.0.864](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.864-community)] - 2023-09-27 diff --git a/backend/canisters/community/impl/src/timer_job_types.rs b/backend/canisters/community/impl/src/timer_job_types.rs index 2fda905280..e19d81b9e3 100644 --- a/backend/canisters/community/impl/src/timer_job_types.rs +++ b/backend/canisters/community/impl/src/timer_job_types.rs @@ -1,8 +1,11 @@ use crate::jobs::import_groups::{finalize_group_import, mark_import_complete, process_channel_members}; -use crate::mutate_state; +use crate::{mutate_state, read_state}; use canister_timer_jobs::Job; +use ledger_utils::process_transaction; use serde::{Deserialize, Serialize}; -use types::{BlobReference, ChannelId, ChatId, MessageId, MessageIndex}; +use tracing::error; +use types::{BlobReference, CanisterId, ChannelId, ChatId, MessageId, MessageIndex, PendingCryptoTransaction}; +use utils::time::HOUR_IN_MS; #[derive(Serialize, Deserialize, Clone)] pub enum TimerJob { @@ -12,6 +15,8 @@ pub enum TimerJob { FinalizeGroupImport(FinalizeGroupImportJob), ProcessGroupImportChannelMembers(ProcessGroupImportChannelMembersJob), MarkGroupImportComplete(MarkGroupImportCompleteJob), + ClosePrize(ClosePrizeJob), + MakeTransfer(MakeTransferJob), } #[derive(Serialize, Deserialize, Clone)] @@ -51,6 +56,18 @@ pub struct MarkGroupImportCompleteJob { pub channel_id: ChannelId, } +#[derive(Serialize, Deserialize, Clone)] +pub struct ClosePrizeJob { + pub channel_id: ChannelId, + pub thread_root_message_index: Option, + pub message_index: MessageIndex, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct MakeTransferJob { + pub pending_transaction: PendingCryptoTransaction, +} + impl Job for TimerJob { fn execute(&self) { match self { @@ -60,6 +77,8 @@ impl Job for TimerJob { TimerJob::FinalizeGroupImport(job) => job.execute(), TimerJob::ProcessGroupImportChannelMembers(job) => job.execute(), TimerJob::MarkGroupImportComplete(job) => job.execute(), + TimerJob::ClosePrize(job) => job.execute(), + TimerJob::MakeTransfer(job) => job.execute(), } } } @@ -130,3 +149,43 @@ impl Job for MarkGroupImportCompleteJob { mark_import_complete(self.group_id, self.channel_id); } } + +impl Job for ClosePrizeJob { + fn execute(&self) { + if let Some(pending_transaction) = mutate_state(|state| { + if let Some(channel) = state.data.channels.get_mut(&self.channel_id) { + channel + .chat + .events + .close_prize(self.thread_root_message_index, self.message_index, state.env.now_nanos()) + } else { + None + } + }) { + let make_transfer_job = MakeTransferJob { pending_transaction }; + make_transfer_job.execute(); + } + } +} + +impl Job for MakeTransferJob { + fn execute(&self) { + let sender = read_state(|state| state.env.canister_id()); + let pending = self.pending_transaction.clone(); + ic_cdk::spawn(make_transfer(pending, sender)); + + async fn make_transfer(pending_transaction: PendingCryptoTransaction, sender: CanisterId) { + if let Err(error) = process_transaction(pending_transaction.clone(), sender).await { + error!(?error, "Transaction failed"); + mutate_state(|state| { + let now = state.env.now(); + state.data.timer_jobs.enqueue_job( + TimerJob::MakeTransfer(MakeTransferJob { pending_transaction }), + now + HOUR_IN_MS, + now, + ); + }); + } + } + } +} diff --git a/backend/canisters/community/impl/src/updates/send_message.rs b/backend/canisters/community/impl/src/updates/send_message.rs index 53b198e1f9..75d3adb12b 100644 --- a/backend/canisters/community/impl/src/updates/send_message.rs +++ b/backend/canisters/community/impl/src/updates/send_message.rs @@ -1,7 +1,7 @@ use crate::activity_notifications::handle_activity_notification; use crate::model::members::CommunityMembers; use crate::model::user_groups::UserGroup; -use crate::timer_job_types::{DeleteFileReferencesJob, EndPollJob, TimerJob}; +use crate::timer_job_types::{ClosePrizeJob, DeleteFileReferencesJob, EndPollJob, TimerJob}; use crate::{mutate_state, run_regular_jobs, RuntimeState}; use canister_api_macros::update_candid_and_msgpack; use canister_timer_jobs::TimerJobs; @@ -173,6 +173,18 @@ fn register_timer_jobs( timer_jobs.enqueue_job(TimerJob::DeleteFileReferences(DeleteFileReferencesJob { files }), expiry, now); } } + + if let MessageContent::Prize(p) = &message_event.event.content { + timer_jobs.enqueue_job( + TimerJob::ClosePrize(ClosePrizeJob { + channel_id, + thread_root_message_index, + message_index: message_event.event.message_index, + }), + p.end_date, + now, + ); + } } lazy_static! { diff --git a/backend/canisters/group/CHANGELOG.md b/backend/canisters/group/CHANGELOG.md index c8927f6a47..ba685bfbb3 100644 --- a/backend/canisters/group/CHANGELOG.md +++ b/backend/canisters/group/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Notifications for custom messages should use the sub-type ([#4465](https://github.com/open-chat-labs/open-chat/pull/4465)) - Support prize messages in any token by getting fee from original transfer ([#4470](https://github.com/open-chat-labs/open-chat/pull/4470)) +- Refund any prize message balance once it has ended ([#4476](https://github.com/open-chat-labs/open-chat/pull/4476)) ## [[2.0.865](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.865-group)] - 2023-09-27 diff --git a/backend/canisters/group/impl/src/timer_job_types.rs b/backend/canisters/group/impl/src/timer_job_types.rs index 1fe9f953a7..63a59e56ed 100644 --- a/backend/canisters/group/impl/src/timer_job_types.rs +++ b/backend/canisters/group/impl/src/timer_job_types.rs @@ -1,14 +1,19 @@ -use crate::activity_notifications::handle_activity_notification; use crate::mutate_state; +use crate::{activity_notifications::handle_activity_notification, read_state}; use canister_timer_jobs::Job; +use ledger_utils::process_transaction; use serde::{Deserialize, Serialize}; -use types::{BlobReference, MessageId, MessageIndex}; +use tracing::error; +use types::{BlobReference, CanisterId, MessageId, MessageIndex, PendingCryptoTransaction}; +use utils::time::HOUR_IN_MS; #[derive(Serialize, Deserialize, Clone)] pub enum TimerJob { HardDeleteMessageContent(HardDeleteMessageContentJob), DeleteFileReferences(DeleteFileReferencesJob), EndPoll(EndPollJob), + ClosePrize(ClosePrizeJob), + MakeTransfer(MakeTransferJob), } #[derive(Serialize, Deserialize, Clone)] @@ -28,12 +33,25 @@ pub struct EndPollJob { pub message_index: MessageIndex, } +#[derive(Serialize, Deserialize, Clone)] +pub struct ClosePrizeJob { + pub thread_root_message_index: Option, + pub message_index: MessageIndex, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct MakeTransferJob { + pub pending_transaction: PendingCryptoTransaction, +} + impl Job for TimerJob { fn execute(&self) { match self { TimerJob::HardDeleteMessageContent(job) => job.execute(), TimerJob::DeleteFileReferences(job) => job.execute(), TimerJob::EndPoll(job) => job.execute(), + TimerJob::ClosePrize(job) => job.execute(), + TimerJob::MakeTransfer(job) => job.execute(), } } } @@ -87,3 +105,40 @@ impl Job for EndPollJob { }); } } + +impl Job for ClosePrizeJob { + fn execute(&self) { + if let Some(pending_transaction) = mutate_state(|state| { + state + .data + .chat + .events + .close_prize(self.thread_root_message_index, self.message_index, state.env.now_nanos()) + }) { + let make_transfer_job = MakeTransferJob { pending_transaction }; + make_transfer_job.execute(); + } + } +} + +impl Job for MakeTransferJob { + fn execute(&self) { + let sender = read_state(|state| state.env.canister_id()); + let pending = self.pending_transaction.clone(); + ic_cdk::spawn(make_transfer(pending, sender)); + + async fn make_transfer(pending_transaction: PendingCryptoTransaction, sender: CanisterId) { + if let Err(error) = process_transaction(pending_transaction.clone(), sender).await { + error!(?error, "Transaction failed"); + mutate_state(|state| { + let now = state.env.now(); + state.data.timer_jobs.enqueue_job( + TimerJob::MakeTransfer(MakeTransferJob { pending_transaction }), + now + HOUR_IN_MS, + now, + ); + }); + } + } + } +} diff --git a/backend/canisters/group/impl/src/updates/send_message.rs b/backend/canisters/group/impl/src/updates/send_message.rs index 2d8fd4d777..7cd800cd6e 100644 --- a/backend/canisters/group/impl/src/updates/send_message.rs +++ b/backend/canisters/group/impl/src/updates/send_message.rs @@ -1,5 +1,5 @@ use crate::activity_notifications::handle_activity_notification; -use crate::timer_job_types::{DeleteFileReferencesJob, EndPollJob}; +use crate::timer_job_types::{ClosePrizeJob, DeleteFileReferencesJob, EndPollJob}; use crate::{mutate_state, run_regular_jobs, RuntimeState, TimerJob}; use canister_api_macros::update_candid_and_msgpack; use canister_timer_jobs::TimerJobs; @@ -117,6 +117,14 @@ fn register_timer_jobs( } } - // TODO: If this is a prize message then set a timer to transfer - // the balance of any remaining prizes to the original sender + if let MessageContent::Prize(p) = &message_event.event.content { + timer_jobs.enqueue_job( + TimerJob::ClosePrize(ClosePrizeJob { + thread_root_message_index, + message_index: message_event.event.message_index, + }), + p.end_date, + now, + ); + } } diff --git a/backend/libraries/chat_events/src/chat_events.rs b/backend/libraries/chat_events/src/chat_events.rs index a2685f0e1e..33f27eb7f9 100644 --- a/backend/libraries/chat_events/src/chat_events.rs +++ b/backend/libraries/chat_events/src/chat_events.rs @@ -4,6 +4,7 @@ use crate::*; use candid::Principal; use ic_ledger_types::Tokens; use itertools::Itertools; +use ledger_utils::create_pending_transaction; use rand::rngs::StdRng; use rand::Rng; use search::{Document, Query}; @@ -13,13 +14,13 @@ use std::cmp::{max, Reverse}; use std::collections::hash_map::Entry::{Occupied, Vacant}; use std::collections::HashMap; use types::{ - CanisterId, Chat, CompletedCryptoTransaction, Cryptocurrency, DirectChatCreated, EventIndex, EventWrapper, - EventsTimeToLiveUpdated, GroupCanisterThreadDetails, GroupCreated, GroupFrozen, GroupUnfrozen, HydratedMention, Mention, - Message, MessageContentInitial, MessageId, MessageIndex, MessageMatch, Milliseconds, MultiUserChat, PollVotes, - PrizeWinnerContent, ProposalUpdate, PushEventResult, PushIfNotContains, RangeSet, Reaction, RegisterVoteResult, - TimestampMillis, Timestamped, Tips, UserId, VoteOperation, + CanisterId, Chat, CompletedCryptoTransaction, CryptoTransaction, Cryptocurrency, DirectChatCreated, EventIndex, + EventWrapper, EventsTimeToLiveUpdated, GroupCanisterThreadDetails, GroupCreated, GroupFrozen, GroupUnfrozen, Hash, + HydratedMention, Mention, Message, MessageContentInitial, MessageId, MessageIndex, MessageMatch, MessageReport, + Milliseconds, MultiUserChat, PendingCryptoTransaction, PollVotes, PrizeWinnerContent, ProposalUpdate, PushEventResult, + PushIfNotContains, RangeSet, Reaction, RegisterVoteResult, ReportedMessageInternal, TimestampMillis, TimestampNanos, + Timestamped, Tips, UserId, VoteOperation, }; -use types::{Hash, MessageReport, ReportedMessageInternal}; pub const OPENCHAT_BOT_USER_ID: UserId = UserId::new(Principal::from_slice(&[228, 104, 142, 9, 133, 211, 135, 217, 129, 1])); @@ -407,6 +408,37 @@ impl ChatEvents { EndPollResult::PollNotFound } + pub fn close_prize( + &mut self, + thread_root_message_index: Option, + message_index: MessageIndex, + now_nanos: TimestampNanos, + ) -> Option { + let now_ms = now_nanos / 1_000_000; + if let Some((message, _)) = + self.message_internal_mut(EventIndex::default(), thread_root_message_index, message_index.into(), now_ms) + { + if let MessageContentInternal::Prize(p) = &mut message.content { + if let CryptoTransaction::Completed(t) = &p.transaction { + let unclaimed = p.prizes_remaining.iter().map(|t| t.e8s() as u128).sum::(); + if unclaimed > 0 { + p.prizes_remaining = Vec::new(); + return Some(create_pending_transaction( + t.token(), + t.ledger_canister_id(), + unclaimed, + t.fee(), + message.sender, + now_nanos, + )); + } + } + } + } + + None + } + pub fn record_proposal_vote( &mut self, user_id: UserId,