From 4db8d60ceff08b1baa255703720f59eeeb4fe82d Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 1 Dec 2023 09:10:29 +0000 Subject: [PATCH 01/14] Don't try to push notification subscription when in anonymous mode (#4899) --- frontend/openchat-client/src/stores/notifications.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/openchat-client/src/stores/notifications.ts b/frontend/openchat-client/src/stores/notifications.ts index 0c193d8848..2a2030ca60 100644 --- a/frontend/openchat-client/src/stores/notifications.ts +++ b/frontend/openchat-client/src/stores/notifications.ts @@ -3,6 +3,7 @@ import { derived, writable } from "svelte/store"; import type { NotificationStatus } from "openchat-shared"; import { createLsBoolStore } from "./localStorageSetting"; import { configKeys } from "../utils/config"; +import { anonUser } from "./user"; const notificationsSupported = "serviceWorker" in navigator && "PushManager" in window && "Notification" in window; @@ -45,7 +46,7 @@ function permissionStateToNotificationPermission(perm: PermissionState): Notific } function permissionToStatus( - permission: NotificationPermission | "pending-init" + permission: NotificationPermission | "pending-init", ): NotificationStatus { switch (permission) { case "pending-init": @@ -60,16 +61,16 @@ function permissionToStatus( } export const notificationStatus = derived( - [softDisabledStore, browserPermissionStore], - ([softDisabled, browserPermission]) => { - if (!notificationsSupported) { + [softDisabledStore, browserPermissionStore, anonUser], + ([softDisabled, browserPermission, isAnonUser]) => { + if (!notificationsSupported || isAnonUser) { return "unsupported"; } if (softDisabled) { return "soft-denied"; } return permissionToStatus(browserPermission); - } + }, ); export async function askForNotificationPermission(): Promise { From 0d84e7a9f7849bf737bc25f0ee78226ce182761f Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 1 Dec 2023 12:11:51 +0000 Subject: [PATCH 02/14] Add `c2c_send_messages_v2` which uses MessageContentInternal (#4902) --- Cargo.lock | 2 + .../canisters/proposals_bot/impl/Cargo.toml | 1 + .../impl/src/jobs/push_proposals.rs | 20 ++-- backend/canisters/user/CHANGELOG.md | 1 + backend/canisters/user/api/Cargo.toml | 1 + .../api/src/updates/c2c_send_messages_v2.rs | 36 +++++++ backend/canisters/user/api/src/updates/mod.rs | 1 + backend/canisters/user/impl/src/lib.rs | 4 +- .../canisters/user/impl/src/lifecycle/init.rs | 4 +- .../user/impl/src/model/communities.rs | 12 +-- .../user/impl/src/model/direct_chats.rs | 2 +- .../user/impl/src/model/group_chats.rs | 16 +-- .../canisters/user/impl/src/openchat_bot.rs | 9 +- .../user/impl/src/timer_job_types.rs | 5 +- .../impl/src/updates/c2c_notify_events.rs | 5 +- .../impl/src/updates/c2c_send_messages.rs | 29 ++++-- .../user/impl/src/updates/send_message.rs | 10 +- .../src/updates/send_message_with_transfer.rs | 35 +------ .../impl/src/updates/set_message_reminder.rs | 5 +- .../user/impl/src/updates/tip_message.rs | 6 +- .../libraries/chat_events/src/chat_events.rs | 2 +- .../src/message_content_internal.rs | 97 +++++++++---------- backend/libraries/group_chat_core/src/lib.rs | 52 +++++----- .../libraries/group_chat_core/src/roles.rs | 4 +- 24 files changed, 184 insertions(+), 175 deletions(-) create mode 100644 backend/canisters/user/api/src/updates/c2c_send_messages_v2.rs diff --git a/Cargo.lock b/Cargo.lock index 5be41b2b11..52f62cd9f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5026,6 +5026,7 @@ dependencies = [ "canister_state_macros", "canister_timer_jobs", "canister_tracing_macros", + "chat_events", "community_canister", "community_canister_c2c_client", "fire_and_forget_handler", @@ -6988,6 +6989,7 @@ version = "0.1.0" dependencies = [ "candid", "candid_gen", + "chat_events", "ic-ledger-types", "icrc-ledger-types", "proposals_bot_canister", diff --git a/backend/canisters/proposals_bot/impl/Cargo.toml b/backend/canisters/proposals_bot/impl/Cargo.toml index f733be60d4..96c9005c2b 100644 --- a/backend/canisters/proposals_bot/impl/Cargo.toml +++ b/backend/canisters/proposals_bot/impl/Cargo.toml @@ -16,6 +16,7 @@ canister_logger = { path = "../../../libraries/canister_logger" } canister_state_macros = { path = "../../../libraries/canister_state_macros" } canister_timer_jobs = { path = "../../../libraries/canister_timer_jobs" } canister_tracing_macros = { path = "../../../libraries/canister_tracing_macros" } +chat_events = { path = "../../../libraries/chat_events" } community_canister = { path = "../../community/api" } community_canister_c2c_client = { path = "../../community/c2c_client" } fire_and_forget_handler = { path = "../../../libraries/fire_and_forget_handler" } diff --git a/backend/canisters/proposals_bot/impl/src/jobs/push_proposals.rs b/backend/canisters/proposals_bot/impl/src/jobs/push_proposals.rs index 2879c69651..66a2105873 100644 --- a/backend/canisters/proposals_bot/impl/src/jobs/push_proposals.rs +++ b/backend/canisters/proposals_bot/impl/src/jobs/push_proposals.rs @@ -1,13 +1,13 @@ use crate::model::nervous_systems::ProposalToPush; use crate::{generate_message_id, mutate_state, RuntimeState}; +use chat_events::{MessageContentInternal, ProposalContentInternal}; use ic_cdk::api::call::{CallResult, RejectionCode}; use ic_cdk_timers::TimerId; use std::cell::Cell; +use std::collections::HashMap; use std::time::Duration; use tracing::trace; -use types::{ - CanisterId, ChannelId, ChatId, CommunityId, MessageContentInitial, MessageId, MultiUserChat, Proposal, ProposalContent, -}; +use types::{CanisterId, ChannelId, ChatId, CommunityId, MessageId, MultiUserChat, Proposal}; thread_local! { static TIMER_ID: Cell> = Cell::default(); @@ -55,12 +55,11 @@ async fn push_group_proposal(governance_canister_id: CanisterId, group_id: ChatI let send_message_args = group_canister::c2c_send_message::Args { message_id, thread_root_message_index: None, - content: MessageContentInitial::GovernanceProposal(ProposalContent { + content: MessageContentInternal::GovernanceProposal(ProposalContentInternal { governance_canister_id, proposal: proposal.clone(), - my_vote: None, - }) - .into(), + votes: HashMap::new(), + }), sender_name: "ProposalsBot".to_string(), sender_display_name: None, replies_to: None, @@ -85,12 +84,11 @@ async fn push_channel_proposal( let send_message_args = community_canister::c2c_send_message::Args { message_id, thread_root_message_index: None, - content: MessageContentInitial::GovernanceProposal(ProposalContent { + content: MessageContentInternal::GovernanceProposal(ProposalContentInternal { governance_canister_id, proposal: proposal.clone(), - my_vote: None, - }) - .into(), + votes: HashMap::new(), + }), sender_name: "ProposalsBot".to_string(), sender_display_name: None, replies_to: None, diff --git a/backend/canisters/user/CHANGELOG.md b/backend/canisters/user/CHANGELOG.md index 1dd17b2a97..50f3ed6145 100644 --- a/backend/canisters/user/CHANGELOG.md +++ b/backend/canisters/user/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Support getting batches of chat events via LocalUserIndex ([#4848](https://github.com/open-chat-labs/open-chat/pull/4848)) +- Add `c2c_send_messages_v2` which uses MessageContentInternal ([#4902](https://github.com/open-chat-labs/open-chat/pull/4902)) ### Changed diff --git a/backend/canisters/user/api/Cargo.toml b/backend/canisters/user/api/Cargo.toml index 6acd1feac7..b5541ed550 100644 --- a/backend/canisters/user/api/Cargo.toml +++ b/backend/canisters/user/api/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] candid = { workspace = true } candid_gen = { path = "../../../libraries/candid_gen" } +chat_events = { path = "../../../libraries/chat_events" } ic-ledger-types = { workspace = true } icrc-ledger-types = { workspace = true } proposals_bot_canister = { path = "../../proposals_bot/api" } diff --git a/backend/canisters/user/api/src/updates/c2c_send_messages_v2.rs b/backend/canisters/user/api/src/updates/c2c_send_messages_v2.rs new file mode 100644 index 0000000000..d63dfbbf9e --- /dev/null +++ b/backend/canisters/user/api/src/updates/c2c_send_messages_v2.rs @@ -0,0 +1,36 @@ +use chat_events::MessageContentInternal; +use serde::{Deserialize, Serialize}; +use types::{MessageContentInitial, MessageId, MessageIndex}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Args { + pub messages: Vec, + pub sender_name: String, + pub sender_display_name: Option, + pub sender_avatar_id: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct SendMessageArgs { + pub message_id: MessageId, + pub sender_message_index: MessageIndex, + pub content: MessageContentInternal, + pub replies_to: Option, + pub forwarding: bool, + pub correlation_id: u64, +} + +pub type Response = crate::c2c_send_messages::Response; + +impl From for SendMessageArgs { + fn from(value: crate::c2c_send_messages::SendMessageArgs) -> Self { + SendMessageArgs { + message_id: value.message_id, + sender_message_index: value.sender_message_index, + content: MessageContentInitial::from(value.content).try_into().unwrap(), + replies_to: value.replies_to, + forwarding: value.forwarding, + correlation_id: value.correlation_id, + } + } +} diff --git a/backend/canisters/user/api/src/updates/mod.rs b/backend/canisters/user/api/src/updates/mod.rs index 72cfe671bb..109bbf2f64 100644 --- a/backend/canisters/user/api/src/updates/mod.rs +++ b/backend/canisters/user/api/src/updates/mod.rs @@ -19,6 +19,7 @@ pub mod c2c_remove_from_community; pub mod c2c_remove_from_group; pub mod c2c_revoke_super_admin; pub mod c2c_send_messages; +pub mod c2c_send_messages_v2; pub mod c2c_set_user_suspended; pub mod c2c_tip_message; pub mod c2c_toggle_reaction; diff --git a/backend/canisters/user/impl/src/lib.rs b/backend/canisters/user/impl/src/lib.rs index 3f03ae19df..02907d4ac0 100644 --- a/backend/canisters/user/impl/src/lib.rs +++ b/backend/canisters/user/impl/src/lib.rs @@ -80,12 +80,12 @@ impl RuntimeState { pub fn is_caller_known_group_canister(&self) -> bool { let caller = self.env.caller(); - self.data.group_chats.get(&caller.into()).is_some() + self.data.group_chats.exists(&caller.into()) } pub fn is_caller_known_community_canister(&self) -> bool { let caller = self.env.caller(); - self.data.communities.get(&caller.into()).is_some() + self.data.communities.exists(&caller.into()) } pub fn push_notification(&mut self, recipient: UserId, notification: Notification) { diff --git a/backend/canisters/user/impl/src/lifecycle/init.rs b/backend/canisters/user/impl/src/lifecycle/init.rs index 12dfd2c2a2..342903d20e 100644 --- a/backend/canisters/user/impl/src/lifecycle/init.rs +++ b/backend/canisters/user/impl/src/lifecycle/init.rs @@ -3,6 +3,7 @@ use crate::{mutate_state, openchat_bot, Data}; use canister_tracing_macros::trace; use ic_cdk_macros::init; use tracing::info; +use types::MessageContentInitial; use user_canister::init::Args; use utils::env::Environment; @@ -30,7 +31,8 @@ fn init(args: Args) { mutate_state(|state| { for message in args.openchat_bot_messages { - openchat_bot::send_message(message, true, state); + let initial_content: MessageContentInitial = message.into(); + openchat_bot::send_message(initial_content.try_into().unwrap(), true, state); } }); diff --git a/backend/canisters/user/impl/src/model/communities.rs b/backend/canisters/user/impl/src/model/communities.rs index d9b69d3ca8..69b5c636fc 100644 --- a/backend/canisters/user/impl/src/model/communities.rs +++ b/backend/canisters/user/impl/src/model/communities.rs @@ -19,8 +19,8 @@ struct RemovedCommunity { } impl Communities { - pub fn get(&self, community_id: &CommunityId) -> Option<&Community> { - self.communities.get(community_id) + pub fn exists(&self, community_id: &CommunityId) -> bool { + self.communities.contains_key(community_id) } pub fn get_mut(&mut self, community_id: &CommunityId) -> Option<&mut Community> { @@ -73,10 +73,6 @@ impl Communities { community } - pub fn exists(&self, community_id: &CommunityId) -> bool { - self.communities.contains_key(community_id) - } - pub fn updated_since(&self, updated_since: TimestampMillis) -> impl Iterator { self.communities.values().filter(move |c| c.last_updated() > updated_since) } @@ -102,10 +98,6 @@ impl Communities { self.communities.len() } - pub fn has(&self, community_id: &CommunityId) -> bool { - self.communities.contains_key(community_id) - } - fn next_index(&self) -> u32 { self.communities.values().map(|c| c.index.value).max().unwrap_or_default() + 1 } diff --git a/backend/canisters/user/impl/src/model/direct_chats.rs b/backend/canisters/user/impl/src/model/direct_chats.rs index a000762428..12834cb5d7 100644 --- a/backend/canisters/user/impl/src/model/direct_chats.rs +++ b/backend/canisters/user/impl/src/model/direct_chats.rs @@ -111,7 +111,7 @@ impl DirectChats { &self.metrics } - pub fn has(&self, chat_id: &ChatId) -> bool { + pub fn exists(&self, chat_id: &ChatId) -> bool { self.direct_chats.contains_key(chat_id) } diff --git a/backend/canisters/user/impl/src/model/group_chats.rs b/backend/canisters/user/impl/src/model/group_chats.rs index 1757f10384..1b08772663 100644 --- a/backend/canisters/user/impl/src/model/group_chats.rs +++ b/backend/canisters/user/impl/src/model/group_chats.rs @@ -19,6 +19,10 @@ struct RemovedGroup { } impl GroupChats { + pub fn exists(&self, chat_id: &ChatId) -> bool { + self.group_chats.contains_key(chat_id) + } + pub fn updated_since(&self, since: TimestampMillis) -> impl Iterator { self.group_chats.values().filter(move |c| c.last_updated() > since) } @@ -40,10 +44,6 @@ impl GroupChats { .collect() } - pub fn get(&self, chat_id: &ChatId) -> Option<&GroupChat> { - self.group_chats.get(chat_id) - } - pub fn get_mut(&mut self, chat_id: &ChatId) -> Option<&mut GroupChat> { self.group_chats.get_mut(chat_id) } @@ -85,10 +85,6 @@ impl GroupChats { group } - pub fn exists(&self, chat_id: &ChatId) -> bool { - self.group_chats.contains_key(chat_id) - } - pub fn iter(&self) -> impl Iterator { self.group_chats.values() } @@ -101,10 +97,6 @@ impl GroupChats { self.group_chats.len() } - pub fn has(&self, chat_id: &ChatId) -> bool { - self.group_chats.contains_key(chat_id) - } - pub fn pin(&mut self, chat_id: ChatId, now: TimestampMillis) { if !self.pinned.value.contains(&chat_id) { self.pinned.timestamp = now; diff --git a/backend/canisters/user/impl/src/openchat_bot.rs b/backend/canisters/user/impl/src/openchat_bot.rs index 3d7695d595..76be7ceb52 100644 --- a/backend/canisters/user/impl/src/openchat_bot.rs +++ b/backend/canisters/user/impl/src/openchat_bot.rs @@ -1,7 +1,8 @@ use crate::updates::c2c_send_messages::{handle_message_impl, HandleMessageArgs}; use crate::{RuntimeState, BASIC_GROUP_CREATION_LIMIT, PREMIUM_GROUP_CREATION_LIMIT}; +use chat_events::{MessageContentInternal, TextContentInternal}; use ic_ledger_types::Tokens; -use types::{ChannelId, CommunityId, EventWrapper, Message, MessageContent, SuspensionDuration, TextContent, UserId}; +use types::{ChannelId, CommunityId, EventWrapper, Message, SuspensionDuration, UserId}; use user_canister::c2c_send_messages::C2CReplyContext; use user_canister::{PhoneNumberConfirmed, ReferredUserRegistered, StorageUpgraded, UserSuspended}; use utils::consts::{OPENCHAT_BOT_USERNAME, OPENCHAT_BOT_USER_ID}; @@ -115,7 +116,7 @@ You can appeal this suspension by sending a direct message to the @OpenChat Twit } pub(crate) fn send_message( - content: MessageContent, + content: MessageContentInternal, mute_notification: bool, state: &mut RuntimeState, ) -> EventWrapper { @@ -123,12 +124,12 @@ pub(crate) fn send_message( } pub(crate) fn send_text_message(text: String, mute_notification: bool, state: &mut RuntimeState) -> EventWrapper { - let content = MessageContent::Text(TextContent { text }); + let content = MessageContentInternal::Text(TextContentInternal { text }); send_message(content, mute_notification, state) } pub(crate) fn send_message_with_reply( - content: MessageContent, + content: MessageContentInternal, replies_to: Option, mute_notification: bool, state: &mut RuntimeState, diff --git a/backend/canisters/user/impl/src/timer_job_types.rs b/backend/canisters/user/impl/src/timer_job_types.rs index da72e9f3d9..c3bde7fa90 100644 --- a/backend/canisters/user/impl/src/timer_job_types.rs +++ b/backend/canisters/user/impl/src/timer_job_types.rs @@ -3,8 +3,9 @@ use crate::updates::send_message::send_to_recipients_canister; use crate::updates::swap_tokens::process_token_swap; use crate::{mutate_state, openchat_bot, read_state}; use canister_timer_jobs::Job; +use chat_events::{MessageContentInternal, MessageReminderContentInternal}; use serde::{Deserialize, Serialize}; -use types::{BlobReference, Chat, ChatId, EventIndex, MessageContent, MessageId, MessageIndex, MessageReminderContent, UserId}; +use types::{BlobReference, Chat, ChatId, EventIndex, MessageId, MessageIndex, UserId}; use user_canister::c2c_send_messages; use user_canister::c2c_send_messages::C2CReplyContext; use utils::consts::OPENCHAT_BOT_USER_ID; @@ -128,7 +129,7 @@ impl Job for DeleteFileReferencesJob { impl Job for MessageReminderJob { fn execute(self) { let replies_to = C2CReplyContext::OtherChat(self.chat, self.thread_root_message_index, self.event_index); - let content = MessageContent::MessageReminder(MessageReminderContent { + let content = MessageContentInternal::MessageReminder(MessageReminderContentInternal { reminder_id: self.reminder_id, notes: self.notes.clone(), }); diff --git a/backend/canisters/user/impl/src/updates/c2c_notify_events.rs b/backend/canisters/user/impl/src/updates/c2c_notify_events.rs index 7460dae188..55ab3461f1 100644 --- a/backend/canisters/user/impl/src/updates/c2c_notify_events.rs +++ b/backend/canisters/user/impl/src/updates/c2c_notify_events.rs @@ -2,7 +2,7 @@ use crate::guards::caller_is_local_user_index; use crate::{mutate_state, openchat_bot, RuntimeState}; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; -use types::Timestamped; +use types::{MessageContentInitial, Timestamped}; use user_canister::c2c_notify_user_events::{Response::*, *}; use user_canister::mark_read::ChannelMessagesRead; use user_canister::Event; @@ -46,7 +46,8 @@ fn process_event(event: Event, state: &mut RuntimeState) { openchat_bot::send_user_suspended_message(&ev, state); } Event::OpenChatBotMessage(content) => { - openchat_bot::send_message(*content, false, state); + let initial_content: MessageContentInitial = (*content).into(); + openchat_bot::send_message(initial_content.try_into().unwrap(), false, state); } Event::UserJoinedGroup(ev) => { let now = state.env.now(); diff --git a/backend/canisters/user/impl/src/updates/c2c_send_messages.rs b/backend/canisters/user/impl/src/updates/c2c_send_messages.rs index 3c84f5aeaa..4492ee6095 100644 --- a/backend/canisters/user/impl/src/updates/c2c_send_messages.rs +++ b/backend/canisters/user/impl/src/updates/c2c_send_messages.rs @@ -6,18 +6,31 @@ use chat_events::{MessageContentInternal, PushMessageArgs, Reader, ReplyContextI use ic_cdk_macros::update; use rand::Rng; use types::{ - CanisterId, DirectMessageNotification, EventWrapper, Message, MessageContent, MessageContentInitial, MessageId, - MessageIndex, Notification, TimestampMillis, UserId, + CanisterId, DirectMessageNotification, EventWrapper, Message, MessageId, MessageIndex, Notification, TimestampMillis, + UserId, }; use user_canister::c2c_send_messages::{Response::*, *}; #[update_msgpack] #[trace] async fn c2c_send_messages(args: Args) -> Response { + let v2_args = user_canister::c2c_send_messages_v2::Args { + messages: args.messages.into_iter().map(|m| m.into()).collect(), + sender_name: args.sender_name, + sender_display_name: args.sender_display_name, + sender_avatar_id: args.sender_avatar_id, + }; + + c2c_send_messages_impl(v2_args).await +} + +#[update_msgpack] +#[trace] +async fn c2c_send_messages_v2(args: user_canister::c2c_send_messages_v2::Args) -> Response { c2c_send_messages_impl(args).await } -async fn c2c_send_messages_impl(args: Args) -> Response { +async fn c2c_send_messages_impl(args: user_canister::c2c_send_messages_v2::Args) -> Response { run_regular_jobs(); let sender_user_id = match read_state(get_sender_status) { @@ -105,7 +118,7 @@ async fn c2c_handle_bot_messages( sender_message_index: None, sender_name: args.bot_name.clone(), sender_display_name: args.bot_display_name.clone(), - content: message.content.into(), + content: message.content.try_into().unwrap(), replies_to: None, forwarding: false, correlation_id: 0, @@ -126,7 +139,7 @@ pub(crate) struct HandleMessageArgs { pub sender_message_index: Option, pub sender_name: String, pub sender_display_name: Option, - pub content: MessageContent, + pub content: MessageContentInternal, pub replies_to: Option, pub forwarding: bool, pub correlation_id: u64, @@ -175,15 +188,13 @@ pub(crate) fn handle_message_impl( state: &mut RuntimeState, ) -> EventWrapper { let replies_to = convert_reply_context(args.replies_to, sender, state); - let initial_content: MessageContentInitial = args.content.into(); - let content = MessageContentInternal::from(initial_content); - let files = content.blob_references(); + let files = args.content.blob_references(); let push_message_args = PushMessageArgs { thread_root_message_index: None, message_id: args.message_id.unwrap_or_else(|| state.env.rng().gen()), sender, - content, + content: args.content, mentioned: Vec::new(), replies_to, forwarded: args.forwarding, diff --git a/backend/canisters/user/impl/src/updates/send_message.rs b/backend/canisters/user/impl/src/updates/send_message.rs index aa4631c58b..fe460f22e7 100644 --- a/backend/canisters/user/impl/src/updates/send_message.rs +++ b/backend/canisters/user/impl/src/updates/send_message.rs @@ -5,7 +5,7 @@ use crate::{mutate_state, read_state, run_regular_jobs, RuntimeState, TimerJob}; use candid::Principal; use canister_timer_jobs::TimerJobs; use canister_tracing_macros::trace; -use chat_events::{PushMessageArgs, Reader}; +use chat_events::{MessageContentInternal, PushMessageArgs, Reader}; use ic_cdk_macros::update; use rand::Rng; use tracing::error; @@ -159,7 +159,11 @@ fn send_message_impl( thread_root_message_index: None, message_id: args.message_id, sender: my_user_id, - content: args.content.clone().into(), + content: if let Some(transfer) = completed_transfer.clone() { + MessageContentInternal::new_with_transfer(args.content.clone(), transfer) + } else { + args.content.clone().try_into().unwrap() + }, mentioned: Vec::new(), replies_to: args.replies_to.as_ref().map(|r| r.into()), forwarded: args.forwarding, @@ -312,7 +316,7 @@ async fn send_to_bot_canister(recipient: UserId, message_index: MessageIndex, ar sender: recipient, thread_root_message_index: None, message_id: message.message_id.unwrap_or_else(|| state.env.rng().gen()), - content: message.content.into(), + content: message.content.try_into().unwrap(), mentioned: Vec::new(), replies_to: None, forwarded: false, diff --git a/backend/canisters/user/impl/src/updates/send_message_with_transfer.rs b/backend/canisters/user/impl/src/updates/send_message_with_transfer.rs index 99dda6da66..7f9326ad0d 100644 --- a/backend/canisters/user/impl/src/updates/send_message_with_transfer.rs +++ b/backend/canisters/user/impl/src/updates/send_message_with_transfer.rs @@ -4,10 +4,7 @@ use crate::{read_state, run_regular_jobs, RuntimeState}; use canister_tracing_macros::trace; use chat_events::MessageContentInternal; use ic_cdk_macros::update; -use types::{ - CompletedCryptoTransaction, CryptoContent, CryptoTransaction, MessageContentInitial, PendingCryptoTransaction, - PrizeContentInitial, MAX_TEXT_LENGTH, MAX_TEXT_LENGTH_USIZE, -}; +use types::{CryptoTransaction, MessageContentInitial, PendingCryptoTransaction, MAX_TEXT_LENGTH, MAX_TEXT_LENGTH_USIZE}; use user_canister::send_message_with_transfer_to_channel; use user_canister::send_message_with_transfer_to_group; use utils::consts::{MEMO_MESSAGE, MEMO_PRIZE}; @@ -22,7 +19,7 @@ async fn send_message_with_transfer_to_channel( run_regular_jobs(); // Check that the user is a member of the community - if read_state(|state| state.data.communities.get(&args.community_id).is_none()) { + if read_state(|state| !state.data.communities.exists(&args.community_id)) { return UserNotInCommunity(None); } @@ -44,7 +41,7 @@ async fn send_message_with_transfer_to_channel( }; // Mutate the content so it now includes the completed transaction - let content = transform_content_with_completed_transaction(args.content, completed_transaction.clone()); + let content = MessageContentInternal::new_with_transfer(args.content, completed_transaction.clone()); // Build the send_message args let c2c_args = community_canister::c2c_send_message::Args { @@ -101,7 +98,7 @@ async fn send_message_with_transfer_to_group( run_regular_jobs(); // Check that the user is a member of the group - if read_state(|state| state.data.group_chats.get(&args.group_id).is_none()) { + if read_state(|state| !state.data.group_chats.exists(&args.group_id)) { return CallerNotInGroup(None); } @@ -123,7 +120,7 @@ async fn send_message_with_transfer_to_group( }; // Mutate the content so it now includes the completed transaction - let content = transform_content_with_completed_transaction(args.content, completed_transaction.clone()); + let content = MessageContentInternal::new_with_transfer(args.content, completed_transaction.clone()); // Build the send_message args let c2c_args = group_canister::c2c_send_message::Args { @@ -229,25 +226,3 @@ fn prepare(content: &MessageContentInitial, state: &RuntimeState) -> PrepareResu TransferCannotBeZero } } - -fn transform_content_with_completed_transaction( - content: MessageContentInitial, - completed_transaction: CompletedCryptoTransaction, -) -> MessageContentInternal { - // Mutate the content so it now includes the completed transaction - MessageContentInternal::from(match content { - MessageContentInitial::Crypto(c) => MessageContentInitial::Crypto(CryptoContent { - recipient: c.recipient, - transfer: CryptoTransaction::Completed(completed_transaction), - caption: c.caption, - }), - MessageContentInitial::Prize(c) => MessageContentInitial::Prize(PrizeContentInitial { - prizes: c.prizes, - transfer: CryptoTransaction::Completed(completed_transaction), - end_date: c.end_date, - caption: c.caption, - diamond_only: c.diamond_only, - }), - _ => unreachable!("Message must include a crypto transfer"), - }) -} diff --git a/backend/canisters/user/impl/src/updates/set_message_reminder.rs b/backend/canisters/user/impl/src/updates/set_message_reminder.rs index e3ea6a8b7e..8d58e1d652 100644 --- a/backend/canisters/user/impl/src/updates/set_message_reminder.rs +++ b/backend/canisters/user/impl/src/updates/set_message_reminder.rs @@ -2,9 +2,10 @@ use crate::guards::caller_is_owner; use crate::timer_job_types::{MessageReminderJob, TimerJob}; use crate::{mutate_state, openchat_bot, run_regular_jobs, RuntimeState}; use canister_tracing_macros::trace; +use chat_events::{MessageContentInternal, MessageReminderCreatedContentInternal}; use ic_cdk_macros::update; use rand::RngCore; -use types::{FieldTooLongResult, MessageContent, MessageReminderCreatedContent}; +use types::FieldTooLongResult; use user_canister::c2c_send_messages::C2CReplyContext; use user_canister::set_message_reminder_v2::{Response::*, *}; @@ -39,7 +40,7 @@ fn set_message_reminder_impl(args: Args, state: &mut RuntimeState) -> Response { let reminder_id = state.env.rng().next_u64(); let reminder_created_message_index = openchat_bot::send_message_with_reply( - MessageContent::MessageReminderCreated(MessageReminderCreatedContent { + MessageContentInternal::MessageReminderCreated(MessageReminderCreatedContentInternal { reminder_id, remind_at: args.remind_at, notes: args.notes.clone(), diff --git a/backend/canisters/user/impl/src/updates/tip_message.rs b/backend/canisters/user/impl/src/updates/tip_message.rs index 889affb146..0500db9894 100644 --- a/backend/canisters/user/impl/src/updates/tip_message.rs +++ b/backend/canisters/user/impl/src/updates/tip_message.rs @@ -92,7 +92,7 @@ fn prepare(args: &Args, state: &RuntimeState) -> Result<(PrepareResult, Timestam } else { let now_nanos = state.env.now_nanos(); match args.chat { - Chat::Direct(chat_id) if state.data.direct_chats.has(&chat_id) => Ok(( + Chat::Direct(chat_id) if state.data.direct_chats.exists(&chat_id) => Ok(( PrepareResult::Direct(TipMessageArgs { user_id: my_user_id, recipient: args.recipient, @@ -105,7 +105,7 @@ fn prepare(args: &Args, state: &RuntimeState) -> Result<(PrepareResult, Timestam }), now_nanos, )), - Chat::Group(group_id) if state.data.group_chats.has(&group_id) => Ok(( + Chat::Group(group_id) if state.data.group_chats.exists(&group_id) => Ok(( PrepareResult::Group( group_id, group_canister::c2c_tip_message::Args { @@ -122,7 +122,7 @@ fn prepare(args: &Args, state: &RuntimeState) -> Result<(PrepareResult, Timestam ), now_nanos, )), - Chat::Channel(community_id, channel_id) if state.data.communities.has(&community_id) => Ok(( + Chat::Channel(community_id, channel_id) if state.data.communities.exists(&community_id) => Ok(( PrepareResult::Channel( community_id, community_canister::c2c_tip_message::Args { diff --git a/backend/libraries/chat_events/src/chat_events.rs b/backend/libraries/chat_events/src/chat_events.rs index 40c7c10e5a..b1c2b72bdc 100644 --- a/backend/libraries/chat_events/src/chat_events.rs +++ b/backend/libraries/chat_events/src/chat_events.rs @@ -163,7 +163,7 @@ impl ChatEvents { ) { if message.sender == args.sender { if !matches!(message.content, MessageContentInternal::Deleted(_)) { - message.content = args.content.into(); + message.content = args.content.try_into().unwrap(); message.last_updated = Some(args.now); message.last_edited = Some(args.now); self.last_updated_timestamps diff --git a/backend/libraries/chat_events/src/message_content_internal.rs b/backend/libraries/chat_events/src/message_content_internal.rs index d5c77e02d8..0d2316d049 100644 --- a/backend/libraries/chat_events/src/message_content_internal.rs +++ b/backend/libraries/chat_events/src/message_content_internal.rs @@ -50,6 +50,18 @@ pub enum MessageContentInternal { } impl MessageContentInternal { + pub fn new_with_transfer(content: MessageContentInitial, transfer: CompletedCryptoTransaction) -> MessageContentInternal { + match content { + MessageContentInitial::Crypto(c) => MessageContentInternal::Crypto(CryptoContentInternal { + recipient: c.recipient, + transfer, + caption: c.caption, + }), + MessageContentInitial::Prize(c) => MessageContentInternal::Prize(PrizeContentInternal::new(c, transfer)), + _ => unreachable!("Message must include a crypto transfer"), + } + } + pub fn hydrate(&self, my_user_id: Option) -> MessageContent { match self { MessageContentInternal::Text(t) => MessageContent::Text(t.hydrate(my_user_id)), @@ -137,23 +149,28 @@ impl MessageContentInternal { } } -impl From for MessageContentInternal { - fn from(value: MessageContentInitial) -> Self { +impl TryFrom for MessageContentInternal { + type Error = (); + + fn try_from(value: MessageContentInitial) -> Result { match value { - MessageContentInitial::Text(t) => MessageContentInternal::Text(t.into()), - MessageContentInitial::Image(i) => MessageContentInternal::Image(i.into()), - MessageContentInitial::Video(v) => MessageContentInternal::Video(v.into()), - MessageContentInitial::Audio(a) => MessageContentInternal::Audio(a.into()), - MessageContentInitial::File(f) => MessageContentInternal::File(f.into()), - MessageContentInitial::Poll(p) => MessageContentInternal::Poll(p.into()), - MessageContentInitial::Crypto(c) => MessageContentInternal::Crypto(c.into()), - MessageContentInitial::Deleted(d) => MessageContentInternal::Deleted(d.into()), - MessageContentInitial::Giphy(g) => MessageContentInternal::Giphy(g.into()), - MessageContentInitial::GovernanceProposal(p) => MessageContentInternal::GovernanceProposal(p.into()), - MessageContentInitial::Prize(p) => MessageContentInternal::Prize(p.into()), - MessageContentInitial::MessageReminderCreated(r) => MessageContentInternal::MessageReminderCreated(r.into()), - MessageContentInitial::MessageReminder(r) => MessageContentInternal::MessageReminder(r.into()), - MessageContentInitial::Custom(c) => MessageContentInternal::Custom(c.into()), + MessageContentInitial::Text(t) => Ok(MessageContentInternal::Text(t.into())), + MessageContentInitial::Image(i) => Ok(MessageContentInternal::Image(i.into())), + MessageContentInitial::Video(v) => Ok(MessageContentInternal::Video(v.into())), + MessageContentInitial::Audio(a) => Ok(MessageContentInternal::Audio(a.into())), + MessageContentInitial::File(f) => Ok(MessageContentInternal::File(f.into())), + MessageContentInitial::Poll(p) => Ok(MessageContentInternal::Poll(p.into())), + MessageContentInitial::Deleted(d) => Ok(MessageContentInternal::Deleted(d.into())), + MessageContentInitial::Giphy(g) => Ok(MessageContentInternal::Giphy(g.into())), + MessageContentInitial::GovernanceProposal(p) => Ok(MessageContentInternal::GovernanceProposal(p.into())), + MessageContentInitial::MessageReminderCreated(r) => Ok(MessageContentInternal::MessageReminderCreated(r.into())), + MessageContentInitial::MessageReminder(r) => Ok(MessageContentInternal::MessageReminder(r.into())), + MessageContentInitial::Custom(c) => Ok(MessageContentInternal::Custom(c.into())), + MessageContentInitial::Crypto(c) => Ok(MessageContentInternal::Crypto(c.into())), + MessageContentInitial::Prize(_) => { + // These should be created via `new_with_transfer` + Err(()) + } } } } @@ -223,9 +240,8 @@ impl From<&MessageContentInternal> for Document { } } -pub(crate) trait MessageContentInternalSubtype: From { +pub(crate) trait MessageContentInternalSubtype { type ContentType; - type ContentTypeInitial; fn hydrate(&self, my_user_id: Option) -> Self::ContentType; } @@ -244,7 +260,6 @@ impl From for TextContentInternal { impl MessageContentInternalSubtype for TextContentInternal { type ContentType = TextContent; - type ContentTypeInitial = TextContent; fn hydrate(&self, _my_user_id: Option) -> Self::ContentType { TextContent { text: self.text.clone() } @@ -282,7 +297,6 @@ impl From for ImageContentInternal { impl MessageContentInternalSubtype for ImageContentInternal { type ContentType = ImageContent; - type ContentTypeInitial = ImageContent; fn hydrate(&self, _my_user_id: Option) -> Self::ContentType { ImageContent { @@ -340,7 +354,6 @@ impl From for VideoContentInternal { impl MessageContentInternalSubtype for VideoContentInternal { type ContentType = VideoContent; - type ContentTypeInitial = VideoContent; fn hydrate(&self, _my_user_id: Option) -> Self::ContentType { VideoContent { @@ -377,7 +390,6 @@ impl From for AudioContentInternal { impl MessageContentInternalSubtype for AudioContentInternal { type ContentType = AudioContent; - type ContentTypeInitial = AudioContent; fn hydrate(&self, _my_user_id: Option) -> Self::ContentType { AudioContent { @@ -416,7 +428,6 @@ impl From for FileContentInternal { impl MessageContentInternalSubtype for FileContentInternal { type ContentType = FileContent; - type ContentTypeInitial = FileContent; fn hydrate(&self, _my_user_id: Option) -> Self::ContentType { FileContent { @@ -451,7 +462,6 @@ impl From for PollContentInternal { impl MessageContentInternalSubtype for PollContentInternal { type ContentType = PollContent; - type ContentTypeInitial = PollContent; fn hydrate(&self, my_user_id: Option) -> Self::ContentType { let user_votes = if let Some(user_id) = my_user_id { @@ -557,7 +567,6 @@ impl From for CryptoContentInternal { impl MessageContentInternalSubtype for CryptoContentInternal { type ContentType = CryptoContent; - type ContentTypeInitial = CryptoContent; fn hydrate(&self, _my_user_id: Option) -> Self::ContentType { CryptoContent { @@ -627,7 +636,6 @@ impl From<&GiphyImageVariantInternal> for GiphyImageVariant { impl MessageContentInternalSubtype for GiphyContentInternal { type ContentType = GiphyContent; - type ContentTypeInitial = GiphyContent; fn hydrate(&self, _my_user_id: Option) -> Self::ContentType { GiphyContent { @@ -661,7 +669,6 @@ impl From for ProposalContentInternal { impl MessageContentInternalSubtype for ProposalContentInternal { type ContentType = ProposalContent; - type ContentTypeInitial = ProposalContent; fn hydrate(&self, my_user_id: Option) -> Self::ContentType { ProposalContent { @@ -691,6 +698,18 @@ pub struct PrizeContentInternal { } impl PrizeContentInternal { + pub fn new(content: PrizeContentInitial, transaction: CompletedCryptoTransaction) -> PrizeContentInternal { + PrizeContentInternal { + prizes_remaining: content.prizes, + reservations: HashSet::new(), + winners: HashSet::new(), + transaction, + end_date: content.end_date, + caption: content.caption, + diamond_only: content.diamond_only, + } + } + pub fn prize_refund(&self, sender: UserId, memo: &[u8], now_nanos: TimestampNanos) -> Option { let fee = self.transaction.fee(); let unclaimed = self.prizes_remaining.iter().map(|t| (t.e8s() as u128) + fee).sum::(); @@ -710,27 +729,8 @@ impl PrizeContentInternal { } } -impl From for PrizeContentInternal { - fn from(value: PrizeContentInitial) -> Self { - if let CryptoTransaction::Completed(transaction) = value.transfer { - PrizeContentInternal { - prizes_remaining: value.prizes, - reservations: HashSet::new(), - winners: HashSet::new(), - transaction, - end_date: value.end_date, - caption: value.caption, - diamond_only: value.diamond_only, - } - } else { - panic!("Unable to convert PrizeContentInitial to PrizeContentInternal"); - } - } -} - impl MessageContentInternalSubtype for PrizeContentInternal { type ContentType = PrizeContent; - type ContentTypeInitial = PrizeContentInitial; fn hydrate(&self, _my_user_id: Option) -> Self::ContentType { PrizeContent { @@ -767,7 +767,6 @@ impl From for PrizeWinnerContentInternal { impl MessageContentInternalSubtype for PrizeWinnerContentInternal { type ContentType = PrizeWinnerContent; - type ContentTypeInitial = PrizeWinnerContent; fn hydrate(&self, _my_user_id: Option) -> Self::ContentType { PrizeWinnerContent { @@ -803,7 +802,6 @@ impl From for MessageReminderCreatedContentIntern impl MessageContentInternalSubtype for MessageReminderCreatedContentInternal { type ContentType = MessageReminderCreatedContent; - type ContentTypeInitial = MessageReminderCreatedContent; fn hydrate(&self, _my_user_id: Option) -> Self::ContentType { MessageReminderCreatedContent { @@ -834,7 +832,6 @@ impl From for MessageReminderContentInternal { impl MessageContentInternalSubtype for MessageReminderContentInternal { type ContentType = MessageReminderContent; - type ContentTypeInitial = MessageReminderContent; fn hydrate(&self, _my_user_id: Option) -> Self::ContentType { MessageReminderContent { @@ -858,7 +855,6 @@ impl From for ReportedMessageInternal { impl MessageContentInternalSubtype for ReportedMessageInternal { type ContentType = ReportedMessage; - type ContentTypeInitial = ReportedMessage; fn hydrate(&self, _my_user_id: Option) -> Self::ContentType { ReportedMessage { @@ -887,7 +883,6 @@ impl From for CustomContentInternal { impl MessageContentInternalSubtype for CustomContentInternal { type ContentType = CustomContent; - type ContentTypeInitial = CustomContent; fn hydrate(&self, _my_user_id: Option) -> Self::ContentType { CustomContent { diff --git a/backend/libraries/group_chat_core/src/lib.rs b/backend/libraries/group_chat_core/src/lib.rs index 67fe6b6ba3..cff949aaac 100644 --- a/backend/libraries/group_chat_core/src/lib.rs +++ b/backend/libraries/group_chat_core/src/lib.rs @@ -9,14 +9,14 @@ use serde::{Deserialize, Serialize}; use std::cmp::{max, min}; use std::collections::{BTreeSet, HashSet}; use types::{ - AccessGate, AvatarChanged, ContentValidationError, CryptoTransaction, CustomPermission, Document, EventIndex, - EventOrExpiredRange, EventWrapper, EventsResponse, FieldTooLongResult, FieldTooShortResult, GroupDescriptionChanged, - GroupGateUpdated, GroupNameChanged, GroupPermissionRole, GroupPermissions, GroupReplyContext, GroupRole, GroupRulesChanged, - GroupSubtype, GroupVisibilityChanged, HydratedMention, InvalidPollReason, MemberLeft, MembersRemoved, Message, - MessageContent, MessageContentInitial, MessageId, MessageIndex, MessageMatch, MessagePermissions, MessagePinned, - MessageUnpinned, MessagesResponse, Milliseconds, OptionUpdate, OptionalGroupPermissions, OptionalMessagePermissions, - PermissionsChanged, PushEventResult, PushIfNotContains, Reaction, RoleChanged, Rules, SelectedGroupUpdates, ThreadPreview, - TimestampMillis, Timestamped, UpdatedRules, UserId, UsersBlocked, UsersInvited, Version, Versioned, VersionedRules, + AccessGate, AvatarChanged, ContentValidationError, CustomPermission, Document, EventIndex, EventOrExpiredRange, + EventWrapper, EventsResponse, FieldTooLongResult, FieldTooShortResult, GroupDescriptionChanged, GroupGateUpdated, + GroupNameChanged, GroupPermissionRole, GroupPermissions, GroupReplyContext, GroupRole, GroupRulesChanged, GroupSubtype, + GroupVisibilityChanged, HydratedMention, InvalidPollReason, MemberLeft, MembersRemoved, Message, MessageContent, + MessageContentInitial, MessageId, MessageIndex, MessageMatch, MessagePermissions, MessagePinned, MessageUnpinned, + MessagesResponse, Milliseconds, OptionUpdate, OptionalGroupPermissions, OptionalMessagePermissions, PermissionsChanged, + PushEventResult, PushIfNotContains, Reaction, RoleChanged, Rules, SelectedGroupUpdates, ThreadPreview, TimestampMillis, + Timestamped, UpdatedRules, UserId, UsersBlocked, UsersInvited, Version, Versioned, VersionedRules, }; use utils::document_validation::validate_avatar; use utils::text_validation::{ @@ -551,28 +551,22 @@ impl GroupChatCore { }; } - if let Some(transfer) = match &content { - MessageContentInitial::Crypto(c) => Some(&c.transfer), - MessageContentInitial::Prize(c) => Some(&c.transfer), - _ => None, - } { - if !matches!(transfer, CryptoTransaction::Completed(_)) { - return InvalidRequest("The crypto transaction must be completed".to_string()); - } + if let Ok(content_internal) = content.try_into() { + self.send_message( + sender, + thread_root_message_index, + message_id, + content_internal, + replies_to, + mentioned, + forwarding, + rules_accepted, + proposals_bot_user_id, + now, + ) + } else { + InvalidRequest("Invalid message content type".to_string()) } - - self.send_message( - sender, - thread_root_message_index, - message_id, - content.into(), - replies_to, - mentioned, - forwarding, - rules_accepted, - proposals_bot_user_id, - now, - ) } pub fn send_message( diff --git a/backend/libraries/group_chat_core/src/roles.rs b/backend/libraries/group_chat_core/src/roles.rs index a032ef3262..e7ab4acf69 100644 --- a/backend/libraries/group_chat_core/src/roles.rs +++ b/backend/libraries/group_chat_core/src/roles.rs @@ -110,7 +110,6 @@ impl GroupRoleInternal { MessageContentInternal::Poll(_) => ps.poll.unwrap_or(ps.default), MessageContentInternal::Crypto(_) => ps.crypto.unwrap_or(ps.default), MessageContentInternal::Giphy(_) => ps.giphy.unwrap_or(ps.default), - MessageContentInternal::GovernanceProposal(_) => GroupPermissionRole::Owner, MessageContentInternal::Prize(_) => ps.prize.unwrap_or(ps.default), MessageContentInternal::Custom(mc) => ps .custom @@ -119,9 +118,10 @@ impl GroupRoleInternal { .map(|cp| cp.role) .unwrap_or(ps.default), MessageContentInternal::Deleted(_) - | MessageContentInternal::PrizeWinner(_) + | MessageContentInternal::GovernanceProposal(_) | MessageContentInternal::MessageReminderCreated(_) | MessageContentInternal::MessageReminder(_) + | MessageContentInternal::PrizeWinner(_) | MessageContentInternal::ReportedMessage(_) => GroupPermissionRole::None, }; From bdc12d861ad6d3c09dc25db5e8a4d219bf91a3c0 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 1 Dec 2023 12:16:00 +0000 Subject: [PATCH 03/14] Update LocalGroupIndex post release (#4901) --- backend/canisters/local_group_index/CHANGELOG.md | 2 ++ canister_commit_ids.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/canisters/local_group_index/CHANGELOG.md b/backend/canisters/local_group_index/CHANGELOG.md index f907b23d8c..738ae9b941 100644 --- a/backend/canisters/local_group_index/CHANGELOG.md +++ b/backend/canisters/local_group_index/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +## [[2.0.954](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.954-local_group_index)] - 2023-12-01 + ### Changed - Add `local_user_index_canister_id` to group/community summaries ([#4857](https://github.com/open-chat-labs/open-chat/pull/4857)) diff --git a/canister_commit_ids.json b/canister_commit_ids.json index 58675c011d..fcb7abafab 100644 --- a/canister_commit_ids.json +++ b/canister_commit_ids.json @@ -3,7 +3,7 @@ "cycles_dispenser": "496fd2aa54d194c29a7ba014ee1b6d7d1a2f9a00", "group": "303772e423d400a4417b6e3141205d17eb5f7a23", "group_index": "f61321c254e82bb07646160b9ca0fc5dd0a759ea", - "local_group_index": "ce3033cea45ce8aa6647c3124d7b99b94caa00c4", + "local_group_index": "bfb8dc5b93c36c34f21e88b52c36ad5d30b68e09", "local_user_index": "303772e423d400a4417b6e3141205d17eb5f7a23", "market_maker": "9a6a38f86194b57dc8718c2a595ef4b00cf82d47", "neuron_controller": "70cd194427a9eb48972ae8924b6e8a6be74199f4", From 29d0ad8a6e0dd0c707902bb2bb0d1144b3c8bd94 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 1 Dec 2023 13:47:13 +0000 Subject: [PATCH 04/14] Introduce the Escrow canister for making P2P trades (#4903) --- Cargo.lock | 39 ++++++++++ Cargo.toml | 2 + backend/canister_installer/Cargo.toml | 1 + backend/canister_installer/src/lib.rs | 17 +++++ backend/canister_installer/src/main.rs | 4 + backend/canister_upgrader/Cargo.toml | 1 + backend/canister_upgrader/src/lib.rs | 19 +++++ backend/canister_upgrader/src/main.rs | 4 + backend/canisters/escrow/CHANGELOG.md | 6 ++ backend/canisters/escrow/api/Cargo.toml | 12 +++ backend/canisters/escrow/api/can.did | 2 + backend/canisters/escrow/api/src/lib.rs | 7 ++ .../escrow/api/src/lifecycle/init.rs | 10 +++ .../canisters/escrow/api/src/lifecycle/mod.rs | 2 + .../escrow/api/src/lifecycle/post_upgrade.rs | 8 ++ backend/canisters/escrow/api/src/main.rs | 5 ++ .../canisters/escrow/api/src/queries/mod.rs | 1 + .../canisters/escrow/api/src/updates/mod.rs | 1 + backend/canisters/escrow/impl/Cargo.toml | 33 +++++++++ backend/canisters/escrow/impl/src/jobs/mod.rs | 3 + backend/canisters/escrow/impl/src/lib.rs | 73 +++++++++++++++++++ .../escrow/impl/src/lifecycle/init.rs | 21 ++++++ .../escrow/impl/src/lifecycle/mod.rs | 40 ++++++++++ .../escrow/impl/src/lifecycle/post_upgrade.rs | 27 +++++++ .../escrow/impl/src/lifecycle/pre_upgrade.rs | 26 +++++++ backend/canisters/escrow/impl/src/memory.rs | 21 ++++++ .../escrow/impl/src/queries/http_request.rs | 26 +++++++ .../canisters/escrow/impl/src/queries/mod.rs | 1 + .../canisters/escrow/impl/src/updates/mod.rs | 1 + .../escrow/impl/src/updates/wallet_receive.rs | 9 +++ .../libraries/canister_agent_utils/src/lib.rs | 4 + canister_ids.json | 4 + scripts/deploy-local.sh | 1 + scripts/deploy-testnet.sh | 1 + scripts/deploy.sh | 2 + scripts/download-all-canister-wasms.sh | 1 + scripts/generate-all-canister-wasms.sh | 1 + scripts/upgrade-canister.sh | 2 + 38 files changed, 438 insertions(+) create mode 100644 backend/canisters/escrow/CHANGELOG.md create mode 100644 backend/canisters/escrow/api/Cargo.toml create mode 100644 backend/canisters/escrow/api/can.did create mode 100644 backend/canisters/escrow/api/src/lib.rs create mode 100644 backend/canisters/escrow/api/src/lifecycle/init.rs create mode 100644 backend/canisters/escrow/api/src/lifecycle/mod.rs create mode 100644 backend/canisters/escrow/api/src/lifecycle/post_upgrade.rs create mode 100644 backend/canisters/escrow/api/src/main.rs create mode 100644 backend/canisters/escrow/api/src/queries/mod.rs create mode 100644 backend/canisters/escrow/api/src/updates/mod.rs create mode 100644 backend/canisters/escrow/impl/Cargo.toml create mode 100644 backend/canisters/escrow/impl/src/jobs/mod.rs create mode 100644 backend/canisters/escrow/impl/src/lib.rs create mode 100644 backend/canisters/escrow/impl/src/lifecycle/init.rs create mode 100644 backend/canisters/escrow/impl/src/lifecycle/mod.rs create mode 100644 backend/canisters/escrow/impl/src/lifecycle/post_upgrade.rs create mode 100644 backend/canisters/escrow/impl/src/lifecycle/pre_upgrade.rs create mode 100644 backend/canisters/escrow/impl/src/memory.rs create mode 100644 backend/canisters/escrow/impl/src/queries/http_request.rs create mode 100644 backend/canisters/escrow/impl/src/queries/mod.rs create mode 100644 backend/canisters/escrow/impl/src/updates/mod.rs create mode 100644 backend/canisters/escrow/impl/src/updates/wallet_receive.rs diff --git a/Cargo.lock b/Cargo.lock index 52f62cd9f2..82ab2a9a36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1022,6 +1022,7 @@ dependencies = [ "canister_agent_utils", "clap", "cycles_dispenser_canister", + "escrow_canister", "futures", "group_index_canister", "group_index_canister_client", @@ -1101,6 +1102,7 @@ dependencies = [ "canister_agent_utils", "clap", "cycles_dispenser_canister", + "escrow_canister", "group_index_canister", "group_index_canister_client", "ic-agent", @@ -2014,6 +2016,43 @@ dependencies = [ "libc", ] +[[package]] +name = "escrow_canister" +version = "0.1.0" +dependencies = [ + "candid", + "candid_gen", + "serde", + "types", +] + +[[package]] +name = "escrow_canister_impl" +version = "0.1.0" +dependencies = [ + "candid", + "canister_api_macros", + "canister_logger", + "canister_state_macros", + "canister_tracing_macros", + "escrow_canister", + "http_request", + "ic-cdk 0.11.3", + "ic-cdk-macros 0.7.0", + "ic-cdk-timers", + "ic-stable-structures", + "icrc-ledger-types", + "icrc_ledger_canister_c2c_client", + "rand", + "serde", + "serde_bytes", + "serializer", + "stable_memory", + "tracing", + "types", + "utils", +] + [[package]] name = "event-listener" version = "2.5.3" diff --git a/Cargo.toml b/Cargo.toml index 7161c24422..31da2866fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,8 @@ members = [ "backend/canisters/community/impl", "backend/canisters/cycles_dispenser/api", "backend/canisters/cycles_dispenser/impl", + "backend/canisters/escrow/api", + "backend/canisters/escrow/impl", "backend/canisters/group/api", "backend/canisters/group/c2c_client", "backend/canisters/group/client", diff --git a/backend/canister_installer/Cargo.toml b/backend/canister_installer/Cargo.toml index c4cff7b606..8e01ea6dc6 100644 --- a/backend/canister_installer/Cargo.toml +++ b/backend/canister_installer/Cargo.toml @@ -10,6 +10,7 @@ candid = { workspace = true } canister_agent_utils = { path = "../libraries/canister_agent_utils" } clap = { workspace = true, features = ["derive"] } cycles_dispenser_canister = { path = "../canisters/cycles_dispenser/api" } +escrow_canister = { path = "../canisters/escrow/api" } futures = { workspace = true } group_index_canister = { path = "../canisters/group_index/api" } group_index_canister_client = { path = "../canisters/group_index/client" } diff --git a/backend/canister_installer/src/lib.rs b/backend/canister_installer/src/lib.rs index 087764eca5..42e3b78b87 100644 --- a/backend/canister_installer/src/lib.rs +++ b/backend/canister_installer/src/lib.rs @@ -32,6 +32,8 @@ async fn install_service_canisters_impl( set_controllers(management_canister, &canister_ids.cycles_dispenser, controllers.clone()), set_controllers(management_canister, &canister_ids.registry, controllers.clone()), set_controllers(management_canister, &canister_ids.market_maker, controllers.clone()), + set_controllers(management_canister, &canister_ids.neuron_controller, controllers.clone()), + set_controllers(management_canister, &canister_ids.escrow, controllers.clone()), set_controllers( management_canister, &canister_ids.local_user_index, @@ -185,6 +187,13 @@ async fn install_service_canisters_impl( test_mode, }; + let escrow_canister_wasm = get_canister_wasm(CanisterName::Escrow, version); + let escrow_init_args = escrow_canister::init::Args { + cycles_dispenser_canister_id: canister_ids.cycles_dispenser, + wasm_version: version, + test_mode, + }; + futures::future::join5( install_wasm( management_canister, @@ -253,6 +262,14 @@ async fn install_service_canisters_impl( ) .await; + install_wasm( + management_canister, + &canister_ids.escrow, + &escrow_canister_wasm.module, + escrow_init_args, + ) + .await; + let user_canister_wasm = get_canister_wasm(CanisterName::User, version); let group_canister_wasm = get_canister_wasm(CanisterName::Group, version); let community_canister_wasm = get_canister_wasm(CanisterName::Community, version); diff --git a/backend/canister_installer/src/main.rs b/backend/canister_installer/src/main.rs index f6d2b8e3dd..d06dab5e9b 100644 --- a/backend/canister_installer/src/main.rs +++ b/backend/canister_installer/src/main.rs @@ -21,6 +21,7 @@ async fn main() { registry: opts.registry, market_maker: opts.market_maker, neuron_controller: opts.neuron_controller, + escrow: opts.escrow, nns_root: opts.nns_root, nns_governance: opts.nns_governance, nns_internet_identity: opts.nns_internet_identity, @@ -85,6 +86,9 @@ struct Opts { #[arg(long)] neuron_controller: CanisterId, + #[arg(long)] + escrow: CanisterId, + #[arg(long)] nns_root: CanisterId, diff --git a/backend/canister_upgrader/Cargo.toml b/backend/canister_upgrader/Cargo.toml index 9b122b77a3..59674e9dfe 100644 --- a/backend/canister_upgrader/Cargo.toml +++ b/backend/canister_upgrader/Cargo.toml @@ -10,6 +10,7 @@ candid = { workspace = true } canister_agent_utils = { path = "../libraries/canister_agent_utils" } clap = { workspace = true, features = ["derive"] } cycles_dispenser_canister = { path = "../canisters/cycles_dispenser/api" } +escrow_canister = { path = "../canisters/escrow/api" } group_index_canister = { path = "../canisters/group_index/api" } group_index_canister_client = { path = "../canisters/group_index/client" } ic-agent = { workspace = true } diff --git a/backend/canister_upgrader/src/lib.rs b/backend/canister_upgrader/src/lib.rs index 5bc98cf35d..f29f70b973 100644 --- a/backend/canister_upgrader/src/lib.rs +++ b/backend/canister_upgrader/src/lib.rs @@ -197,6 +197,25 @@ pub async fn upgrade_neuron_controller_canister( println!("Neuron controller canister upgraded"); } +pub async fn upgrade_escrow_canister( + identity: Box, + url: String, + escrow_canister_id: CanisterId, + version: BuildVersion, +) { + upgrade_top_level_canister( + identity, + url, + escrow_canister_id, + version, + escrow_canister::post_upgrade::Args { wasm_version: version }, + CanisterName::Escrow, + ) + .await; + + println!("Escrow canister upgraded"); +} + pub async fn upgrade_local_group_index_canister( identity: Box, url: String, diff --git a/backend/canister_upgrader/src/main.rs b/backend/canister_upgrader/src/main.rs index 31b4cd5356..821772dd5d 100644 --- a/backend/canister_upgrader/src/main.rs +++ b/backend/canister_upgrader/src/main.rs @@ -14,6 +14,7 @@ async fn main() { CanisterName::CyclesDispenser => { upgrade_cycles_dispenser_canister(identity, opts.url, opts.cycles_dispenser, opts.version).await } + CanisterName::Escrow => upgrade_escrow_canister(identity, opts.url, opts.escrow, opts.version).await, CanisterName::Group => upgrade_group_canister(identity, opts.url, opts.group_index, opts.version).await, CanisterName::LocalGroupIndex => { upgrade_local_group_index_canister(identity, opts.url, opts.group_index, opts.version).await @@ -86,6 +87,9 @@ struct Opts { #[arg(long)] neuron_controller: CanisterId, + #[arg(long)] + escrow: CanisterId, + #[arg(long)] canister_to_upgrade: CanisterName, diff --git a/backend/canisters/escrow/CHANGELOG.md b/backend/canisters/escrow/CHANGELOG.md new file mode 100644 index 0000000000..8646827d5a --- /dev/null +++ b/backend/canisters/escrow/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [unreleased] diff --git a/backend/canisters/escrow/api/Cargo.toml b/backend/canisters/escrow/api/Cargo.toml new file mode 100644 index 0000000000..58e4686af1 --- /dev/null +++ b/backend/canisters/escrow/api/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "escrow_canister" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +candid = { workspace = true } +candid_gen = { path = "../../../libraries/candid_gen" } +serde = { workspace = true } +types = { path = "../../../libraries/types" } diff --git a/backend/canisters/escrow/api/can.did b/backend/canisters/escrow/api/can.did new file mode 100644 index 0000000000..c2a36388d1 --- /dev/null +++ b/backend/canisters/escrow/api/can.did @@ -0,0 +1,2 @@ +service : { +} \ No newline at end of file diff --git a/backend/canisters/escrow/api/src/lib.rs b/backend/canisters/escrow/api/src/lib.rs new file mode 100644 index 0000000000..048db36a9f --- /dev/null +++ b/backend/canisters/escrow/api/src/lib.rs @@ -0,0 +1,7 @@ +mod lifecycle; +mod queries; +mod updates; + +pub use lifecycle::*; +pub use queries::*; +pub use updates::*; diff --git a/backend/canisters/escrow/api/src/lifecycle/init.rs b/backend/canisters/escrow/api/src/lifecycle/init.rs new file mode 100644 index 0000000000..bf6aebaba6 --- /dev/null +++ b/backend/canisters/escrow/api/src/lifecycle/init.rs @@ -0,0 +1,10 @@ +use candid::CandidType; +use serde::{Deserialize, Serialize}; +use types::{BuildVersion, CanisterId}; + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Args { + pub cycles_dispenser_canister_id: CanisterId, + pub wasm_version: BuildVersion, + pub test_mode: bool, +} diff --git a/backend/canisters/escrow/api/src/lifecycle/mod.rs b/backend/canisters/escrow/api/src/lifecycle/mod.rs new file mode 100644 index 0000000000..70bd4f5a23 --- /dev/null +++ b/backend/canisters/escrow/api/src/lifecycle/mod.rs @@ -0,0 +1,2 @@ +pub mod init; +pub mod post_upgrade; diff --git a/backend/canisters/escrow/api/src/lifecycle/post_upgrade.rs b/backend/canisters/escrow/api/src/lifecycle/post_upgrade.rs new file mode 100644 index 0000000000..470a25ac40 --- /dev/null +++ b/backend/canisters/escrow/api/src/lifecycle/post_upgrade.rs @@ -0,0 +1,8 @@ +use candid::CandidType; +use serde::{Deserialize, Serialize}; +use types::BuildVersion; + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub struct Args { + pub wasm_version: BuildVersion, +} diff --git a/backend/canisters/escrow/api/src/main.rs b/backend/canisters/escrow/api/src/main.rs new file mode 100644 index 0000000000..37e8c25054 --- /dev/null +++ b/backend/canisters/escrow/api/src/main.rs @@ -0,0 +1,5 @@ +#[allow(deprecated)] +fn main() { + candid::export_service!(); + std::print!("{}", __export_service()); +} diff --git a/backend/canisters/escrow/api/src/queries/mod.rs b/backend/canisters/escrow/api/src/queries/mod.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/backend/canisters/escrow/api/src/queries/mod.rs @@ -0,0 +1 @@ + diff --git a/backend/canisters/escrow/api/src/updates/mod.rs b/backend/canisters/escrow/api/src/updates/mod.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/backend/canisters/escrow/api/src/updates/mod.rs @@ -0,0 +1 @@ + diff --git a/backend/canisters/escrow/impl/Cargo.toml b/backend/canisters/escrow/impl/Cargo.toml new file mode 100644 index 0000000000..825d529e9c --- /dev/null +++ b/backend/canisters/escrow/impl/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "escrow_canister_impl" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +candid = { workspace = true } +canister_api_macros = { path = "../../../libraries/canister_api_macros" } +canister_logger = { path = "../../../libraries/canister_logger" } +canister_state_macros = { path = "../../../libraries/canister_state_macros" } +canister_tracing_macros = { path = "../../../libraries/canister_tracing_macros" } +escrow_canister = { path = "../api" } +http_request = { path = "../../../libraries/http_request" } +ic-cdk = { workspace = true } +ic-cdk-macros = { workspace = true } +ic-cdk-timers = { workspace = true } +ic-stable-structures = { workspace = true } +icrc_ledger_canister_c2c_client = { path = "../../../external_canisters/icrc_ledger/c2c_client" } +icrc-ledger-types = { workspace = true } +rand = { workspace = true } +serde = { workspace = true } +serde_bytes = { workspace = true } +serializer = { path = "../../../libraries/serializer" } +stable_memory = { path = "../../../libraries/stable_memory" } +tracing = { workspace = true } +types = { path = "../../../libraries/types" } +utils = { path = "../../../libraries/utils" } diff --git a/backend/canisters/escrow/impl/src/jobs/mod.rs b/backend/canisters/escrow/impl/src/jobs/mod.rs new file mode 100644 index 0000000000..01f9150d4a --- /dev/null +++ b/backend/canisters/escrow/impl/src/jobs/mod.rs @@ -0,0 +1,3 @@ +use crate::RuntimeState; + +pub(crate) fn start(_state: &RuntimeState) {} diff --git a/backend/canisters/escrow/impl/src/lib.rs b/backend/canisters/escrow/impl/src/lib.rs new file mode 100644 index 0000000000..327c04b02a --- /dev/null +++ b/backend/canisters/escrow/impl/src/lib.rs @@ -0,0 +1,73 @@ +use canister_state_macros::canister_state; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use types::{BuildVersion, CanisterId, Cycles, TimestampMillis, Timestamped}; +use utils::env::Environment; + +mod jobs; +mod lifecycle; +mod memory; +mod queries; +mod updates; + +thread_local! { + static WASM_VERSION: RefCell> = RefCell::default(); +} + +canister_state!(RuntimeState); + +struct RuntimeState { + pub env: Box, + pub data: Data, +} + +impl RuntimeState { + pub fn new(env: Box, data: Data) -> RuntimeState { + RuntimeState { env, data } + } + + pub fn metrics(&self) -> Metrics { + Metrics { + memory_used: utils::memory::used(), + now: self.env.now(), + cycles_balance: self.env.cycles_balance(), + wasm_version: WASM_VERSION.with_borrow(|v| **v), + git_commit_id: utils::git::git_commit_id().to_string(), + canister_ids: CanisterIds { + cycles_dispenser: self.data.cycles_dispenser_canister_id, + }, + } + } +} + +#[derive(Serialize, Deserialize)] +struct Data { + pub cycles_dispenser_canister_id: CanisterId, + pub rng_seed: [u8; 32], + pub test_mode: bool, +} + +impl Data { + pub fn new(cycles_dispenser_canister_id: CanisterId, test_mode: bool) -> Data { + Data { + cycles_dispenser_canister_id, + rng_seed: [0; 32], + test_mode, + } + } +} + +#[derive(Serialize, Debug)] +pub struct Metrics { + pub now: TimestampMillis, + pub memory_used: u64, + pub cycles_balance: Cycles, + pub wasm_version: BuildVersion, + pub git_commit_id: String, + pub canister_ids: CanisterIds, +} + +#[derive(Serialize, Debug)] +pub struct CanisterIds { + pub cycles_dispenser: CanisterId, +} diff --git a/backend/canisters/escrow/impl/src/lifecycle/init.rs b/backend/canisters/escrow/impl/src/lifecycle/init.rs new file mode 100644 index 0000000000..3e24702aa2 --- /dev/null +++ b/backend/canisters/escrow/impl/src/lifecycle/init.rs @@ -0,0 +1,21 @@ +use crate::lifecycle::{init_env, init_state}; +use crate::Data; +use canister_tracing_macros::trace; +use escrow_canister::init::Args; +use ic_cdk_macros::init; +use tracing::info; +use utils::cycles::init_cycles_dispenser_client; + +#[init] +#[trace] +fn init(args: Args) { + canister_logger::init(args.test_mode); + init_cycles_dispenser_client(args.cycles_dispenser_canister_id, args.test_mode); + + let env = init_env([0; 32]); + let data = Data::new(args.cycles_dispenser_canister_id, args.test_mode); + + init_state(env, data, args.wasm_version); + + info!(version = %args.wasm_version, "Initialization complete"); +} diff --git a/backend/canisters/escrow/impl/src/lifecycle/mod.rs b/backend/canisters/escrow/impl/src/lifecycle/mod.rs new file mode 100644 index 0000000000..7a08079081 --- /dev/null +++ b/backend/canisters/escrow/impl/src/lifecycle/mod.rs @@ -0,0 +1,40 @@ +use crate::{mutate_state, Data, RuntimeState, WASM_VERSION}; +use std::time::Duration; +use tracing::trace; +use types::{BuildVersion, Timestamped}; +use utils::canister::get_random_seed; +use utils::env::canister::CanisterEnv; +use utils::env::Environment; + +mod init; +mod post_upgrade; +mod pre_upgrade; + +fn init_env(rng_seed: [u8; 32]) -> Box { + if rng_seed == [0; 32] { + ic_cdk_timers::set_timer(Duration::ZERO, reseed_rng); + } + Box::new(CanisterEnv::new(rng_seed)) +} + +fn init_state(env: Box, data: Data, wasm_version: BuildVersion) { + let now = env.now(); + let state = RuntimeState::new(env, data); + + crate::jobs::start(&state); + crate::init_state(state); + WASM_VERSION.set(Timestamped::new(wasm_version, now)); +} + +fn reseed_rng() { + ic_cdk::spawn(reseed_rng_inner()); + + async fn reseed_rng_inner() { + let seed = get_random_seed().await; + mutate_state(|state| { + state.data.rng_seed = seed; + state.env = Box::new(CanisterEnv::new(seed)) + }); + trace!("Successfully reseeded rng"); + } +} diff --git a/backend/canisters/escrow/impl/src/lifecycle/post_upgrade.rs b/backend/canisters/escrow/impl/src/lifecycle/post_upgrade.rs new file mode 100644 index 0000000000..323d1e14ea --- /dev/null +++ b/backend/canisters/escrow/impl/src/lifecycle/post_upgrade.rs @@ -0,0 +1,27 @@ +use crate::lifecycle::{init_env, init_state}; +use crate::memory::get_upgrades_memory; +use crate::Data; +use canister_logger::LogEntry; +use canister_tracing_macros::trace; +use escrow_canister::post_upgrade::Args; +use ic_cdk_macros::post_upgrade; +use stable_memory::get_reader; +use tracing::info; +use utils::cycles::init_cycles_dispenser_client; + +#[post_upgrade] +#[trace] +fn post_upgrade(args: Args) { + let memory = get_upgrades_memory(); + let reader = get_reader(&memory); + + let (data, logs, traces): (Data, Vec, Vec) = serializer::deserialize(reader).unwrap(); + + canister_logger::init_with_logs(data.test_mode, logs, traces); + + let env = init_env(data.rng_seed); + init_cycles_dispenser_client(data.cycles_dispenser_canister_id, data.test_mode); + init_state(env, data, args.wasm_version); + + info!(version = %args.wasm_version, "Post-upgrade complete"); +} diff --git a/backend/canisters/escrow/impl/src/lifecycle/pre_upgrade.rs b/backend/canisters/escrow/impl/src/lifecycle/pre_upgrade.rs new file mode 100644 index 0000000000..220192443f --- /dev/null +++ b/backend/canisters/escrow/impl/src/lifecycle/pre_upgrade.rs @@ -0,0 +1,26 @@ +use crate::memory::get_upgrades_memory; +use crate::take_state; +use canister_tracing_macros::trace; +use ic_cdk_macros::pre_upgrade; +use rand::Rng; +use stable_memory::get_writer; +use tracing::info; + +#[pre_upgrade] +#[trace] +fn pre_upgrade() { + info!("Pre-upgrade starting"); + + let mut state = take_state(); + state.data.rng_seed = state.env.rng().gen(); + + let logs = canister_logger::export_logs(); + let traces = canister_logger::export_traces(); + + let stable_state = (state.data, logs, traces); + + let mut memory = get_upgrades_memory(); + let writer = get_writer(&mut memory); + + serializer::serialize(stable_state, writer).unwrap(); +} diff --git a/backend/canisters/escrow/impl/src/memory.rs b/backend/canisters/escrow/impl/src/memory.rs new file mode 100644 index 0000000000..3c92f33419 --- /dev/null +++ b/backend/canisters/escrow/impl/src/memory.rs @@ -0,0 +1,21 @@ +use ic_stable_structures::{ + memory_manager::{MemoryId, MemoryManager, VirtualMemory}, + DefaultMemoryImpl, +}; + +const UPGRADES: MemoryId = MemoryId::new(0); + +pub type Memory = VirtualMemory; + +thread_local! { + static MEMORY_MANAGER: MemoryManager + = MemoryManager::init_with_bucket_size(DefaultMemoryImpl::default(), 1); +} + +pub fn get_upgrades_memory() -> Memory { + get_memory(UPGRADES) +} + +fn get_memory(id: MemoryId) -> Memory { + MEMORY_MANAGER.with(|m| m.get(id)) +} diff --git a/backend/canisters/escrow/impl/src/queries/http_request.rs b/backend/canisters/escrow/impl/src/queries/http_request.rs new file mode 100644 index 0000000000..fea6d8d402 --- /dev/null +++ b/backend/canisters/escrow/impl/src/queries/http_request.rs @@ -0,0 +1,26 @@ +use crate::{read_state, RuntimeState}; +use http_request::{build_json_response, encode_logs, extract_route, Route}; +use ic_cdk_macros::query; +use types::{HttpRequest, HttpResponse, TimestampMillis}; + +#[query] +fn http_request(request: HttpRequest) -> HttpResponse { + fn get_logs_impl(since: Option) -> HttpResponse { + encode_logs(canister_logger::export_logs(), since.unwrap_or(0)) + } + + fn get_traces_impl(since: Option) -> HttpResponse { + encode_logs(canister_logger::export_traces(), since.unwrap_or(0)) + } + + fn get_metrics_impl(state: &RuntimeState) -> HttpResponse { + build_json_response(&state.metrics()) + } + + match extract_route(&request.url) { + Route::Logs(since) => get_logs_impl(since), + Route::Traces(since) => get_traces_impl(since), + Route::Metrics => read_state(get_metrics_impl), + _ => HttpResponse::not_found(), + } +} diff --git a/backend/canisters/escrow/impl/src/queries/mod.rs b/backend/canisters/escrow/impl/src/queries/mod.rs new file mode 100644 index 0000000000..1cfa1ad736 --- /dev/null +++ b/backend/canisters/escrow/impl/src/queries/mod.rs @@ -0,0 +1 @@ +mod http_request; diff --git a/backend/canisters/escrow/impl/src/updates/mod.rs b/backend/canisters/escrow/impl/src/updates/mod.rs new file mode 100644 index 0000000000..f77132607f --- /dev/null +++ b/backend/canisters/escrow/impl/src/updates/mod.rs @@ -0,0 +1 @@ +mod wallet_receive; diff --git a/backend/canisters/escrow/impl/src/updates/wallet_receive.rs b/backend/canisters/escrow/impl/src/updates/wallet_receive.rs new file mode 100644 index 0000000000..d08987204c --- /dev/null +++ b/backend/canisters/escrow/impl/src/updates/wallet_receive.rs @@ -0,0 +1,9 @@ +use canister_tracing_macros::trace; +use ic_cdk_macros::update; +use utils::cycles::accept_cycles; + +#[update] +#[trace] +fn wallet_receive() { + accept_cycles(); +} diff --git a/backend/libraries/canister_agent_utils/src/lib.rs b/backend/libraries/canister_agent_utils/src/lib.rs index b21ccdeb72..d60ba22c8f 100644 --- a/backend/libraries/canister_agent_utils/src/lib.rs +++ b/backend/libraries/canister_agent_utils/src/lib.rs @@ -14,6 +14,7 @@ use types::{BuildVersion, CanisterId, CanisterWasm}; pub enum CanisterName { Community, CyclesDispenser, + Escrow, Group, GroupIndex, LocalGroupIndex, @@ -38,6 +39,7 @@ impl FromStr for CanisterName { match s { "community" => Ok(CanisterName::Community), "cycles_dispenser" => Ok(CanisterName::CyclesDispenser), + "escrow" => Ok(CanisterName::Escrow), "group" => Ok(CanisterName::Group), "group_index" => Ok(CanisterName::GroupIndex), "local_group_index" => Ok(CanisterName::LocalGroupIndex), @@ -63,6 +65,7 @@ impl Display for CanisterName { let name = match self { CanisterName::Community => "community", CanisterName::CyclesDispenser => "cycles_dispenser", + CanisterName::Escrow => "escrow", CanisterName::Group => "group", CanisterName::GroupIndex => "group_index", CanisterName::LocalGroupIndex => "local_group_index", @@ -99,6 +102,7 @@ pub struct CanisterIds { pub registry: CanisterId, pub market_maker: CanisterId, pub neuron_controller: CanisterId, + pub escrow: CanisterId, pub nns_root: CanisterId, pub nns_governance: CanisterId, pub nns_internet_identity: CanisterId, diff --git a/canister_ids.json b/canister_ids.json index a2a2cce8c3..a437d5ee22 100644 --- a/canister_ids.json +++ b/canister_ids.json @@ -8,6 +8,10 @@ "ic": "gonut-hqaaa-aaaaf-aby7a-cai", "ic_test": "mq2tp-baaaa-aaaaf-aucva-cai" }, + "escrow": { + "ic": "s4yi7-yiaaa-aaaar-qacpq-cai", + "ic_test": "tspqt-xaaaa-aaaal-qcnna-cai" + }, "group_index": { "ic": "4ijyc-kiaaa-aaaaf-aaaja-cai", "ic_test": "7kifq-3yaaa-aaaaf-ab2cq-cai" diff --git a/scripts/deploy-local.sh b/scripts/deploy-local.sh index 0f3a6f8566..a37186997f 100755 --- a/scripts/deploy-local.sh +++ b/scripts/deploy-local.sh @@ -35,6 +35,7 @@ dfx --identity $IDENTITY canister create --no-wallet --with-cycles 1000000000000 dfx --identity $IDENTITY canister create --no-wallet --with-cycles 100000000000000 registry dfx --identity $IDENTITY canister create --no-wallet --with-cycles 100000000000000 market_maker dfx --identity $IDENTITY canister create --no-wallet --with-cycles 100000000000000 neuron_controller +dfx --identity $IDENTITY canister create --no-wallet --with-cycles 100000000000000 escrow # Install the OpenChat canisters ./scripts/deploy.sh local \ diff --git a/scripts/deploy-testnet.sh b/scripts/deploy-testnet.sh index c3e2473417..63184a9ca3 100755 --- a/scripts/deploy-testnet.sh +++ b/scripts/deploy-testnet.sh @@ -31,6 +31,7 @@ dfx --identity $IDENTITY canister create --provisional-create-canister-effective dfx --identity $IDENTITY canister create --provisional-create-canister-effective-canister-id jrlun-jiaaa-aaaab-aaaaa-cai --network $NETWORK --no-wallet --with-cycles 100000000000000 registry dfx --identity $IDENTITY canister create --provisional-create-canister-effective-canister-id jrlun-jiaaa-aaaab-aaaaa-cai --network $NETWORK --no-wallet --with-cycles 100000000000000 market_maker dfx --identity $IDENTITY canister create --provisional-create-canister-effective-canister-id jrlun-jiaaa-aaaab-aaaaa-cai --network $NETWORK --no-wallet --with-cycles 100000000000000 neuron_controller +dfx --identity $IDENTITY canister create --provisional-create-canister-effective-canister-id jrlun-jiaaa-aaaab-aaaaa-cai --network $NETWORK --no-wallet --with-cycles 100000000000000 escrow # Install the OpenChat canisters ./scripts/deploy.sh $NETWORK \ diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 99f069dbe9..501326db89 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -41,6 +41,7 @@ CYCLES_DISPENSER_CANISTER_ID=$(dfx canister --network $NETWORK id cycles_dispens REGISTRY_CANISTER_ID=$(dfx canister --network $NETWORK id registry) MARKET_MAKER_CANISTER_ID=$(dfx canister --network $NETWORK id market_maker) NEURON_CONTROLLER_CANISTER_ID=$(dfx canister --network $NETWORK id neuron_controller) +ESCROW_CANISTER_ID=$(dfx canister --network $NETWORK id escrow) cargo run \ --manifest-path backend/canister_installer/Cargo.toml -- \ @@ -60,6 +61,7 @@ cargo run \ --registry $REGISTRY_CANISTER_ID \ --market-maker $MARKET_MAKER_CANISTER_ID \ --neuron-controller $NEURON_CONTROLLER_CANISTER_ID \ + --escrow $ESCROW_CANISTER_ID \ --nns-root $NNS_ROOT_CANISTER_ID \ --nns-governance $NNS_GOVERNANCE_CANISTER_ID \ --nns-internet-identity $NNS_INTERNET_IDENTITY_CANISTER_ID \ diff --git a/scripts/download-all-canister-wasms.sh b/scripts/download-all-canister-wasms.sh index 3c4265f149..43bd0f0d71 100755 --- a/scripts/download-all-canister-wasms.sh +++ b/scripts/download-all-canister-wasms.sh @@ -15,6 +15,7 @@ echo "Downloading wasms" ./download-canister-wasm.sh community $WASM_SRC || exit 1 ./download-canister-wasm.sh cycles_dispenser $WASM_SRC || exit 1 +./download-canister-wasm.sh escrow $WASM_SRC || exit 1 ./download-canister-wasm.sh group $WASM_SRC || exit 1 ./download-canister-wasm.sh group_index $WASM_SRC || exit 1 ./download-canister-wasm.sh local_group_index $WASM_SRC || exit 1 diff --git a/scripts/generate-all-canister-wasms.sh b/scripts/generate-all-canister-wasms.sh index 79bb663c3e..6ada30eb40 100755 --- a/scripts/generate-all-canister-wasms.sh +++ b/scripts/generate-all-canister-wasms.sh @@ -6,6 +6,7 @@ cd $SCRIPT_DIR/.. ./scripts/generate-wasm.sh community ./scripts/generate-wasm.sh cycles_dispenser +./scripts/generate-wasm.sh escrow ./scripts/generate-wasm.sh group ./scripts/generate-wasm.sh group_index ./scripts/generate-wasm.sh local_group_index diff --git a/scripts/upgrade-canister.sh b/scripts/upgrade-canister.sh index 5758fd37a2..c061dbd227 100755 --- a/scripts/upgrade-canister.sh +++ b/scripts/upgrade-canister.sh @@ -32,6 +32,7 @@ CYCLES_DISPENSER_CANISTER_ID=$(dfx canister --network $NETWORK id cycles_dispens REGISTRY_CANISTER_ID=$(dfx canister --network $NETWORK id registry) MARKET_MAKER_CANISTER_ID=$(dfx canister --network $NETWORK id market_maker) NEURON_CONTROLLER_CANISTER_ID=$(dfx canister --network $NETWORK id neuron_controller) +ESCROW_CANISTER_ID=$(dfx canister --network $NETWORK id escrow) cargo run \ --manifest-path backend/canister_upgrader/Cargo.toml -- \ @@ -47,5 +48,6 @@ cargo run \ --registry $REGISTRY_CANISTER_ID \ --market-maker $MARKET_MAKER_CANISTER_ID \ --neuron-controller $NEURON_CONTROLLER_CANISTER_ID \ + --escrow $ESCROW_CANISTER_ID \ --canister-to-upgrade $CANISTER_NAME \ --version $VERSION \ \ No newline at end of file From 45a9a92fc8d7915fd78f749a3dfd872a3871dbc4 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 1 Dec 2023 17:22:10 +0000 Subject: [PATCH 05/14] Implement `create_offer` and `notify_deposit` (#4904) --- Cargo.lock | 2 + backend/canisters/escrow/CHANGELOG.md | 5 + backend/canisters/escrow/api/Cargo.toml | 1 + backend/canisters/escrow/api/src/lib.rs | 13 +++ .../escrow/api/src/updates/create_offer.rs | 22 +++++ .../canisters/escrow/api/src/updates/mod.rs | 3 +- .../escrow/api/src/updates/notify_deposit.rs | 24 +++++ backend/canisters/escrow/impl/Cargo.toml | 1 + backend/canisters/escrow/impl/src/lib.rs | 4 + .../canisters/escrow/impl/src/model/mod.rs | 1 + .../canisters/escrow/impl/src/model/offers.rs | 57 +++++++++++ .../escrow/impl/src/updates/create_offer.rs | 37 +++++++ .../canisters/escrow/impl/src/updates/mod.rs | 4 +- .../escrow/impl/src/updates/notify_deposit.rs | 98 +++++++++++++++++++ 14 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 backend/canisters/escrow/api/src/updates/create_offer.rs create mode 100644 backend/canisters/escrow/api/src/updates/notify_deposit.rs create mode 100644 backend/canisters/escrow/impl/src/model/mod.rs create mode 100644 backend/canisters/escrow/impl/src/model/offers.rs create mode 100644 backend/canisters/escrow/impl/src/updates/create_offer.rs create mode 100644 backend/canisters/escrow/impl/src/updates/notify_deposit.rs diff --git a/Cargo.lock b/Cargo.lock index 82ab2a9a36..e83eece733 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2022,6 +2022,7 @@ version = "0.1.0" dependencies = [ "candid", "candid_gen", + "icrc-ledger-types", "serde", "types", ] @@ -2043,6 +2044,7 @@ dependencies = [ "ic-stable-structures", "icrc-ledger-types", "icrc_ledger_canister_c2c_client", + "msgpack", "rand", "serde", "serde_bytes", diff --git a/backend/canisters/escrow/CHANGELOG.md b/backend/canisters/escrow/CHANGELOG.md index 8646827d5a..bf133421b9 100644 --- a/backend/canisters/escrow/CHANGELOG.md +++ b/backend/canisters/escrow/CHANGELOG.md @@ -4,3 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] + +### Added + +- Introduce the Escrow canister for supporting P2P trades ([#4903](https://github.com/open-chat-labs/open-chat/pull/4903)) +- Implement `create_offer` and `notify_deposit` ([#4904](https://github.com/open-chat-labs/open-chat/pull/4904)) diff --git a/backend/canisters/escrow/api/Cargo.toml b/backend/canisters/escrow/api/Cargo.toml index 58e4686af1..9b670b179b 100644 --- a/backend/canisters/escrow/api/Cargo.toml +++ b/backend/canisters/escrow/api/Cargo.toml @@ -8,5 +8,6 @@ edition = "2021" [dependencies] candid = { workspace = true } candid_gen = { path = "../../../libraries/candid_gen" } +icrc-ledger-types = { workspace = true } serde = { workspace = true } types = { path = "../../../libraries/types" } diff --git a/backend/canisters/escrow/api/src/lib.rs b/backend/canisters/escrow/api/src/lib.rs index 048db36a9f..017aede912 100644 --- a/backend/canisters/escrow/api/src/lib.rs +++ b/backend/canisters/escrow/api/src/lib.rs @@ -1,3 +1,7 @@ +use candid::Principal; +use icrc_ledger_types::icrc1::account::Subaccount; +use types::UserId; + mod lifecycle; mod queries; mod updates; @@ -5,3 +9,12 @@ mod updates; pub use lifecycle::*; pub use queries::*; pub use updates::*; + +pub fn deposit_subaccount(user_id: UserId, offer_id: u32) -> Subaccount { + let mut subaccount = [0; 32]; + let principal = Principal::from(user_id); + let user_id_bytes = principal.as_slice(); + subaccount[..user_id_bytes.len()].copy_from_slice(user_id_bytes); + subaccount[28..].copy_from_slice(&offer_id.to_be_bytes()); + subaccount +} diff --git a/backend/canisters/escrow/api/src/updates/create_offer.rs b/backend/canisters/escrow/api/src/updates/create_offer.rs new file mode 100644 index 0000000000..46d8673d1b --- /dev/null +++ b/backend/canisters/escrow/api/src/updates/create_offer.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; +use types::{TimestampMillis, TokenInfo}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Args { + pub input_token: TokenInfo, + pub input_amount: u128, + pub output_token: TokenInfo, + pub output_amount: u128, + pub expires_at: TimestampMillis, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum Response { + Success(SuccessResult), + InvalidOffer(String), +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SuccessResult { + pub id: u32, +} diff --git a/backend/canisters/escrow/api/src/updates/mod.rs b/backend/canisters/escrow/api/src/updates/mod.rs index 8b13789179..dff0cb6200 100644 --- a/backend/canisters/escrow/api/src/updates/mod.rs +++ b/backend/canisters/escrow/api/src/updates/mod.rs @@ -1 +1,2 @@ - +pub mod create_offer; +pub mod notify_deposit; diff --git a/backend/canisters/escrow/api/src/updates/notify_deposit.rs b/backend/canisters/escrow/api/src/updates/notify_deposit.rs new file mode 100644 index 0000000000..e559888345 --- /dev/null +++ b/backend/canisters/escrow/api/src/updates/notify_deposit.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; +use types::UserId; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Args { + pub offer_id: u32, + pub user_id: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum Response { + Success, + BalanceTooLow(BalanceTooLowResult), + OfferAlreadyAccepted, + OfferExpired, + OfferNotFound, + InternalError(String), +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct BalanceTooLowResult { + pub balance: u128, + pub balance_required: u128, +} diff --git a/backend/canisters/escrow/impl/Cargo.toml b/backend/canisters/escrow/impl/Cargo.toml index 825d529e9c..e3b9e7ed85 100644 --- a/backend/canisters/escrow/impl/Cargo.toml +++ b/backend/canisters/escrow/impl/Cargo.toml @@ -23,6 +23,7 @@ ic-cdk-timers = { workspace = true } ic-stable-structures = { workspace = true } icrc_ledger_canister_c2c_client = { path = "../../../external_canisters/icrc_ledger/c2c_client" } icrc-ledger-types = { workspace = true } +msgpack = { path = "../../../libraries/msgpack" } rand = { workspace = true } serde = { workspace = true } serde_bytes = { workspace = true } diff --git a/backend/canisters/escrow/impl/src/lib.rs b/backend/canisters/escrow/impl/src/lib.rs index 327c04b02a..9211986682 100644 --- a/backend/canisters/escrow/impl/src/lib.rs +++ b/backend/canisters/escrow/impl/src/lib.rs @@ -1,3 +1,4 @@ +use crate::model::offers::Offers; use canister_state_macros::canister_state; use serde::{Deserialize, Serialize}; use std::cell::RefCell; @@ -7,6 +8,7 @@ use utils::env::Environment; mod jobs; mod lifecycle; mod memory; +mod model; mod queries; mod updates; @@ -42,6 +44,7 @@ impl RuntimeState { #[derive(Serialize, Deserialize)] struct Data { + pub offers: Offers, pub cycles_dispenser_canister_id: CanisterId, pub rng_seed: [u8; 32], pub test_mode: bool, @@ -50,6 +53,7 @@ struct Data { impl Data { pub fn new(cycles_dispenser_canister_id: CanisterId, test_mode: bool) -> Data { Data { + offers: Offers::default(), cycles_dispenser_canister_id, rng_seed: [0; 32], test_mode, diff --git a/backend/canisters/escrow/impl/src/model/mod.rs b/backend/canisters/escrow/impl/src/model/mod.rs new file mode 100644 index 0000000000..4791300f15 --- /dev/null +++ b/backend/canisters/escrow/impl/src/model/mod.rs @@ -0,0 +1 @@ +pub mod offers; diff --git a/backend/canisters/escrow/impl/src/model/offers.rs b/backend/canisters/escrow/impl/src/model/offers.rs new file mode 100644 index 0000000000..329a8e3713 --- /dev/null +++ b/backend/canisters/escrow/impl/src/model/offers.rs @@ -0,0 +1,57 @@ +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use types::{CompletedCryptoTransaction, TimestampMillis, TokenInfo, UserId}; + +#[derive(Serialize, Deserialize, Default)] +pub struct Offers { + map: BTreeMap, +} + +impl Offers { + pub fn push(&mut self, caller: UserId, args: escrow_canister::create_offer::Args, now: TimestampMillis) -> u32 { + let id = self.map.last_key_value().map_or(1, |(k, _)| *k); + self.map.insert(id, Offer::new(id, caller, args, now)); + id + } + + pub fn get_mut(&mut self, id: u32) -> Option<&mut Offer> { + self.map.get_mut(&id) + } +} + +#[derive(Serialize, Deserialize)] +pub struct Offer { + pub id: u32, + pub created_at: TimestampMillis, + pub created_by: UserId, + pub token0: TokenInfo, + pub amount0: u128, + pub token1: TokenInfo, + pub amount1: u128, + pub expires_at: TimestampMillis, + pub accepted_by: Option<(UserId, TimestampMillis)>, + pub token0_received: bool, + pub token1_received: bool, + pub transfer_out0: Option, + pub transfer_out1: Option, +} + +impl Offer { + pub fn new(id: u32, caller: UserId, args: escrow_canister::create_offer::Args, now: TimestampMillis) -> Offer { + Offer { + id, + created_at: now, + created_by: caller, + token0: args.input_token, + amount0: args.input_amount, + token1: args.output_token, + amount1: args.output_amount, + expires_at: args.expires_at, + accepted_by: None, + token0_received: false, + token1_received: false, + transfer_out0: None, + transfer_out1: None, + } + } +} diff --git a/backend/canisters/escrow/impl/src/updates/create_offer.rs b/backend/canisters/escrow/impl/src/updates/create_offer.rs new file mode 100644 index 0000000000..218c7df691 --- /dev/null +++ b/backend/canisters/escrow/impl/src/updates/create_offer.rs @@ -0,0 +1,37 @@ +use crate::{mutate_state, RuntimeState}; +use canister_api_macros::update_msgpack; +use canister_tracing_macros::trace; +use escrow_canister::create_offer::{Response::*, *}; +use types::TimestampMillis; + +#[update_msgpack] +#[trace] +fn create_offer(args: Args) -> Response { + mutate_state(|state| create_offer_impl(args, state)) +} + +fn create_offer_impl(args: Args, state: &mut RuntimeState) -> Response { + let now = state.env.now(); + if let Err(error) = validate_offer(&args, now) { + InvalidOffer(error) + } else { + let caller = state.env.caller().into(); + let id = state.data.offers.push(caller, args, now); + + Success(SuccessResult { id }) + } +} + +fn validate_offer(args: &Args, now: TimestampMillis) -> Result<(), String> { + if args.input_token.ledger == args.output_token.ledger { + Err("Input token must be different to output token".to_string()) + } else if args.input_amount == 0 { + Err("Input amount cannot be 0".to_string()) + } else if args.output_amount == 0 { + Err("Output amount cannot be 0".to_string()) + } else if args.expires_at < now { + Err("Expiry cannot be in the past".to_string()) + } else { + Ok(()) + } +} diff --git a/backend/canisters/escrow/impl/src/updates/mod.rs b/backend/canisters/escrow/impl/src/updates/mod.rs index f77132607f..369ee5973c 100644 --- a/backend/canisters/escrow/impl/src/updates/mod.rs +++ b/backend/canisters/escrow/impl/src/updates/mod.rs @@ -1 +1,3 @@ -mod wallet_receive; +pub mod create_offer; +pub mod notify_deposit; +pub mod wallet_receive; diff --git a/backend/canisters/escrow/impl/src/updates/notify_deposit.rs b/backend/canisters/escrow/impl/src/updates/notify_deposit.rs new file mode 100644 index 0000000000..c49e618695 --- /dev/null +++ b/backend/canisters/escrow/impl/src/updates/notify_deposit.rs @@ -0,0 +1,98 @@ +use crate::{mutate_state, RuntimeState}; +use canister_api_macros::update_msgpack; +use canister_tracing_macros::trace; +use escrow_canister::deposit_subaccount; +use escrow_canister::notify_deposit::{Response::*, *}; +use icrc_ledger_types::icrc1::account::Account; +use types::{CanisterId, UserId}; + +#[update_msgpack] +#[trace] +async fn notify_deposit(args: Args) -> Response { + let PrepareResult { + user_id, + ledger, + account, + balance_required, + } = match mutate_state(|state| prepare(&args, state)) { + Ok(ok) => ok, + Err(response) => return response, + }; + + match icrc_ledger_canister_c2c_client::icrc1_balance_of(ledger, &account) + .await + .map(|b| u128::try_from(b.0).unwrap()) + { + Ok(balance) => mutate_state(|state| { + let offer = state.data.offers.get_mut(args.offer_id).unwrap(); + if balance < balance_required { + BalanceTooLow(BalanceTooLowResult { + balance, + balance_required, + }) + } else { + let now = state.env.now(); + if user_id == offer.created_by { + offer.token0_received = true; + } else { + offer.accepted_by = Some((user_id, now)); + offer.token1_received = true; + } + if offer.token0_received && offer.token1_received { + // TODO queue up transfers + } + Success + } + }), + Err(error) => InternalError(format!("{error:?}")), + } +} + +struct PrepareResult { + user_id: UserId, + ledger: CanisterId, + account: Account, + balance_required: u128, +} + +fn prepare(args: &Args, state: &mut RuntimeState) -> Result { + let now = state.env.now(); + if let Some(offer) = state.data.offers.get_mut(args.offer_id) { + let user_id = args.user_id.unwrap_or_else(|| state.env.caller().into()); + if offer.created_by == user_id { + if offer.token0_received { + Err(Success) + } else { + Ok(PrepareResult { + user_id, + ledger: offer.token0.ledger, + account: Account { + owner: state.env.canister_id(), + subaccount: Some(deposit_subaccount(user_id, offer.id)), + }, + balance_required: offer.amount0 + offer.token0.fee, + }) + } + } else if let Some((accepted_by, _)) = offer.accepted_by { + if accepted_by == user_id { + Err(Success) + } else { + Err(OfferAlreadyAccepted) + } + } else if offer.expires_at < now { + Err(OfferExpired) + } else { + Ok(PrepareResult { + user_id, + ledger: offer.token1.ledger, + account: Account { + owner: state.env.canister_id(), + subaccount: Some(deposit_subaccount(user_id, offer.id)), + }, + balance_required: offer.amount1 + offer.token1.fee, + }) + } + } else { + Err(OfferNotFound) + } +} From adcdf0d4760be9904e3c1926cc3a7eac30e6361f Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 4 Dec 2023 09:09:49 +0000 Subject: [PATCH 06/14] Transfer out funds once trade is complete (#4906) --- .../impl/src/jobs/process_pending_actions.rs | 4 +- backend/canisters/escrow/CHANGELOG.md | 1 + .../impl/src/jobs/make_pending_payments.rs | 102 ++++++++++++++++++ backend/canisters/escrow/impl/src/jobs/mod.rs | 6 +- backend/canisters/escrow/impl/src/lib.rs | 3 + .../canisters/escrow/impl/src/model/mod.rs | 1 + .../canisters/escrow/impl/src/model/offers.rs | 8 +- .../impl/src/model/pending_payments_queue.rs | 38 +++++++ .../escrow/impl/src/updates/notify_deposit.rs | 20 +++- .../impl/src/jobs/make_btc_miami_payments.rs | 5 +- backend/libraries/ledger_utils/src/icrc1.rs | 8 +- backend/libraries/types/src/cryptocurrency.rs | 6 ++ 12 files changed, 186 insertions(+), 16 deletions(-) create mode 100644 backend/canisters/escrow/impl/src/jobs/make_pending_payments.rs create mode 100644 backend/canisters/escrow/impl/src/model/pending_payments_queue.rs diff --git a/backend/bots/examples/satoshi_dice/impl/src/jobs/process_pending_actions.rs b/backend/bots/examples/satoshi_dice/impl/src/jobs/process_pending_actions.rs index 2a7c2da2b9..4518362f40 100644 --- a/backend/bots/examples/satoshi_dice/impl/src/jobs/process_pending_actions.rs +++ b/backend/bots/examples/satoshi_dice/impl/src/jobs/process_pending_actions.rs @@ -108,8 +108,8 @@ async fn process_action(action: Action) { token: Cryptocurrency::CKBTC, amount: amount as u128, fee: 10, - from: icrc1::CryptoAccount::Account(from), - to: icrc1::CryptoAccount::Account(Account::from(Principal::from(user_id))), + from: from.into(), + to: Account::from(Principal::from(user_id)).into(), memo: None, created: now_nanos, block_index: block_index.0.try_into().unwrap(), diff --git a/backend/canisters/escrow/CHANGELOG.md b/backend/canisters/escrow/CHANGELOG.md index bf133421b9..a02c445d54 100644 --- a/backend/canisters/escrow/CHANGELOG.md +++ b/backend/canisters/escrow/CHANGELOG.md @@ -9,3 +9,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Introduce the Escrow canister for supporting P2P trades ([#4903](https://github.com/open-chat-labs/open-chat/pull/4903)) - Implement `create_offer` and `notify_deposit` ([#4904](https://github.com/open-chat-labs/open-chat/pull/4904)) +- Transfer out funds once trade is complete ([#4906](https://github.com/open-chat-labs/open-chat/pull/4906)) \ No newline at end of file diff --git a/backend/canisters/escrow/impl/src/jobs/make_pending_payments.rs b/backend/canisters/escrow/impl/src/jobs/make_pending_payments.rs new file mode 100644 index 0000000000..1a587fa69a --- /dev/null +++ b/backend/canisters/escrow/impl/src/jobs/make_pending_payments.rs @@ -0,0 +1,102 @@ +use crate::model::pending_payments_queue::{PendingPayment, PendingPaymentReason}; +use crate::{mutate_state, RuntimeState}; +use candid::Principal; +use escrow_canister::deposit_subaccount; +use ic_cdk_timers::TimerId; +use icrc_ledger_types::icrc1::account::Account; +use icrc_ledger_types::icrc1::transfer::TransferArg; +use std::cell::Cell; +use std::time::Duration; +use tracing::{error, trace}; +use types::icrc1::CompletedCryptoTransaction; +use types::CanisterId; +use utils::time::NANOS_PER_MILLISECOND; + +thread_local! { + static TIMER_ID: Cell> = Cell::default(); +} + +pub(crate) fn start_job_if_required(state: &RuntimeState) -> bool { + if TIMER_ID.get().is_none() && !state.data.pending_payments_queue.is_empty() { + let timer_id = ic_cdk_timers::set_timer_interval(Duration::ZERO, run); + TIMER_ID.set(Some(timer_id)); + trace!("'make_pending_payments' job started"); + true + } else { + false + } +} + +pub fn run() { + if let Some(pending_payment) = mutate_state(|state| state.data.pending_payments_queue.pop()) { + ic_cdk::spawn(process_payment(pending_payment)); + } else if let Some(timer_id) = TIMER_ID.take() { + ic_cdk_timers::clear_timer(timer_id); + trace!("'make_pending_payments' job stopped"); + } +} + +async fn process_payment(pending_payment: PendingPayment) { + let from_user = match pending_payment.reason { + PendingPaymentReason::Trade(other_user_id) => other_user_id, + PendingPaymentReason::Refund => pending_payment.user_id, + }; + let created_at_time = pending_payment.timestamp * NANOS_PER_MILLISECOND; + + let args = TransferArg { + from_subaccount: Some(deposit_subaccount(from_user, pending_payment.offer_id)), + to: Principal::from(pending_payment.user_id).into(), + fee: Some(pending_payment.token_info.fee.into()), + created_at_time: Some(created_at_time), + memo: None, + amount: pending_payment.amount.into(), + }; + + match make_payment(pending_payment.token_info.ledger, &args).await { + Ok(block_index) => { + mutate_state(|state| { + if let Some(offer) = state.data.offers.get_mut(pending_payment.offer_id) { + let transfer = CompletedCryptoTransaction { + ledger: pending_payment.token_info.ledger, + token: pending_payment.token_info.token, + amount: pending_payment.amount, + from: Account { + owner: state.env.canister_id(), + subaccount: args.from_subaccount, + } + .into(), + to: Account::from(Principal::from(pending_payment.user_id)).into(), + fee: pending_payment.token_info.fee, + memo: None, + created: created_at_time, + block_index, + }; + offer.transfers_out.push(transfer); + } + }); + } + Err(retry) => { + if retry { + mutate_state(|state| { + state.data.pending_payments_queue.push(pending_payment); + start_job_if_required(state); + }); + } + } + } +} + +// Error response contains a boolean stating if the transfer should be retried +async fn make_payment(ledger_canister_id: CanisterId, args: &TransferArg) -> Result { + match icrc_ledger_canister_c2c_client::icrc1_transfer(ledger_canister_id, args).await { + Ok(Ok(block_index)) => Ok(block_index.0.try_into().unwrap()), + Ok(Err(transfer_error)) => { + error!(?transfer_error, ?args, "Transfer failed"); + Err(false) + } + Err(error) => { + error!(?error, ?args, "Transfer failed"); + Err(true) + } + } +} diff --git a/backend/canisters/escrow/impl/src/jobs/mod.rs b/backend/canisters/escrow/impl/src/jobs/mod.rs index 01f9150d4a..daeac2a616 100644 --- a/backend/canisters/escrow/impl/src/jobs/mod.rs +++ b/backend/canisters/escrow/impl/src/jobs/mod.rs @@ -1,3 +1,7 @@ use crate::RuntimeState; -pub(crate) fn start(_state: &RuntimeState) {} +pub mod make_pending_payments; + +pub(crate) fn start(state: &RuntimeState) { + make_pending_payments::start_job_if_required(state); +} diff --git a/backend/canisters/escrow/impl/src/lib.rs b/backend/canisters/escrow/impl/src/lib.rs index 9211986682..220648cf78 100644 --- a/backend/canisters/escrow/impl/src/lib.rs +++ b/backend/canisters/escrow/impl/src/lib.rs @@ -1,4 +1,5 @@ use crate::model::offers::Offers; +use crate::model::pending_payments_queue::PendingPaymentsQueue; use canister_state_macros::canister_state; use serde::{Deserialize, Serialize}; use std::cell::RefCell; @@ -45,6 +46,7 @@ impl RuntimeState { #[derive(Serialize, Deserialize)] struct Data { pub offers: Offers, + pub pending_payments_queue: PendingPaymentsQueue, pub cycles_dispenser_canister_id: CanisterId, pub rng_seed: [u8; 32], pub test_mode: bool, @@ -54,6 +56,7 @@ impl Data { pub fn new(cycles_dispenser_canister_id: CanisterId, test_mode: bool) -> Data { Data { offers: Offers::default(), + pending_payments_queue: PendingPaymentsQueue::default(), cycles_dispenser_canister_id, rng_seed: [0; 32], test_mode, diff --git a/backend/canisters/escrow/impl/src/model/mod.rs b/backend/canisters/escrow/impl/src/model/mod.rs index 4791300f15..e9ce076028 100644 --- a/backend/canisters/escrow/impl/src/model/mod.rs +++ b/backend/canisters/escrow/impl/src/model/mod.rs @@ -1 +1,2 @@ pub mod offers; +pub mod pending_payments_queue; diff --git a/backend/canisters/escrow/impl/src/model/offers.rs b/backend/canisters/escrow/impl/src/model/offers.rs index 329a8e3713..317aff7aab 100644 --- a/backend/canisters/escrow/impl/src/model/offers.rs +++ b/backend/canisters/escrow/impl/src/model/offers.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; -use types::{CompletedCryptoTransaction, TimestampMillis, TokenInfo, UserId}; +use types::{icrc1::CompletedCryptoTransaction, TimestampMillis, TokenInfo, UserId}; #[derive(Serialize, Deserialize, Default)] pub struct Offers { @@ -32,8 +32,7 @@ pub struct Offer { pub accepted_by: Option<(UserId, TimestampMillis)>, pub token0_received: bool, pub token1_received: bool, - pub transfer_out0: Option, - pub transfer_out1: Option, + pub transfers_out: Vec, } impl Offer { @@ -50,8 +49,7 @@ impl Offer { accepted_by: None, token0_received: false, token1_received: false, - transfer_out0: None, - transfer_out1: None, + transfers_out: Vec::new(), } } } diff --git a/backend/canisters/escrow/impl/src/model/pending_payments_queue.rs b/backend/canisters/escrow/impl/src/model/pending_payments_queue.rs new file mode 100644 index 0000000000..c9fba60c28 --- /dev/null +++ b/backend/canisters/escrow/impl/src/model/pending_payments_queue.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; +use types::{TimestampMillis, TokenInfo, UserId}; + +#[derive(Serialize, Deserialize, Default)] +pub struct PendingPaymentsQueue { + pending_payments: VecDeque, +} + +impl PendingPaymentsQueue { + pub fn push(&mut self, pending_payment: PendingPayment) { + self.pending_payments.push_back(pending_payment); + } + + pub fn pop(&mut self) -> Option { + self.pending_payments.pop_front() + } + + pub fn is_empty(&self) -> bool { + self.pending_payments.is_empty() + } +} + +#[derive(Serialize, Deserialize)] +pub struct PendingPayment { + pub user_id: UserId, + pub timestamp: TimestampMillis, + pub token_info: TokenInfo, + pub amount: u128, + pub offer_id: u32, + pub reason: PendingPaymentReason, +} + +#[derive(Serialize, Deserialize, Clone, Copy)] +pub enum PendingPaymentReason { + Trade(UserId), + Refund, +} diff --git a/backend/canisters/escrow/impl/src/updates/notify_deposit.rs b/backend/canisters/escrow/impl/src/updates/notify_deposit.rs index c49e618695..455bfcc01e 100644 --- a/backend/canisters/escrow/impl/src/updates/notify_deposit.rs +++ b/backend/canisters/escrow/impl/src/updates/notify_deposit.rs @@ -1,3 +1,4 @@ +use crate::model::pending_payments_queue::{PendingPayment, PendingPaymentReason}; use crate::{mutate_state, RuntimeState}; use canister_api_macros::update_msgpack; use canister_tracing_macros::trace; @@ -39,7 +40,24 @@ async fn notify_deposit(args: Args) -> Response { offer.token1_received = true; } if offer.token0_received && offer.token1_received { - // TODO queue up transfers + let accepted_by = offer.accepted_by.unwrap().0; + state.data.pending_payments_queue.push(PendingPayment { + user_id: offer.created_by, + timestamp: now, + token_info: offer.token1.clone(), + amount: offer.amount1, + offer_id: offer.id, + reason: PendingPaymentReason::Trade(accepted_by), + }); + state.data.pending_payments_queue.push(PendingPayment { + user_id: accepted_by, + timestamp: now, + token_info: offer.token0.clone(), + amount: offer.amount0, + offer_id: offer.id, + reason: PendingPaymentReason::Trade(offer.created_by), + }); + crate::jobs::make_pending_payments::start_job_if_required(state); } Success } diff --git a/backend/canisters/local_user_index/impl/src/jobs/make_btc_miami_payments.rs b/backend/canisters/local_user_index/impl/src/jobs/make_btc_miami_payments.rs index bd7b8a9646..229b752ccb 100644 --- a/backend/canisters/local_user_index/impl/src/jobs/make_btc_miami_payments.rs +++ b/backend/canisters/local_user_index/impl/src/jobs/make_btc_miami_payments.rs @@ -7,7 +7,6 @@ use icrc_ledger_types::icrc1::transfer::{BlockIndex, TransferArg}; use std::cell::Cell; use std::time::Duration; use tracing::{error, trace}; -use types::icrc1::CryptoAccount; use types::{ icrc1, CompletedCryptoTransaction, CryptoContent, CryptoTransaction, Cryptocurrency, CustomContent, MessageContent, TextContent, @@ -85,8 +84,8 @@ fn send_oc_bot_messages(pending_payment: &PendingPayment, block_index: BlockInde token: Cryptocurrency::CKBTC, amount, fee: 10, - from: CryptoAccount::Account(Account::from(Principal::from(OPENCHAT_BOT_USER_ID))), - to: CryptoAccount::Account(Account::from(Principal::from(user_id))), + from: Account::from(Principal::from(OPENCHAT_BOT_USER_ID)).into(), + to: Account::from(Principal::from(user_id)).into(), memo: None, created: pending_payment.timestamp, block_index: block_index.0.try_into().unwrap(), diff --git a/backend/libraries/ledger_utils/src/icrc1.rs b/backend/libraries/ledger_utils/src/icrc1.rs index f6c30b75f3..1de8f6ef67 100644 --- a/backend/libraries/ledger_utils/src/icrc1.rs +++ b/backend/libraries/ledger_utils/src/icrc1.rs @@ -25,8 +25,8 @@ pub async fn process_transaction( token: transaction.token.clone(), amount: transaction.amount, fee: transaction.fee, - from: types::icrc1::CryptoAccount::Account(from), - to: types::icrc1::CryptoAccount::Account(transaction.to), + from: from.into(), + to: transaction.to.into(), memo: transaction.memo.clone(), created: transaction.created, block_index: block_index.0.try_into().unwrap(), @@ -45,8 +45,8 @@ pub async fn process_transaction( token: transaction.token, amount: transaction.amount, fee: transaction.fee, - from: types::icrc1::CryptoAccount::Account(from), - to: types::icrc1::CryptoAccount::Account(transaction.to), + from: from.into(), + to: transaction.to.into(), memo: transaction.memo, created: transaction.created, error_message: error, diff --git a/backend/libraries/types/src/cryptocurrency.rs b/backend/libraries/types/src/cryptocurrency.rs index 2c9b332a0a..3ba7155eb8 100644 --- a/backend/libraries/types/src/cryptocurrency.rs +++ b/backend/libraries/types/src/cryptocurrency.rs @@ -402,6 +402,12 @@ pub mod icrc1 { super::FailedCryptoTransaction::ICRC1(value) } } + + impl From for CryptoAccount { + fn from(value: Account) -> Self { + CryptoAccount::Account(value) + } + } } impl From for nns::PendingCryptoTransaction { From 612c1f7e81f5a53a12bf1e8bd1bef89393af01b8 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 4 Dec 2023 09:15:49 +0000 Subject: [PATCH 07/14] Implement `cancel_offer` (#4907) --- Cargo.lock | 1 + backend/canisters/escrow/CHANGELOG.md | 3 +- backend/canisters/escrow/api/Cargo.toml | 1 + backend/canisters/escrow/api/src/lib.rs | 11 ++-- .../escrow/api/src/updates/cancel_offer.rs | 15 ++++++ .../canisters/escrow/api/src/updates/mod.rs | 1 + .../escrow/api/src/updates/notify_deposit.rs | 1 + .../canisters/escrow/impl/src/model/offers.rs | 2 + .../escrow/impl/src/updates/cancel_offer.rs | 31 +++++++++++ .../canisters/escrow/impl/src/updates/mod.rs | 1 + .../escrow/impl/src/updates/notify_deposit.rs | 53 ++++++++++--------- 11 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 backend/canisters/escrow/api/src/updates/cancel_offer.rs create mode 100644 backend/canisters/escrow/impl/src/updates/cancel_offer.rs diff --git a/Cargo.lock b/Cargo.lock index e83eece733..0c9d4aa03d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2024,6 +2024,7 @@ dependencies = [ "candid_gen", "icrc-ledger-types", "serde", + "sha256", "types", ] diff --git a/backend/canisters/escrow/CHANGELOG.md b/backend/canisters/escrow/CHANGELOG.md index a02c445d54..9b325cc3ac 100644 --- a/backend/canisters/escrow/CHANGELOG.md +++ b/backend/canisters/escrow/CHANGELOG.md @@ -9,4 +9,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Introduce the Escrow canister for supporting P2P trades ([#4903](https://github.com/open-chat-labs/open-chat/pull/4903)) - Implement `create_offer` and `notify_deposit` ([#4904](https://github.com/open-chat-labs/open-chat/pull/4904)) -- Transfer out funds once trade is complete ([#4906](https://github.com/open-chat-labs/open-chat/pull/4906)) \ No newline at end of file +- Transfer out funds once trade is complete ([#4906](https://github.com/open-chat-labs/open-chat/pull/4906)) +- Implement `cancel_offer` ([#4907](https://github.com/open-chat-labs/open-chat/pull/4907)) diff --git a/backend/canisters/escrow/api/Cargo.toml b/backend/canisters/escrow/api/Cargo.toml index 9b670b179b..03c44de93f 100644 --- a/backend/canisters/escrow/api/Cargo.toml +++ b/backend/canisters/escrow/api/Cargo.toml @@ -10,4 +10,5 @@ candid = { workspace = true } candid_gen = { path = "../../../libraries/candid_gen" } icrc-ledger-types = { workspace = true } serde = { workspace = true } +sha256 = { path = "../../../libraries/sha256" } types = { path = "../../../libraries/types" } diff --git a/backend/canisters/escrow/api/src/lib.rs b/backend/canisters/escrow/api/src/lib.rs index 017aede912..074f4e9207 100644 --- a/backend/canisters/escrow/api/src/lib.rs +++ b/backend/canisters/escrow/api/src/lib.rs @@ -1,5 +1,6 @@ use candid::Principal; use icrc_ledger_types::icrc1::account::Subaccount; +use sha256::sha256; use types::UserId; mod lifecycle; @@ -11,10 +12,8 @@ pub use queries::*; pub use updates::*; pub fn deposit_subaccount(user_id: UserId, offer_id: u32) -> Subaccount { - let mut subaccount = [0; 32]; - let principal = Principal::from(user_id); - let user_id_bytes = principal.as_slice(); - subaccount[..user_id_bytes.len()].copy_from_slice(user_id_bytes); - subaccount[28..].copy_from_slice(&offer_id.to_be_bytes()); - subaccount + let mut bytes = Vec::new(); + bytes.extend_from_slice(Principal::from(user_id).as_slice()); + bytes.extend_from_slice(&offer_id.to_be_bytes()); + sha256(&bytes) } diff --git a/backend/canisters/escrow/api/src/updates/cancel_offer.rs b/backend/canisters/escrow/api/src/updates/cancel_offer.rs new file mode 100644 index 0000000000..628629b678 --- /dev/null +++ b/backend/canisters/escrow/api/src/updates/cancel_offer.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Args { + pub offer_id: u32, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum Response { + Success, + OfferAlreadyAccepted, + OfferExpired, + OfferNotFound, + NotAuthorized, +} diff --git a/backend/canisters/escrow/api/src/updates/mod.rs b/backend/canisters/escrow/api/src/updates/mod.rs index dff0cb6200..7473275f87 100644 --- a/backend/canisters/escrow/api/src/updates/mod.rs +++ b/backend/canisters/escrow/api/src/updates/mod.rs @@ -1,2 +1,3 @@ +pub mod cancel_offer; pub mod create_offer; pub mod notify_deposit; diff --git a/backend/canisters/escrow/api/src/updates/notify_deposit.rs b/backend/canisters/escrow/api/src/updates/notify_deposit.rs index e559888345..216543b122 100644 --- a/backend/canisters/escrow/api/src/updates/notify_deposit.rs +++ b/backend/canisters/escrow/api/src/updates/notify_deposit.rs @@ -12,6 +12,7 @@ pub enum Response { Success, BalanceTooLow(BalanceTooLowResult), OfferAlreadyAccepted, + OfferCancelled, OfferExpired, OfferNotFound, InternalError(String), diff --git a/backend/canisters/escrow/impl/src/model/offers.rs b/backend/canisters/escrow/impl/src/model/offers.rs index 317aff7aab..472f93e2f1 100644 --- a/backend/canisters/escrow/impl/src/model/offers.rs +++ b/backend/canisters/escrow/impl/src/model/offers.rs @@ -29,6 +29,7 @@ pub struct Offer { pub token1: TokenInfo, pub amount1: u128, pub expires_at: TimestampMillis, + pub cancelled_at: Option, pub accepted_by: Option<(UserId, TimestampMillis)>, pub token0_received: bool, pub token1_received: bool, @@ -46,6 +47,7 @@ impl Offer { token1: args.output_token, amount1: args.output_amount, expires_at: args.expires_at, + cancelled_at: None, accepted_by: None, token0_received: false, token1_received: false, diff --git a/backend/canisters/escrow/impl/src/updates/cancel_offer.rs b/backend/canisters/escrow/impl/src/updates/cancel_offer.rs new file mode 100644 index 0000000000..95cd48ff36 --- /dev/null +++ b/backend/canisters/escrow/impl/src/updates/cancel_offer.rs @@ -0,0 +1,31 @@ +use crate::{mutate_state, RuntimeState}; +use canister_api_macros::update_msgpack; +use canister_tracing_macros::trace; +use escrow_canister::cancel_offer::{Response::*, *}; + +#[update_msgpack] +#[trace] +fn cancel_offer(args: Args) -> Response { + mutate_state(|state| cancel_offer_impl(args, state)) +} + +fn cancel_offer_impl(args: Args, state: &mut RuntimeState) -> Response { + if let Some(offer) = state.data.offers.get_mut(args.offer_id) { + let user_id = state.env.caller().into(); + let now = state.env.now(); + if offer.created_by != user_id { + NotAuthorized + } else if offer.accepted_by.is_some() { + OfferAlreadyAccepted + } else if offer.expires_at < now { + OfferExpired + } else { + if offer.cancelled_at.is_none() { + offer.cancelled_at = Some(now); + } + Success + } + } else { + OfferNotFound + } +} diff --git a/backend/canisters/escrow/impl/src/updates/mod.rs b/backend/canisters/escrow/impl/src/updates/mod.rs index 369ee5973c..c36b86af16 100644 --- a/backend/canisters/escrow/impl/src/updates/mod.rs +++ b/backend/canisters/escrow/impl/src/updates/mod.rs @@ -1,3 +1,4 @@ +pub mod cancel_offer; pub mod create_offer; pub mod notify_deposit; pub mod wallet_receive; diff --git a/backend/canisters/escrow/impl/src/updates/notify_deposit.rs b/backend/canisters/escrow/impl/src/updates/notify_deposit.rs index 455bfcc01e..7ffd7f6626 100644 --- a/backend/canisters/escrow/impl/src/updates/notify_deposit.rs +++ b/backend/canisters/escrow/impl/src/updates/notify_deposit.rs @@ -76,39 +76,44 @@ struct PrepareResult { fn prepare(args: &Args, state: &mut RuntimeState) -> Result { let now = state.env.now(); if let Some(offer) = state.data.offers.get_mut(args.offer_id) { - let user_id = args.user_id.unwrap_or_else(|| state.env.caller().into()); - if offer.created_by == user_id { - if offer.token0_received { - Err(Success) + if offer.cancelled_at.is_some() { + Err(OfferCancelled) + } else if offer.expires_at < now { + Err(OfferExpired) + } else { + let user_id = args.user_id.unwrap_or_else(|| state.env.caller().into()); + + if offer.created_by == user_id { + if offer.token0_received { + Err(Success) + } else { + Ok(PrepareResult { + user_id, + ledger: offer.token0.ledger, + account: Account { + owner: state.env.canister_id(), + subaccount: Some(deposit_subaccount(user_id, offer.id)), + }, + balance_required: offer.amount0 + offer.token0.fee, + }) + } + } else if let Some((accepted_by, _)) = offer.accepted_by { + if accepted_by == user_id { + Err(Success) + } else { + Err(OfferAlreadyAccepted) + } } else { Ok(PrepareResult { user_id, - ledger: offer.token0.ledger, + ledger: offer.token1.ledger, account: Account { owner: state.env.canister_id(), subaccount: Some(deposit_subaccount(user_id, offer.id)), }, - balance_required: offer.amount0 + offer.token0.fee, + balance_required: offer.amount1 + offer.token1.fee, }) } - } else if let Some((accepted_by, _)) = offer.accepted_by { - if accepted_by == user_id { - Err(Success) - } else { - Err(OfferAlreadyAccepted) - } - } else if offer.expires_at < now { - Err(OfferExpired) - } else { - Ok(PrepareResult { - user_id, - ledger: offer.token1.ledger, - account: Account { - owner: state.env.canister_id(), - subaccount: Some(deposit_subaccount(user_id, offer.id)), - }, - balance_required: offer.amount1 + offer.token1.fee, - }) } } else { Err(OfferNotFound) From ce16c62bf75c5d72f2d997c1a8e3626edfd263db Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 4 Dec 2023 09:24:08 +0000 Subject: [PATCH 08/14] Add tests to cover upgrading to Diamond by paying in CHAT (#4908) --- backend/integration_tests/src/client/mod.rs | 29 +++------------ .../src/client/user_index.rs | 5 +-- .../src/diamond_membership_tests.rs | 35 +++++++++++-------- backend/integration_tests/src/lib.rs | 1 + .../integration_tests/src/registry_tests.rs | 8 +++-- backend/integration_tests/src/setup.rs | 25 +++++++++++-- 6 files changed, 55 insertions(+), 48 deletions(-) diff --git a/backend/integration_tests/src/client/mod.rs b/backend/integration_tests/src/client/mod.rs index 640aa01857..8c79bd63d1 100644 --- a/backend/integration_tests/src/client/mod.rs +++ b/backend/integration_tests/src/client/mod.rs @@ -32,12 +32,8 @@ pub fn create_canister(env: &mut PocketIc, controller: Principal) -> CanisterId } pub fn create_canister_with_id(env: &mut PocketIc, controller: Principal, canister_id: &str) -> CanisterId { - let canister_id = env - .create_canister_with_id( - Some(controller), - None, - Principal::from_text(canister_id).expect("Invalid canister ID"), - ) + let canister_id = canister_id.try_into().expect("Invalid canister ID"); + env.create_canister_with_id(Some(controller), None, canister_id) .expect("Create canister with ID failed"); env.add_cycles(canister_id, INIT_CYCLES_BALANCE); canister_id @@ -94,25 +90,7 @@ pub fn execute_update_no_response( pub fn register_diamond_user(env: &mut PocketIc, canister_ids: &CanisterIds, controller: Principal) -> User { let user = local_user_index::happy_path::register_user(env, canister_ids.local_user_index); - - icrc1::happy_path::transfer( - env, - controller, - canister_ids.icp_ledger, - user.user_id.into(), - 10_000_000_000u64, - ); - - user_index::happy_path::pay_for_diamond_membership( - env, - user.principal, - canister_ids.user_index, - DiamondMembershipPlanDuration::OneMonth, - true, - ); - - tick_many(env, 3); - + upgrade_user(&user, env, canister_ids, controller); user } @@ -130,6 +108,7 @@ pub fn upgrade_user(user: &User, env: &mut PocketIc, canister_ids: &CanisterIds, user.principal, canister_ids.user_index, DiamondMembershipPlanDuration::OneMonth, + false, true, ); diff --git a/backend/integration_tests/src/client/user_index.rs b/backend/integration_tests/src/client/user_index.rs index ee63ddb3c7..4b8847ce4b 100644 --- a/backend/integration_tests/src/client/user_index.rs +++ b/backend/integration_tests/src/client/user_index.rs @@ -77,6 +77,7 @@ pub mod happy_path { sender: Principal, canister_id: CanisterId, duration: DiamondMembershipPlanDuration, + pay_in_chat: bool, recurring: bool, ) -> DiamondMembershipDetails { let response = super::pay_for_diamond_membership( @@ -85,8 +86,8 @@ pub mod happy_path { canister_id, &user_index_canister::pay_for_diamond_membership::Args { duration, - token: Cryptocurrency::InternetComputer, - expected_price_e8s: duration.icp_price_e8s(), + token: if pay_in_chat { Cryptocurrency::CHAT } else { Cryptocurrency::InternetComputer }, + expected_price_e8s: if pay_in_chat { duration.chat_price_e8s() } else { duration.icp_price_e8s() }, recurring, }, ); diff --git a/backend/integration_tests/src/diamond_membership_tests.rs b/backend/integration_tests/src/diamond_membership_tests.rs index d1c5f703ec..31f0c3a391 100644 --- a/backend/integration_tests/src/diamond_membership_tests.rs +++ b/backend/integration_tests/src/diamond_membership_tests.rs @@ -10,10 +10,12 @@ use types::{Cryptocurrency, DiamondMembershipPlanDuration, DiamondMembershipSubs use utils::consts::SNS_GOVERNANCE_CANISTER_ID; use utils::time::MINUTE_IN_MS; -#[test_case(false)] -#[test_case(true)] +#[test_case(true, false)] +#[test_case(true, true)] +#[test_case(false, false)] +#[test_case(false, true)] #[serial] -fn can_upgrade_to_diamond(lifetime: bool) { +fn can_upgrade_to_diamond(pay_in_chat: bool, lifetime: bool) { let mut wrapper = ENV.deref().get(); let TestEnv { env, @@ -22,17 +24,13 @@ fn can_upgrade_to_diamond(lifetime: bool) { .. } = wrapper.env(); - let init_treasury_balance = client::icrc1::happy_path::balance_of(env, canister_ids.icp_ledger, SNS_GOVERNANCE_CANISTER_ID); + let ledger = if pay_in_chat { canister_ids.chat_ledger } else { canister_ids.icp_ledger }; + + let init_treasury_balance = client::icrc1::happy_path::balance_of(env, ledger, SNS_GOVERNANCE_CANISTER_ID); let user = client::local_user_index::happy_path::register_user(env, canister_ids.local_user_index); - client::icrc1::happy_path::transfer( - env, - *controller, - canister_ids.icp_ledger, - user.user_id.into(), - 1_000_000_000u64, - ); + client::icrc1::happy_path::transfer(env, *controller, ledger, user.user_id.into(), 10_000_000_000u64); let now = now_millis(env); @@ -49,6 +47,7 @@ fn can_upgrade_to_diamond(lifetime: bool) { user.principal, canister_ids.user_index, duration, + pay_in_chat, false, ); @@ -69,14 +68,20 @@ fn can_upgrade_to_diamond(lifetime: bool) { .subscription .is_active()); - let new_balance = client::icrc1::happy_path::balance_of(env, canister_ids.icp_ledger, user.user_id.into()); - assert_eq!(new_balance, 1_000_000_000 - duration.icp_price_e8s()); + let (expected_price, transfer_fee) = if pay_in_chat { + (duration.chat_price_e8s(), Cryptocurrency::CHAT.fee().unwrap()) + } else { + (duration.icp_price_e8s(), Cryptocurrency::InternetComputer.fee().unwrap()) + }; - let treasury_balance = client::icrc1::happy_path::balance_of(env, canister_ids.icp_ledger, SNS_GOVERNANCE_CANISTER_ID); + let new_balance = client::icrc1::happy_path::balance_of(env, ledger, user.user_id.into()); + assert_eq!(new_balance, 10_000_000_000 - expected_price); + + let treasury_balance = client::icrc1::happy_path::balance_of(env, ledger, SNS_GOVERNANCE_CANISTER_ID); assert_eq!( treasury_balance - init_treasury_balance, - duration.icp_price_e8s() - (2 * Cryptocurrency::InternetComputer.fee().unwrap()) as u64 + expected_price - (2 * transfer_fee) as u64 ); } diff --git a/backend/integration_tests/src/lib.rs b/backend/integration_tests/src/lib.rs index 50bacb902d..a04a5e70a7 100644 --- a/backend/integration_tests/src/lib.rs +++ b/backend/integration_tests/src/lib.rs @@ -78,6 +78,7 @@ pub struct CanisterIds { pub cycles_dispenser: CanisterId, pub registry: CanisterId, pub icp_ledger: CanisterId, + pub chat_ledger: CanisterId, pub cycles_minting_canister: CanisterId, } diff --git a/backend/integration_tests/src/registry_tests.rs b/backend/integration_tests/src/registry_tests.rs index 7d4894f449..6e5be4af87 100644 --- a/backend/integration_tests/src/registry_tests.rs +++ b/backend/integration_tests/src/registry_tests.rs @@ -1,6 +1,6 @@ use crate::env::ENV; use crate::rng::{random_principal, random_string}; -use crate::setup::install_icrc1_ledger; +use crate::setup::install_icrc_ledger; use crate::utils::now_millis; use crate::{client, TestEnv}; use registry_canister::TokenStandard; @@ -17,12 +17,13 @@ fn add_token_succeeds() { .. } = wrapper.env(); - let ledger_canister_id = install_icrc1_ledger( + let ledger_canister_id = install_icrc_ledger( env, *controller, "ABC Token".to_string(), "ABC".to_string(), 10_000, + None, Vec::new(), ); @@ -105,12 +106,13 @@ fn update_token_succeeds() { .. } = wrapper.env(); - let ledger_canister_id = install_icrc1_ledger( + let ledger_canister_id = install_icrc_ledger( env, *controller, "ABC Token".to_string(), "ABC".to_string(), 10_000, + None, Vec::new(), ); diff --git a/backend/integration_tests/src/setup.rs b/backend/integration_tests/src/setup.rs index 2b5cf19901..5b59da9d2f 100644 --- a/backend/integration_tests/src/setup.rs +++ b/backend/integration_tests/src/setup.rs @@ -38,7 +38,11 @@ pub fn setup_new_env() -> TestEnv { ", &path, &env::current_dir().map(|x| x.display().to_string()).unwrap_or_else(|_| "an unknown directory".to_string())); } - let mut env = PocketIcBuilder::new().with_nns_subnet().with_application_subnet().build(); + let mut env = PocketIcBuilder::new() + .with_nns_subnet() + .with_sns_subnet() + .with_application_subnet() + .build(); let controller = random_principal(); let canister_ids = install_canisters(&mut env, controller); @@ -56,6 +60,15 @@ fn install_canisters(env: &mut PocketIc, controller: Principal) -> CanisterIds { let cycles_minting_canister_id = create_canister_with_id(env, controller, "rkp4c-7iaaa-aaaaa-aaaca-cai"); let sns_wasm_canister_id = create_canister_with_id(env, controller, "qaa6y-5yaaa-aaaaa-aaafa-cai"); let nns_index_canister_id = create_canister_with_id(env, controller, "qhbym-qaaaa-aaaaa-aaafq-cai"); + let chat_ledger_canister_id = install_icrc_ledger( + env, + controller, + "OpenChat".to_string(), + "CHAT".to_string(), + 100000, + Some("2ouva-viaaa-aaaaq-aaamq-cai"), + Vec::new(), + ); let user_index_canister_id = create_canister(env, controller); let group_index_canister_id = create_canister(env, controller); @@ -364,16 +377,18 @@ fn install_canisters(env: &mut PocketIc, controller: Principal) -> CanisterIds { cycles_dispenser: cycles_dispenser_canister_id, registry: registry_canister_id, icp_ledger: nns_ledger_canister_id, + chat_ledger: chat_ledger_canister_id, cycles_minting_canister: cycles_minting_canister_id, } } -pub fn install_icrc1_ledger( +pub fn install_icrc_ledger( env: &mut PocketIc, controller: Principal, token_name: String, token_symbol: String, transfer_fee: u64, + canister_id: Option<&str>, initial_balances: Vec<(Account, u64)>, ) -> CanisterId { #[derive(CandidType)] @@ -413,7 +428,11 @@ pub fn install_icrc1_ledger( }, }); - let canister_id = create_canister(env, controller); + let canister_id = if let Some(id) = canister_id { + create_canister_with_id(env, controller, id) + } else { + create_canister(env, controller) + }; install_canister(env, controller, canister_id, wasms::ICRC_LEDGER.clone(), args); canister_id From 81d08c6274156584ef4e8abfcd5362e5fec358ed Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 4 Dec 2023 09:27:58 +0000 Subject: [PATCH 09/14] Allow extending Diamond membership even if > 3 month remaining (#4909) --- backend/canisters/user_index/CHANGELOG.md | 1 + backend/canisters/user_index/api/can.did | 5 +--- .../src/updates/pay_for_diamond_membership.rs | 10 ++------ .../src/model/diamond_membership_details.rs | 23 +------------------ .../src/updates/pay_for_diamond_membership.rs | 4 ++-- 5 files changed, 7 insertions(+), 36 deletions(-) diff --git a/backend/canisters/user_index/CHANGELOG.md b/backend/canisters/user_index/CHANGELOG.md index 4a9022a1c3..e74120cc14 100644 --- a/backend/canisters/user_index/CHANGELOG.md +++ b/backend/canisters/user_index/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Top up NNS neuron when users pay ICP for lifetime Diamond membership ([#4880](https://github.com/open-chat-labs/open-chat/pull/4880)) - Add `diamond_membership_status` to user summaries ([#4887](https://github.com/open-chat-labs/open-chat/pull/4887)) +- Allow extending Diamond membership even if > 3 month remaining ([#4909](https://github.com/open-chat-labs/open-chat/pull/4909)) ## [[2.0.952](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.952-user_index)] - 2023-11-28 diff --git a/backend/canisters/user_index/api/can.did b/backend/canisters/user_index/api/can.did index dbc416c815..e3a864b24f 100644 --- a/backend/canisters/user_index/api/can.did +++ b/backend/canisters/user_index/api/can.did @@ -239,10 +239,7 @@ type PayForDiamondMembershipArgs = record { type PayForDiamondMembershipResponse = variant { Success : DiamondMembershipDetails; - CannotExtend : record { - diamond_membership_expires_at : TimestampMillis; - can_extend_at : TimestampMillis; - }; + AlreadyLifetimeDiamondMember; CurrencyNotSupported; PriceMismatch; PaymentAlreadyInProgress; diff --git a/backend/canisters/user_index/api/src/updates/pay_for_diamond_membership.rs b/backend/canisters/user_index/api/src/updates/pay_for_diamond_membership.rs index 09bde3bdd9..a570f769ef 100644 --- a/backend/canisters/user_index/api/src/updates/pay_for_diamond_membership.rs +++ b/backend/canisters/user_index/api/src/updates/pay_for_diamond_membership.rs @@ -1,6 +1,6 @@ use candid::CandidType; use serde::{Deserialize, Serialize}; -use types::{Cryptocurrency, DiamondMembershipDetails, DiamondMembershipPlanDuration, TimestampMillis}; +use types::{Cryptocurrency, DiamondMembershipDetails, DiamondMembershipPlanDuration}; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { @@ -13,7 +13,7 @@ pub struct Args { #[derive(CandidType, Serialize, Deserialize, Debug)] pub enum Response { Success(DiamondMembershipDetails), - CannotExtend(CannotExtendResult), + AlreadyLifetimeDiamondMember, CurrencyNotSupported, PriceMismatch, PaymentAlreadyInProgress, @@ -22,9 +22,3 @@ pub enum Response { TransferFailed(String), InternalError(String), } - -#[derive(CandidType, Serialize, Deserialize, Debug)] -pub struct CannotExtendResult { - pub diamond_membership_expires_at: TimestampMillis, - pub can_extend_at: TimestampMillis, -} diff --git a/backend/canisters/user_index/impl/src/model/diamond_membership_details.rs b/backend/canisters/user_index/impl/src/model/diamond_membership_details.rs index 3c730cc848..4f396267ce 100644 --- a/backend/canisters/user_index/impl/src/model/diamond_membership_details.rs +++ b/backend/canisters/user_index/impl/src/model/diamond_membership_details.rs @@ -2,9 +2,8 @@ use serde::{Deserialize, Serialize}; use std::cmp::max; use types::{ Cryptocurrency, DiamondMembershipDetails, DiamondMembershipPlanDuration, DiamondMembershipStatus, - DiamondMembershipStatusFull, DiamondMembershipSubscription, Milliseconds, TimestampMillis, + DiamondMembershipStatusFull, DiamondMembershipSubscription, TimestampMillis, }; -use user_index_canister::pay_for_diamond_membership::CannotExtendResult; use utils::time::DAY_IN_MS; const LIFETIME_TIMESTAMP: TimestampMillis = 30000000000000; // This timestamp is in the year 2920 @@ -62,8 +61,6 @@ pub struct DiamondMembershipPayment { pub manual_payment: bool, } -const THREE_MONTHS: Milliseconds = DiamondMembershipPlanDuration::ThreeMonths.as_millis(); - impl DiamondMembershipDetailsInternal { pub fn expires_at(&self) -> Option { self.expires_at @@ -117,24 +114,6 @@ impl DiamondMembershipDetailsInternal { }) } - pub fn can_extend(&self, now: TimestampMillis) -> Result<(), CannotExtendResult> { - self.expires_at.map_or(Ok(()), |ts| { - let remaining_until_expired = ts.saturating_sub(now); - - // Users can extend when there is < 3 months remaining - let remaining_until_can_extend = remaining_until_expired.saturating_sub(THREE_MONTHS); - - if remaining_until_can_extend == 0 { - Ok(()) - } else { - Err(CannotExtendResult { - can_extend_at: now.saturating_add(remaining_until_can_extend), - diamond_membership_expires_at: ts, - }) - } - }) - } - pub fn is_lifetime_diamond_member(&self) -> bool { self.expires_at > Some(LIFETIME_TIMESTAMP) } diff --git a/backend/canisters/user_index/impl/src/updates/pay_for_diamond_membership.rs b/backend/canisters/user_index/impl/src/updates/pay_for_diamond_membership.rs index 10e5bac995..6dce71fff8 100644 --- a/backend/canisters/user_index/impl/src/updates/pay_for_diamond_membership.rs +++ b/backend/canisters/user_index/impl/src/updates/pay_for_diamond_membership.rs @@ -66,8 +66,8 @@ fn prepare(args: &Args, user_id: UserId, state: &mut RuntimeState) -> Result<(), let diamond_membership = state.data.users.diamond_membership_details_mut(&user_id).unwrap(); if diamond_membership.payment_in_progress() { Err(PaymentAlreadyInProgress) - } else if let Err(result) = diamond_membership.can_extend(state.env.now()) { - Err(CannotExtend(result)) + } else if diamond_membership.is_lifetime_diamond_member() { + Err(AlreadyLifetimeDiamondMember) } else { match args.token { Cryptocurrency::CHAT => { From a954b41ddc288369804b9bd69fd552ca14d8bb9a Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 4 Dec 2023 09:36:20 +0000 Subject: [PATCH 10/14] Require Diamond membership to set a display name (#4910) --- .../canisters/local_user_index/CHANGELOG.md | 4 +++ .../canisters/local_user_index/api/can.did | 4 --- .../api/src/updates/register_user.rs | 4 --- .../impl/src/updates/register_user.rs | 26 ++------------ backend/canisters/user/CHANGELOG.md | 1 + .../canisters/user/api/src/lifecycle/init.rs | 1 - backend/canisters/user/impl/src/lib.rs | 3 +- .../canisters/user/impl/src/lifecycle/init.rs | 1 - backend/canisters/user_index/CHANGELOG.md | 1 + backend/canisters/user_index/api/can.did | 1 + backend/canisters/user_index/api/src/lib.rs | 1 - .../api/src/updates/set_display_name.rs | 1 + backend/canisters/user_index/impl/src/lib.rs | 1 - .../user_index/impl/src/model/user.rs | 6 ++-- .../user_index/impl/src/model/user_map.rs | 13 +++---- .../impl/src/updates/c2c_notify_events.rs | 24 +++---------- .../impl/src/updates/c2c_register_bot.rs | 2 +- .../impl/src/updates/set_display_name.rs | 23 +++++++------ .../src/client/local_user_index.rs | 1 - .../src/register_user_tests.rs | 1 - .../src/update_profile_tests.rs | 34 +++++++++++++++++-- upgrade_order.md | 4 ++- 22 files changed, 72 insertions(+), 85 deletions(-) diff --git a/backend/canisters/local_user_index/CHANGELOG.md b/backend/canisters/local_user_index/CHANGELOG.md index 8319566e80..787db7d88f 100644 --- a/backend/canisters/local_user_index/CHANGELOG.md +++ b/backend/canisters/local_user_index/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Changed + +- Remove `display_name` from `register_user` args ([#4910](https://github.com/open-chat-labs/open-chat/pull/4910)) + ## [[2.0.948](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.948-local_user_index)] - 2023-11-28 ### Added diff --git a/backend/canisters/local_user_index/api/can.did b/backend/canisters/local_user_index/api/can.did index 176d89627b..d7ca72807c 100644 --- a/backend/canisters/local_user_index/api/can.did +++ b/backend/canisters/local_user_index/api/can.did @@ -123,7 +123,6 @@ type InviteUsersToGroupResponse = variant { type RegisterUserArgs = record { username : text; - display_name : opt text; referral_code : opt text; public_key : blob; }; @@ -138,9 +137,6 @@ type RegisterUserResponse = variant { UsernameInvalid; UsernameTooShort : nat16; UsernameTooLong : nat16; - DisplayNameInvalid; - DisplayNameTooShort : nat16; - DisplayNameTooLong : nat16; CyclesBalanceTooLow; InternalError : text; PublicKeyInvalid : text; diff --git a/backend/canisters/local_user_index/api/src/updates/register_user.rs b/backend/canisters/local_user_index/api/src/updates/register_user.rs index f3bf4fbdaf..9a4207b7b3 100644 --- a/backend/canisters/local_user_index/api/src/updates/register_user.rs +++ b/backend/canisters/local_user_index/api/src/updates/register_user.rs @@ -6,7 +6,6 @@ use types::UserId; #[derive(CandidType, Serialize, Deserialize, Debug)] pub struct Args { pub username: String, - pub display_name: Option, pub referral_code: Option, pub public_key: Vec, } @@ -19,9 +18,6 @@ pub enum Response { UsernameInvalid, UsernameTooShort(u16), UsernameTooLong(u16), - DisplayNameInvalid, - DisplayNameTooShort(u16), - DisplayNameTooLong(u16), CyclesBalanceTooLow, InternalError(String), PublicKeyInvalid(String), diff --git a/backend/canisters/local_user_index/impl/src/updates/register_user.rs b/backend/canisters/local_user_index/impl/src/updates/register_user.rs index 122abd7043..46b2efcf0b 100644 --- a/backend/canisters/local_user_index/impl/src/updates/register_user.rs +++ b/backend/canisters/local_user_index/impl/src/updates/register_user.rs @@ -14,7 +14,7 @@ use user_index_canister::{Event as UserIndexEvent, JoinUserToGroup, UserRegister use utils::canister; use utils::canister::CreateAndInstallError; use utils::consts::{min_cycles_balance, CREATE_CANISTER_CYCLES_FEE}; -use utils::text_validation::{validate_display_name, validate_username, UsernameValidationError}; +use utils::text_validation::{validate_username, UsernameValidationError}; use x509_parser::prelude::FromDer; use x509_parser::x509::SubjectPublicKeyInfo; @@ -49,17 +49,7 @@ async fn register_user(args: Args) -> Response { { Ok(canister_id) => { let user_id = canister_id.into(); - mutate_state(|state| { - commit( - caller, - user_id, - args.username, - args.display_name, - wasm_version, - referral_code, - state, - ) - }); + mutate_state(|state| commit(caller, user_id, args.username, wasm_version, referral_code, state)); Success(SuccessResult { user_id, icp_account: default_ledger_account(user_id.into()), @@ -121,15 +111,6 @@ fn prepare(args: &Args, state: &mut RuntimeState) -> Result Err(UsernameValidationError::Invalid) => return Err(UsernameInvalid), }; - if let Some(display_name) = args.display_name.as_ref() { - match validate_display_name(display_name) { - Ok(_) => {} - Err(UsernameValidationError::TooShort(s)) => return Err(DisplayNameTooShort(s.min_length as u16)), - Err(UsernameValidationError::TooLong(l)) => return Err(DisplayNameTooLong(l.max_length as u16)), - Err(UsernameValidationError::Invalid) => return Err(DisplayNameInvalid), - }; - } - let openchat_bot_messages = if referral_code .as_ref() .filter(|c| matches!(c, ReferralCode::BtcMiami(_))) @@ -171,7 +152,6 @@ fn prepare(args: &Args, state: &mut RuntimeState) -> Result proposals_bot_canister_id: state.data.proposals_bot_canister_id, wasm_version: canister_wasm.version, username: args.username.clone(), - display_name: args.display_name.clone(), openchat_bot_messages, test_mode: state.data.test_mode, }; @@ -192,7 +172,6 @@ fn commit( principal: Principal, user_id: UserId, username: String, - display_name: Option, wasm_version: BuildVersion, referral_code: Option, state: &mut RuntimeState, @@ -205,7 +184,6 @@ fn commit( principal, user_id, username: username.clone(), - display_name: display_name.clone(), referred_by: referral_code.as_ref().and_then(|r| r.user()), }))); diff --git a/backend/canisters/user/CHANGELOG.md b/backend/canisters/user/CHANGELOG.md index 50f3ed6145..5c1bf9a917 100644 --- a/backend/canisters/user/CHANGELOG.md +++ b/backend/canisters/user/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - In modclub reports only show public message links ([#4847](https://github.com/open-chat-labs/open-chat/pull/4847)) - Add `local_user_index_canister_id` to group/community summaries ([#4857](https://github.com/open-chat-labs/open-chat/pull/4857)) - Switch to `c2c_send_message` when sending messages c2c to groups or channels ([#4895](https://github.com/open-chat-labs/open-chat/pull/4895)) +- Remove `display_name` from `init` args ([#4910](https://github.com/open-chat-labs/open-chat/pull/4910)) ## [[2.0.947](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.947-user)] - 2023-11-24 diff --git a/backend/canisters/user/api/src/lifecycle/init.rs b/backend/canisters/user/api/src/lifecycle/init.rs index ac6d82e735..702f046684 100644 --- a/backend/canisters/user/api/src/lifecycle/init.rs +++ b/backend/canisters/user/api/src/lifecycle/init.rs @@ -12,7 +12,6 @@ pub struct Args { pub proposals_bot_canister_id: CanisterId, pub wasm_version: BuildVersion, pub username: String, - pub display_name: Option, pub openchat_bot_messages: Vec, pub test_mode: bool, } diff --git a/backend/canisters/user/impl/src/lib.rs b/backend/canisters/user/impl/src/lib.rs index 02907d4ac0..bf3a3684fe 100644 --- a/backend/canisters/user/impl/src/lib.rs +++ b/backend/canisters/user/impl/src/lib.rs @@ -192,7 +192,6 @@ impl Data { notifications_canister_id: CanisterId, proposals_bot_canister_id: CanisterId, username: String, - display_name: Option, test_mode: bool, now: TimestampMillis, ) -> Data { @@ -213,7 +212,7 @@ impl Data { is_platform_moderator: false, hot_group_exclusions: HotGroupExclusions::default(), username: Timestamped::new(username, now), - display_name: Timestamped::new(display_name, now), + display_name: Timestamped::default(), bio: Timestamped::new("".to_string(), now), cached_group_summaries: None, storage_limit: 0, diff --git a/backend/canisters/user/impl/src/lifecycle/init.rs b/backend/canisters/user/impl/src/lifecycle/init.rs index 342903d20e..ae78feff93 100644 --- a/backend/canisters/user/impl/src/lifecycle/init.rs +++ b/backend/canisters/user/impl/src/lifecycle/init.rs @@ -22,7 +22,6 @@ fn init(args: Args) { args.notifications_canister_id, args.proposals_bot_canister_id, args.username, - args.display_name, args.test_mode, env.now(), ); diff --git a/backend/canisters/user_index/CHANGELOG.md b/backend/canisters/user_index/CHANGELOG.md index e74120cc14..92da5c30cd 100644 --- a/backend/canisters/user_index/CHANGELOG.md +++ b/backend/canisters/user_index/CHANGELOG.md @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Top up NNS neuron when users pay ICP for lifetime Diamond membership ([#4880](https://github.com/open-chat-labs/open-chat/pull/4880)) - Add `diamond_membership_status` to user summaries ([#4887](https://github.com/open-chat-labs/open-chat/pull/4887)) - Allow extending Diamond membership even if > 3 month remaining ([#4909](https://github.com/open-chat-labs/open-chat/pull/4909)) +- Require Diamond membership to set a display name ([#4910](https://github.com/open-chat-labs/open-chat/pull/4910)) ## [[2.0.952](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.952-user_index)] - 2023-11-28 diff --git a/backend/canisters/user_index/api/can.did b/backend/canisters/user_index/api/can.did index e3a864b24f..dbfe21e803 100644 --- a/backend/canisters/user_index/api/can.did +++ b/backend/canisters/user_index/api/can.did @@ -24,6 +24,7 @@ type SetDisplayNameArgs = record { type SetDisplayNameResponse = variant { Success; + Unauthorized; UserNotFound; DisplayNameInvalid; DisplayNameTooShort : nat16; diff --git a/backend/canisters/user_index/api/src/lib.rs b/backend/canisters/user_index/api/src/lib.rs index 5f1418c634..e5e44ff419 100644 --- a/backend/canisters/user_index/api/src/lib.rs +++ b/backend/canisters/user_index/api/src/lib.rs @@ -27,7 +27,6 @@ pub struct UserRegistered { pub principal: Principal, pub user_id: UserId, pub username: String, - pub display_name: Option, pub referred_by: Option, } diff --git a/backend/canisters/user_index/api/src/updates/set_display_name.rs b/backend/canisters/user_index/api/src/updates/set_display_name.rs index ad96bdde94..a710d70e97 100644 --- a/backend/canisters/user_index/api/src/updates/set_display_name.rs +++ b/backend/canisters/user_index/api/src/updates/set_display_name.rs @@ -9,6 +9,7 @@ pub struct Args { #[derive(CandidType, Serialize, Deserialize, Debug)] pub enum Response { Success, + Unauthorized, UserNotFound, DisplayNameInvalid, DisplayNameTooShort(u16), diff --git a/backend/canisters/user_index/impl/src/lib.rs b/backend/canisters/user_index/impl/src/lib.rs index 190a642944..fa8e712c0e 100644 --- a/backend/canisters/user_index/impl/src/lib.rs +++ b/backend/canisters/user_index/impl/src/lib.rs @@ -288,7 +288,6 @@ impl Data { proposals_bot_canister_id, proposals_bot_canister_id.into(), "ProposalsBot".to_string(), - None, 0, None, true, diff --git a/backend/canisters/user_index/impl/src/model/user.rs b/backend/canisters/user_index/impl/src/model/user.rs index 21226ecd13..92fafd3416 100644 --- a/backend/canisters/user_index/impl/src/model/user.rs +++ b/backend/canisters/user_index/impl/src/model/user.rs @@ -51,18 +51,16 @@ impl User { principal: Principal, user_id: UserId, username: String, - display_name: Option, now: TimestampMillis, referred_by: Option, is_bot: bool, ) -> User { - let display_name_upper = display_name.as_ref().map(|s| s.to_uppercase()); User { principal, user_id, username, - display_name, - display_name_upper, + display_name: None, + display_name_upper: None, date_created: now, date_updated: now, upgrade_in_progress: false, diff --git a/backend/canisters/user_index/impl/src/model/user_map.rs b/backend/canisters/user_index/impl/src/model/user_map.rs index f2d63f4a64..ef91a53570 100644 --- a/backend/canisters/user_index/impl/src/model/user_map.rs +++ b/backend/canisters/user_index/impl/src/model/user_map.rs @@ -41,13 +41,11 @@ impl UserMap { } } - #[allow(clippy::too_many_arguments)] pub fn register( &mut self, principal: Principal, user_id: UserId, username: String, - display_name: Option, now: TimestampMillis, referred_by: Option, is_bot: bool, @@ -55,7 +53,7 @@ impl UserMap { self.username_to_user_id.insert(&username, user_id); self.principal_to_user_id.insert(principal, user_id); - let user = User::new(principal, user_id, username, display_name, now, referred_by, is_bot); + let user = User::new(principal, user_id, username, now, referred_by, is_bot); self.users.insert(user_id, user); if let Some(ref_by) = referred_by { @@ -282,7 +280,6 @@ impl UserMap { user.principal, user.user_id, user.username.clone(), - user.display_name.clone(), user.date_created, None, false, @@ -346,9 +343,9 @@ mod tests { let user_id2: UserId = Principal::from_slice(&[3, 2]).into(); let user_id3: UserId = Principal::from_slice(&[3, 3]).into(); - user_map.register(principal1, user_id1, username1.clone(), None, 1, None, false); - user_map.register(principal2, user_id2, username2.clone(), None, 2, None, false); - user_map.register(principal3, user_id3, username3.clone(), None, 3, None, false); + user_map.register(principal1, user_id1, username1.clone(), 1, None, false); + user_map.register(principal2, user_id2, username2.clone(), 2, None, false); + user_map.register(principal3, user_id3, username3.clone(), 3, None, false); let principal_to_user_id: Vec<_> = user_map .principal_to_user_id @@ -385,7 +382,7 @@ mod tests { let user_id = Principal::from_slice(&[1, 1]).into(); - user_map.register(principal, user_id, username1, None, 1, None, false); + user_map.register(principal, user_id, username1, 1, None, false); if let Some(original) = user_map.get_by_principal(&principal) { let mut updated = original.clone(); diff --git a/backend/canisters/user_index/impl/src/updates/c2c_notify_events.rs b/backend/canisters/user_index/impl/src/updates/c2c_notify_events.rs index f7cf081535..dffdc9b623 100644 --- a/backend/canisters/user_index/impl/src/updates/c2c_notify_events.rs +++ b/backend/canisters/user_index/impl/src/updates/c2c_notify_events.rs @@ -31,15 +31,7 @@ fn handle_event(event: Event, state: &mut RuntimeState) { let caller: CanisterId = state.env.caller(); match event { - Event::UserRegistered(ev) => process_new_user( - ev.principal, - ev.username, - ev.display_name, - ev.user_id, - ev.referred_by, - caller, - state, - ), + Event::UserRegistered(ev) => process_new_user(ev.principal, ev.username, ev.user_id, ev.referred_by, caller, state), Event::UserJoinedGroup(ev) => { state.push_event_to_local_user_index( ev.user_id, @@ -89,7 +81,6 @@ fn handle_event(event: Event, state: &mut RuntimeState) { fn process_new_user( caller: Principal, username: String, - display_name: Option, user_id: UserId, referred_by: Option, local_user_index_canister_id: CanisterId, @@ -106,15 +97,10 @@ fn process_new_user( } }; - state.data.users.register( - caller, - user_id, - username.clone(), - display_name.clone(), - now, - referred_by, - false, - ); + state + .data + .users + .register(caller, user_id, username.clone(), now, referred_by, false); state.data.local_index_map.add_user(local_user_index_canister_id, user_id); diff --git a/backend/canisters/user_index/impl/src/updates/c2c_register_bot.rs b/backend/canisters/user_index/impl/src/updates/c2c_register_bot.rs index c036e2613f..f6b5929b47 100644 --- a/backend/canisters/user_index/impl/src/updates/c2c_register_bot.rs +++ b/backend/canisters/user_index/impl/src/updates/c2c_register_bot.rs @@ -47,7 +47,7 @@ fn c2c_register_bot_impl(args: Args, state: &mut RuntimeState) -> Response { state .data .users - .register(caller, user_id, args.username.clone(), None, now, None, true); + .register(caller, user_id, args.username.clone(), now, None, true); state.push_event_to_all_local_user_indexes( Event::UserRegistered(UserRegistered { diff --git a/backend/canisters/user_index/impl/src/updates/set_display_name.rs b/backend/canisters/user_index/impl/src/updates/set_display_name.rs index 3612e6c401..7dc8a466bb 100644 --- a/backend/canisters/user_index/impl/src/updates/set_display_name.rs +++ b/backend/canisters/user_index/impl/src/updates/set_display_name.rs @@ -15,21 +15,24 @@ fn set_display_name(args: Args) -> Response { fn set_display_name_impl(args: Args, state: &mut RuntimeState) -> Response { let caller = state.env.caller(); + if let Some(user) = state.data.users.get_by_principal(&caller) { + if let Some(display_name) = args.display_name.as_ref() { + match validate_display_name(display_name) { + Ok(_) => {} + Err(UsernameValidationError::TooShort(s)) => return DisplayNameTooShort(s.min_length as u16), + Err(UsernameValidationError::TooLong(l)) => return DisplayNameTooLong(l.max_length as u16), + Err(UsernameValidationError::Invalid) => return DisplayNameInvalid, + }; + } - if let Some(display_name) = args.display_name.as_ref() { - match validate_display_name(display_name) { - Ok(_) => {} - Err(UsernameValidationError::TooShort(s)) => return DisplayNameTooShort(s.min_length as u16), - Err(UsernameValidationError::TooLong(l)) => return DisplayNameTooLong(l.max_length as u16), - Err(UsernameValidationError::Invalid) => return DisplayNameInvalid, - }; - } + let now = state.env.now(); + if !user.diamond_membership_details.is_active(now) && user.display_name.is_none() { + return Unauthorized; + } - if let Some(user) = state.data.users.get_by_principal(&caller) { let mut user_to_update = user.clone(); user_to_update.display_name = args.display_name.clone(); let user_id = user.user_id; - let now = state.env.now(); match state.data.users.update(user_to_update, now) { UpdateUserResult::Success => { state.push_event_to_local_user_index( diff --git a/backend/integration_tests/src/client/local_user_index.rs b/backend/integration_tests/src/client/local_user_index.rs index 4976f419a4..a1237a5063 100644 --- a/backend/integration_tests/src/client/local_user_index.rs +++ b/backend/integration_tests/src/client/local_user_index.rs @@ -35,7 +35,6 @@ pub mod happy_path { canister_id, &local_user_index_canister::register_user::Args { username: principal_to_username(principal), - display_name: None, referral_code, public_key, }, diff --git a/backend/integration_tests/src/register_user_tests.rs b/backend/integration_tests/src/register_user_tests.rs index 31ac42c3bd..1acc0fe663 100644 --- a/backend/integration_tests/src/register_user_tests.rs +++ b/backend/integration_tests/src/register_user_tests.rs @@ -46,7 +46,6 @@ fn register_user_with_duplicate_username_appends_suffix() { canister_ids.local_user_index, &local_user_index_canister::register_user::Args { username: username.clone(), - display_name: None, referral_code: None, public_key, }, diff --git a/backend/integration_tests/src/update_profile_tests.rs b/backend/integration_tests/src/update_profile_tests.rs index 07f8f86397..67d45732e4 100644 --- a/backend/integration_tests/src/update_profile_tests.rs +++ b/backend/integration_tests/src/update_profile_tests.rs @@ -36,9 +36,13 @@ fn update_username_succeeds() { #[test] fn update_display_name_succeeds() { let mut wrapper = ENV.deref().get(); - let TestEnv { env, canister_ids, .. } = wrapper.env(); + let TestEnv { + env, + canister_ids, + controller, + } = wrapper.env(); - let user = client::local_user_index::happy_path::register_user(env, canister_ids.local_user_index); + let user = client::register_diamond_user(env, canister_ids, *controller); env.advance_time(Duration::from_secs(10)); @@ -59,3 +63,29 @@ fn update_display_name_succeeds() { let updates = client::user::happy_path::updates(env, &user, now - 1); assert_eq!(updates.unwrap().display_name, OptionUpdate::SetToSome(display_name)); } + +#[test] +fn update_display_name_unauthorized_if_not_diamond_member() { + let mut wrapper = ENV.deref().get(); + let TestEnv { env, canister_ids, .. } = wrapper.env(); + + let user = client::local_user_index::happy_path::register_user(env, canister_ids.local_user_index); + + env.advance_time(Duration::from_secs(10)); + + let display_name = random_string(); + + let response = client::user_index::set_display_name( + env, + user.principal, + canister_ids.user_index, + &user_index_canister::set_display_name::Args { + display_name: Some(display_name.clone()), + }, + ); + + assert!(matches!( + response, + user_index_canister::set_display_name::Response::Unauthorized + )); +} diff --git a/upgrade_order.md b/upgrade_order.md index ba97a17d96..a30c736119 100644 --- a/upgrade_order.md +++ b/upgrade_order.md @@ -1,3 +1,5 @@ Group/Community -> User/ProposalsBot -Because User and ProposalsBot canisters now use the new `c2c_send_message` endpoint which need to be released first. \ No newline at end of file +Because User and ProposalsBot canisters now use the new `c2c_send_message` endpoint which need to be released first. + +UserIndex -> LocalUserIndex because `display_name` has been removed from `UserRegistered` event \ No newline at end of file From d335c121ec0da3f9f5ed091fb49a42a1b6612099 Mon Sep 17 00:00:00 2001 From: Julian Jelfs Date: Mon, 4 Dec 2023 11:04:33 +0000 Subject: [PATCH 11/14] Lifetime membership (#4905) --- frontend/app/public/assets/diamond.svg | 17 ++++ .../app/public/assets/lifetime_diamond.svg | 15 +++ .../src/components/DisplayNameInput.svelte | 39 +++++--- frontend/app/src/components/FindUser.svelte | 12 +-- .../app/src/components/SelectChatModal.svelte | 31 +++--- .../src/components/home/AccessGateIcon.svelte | 15 +-- .../app/src/components/home/ChatEvent.svelte | 12 ++- .../app/src/components/home/ChatList.svelte | 8 +- .../src/components/home/ChatMessage.svelte | 32 +++--- .../src/components/home/ChatSummary.svelte | 52 +++++++--- .../components/home/CurrentChatHeader.svelte | 14 ++- .../src/components/home/PrizeContent.svelte | 10 +- .../components/home/groupdetails/User.svelte | 20 ++-- .../src/components/home/nav/MainMenu.svelte | 15 ++- .../home/profile/CommunityProfile.svelte | 1 + .../components/home/profile/ReferUsers.svelte | 8 +- .../home/profile/UserProfile.svelte | 6 +- .../home/profile/ViewUserProfile.svelte | 22 ++--- .../src/components/home/upgrade/Expiry.svelte | 24 +++-- .../components/home/upgrade/Features.svelte | 10 ++ .../components/home/upgrade/Payment.svelte | 99 ++++++++++++++----- .../components/home/upgrade/Upgrade.svelte | 20 +++- .../app/src/components/icons/Diamond.svelte | 67 +++++++++++++ .../src/components/register/Register.svelte | 30 ++---- frontend/app/src/i18n/cn.json | 16 +-- frontend/app/src/i18n/de.json | 16 +-- frontend/app/src/i18n/en.json | 12 ++- frontend/app/src/i18n/es.json | 16 +-- frontend/app/src/i18n/fr.json | 16 +-- frontend/app/src/i18n/it.json | 16 +-- frontend/app/src/i18n/iw.json | 16 +-- frontend/app/src/i18n/jp.json | 16 +-- frontend/app/src/i18n/ru.json | 16 +-- frontend/app/src/i18n/vi.json | 16 +-- frontend/app/src/styles/mixins.scss | 8 -- .../src/services/community/candid/types.d.ts | 23 +++-- .../src/services/group/candid/types.d.ts | 23 +++-- .../src/services/groupIndex/candid/types.d.ts | 23 +++-- .../services/localUserIndex/candid/types.d.ts | 23 +++-- .../localUserIndex/localUserIndex.client.ts | 27 +++-- .../services/notifications/candid/types.d.ts | 23 +++-- .../src/services/online/candid/types.d.ts | 23 +++-- .../src/services/openchatAgent.ts | 7 +- .../services/proposalsBot/candid/types.d.ts | 23 +++-- .../src/services/registry/candid/types.d.ts | 23 +++-- .../services/storageBucket/candid/types.d.ts | 23 +++-- .../services/storageIndex/candid/types.d.ts | 23 +++-- .../src/services/user/candid/types.d.ts | 23 +++-- .../src/services/userIndex/candid/idl.d.ts | 6 ++ .../src/services/userIndex/candid/idl.js | 77 ++++++++------- .../src/services/userIndex/candid/types.d.ts | 49 +++++---- .../src/services/userIndex/mappers.ts | 70 +++++++++++-- .../services/userIndex/userIndex.client.ts | 14 ++- frontend/openchat-agent/src/utils/caching.ts | 17 ++++ .../openchat-agent/src/utils/userCache.ts | 18 ++-- frontend/openchat-client/src/liveState.ts | 10 +- frontend/openchat-client/src/openchat.ts | 61 +++++++----- .../openchat-client/src/stores/diamond.ts | 29 +++--- frontend/openchat-client/src/stores/user.ts | 6 +- .../openchat-client/src/utils/chat.spec.ts | 2 +- .../openchat-client/src/utils/user.spec.ts | 10 +- frontend/openchat-client/src/utils/user.ts | 2 +- frontend/openchat-shared/src/constants.ts | 4 + .../openchat-shared/src/domain/user/user.ts | 20 ++-- .../src/domain/user/user.utils.spec.ts | 10 +- frontend/openchat-shared/src/domain/worker.ts | 1 - frontend/openchat-worker/src/worker.ts | 2 +- 67 files changed, 948 insertions(+), 490 deletions(-) create mode 100644 frontend/app/public/assets/diamond.svg create mode 100644 frontend/app/public/assets/lifetime_diamond.svg create mode 100644 frontend/app/src/components/icons/Diamond.svelte diff --git a/frontend/app/public/assets/diamond.svg b/frontend/app/public/assets/diamond.svg new file mode 100644 index 0000000000..44419127d9 --- /dev/null +++ b/frontend/app/public/assets/diamond.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/app/public/assets/lifetime_diamond.svg b/frontend/app/public/assets/lifetime_diamond.svg new file mode 100644 index 0000000000..58a2d387fe --- /dev/null +++ b/frontend/app/public/assets/lifetime_diamond.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/app/src/components/DisplayNameInput.svelte b/frontend/app/src/components/DisplayNameInput.svelte index d01fc6b228..838fa39dff 100644 --- a/frontend/app/src/components/DisplayNameInput.svelte +++ b/frontend/app/src/components/DisplayNameInput.svelte @@ -1,11 +1,13 @@ - - - +{#if $isDiamond || originalDisplayName !== undefined} + + + +{:else} +
+ +
+{/if} + + diff --git a/frontend/app/src/components/FindUser.svelte b/frontend/app/src/components/FindUser.svelte index e371288112..b5d346e742 100644 --- a/frontend/app/src/components/FindUser.svelte +++ b/frontend/app/src/components/FindUser.svelte @@ -11,6 +11,7 @@ import { iconSize } from "../stores/iconSize"; import type { OpenChat } from "openchat-client"; import FilteredUsername from "./FilteredUsername.svelte"; + import Diamond from "./icons/Diamond.svelte"; const client = getContext("client"); @@ -105,11 +106,12 @@ size={AvatarSize.Default} />
-

+

+

@@ -172,7 +174,9 @@ color: var(--txt); padding: $sp4; margin: 0 0 $sp3 0; - transition: background-color ease-in-out 100ms, border-color ease-in-out 100ms; + transition: + background-color ease-in-out 100ms, + border-color ease-in-out 100ms; cursor: pointer; gap: 12px; @@ -195,10 +199,6 @@ flex-direction: column; padding: 0 5px; - .diamond { - @include diamond(); - } - .username { font-weight: 200; color: var(--txt-light); diff --git a/frontend/app/src/components/SelectChatModal.svelte b/frontend/app/src/components/SelectChatModal.svelte index 3d5e03a5a7..52c6ea4d3a 100644 --- a/frontend/app/src/components/SelectChatModal.svelte +++ b/frontend/app/src/components/SelectChatModal.svelte @@ -6,6 +6,7 @@ ChatSummary, CommunityIdentifier, CommunitySummary, + DiamondMembershipStatus, DirectChatSummary, GlobalState, MultiUserChat, @@ -27,6 +28,7 @@ import HeartOutline from "svelte-material-icons/HeartOutline.svelte"; import Search from "./Search.svelte"; import { compareBigints } from "../utils/bigints"; + import Diamond from "./icons/Diamond.svelte"; const client = getContext("client"); const dispatch = createEventDispatcher(); @@ -42,6 +44,7 @@ id: ChatIdentifier; userId: string | undefined; name: string; + diamondStatus: DiamondMembershipStatus["kind"]; avatarUrl: string; description: string; username: string | undefined; @@ -72,7 +75,7 @@ $: { buildListOfTargets($globalState, $now, $selectedChatId, searchTermLower).then( - (t) => (targets = t) + (t) => (targets = t), ); } $: noTargets = getNumberOfTargets(targets) === 0; @@ -88,10 +91,10 @@ async function targetsFromChatList( now: number, chats: ChatSummary[], - selectedChatId: ChatIdentifier | undefined + selectedChatId: ChatIdentifier | undefined, ): Promise { return Promise.all( - filterChatSelection(chats, selectedChatId).map((c) => normaliseChatSummary(now, c)) + filterChatSelection(chats, selectedChatId).map((c) => normaliseChatSummary(now, c)), ); } @@ -101,7 +104,7 @@ (c) => searchTerm === "" || c.name.toLowerCase().includes(searchTerm) || - c.username?.toLowerCase()?.includes(searchTerm) + c.username?.toLowerCase()?.includes(searchTerm), ) .sort((a, b) => compareBigints(b.lastUpdated, a.lastUpdated)); } @@ -125,7 +128,7 @@ global: GlobalState, now: number, selectedChatId: ChatIdentifier | undefined, - searchTerm: string + searchTerm: string, ): Promise { let targets: ShareTo = { directChats: [], @@ -143,7 +146,7 @@ const groupChats = await targetsFromChatList(now, group, selectedChatId); const favourites = await targetsFromChatList(now, favs, selectedChatId); const communities = await Promise.all( - global.communities.values().map((c) => normaliseCommunity(now, selectedChatId, c)) + global.communities.values().map((c) => normaliseCommunity(now, selectedChatId, c)), ); return { directChats: chatMatchesSearch(directChats, searchTerm), @@ -158,10 +161,10 @@ async function normaliseCommunity( now: number, selectedChatId: ChatIdentifier | undefined, - { id, name, avatar, description, channels, lastUpdated }: CommunitySummary + { id, name, avatar, description, channels, lastUpdated }: CommunitySummary, ): Promise { const normalisedChannels = await Promise.all( - filterChatSelection(channels, selectedChatId).map((c) => normaliseChatSummary(now, c)) + filterChatSelection(channels, selectedChatId).map((c) => normaliseChatSummary(now, c)), ); return { kind: "community", @@ -183,7 +186,8 @@ kind: "chat", id: chatSummary.id, userId: chatSummary.them.userId, - name: client.displayNameAndIcon(them), + name: client.displayName(them), + diamondStatus: them.diamondStatus, avatarUrl: client.userAvatarUrl(them), description, username: "@" + them.username, @@ -196,6 +200,7 @@ id: chatSummary.id, userId: undefined, name: chatSummary.name, + diamondStatus: "inactive" as DiamondMembershipStatus["kind"], avatarUrl: client.groupAvatarUrl(chatSummary), description: buildGroupChatDescription(chatSummary), username: undefined, @@ -206,7 +211,7 @@ async function buildDirectChatDescription( chat: DirectChatSummary, - now: number + now: number, ): Promise { return client.getLastOnlineDate(chat.them.userId, now).then((lastOnline) => { if (lastOnline !== undefined && lastOnline !== 0) { @@ -231,12 +236,12 @@ function filterChatSelection( chats: ChatSummary[], - selectedChatId: ChatIdentifier | undefined + selectedChatId: ChatIdentifier | undefined, ): ChatSummary[] { return chats.filter( (c) => !chatIdentifiersEqual(selectedChatId, c.id) && - client.canSendMessage(c.id, "message", "text") + client.canSendMessage(c.id, "message", "text"), ); } @@ -300,6 +305,7 @@ {target.name} + {#if target.username !== undefined} {target.username} {/if} @@ -500,6 +506,7 @@ @include font(book, normal, fs-100); display: flex; + align-items: center; gap: $sp2; .username { diff --git a/frontend/app/src/components/home/AccessGateIcon.svelte b/frontend/app/src/components/home/AccessGateIcon.svelte index 20bcad7a29..6bf643272d 100644 --- a/frontend/app/src/components/home/AccessGateIcon.svelte +++ b/frontend/app/src/components/home/AccessGateIcon.svelte @@ -11,6 +11,7 @@ } from "openchat-client"; import { createEventDispatcher, getContext } from "svelte"; import type { Alignment, Position } from "../../utils/alignment"; + import Diamond from "../icons/Diamond.svelte"; export let gate: AccessGate; export let position: Position = "top"; @@ -25,7 +26,7 @@ function formatParams( gate: AccessGate, - tokenDetails: CryptocurrencyDetails | undefined + tokenDetails: CryptocurrencyDetails | undefined, ): string { const parts = []; if (isNeuronGate(gate)) { @@ -33,7 +34,7 @@ parts.push( `${$_("access.minDissolveDelayN", { values: { n: gate.minDissolveDelay / (24 * 60 * 60 * 1000) }, - })}` + })}`, ); } if (gate.minStakeE8s) { @@ -43,17 +44,17 @@ n: client.formatTokens( BigInt(gate.minStakeE8s), 0, - tokenDetails?.decimals ?? 8 + tokenDetails?.decimals ?? 8, ), }, - })}` + })}`, ); } } else if (isPaymentGate(gate)) { parts.push( `${$_("access.amountN", { values: { n: client.formatTokens(gate.amount, 0, tokenDetails?.decimals ?? 8) }, - })}` + })}`, ); } return parts.length > 0 ? ` (${parts.join(", ")})` : ""; @@ -63,7 +64,9 @@ {#if gate.kind !== "no_gate"} {#if gate.kind === "diamond_gate"} -
dispatch("upgrade")} slot="target" class="diamond">💎
+
dispatch("upgrade")} slot="target" class="diamond"> + +
{$_("access.diamondGateInfo")} diff --git a/frontend/app/src/components/home/ChatEvent.svelte b/frontend/app/src/components/home/ChatEvent.svelte index ceb943dc08..8edc164aa8 100644 --- a/frontend/app/src/components/home/ChatEvent.svelte +++ b/frontend/app/src/components/home/ChatEvent.svelte @@ -82,7 +82,7 @@ displayName: user.displayName, updated: BigInt(0), suspended: false, - diamond: false, + diamondStatus: "inactive", }; } @@ -94,7 +94,7 @@ client.deleteFailedMessage( chatId, event as EventWrapper, - threadRootMessage?.messageIndex + threadRootMessage?.messageIndex, ); } @@ -108,7 +108,7 @@ page( `${routeForChatIdentifier($chatListScope.kind, chatId)}/${ event.event.messageIndex - }` + }`, ); } else { client.openThread(event as EventWrapper, true); @@ -272,7 +272,11 @@ event={event.event} timestamp={event.timestamp} /> {:else if event.event.kind === "events_ttl_updated"} - + {:else if event.event.kind === "chat_frozen"} {:else if event.event.kind === "chat_unfrozen"} diff --git a/frontend/app/src/components/home/ChatList.svelte b/frontend/app/src/components/home/ChatList.svelte index 9cd38f57de..cd0124f79b 100644 --- a/frontend/app/src/components/home/ChatList.svelte +++ b/frontend/app/src/components/home/ChatList.svelte @@ -36,6 +36,7 @@ import ButtonGroup from "../ButtonGroup.svelte"; import FilteredUsername from "../FilteredUsername.svelte"; import ChatListSectionButton from "./ChatListSectionButton.svelte"; + import Diamond from "../icons/Diamond.svelte"; const client = getContext("client"); @@ -300,10 +301,11 @@ avatarUrl={client.userAvatarUrl(user)} on:click={() => chatWith(user.userId)}>
-

+

+

("client"); const dispatch = createEventDispatcher(); @@ -128,7 +129,10 @@ $: isProposal = msg.content.kind === "proposal_content"; $: isPrize = msg.content.kind === "prize_content"; $: isPrizeWinner = msg.content.kind === "prize_winner_content"; - $: inert = msg.content.kind === "deleted_content" || msg.content.kind === "blocked_content" || collapsed; + $: inert = + msg.content.kind === "deleted_content" || + msg.content.kind === "blocked_content" || + collapsed; $: undeletingMessagesStore = client.undeletingMessagesStore; $: undeleting = $undeletingMessagesStore.has(msg.messageId); $: showChatMenu = (!inert || canRevealDeleted || canRevealBlocked) && !readonly; @@ -258,7 +262,7 @@ reaction, user.username, user.displayName, - kind + kind, ) .then((success) => { if (success && kind === "add") { @@ -321,7 +325,7 @@ messageWrapperWidth, msgBubblePaddingWidth, window.innerHeight, - maxWidthFraction + maxWidthFraction, ); mediaCalculatedHeight = targetMediaDimensions.height; msgBubbleCalculatedWidth = targetMediaDimensions.width + msgBubblePaddingWidth; @@ -332,7 +336,7 @@ new CustomEvent("profile-clicked", { detail: { userId: msg.sender, chatButton: multiUserChat, inGlobalContext: false }, bubbles: true, - }) + }), ); } @@ -346,7 +350,7 @@ msg.messageId, msg.messageIndex, ev.detail.answerIndex, - ev.detail.type + ev.detail.type, ) .then((success) => { if (!success) { @@ -467,13 +471,10 @@ {#if first && !isProposal && !isPrize}
-

+

{senderDisplayName}

+ {#if senderTyping} @@ -796,8 +797,11 @@ .message-bubble { $radius: var(--currentChat-msg-r1); $inner-radius: var(--currentChat-msg-r2); - transition: box-shadow ease-in-out 200ms, background-color ease-in-out 200ms, - border ease-in-out 300ms, transform ease-in-out 200ms; + transition: + box-shadow ease-in-out 200ms, + background-color ease-in-out 200ms, + border ease-in-out 300ms, + transform ease-in-out 200ms; position: relative; padding: toRem(8) toRem(12) toRem(8) toRem(12); background-color: var(--currentChat-msg-bg); @@ -825,10 +829,6 @@ &.crypto { color: #fff; } - - &.diamond { - @include diamond(); - } } &:not(.readByMe) { diff --git a/frontend/app/src/components/home/ChatSummary.svelte b/frontend/app/src/components/home/ChatSummary.svelte index 600bef7f39..efac667798 100644 --- a/frontend/app/src/components/home/ChatSummary.svelte +++ b/frontend/app/src/components/home/ChatSummary.svelte @@ -1,7 +1,13 @@ @@ -78,7 +79,8 @@ size={AvatarSize.Default} />
- {client.displayNameAndIcon($userStore[userId])} + {client.displayName($userStore[userId])} +
{/each} diff --git a/frontend/app/src/components/home/profile/UserProfile.svelte b/frontend/app/src/components/home/profile/UserProfile.svelte index 45dc159d9d..90ab6b4c7a 100644 --- a/frontend/app/src/components/home/profile/UserProfile.svelte +++ b/frontend/app/src/components/home/profile/UserProfile.svelte @@ -96,6 +96,7 @@ $: userMetrics = client.userMetrics; $: notificationStatus = client.notificationStatus; $: isDiamond = client.isDiamond; + $: isLifetimeDiamond = client.isLifetimeDiamond; $: canExtendDiamond = client.canExtendDiamond; $: { setLocale(selectedLocale); @@ -302,6 +303,7 @@ dispatch("upgrade")} small >{$_("upgrade.button")} + {:else if $isLifetimeDiamond} + {$_("upgrade.lifetimeMessage")} {:else} @@ -503,7 +507,7 @@
{#if selectedCommunity !== undefined} - + {/if} {/if} diff --git a/frontend/app/src/components/home/profile/ViewUserProfile.svelte b/frontend/app/src/components/home/profile/ViewUserProfile.svelte index 4513494431..1fc8910751 100644 --- a/frontend/app/src/components/home/profile/ViewUserProfile.svelte +++ b/frontend/app/src/components/home/profile/ViewUserProfile.svelte @@ -18,6 +18,7 @@ import { mobileWidth } from "../../../stores/screenDimensions"; import { rightPanelHistory } from "../../../stores/rightPanel"; import { toastStore } from "../../../stores/toast"; + import Diamond from "../../icons/Diamond.svelte"; const client = getContext("client"); const dispatch = createEventDispatcher(); @@ -31,6 +32,7 @@ let user: UserSummary | undefined; let lastOnline: number | undefined; + $: diamondStatus = user?.diamondStatus; $: createdUser = client.user; $: platformModerator = client.platformModerator; $: me = userId === $createdUser.userId; @@ -47,7 +49,6 @@ $: joined = profile !== undefined ? `${$_("joined")} ${formatDate(profile.created)}` : undefined; $: isPremium = profile?.isPremium ?? false; - $: diamond = user?.diamond ?? false; $: phoneIsVerified = profile?.phoneIsVerified ?? false; $: selectedChat = client.selectedChatStore; $: blockedUsers = client.blockedUsers; @@ -61,21 +62,21 @@ username: profile?.username ?? "", displayName: profile?.displayName, }, - inGlobalContext ? undefined : $communityMembers + inGlobalContext ? undefined : $communityMembers, ); $: canBlock = canBlockUser( $selectedChat, $selectedCommunity, $blockedUsers, $currentChatBlockedUsers, - $currentCommunityBlockedUsers + $currentCommunityBlockedUsers, ); $: canUnblock = canUnblockUser( $selectedChat, $selectedCommunity, $blockedUsers, $currentChatBlockedUsers, - $currentCommunityBlockedUsers + $currentCommunityBlockedUsers, ); onMount(async () => { @@ -146,7 +147,7 @@ client .unblockCommunityUser($selectedCommunity.id, userId) .then((success) => - afterBlock(success, "unblockUserSucceeded", "unblockUserFailed") + afterBlock(success, "unblockUserSucceeded", "unblockUserFailed"), ); onClose(); return; @@ -158,7 +159,7 @@ community: CommunitySummary | undefined, blockedUsers: Set, blockedChatUsers: Set, - blockedCommunityUsers: Set + blockedCommunityUsers: Set, ) { if (me || inGlobalContext) return false; @@ -178,7 +179,7 @@ community: CommunitySummary | undefined, blockedUsers: Set, blockedChatUsers: Set, - blockedCommunityUsers: Set + blockedCommunityUsers: Set, ) { if (me || inGlobalContext) return false; if (chat !== undefined) { @@ -236,9 +237,10 @@ on:close>
- + {displayName} + @{profile.username} @@ -363,10 +365,6 @@ display: inline; overflow-wrap: anywhere; - .diamond { - @include diamond(); - } - .username { font-weight: 200; color: var(--txt-light); diff --git a/frontend/app/src/components/home/upgrade/Expiry.svelte b/frontend/app/src/components/home/upgrade/Expiry.svelte index e5eff1ef41..8e6800da4e 100644 --- a/frontend/app/src/components/home/upgrade/Expiry.svelte +++ b/frontend/app/src/components/home/upgrade/Expiry.svelte @@ -8,7 +8,7 @@ export let extendBy: DiamondMembershipDuration | undefined = undefined; - $: diamondMembership = client.diamondMembership; + $: diamondStatus = client.diamondStatus; $: extendByMs = durationToMs(extendBy); let expiresIn: string | undefined = undefined; @@ -16,26 +16,30 @@ let extendTo: string | undefined = undefined; $: { - if ($diamondMembership !== undefined) { - const expiresAtMs = Number($diamondMembership.expiresAt); + if ($diamondStatus.kind === "active") { expiresIn = client.diamondExpiresIn($now, $locale); - expiresAt = client.toDateString(new Date(expiresAtMs)); - if (extendByMs !== undefined) { - extendTo = client.toDateString(new Date(expiresAtMs + extendByMs)); + if (extendBy !== "lifetime") { + const expiresAtMs = Number($diamondStatus.expiresAt); + expiresAt = client.toDateString(new Date(expiresAtMs)); + if (extendByMs !== undefined) { + extendTo = client.toDateString(new Date(expiresAtMs + extendByMs)); + } + } else { + extendTo = $_("upgrade.lifetime"); } } } function durationToMs(duration: DiamondMembershipDuration | undefined): number | undefined { - if (duration !== undefined) { + if (duration !== undefined && duration !== "lifetime") { return client.diamondDurationToMs(duration); } return undefined; } -{#if $diamondMembership !== undefined} +{#if $diamondStatus.kind !== "inactive"}

{$_("upgrade.expiryMessage", { values: { relative: expiresIn } })} @@ -46,10 +50,10 @@ {#if extendTo !== undefined} - {$_("upgrade.extendTo", { values: { date: extendTo } })} + {$_("upgrade.extendTo")} - {extendTo} + {extendTo}. {/if}

diff --git a/frontend/app/src/components/home/upgrade/Features.svelte b/frontend/app/src/components/home/upgrade/Features.svelte index cfe48977c6..42d9b600ea 100644 --- a/frontend/app/src/components/home/upgrade/Features.svelte +++ b/frontend/app/src/components/home/upgrade/Features.svelte @@ -178,6 +178,16 @@
+ +
{$_("upgrade.displayNames")}
+
+ +
+
+ +
+
+
{$_("upgrade.airdrops")}
diff --git a/frontend/app/src/components/home/upgrade/Payment.svelte b/frontend/app/src/components/home/upgrade/Payment.svelte index 75f59ecef2..6188cca451 100644 --- a/frontend/app/src/components/home/upgrade/Payment.svelte +++ b/frontend/app/src/components/home/upgrade/Payment.svelte @@ -2,7 +2,7 @@ import { _ } from "svelte-i18n"; import Button from "../../Button.svelte"; import ErrorMessage from "../../ErrorMessage.svelte"; - import { createEventDispatcher, getContext } from "svelte"; + import { createEventDispatcher, getContext, onMount } from "svelte"; import Footer from "./Footer.svelte"; import Loading from "../../Loading.svelte"; import Congratulations from "./Congratulations.svelte"; @@ -10,20 +10,25 @@ type DiamondMembershipDuration, type OpenChat, E8S_PER_TOKEN, - ICP_SYMBOL, - LEDGER_CANISTER_ICP, + type DiamondMembershipFees, } from "openchat-client"; import AccountInfo from "../AccountInfo.svelte"; import { mobileWidth } from "../../../stores/screenDimensions"; import Checkbox from "../../Checkbox.svelte"; import { toastStore } from "../../../stores/toast"; import Expiry from "./Expiry.svelte"; + import Diamond from "../../icons/Diamond.svelte"; + import type { RemoteData } from "../../../utils/remoteData"; export let accountBalance = 0; export let error: string | undefined; export let confirming = false; export let confirmed = false; export let refreshingBalance = false; + export let ledger: string; + + type FeeKey = keyof Omit; + type FeeData = RemoteData, string>; const client = getContext("client"); @@ -32,17 +37,22 @@ { index: 0, duration: $_("upgrade.oneMonth"), - amount: 0.2, + fee: "oneMonth", }, { index: 1, duration: $_("upgrade.threeMonths"), - amount: 0.5, + fee: "threeMonths", }, { index: 2, duration: $_("upgrade.oneYear"), - amount: 1.5, + fee: "oneYear", + }, + { + index: 3, + duration: $_("upgrade.lifetime"), + fee: "lifetime", }, ]; @@ -52,15 +62,17 @@ type Option = { index: number; duration: string; - amount: number; + fee: FeeKey; }; - const token = ICP_SYMBOL; - const ledger = LEDGER_CANISTER_ICP; + let diamondFees: FeeData = { + kind: "idle", + }; $: user = client.user; $: icpBalance = accountBalance / E8S_PER_TOKEN; //balance in the user's account expressed as ICP - $: toPay = selectedOption?.amount ?? 0; + $: toPayE8s = amountInE8s(tokenDetails.symbol, diamondFees, selectedOption); + $: toPay = amount(toPayE8s); $: insufficientFunds = toPay - icpBalance > 0.0001; //we need to account for the fact that js cannot do maths $: cryptoLookup = client.cryptoLookup; $: tokenDetails = $cryptoLookup[ledger]; @@ -71,8 +83,20 @@ 0: "one_month", 1: "three_months", 2: "one_year", + 3: "lifetime", }; + function amount(e8s: bigint): number { + return Number(e8s) / E8S_PER_TOKEN; + } + + function amountInE8s(symbol: string, fees: FeeData, option: Option | undefined): bigint { + if (fees.kind !== "success" || option === undefined) { + return 0n; + } + return fees.data[symbol as "ICP" | "CHAT"][option.fee] ?? 0n; + } + function cancel() { dispatch("cancel"); } @@ -81,17 +105,15 @@ dispatch("features"); } - function expectedPrice(): bigint { - if (selectedOption !== undefined) { - return BigInt(selectedOption.amount * E8S_PER_TOKEN); - } - return BigInt(options[0].amount * E8S_PER_TOKEN); - } - function confirm() { confirming = true; client - .payForDiamondMembership(token, selectedDuration, autoRenew, expectedPrice()) + .payForDiamondMembership( + tokenDetails.symbol, + selectedDuration, + autoRenew && selectedDuration !== "lifetime", + toPayE8s, + ) .then((success) => { if (success) { confirmed = true; @@ -101,6 +123,21 @@ }) .finally(() => (confirming = false)); } + + onMount(() => { + diamondFees = { kind: "loading" }; + client + .diamondMembershipFees() + .then((fees) => { + diamondFees = { + kind: "success", + data: client.toRecord(fees, (f) => f.token), + }; + }) + .catch((err) => { + diamondFees = { kind: "error", error: err }; + }); + });
@@ -114,12 +151,23 @@
{#each options as option}
(selectedOption = option)}> -

{option.duration}

-

{`${option.amount} ICP`}

+
+

{option.duration}

+

+ {`${amount( + amountInE8s(tokenDetails.symbol, diamondFees, option), + )} ${tokenDetails.symbol}`} +

+
+ {#if option.index === 3} + + {/if}
{/each}
@@ -134,7 +182,8 @@ on:change={() => (autoRenew = !autoRenew)} label={$_("upgrade.autorenew")} align={"start"} - checked={autoRenew}> + disabled={selectedDuration === "lifetime"} + checked={autoRenew && selectedDuration !== "lifetime"}>
{$_("upgrade.autorenew")}
{$_("upgrade.paymentSmallprint")} @@ -147,7 +196,7 @@ {/if} - {$_("howToBuyToken", { values: { token } })} + {$_("howToBuyToken", { values: { token: tokenDetails.symbol } })} {#if error} @@ -208,6 +257,9 @@ margin-bottom: $sp4; cursor: pointer; transition: background-color 250ms ease-in-out; + display: flex; + justify-content: space-between; + align-items: center; &.selected { background-color: var(--primary); @@ -220,7 +272,8 @@ @include mobile() { text-align: center; - padding: 12px $sp4; + padding: 10px $sp4; + margin-bottom: $sp3; } } diff --git a/frontend/app/src/components/home/upgrade/Upgrade.svelte b/frontend/app/src/components/home/upgrade/Upgrade.svelte index e222497fee..d2895b380e 100644 --- a/frontend/app/src/components/home/upgrade/Upgrade.svelte +++ b/frontend/app/src/components/home/upgrade/Upgrade.svelte @@ -8,9 +8,11 @@ import { LEDGER_CANISTER_ICP } from "openchat-client"; import { getContext, onMount } from "svelte"; import BalanceWithRefresh from "../BalanceWithRefresh.svelte"; + import Diamond from "../../icons/Diamond.svelte"; + import CryptoSelector from "../CryptoSelector.svelte"; const client = getContext("client"); - const ledger: string = LEDGER_CANISTER_ICP; + let ledger: string = LEDGER_CANISTER_ICP; let step: "features" | "payment" = "features"; let error: string | undefined; @@ -43,10 +45,11 @@ - +
{#if !confirming && !confirmed}
+ {#if step === "features"} {#if $canExtendDiamond} {$_("upgrade.extend")} @@ -56,7 +59,11 @@ {$_("upgrade.featuresTitle")} {/if} {:else if step === "payment"} - {$_("upgrade.paymentTitle")} +
+ ["chat", "icp"].includes(t.symbol.toLowerCase())} /> +
{/if}
{#if step === "payment"} @@ -84,6 +91,7 @@ bind:confirmed bind:confirming bind:refreshingBalance + {ledger} {error} accountBalance={Number(tokenDetails.balance)} on:cancel @@ -105,4 +113,10 @@ flex-direction: column; height: 100%; } + + .title { + display: flex; + align-items: center; + gap: $sp3; + } diff --git a/frontend/app/src/components/icons/Diamond.svelte b/frontend/app/src/components/icons/Diamond.svelte new file mode 100644 index 0000000000..df32a39908 --- /dev/null +++ b/frontend/app/src/components/icons/Diamond.svelte @@ -0,0 +1,67 @@ + + +{#if status !== "inactive" || show} + + + + + + + + + + + + + + + + +{/if} diff --git a/frontend/app/src/components/register/Register.svelte b/frontend/app/src/components/register/Register.svelte index 0f844e936a..e4ef07df4e 100644 --- a/frontend/app/src/components/register/Register.svelte +++ b/frontend/app/src/components/register/Register.svelte @@ -15,7 +15,6 @@ import Legend from "../Legend.svelte"; import GuidelinesContent from "../landingpages/GuidelinesContent.svelte"; import ButtonGroup from "../ButtonGroup.svelte"; - import DisplayNameInput from "../DisplayNameInput.svelte"; const dispatch = createEventDispatcher(); @@ -29,14 +28,11 @@ let state: Writable = writable({ kind: "awaiting_username" }); let error: Writable = writable(undefined); let usernameStore: Writable = writable(undefined); - let displayNameStore: Writable = writable(undefined); let createdUser: CreatedUser | undefined = undefined; let closed: boolean = false; let showGuidelines = false; let username = ""; let usernameValid = false; - let displayName: string | undefined = undefined; - let displayNameValid = false; let checkingUsername: boolean; let badCode = false; @@ -51,17 +47,15 @@ } function register() { - if (usernameValid && displayNameValid) { + if (usernameValid) { usernameStore.set(username); - displayNameStore.set(displayName); - // TODO: Ask user for a display name - registerUser(username, displayName); + registerUser(username); } } - function registerUser(username: string, displayName: string | undefined): void { + function registerUser(username: string): void { state.set({ kind: "spinning" }); - client.registerUser(username, displayName).then((resp) => { + client.registerUser(username).then((resp) => { badCode = false; state.set({ kind: "awaiting_username" }); if (resp.kind === "username_taken") { @@ -96,7 +90,7 @@ createdUser = { kind: "created_user", username, - displayName, + displayName: undefined, cryptoAccount: resp.icpAccount, userId: resp.userId, canisterUpgradeStatus: "not_required", @@ -104,7 +98,7 @@ isPlatformModerator: false, suspensionDetails: undefined, isSuspectedBot: false, - diamondMembership: undefined, + diamondStatus: { kind: "inactive" }, moderationFlagsEnabled: 0, }; dispatch("createdUser", createdUser); @@ -170,14 +164,6 @@ bind:usernameValid bind:checking={checkingUsername} bind:error={$error} /> - - - {#if $error} @@ -196,13 +182,13 @@ {:else} diff --git a/frontend/app/src/i18n/cn.json b/frontend/app/src/i18n/cn.json index 0fe3bea72d..166a86b084 100644 --- a/frontend/app/src/i18n/cn.json +++ b/frontend/app/src/i18n/cn.json @@ -267,13 +267,13 @@ "airdrop_a": "4 月,向符合条件的钻石用户空投了 100 万个 CHAT 代币。有关详细信息,请参阅此动议提案

今年晚些时候,OpenChat 开发团队将提出一项实施提案用户奖励计划。钻石用户将能够根据他们在 OpenChat 中的声誉得分获得 CHAT 代币。具体如何计算声誉尚未确定,但将付出巨大努力确保垃圾邮件发送者受到惩罚。我们希望鼓励真正的聊天,而不是为机器人或不道德的用户提供“农场”令牌的机会。", "wallet_q": "我的钱包如何运作?", "wallet_a": "创建 OpenChat 用户时,他们会自动拥有一个用于一堆令牌(ICP、CHAT、ckBTC、SNS1)的帐户。该帐户最初是空的,但您可以通过将代币转移到其地址来充值该帐户。一旦您的 OpenChat 钱包中有了一些令牌,您就可以通过一种特殊的消息将它们直接发送到任何其他 OpenChat 用户的帐户。您也可以使用钱包中的ICP(即将CHAT)升级为钻石会员。如果您想将代币发送到另一个帐户,您可以通过从主菜单打开钱包并单击“发送”链接来实现。此屏幕让您可以选择将您的代币发送到您选择的任何其他地址。", - "diamond_q": "什么是💎钻石会员?", "diamond_a": "您可以升级为钻石会员,以访问 OpenChat 中的额外或增强功能。 参见此处 进行功能比较和升级。", "info_q": "我在哪里可以找到有关 OpenChat 的更多信息?", "info_a": "转到我们的主页并找到指向 OpenChat 功能路线图白皮书架构博客", "referral_rewards_q": "什么是推荐奖励?", "referral_rewards_a": "作为钻石会员,您可以为您推荐的每位后来成为钻石会员的用户赚取奖励。您将在加入 OpenChat 的第一年内收到他们支付的钻石付款的一半。您可以在主菜单“个人资料设置”的“邀请朋友/家人”部分找到您的推荐链接。

你会怎么做?有很多方法。您可以告诉朋友和家人、发布有关 OpenChat 的推文、创建有关 OpenChat 的视频来描述您喜欢它的地方,并将其分享到 TikTok 或 YouTube。您可以在 Twitch 上进行直播、撰写博客文章、创建播客、开设 Twitter 空间,甚至举办现实生活中的聚会。我们期待看到您提出的所有创意想法!

未来我们将为您推荐的所有用户开放推荐奖励,无论他们是否成为钻石会员。然而,由于这很容易被利用,我们首先需要引入某种类型的“独特人格证明”。", - "buychat_a": "现在您必须首先购买 ICP ,然后您可以在各种去中心化交易所(DEX)上兑换 CHAT。DFINITY 正在为 ICRC1 代币(包括 CHAT)添加对Rosetta API的支持,使其更容易与集中式交易所 (CEX) 集成。一旦发生这种情况,CHAT 很可能可以在 CEX 上交易 ICP 或 USDT。" + "buychat_a": "现在您必须首先购买 ICP ,然后您可以在各种去中心化交易所(DEX)上兑换 CHAT。DFINITY 正在为 ICRC1 代币(包括 CHAT)添加对Rosetta API的支持,使其更容易与集中式交易所 (CEX) 集成。一旦发生这种情况,CHAT 很可能可以在 CEX 上交易 ICP 或 USDT。", + "diamond_q": "什么是钻石会员?" }, "roadmap": "路线图", "roleChanged": "{changedBy} 将 {changed} 的角色从 {oldRole} 更改为 {newRole}", @@ -785,7 +785,6 @@ "blurb": "某些 OpenChat 功能仅适用于高级帐户。您可以通过短信验证电话号码或支付少量 ICP 费用来升级您的帐户。", "congratulations": "您现在可以享受我们的高级功能了!", "button": "升级", - "featuresTitle": "💎 升级为钻石会员", "paymentTitle": "支付", "feature": "功能", "features": "功能", @@ -833,8 +832,6 @@ "chatAndIcp": "ICP & 聊天", "paymentFailed": "钻石会员支付失败", "upgradeSuccess": "您的付款已成功完成", - "benefits": "💎 钻石会员的好处", - "message": "💎 告诉我钻石会员", "extend": "延长钻石", "extendShort": "延长", "membership": "会籍", @@ -848,7 +845,14 @@ "notifications": "通知", "freeNotifications": "iOS、安卓和桌面", "diamondNotifications": "iOS、安卓和桌面", - "createCommunities": "创建社区" + "createCommunities": "创建社区", + "featuresTitle": "升级至钻石", + "forDisplayName": "升级以设置显示名称", + "displayNames": "显示名称", + "benefits": "钻石的好处", + "message": "告诉我有关钻石会员的信息", + "lifetimeMessage": "您拥有终身钻石会员资格。", + "lifetime": "寿命" }, "currentUsage": "当前存储使用情况", "promoteTo": "晋升为{role}", diff --git a/frontend/app/src/i18n/de.json b/frontend/app/src/i18n/de.json index dc6b3bc188..9122c05b1b 100644 --- a/frontend/app/src/i18n/de.json +++ b/frontend/app/src/i18n/de.json @@ -214,13 +214,13 @@ "airdrop_a": "Im April gab es einen Airdrop von 1 Mio. CHAT-Tokens an berechtigte Diamond-Benutzer. Einzelheiten finden Sie in diesem Antrag.

Später in diesem Jahr wird das OpenChat-Entwicklerteam einen Vorschlag zur Implementierung eines vorlegen Benutzerprämienprogramm. Diamond-Benutzer können CHAT-Tokens basierend auf ihrem Reputations-Score in OpenChat verdienen. Wie genau die Reputation berechnet wird, ist noch nicht entschieden, aber es werden große Anstrengungen unternommen, um sicherzustellen, dass Spammer bestraft werden. Wir möchten echten Chat fördern und Bots oder skrupellosen Benutzern keine Möglichkeit bieten, Token zu „farmen“.", "wallet_q": "Wie funktioniert mein Wallet?", "wallet_a": "Wenn ein OpenChat-Benutzer erstellt wird, verfügt er automatisch über ein Konto für eine Reihe von Token (ICP, CHAT, ckBTC, SNS1). Dieses Konto ist zunächst leer, aber Sie können das Konto aufladen, indem Sie Token an die Adresse übertragen. Sobald Sie einige Token in Ihrem OpenChat-Wallet haben, können Sie diese über eine spezielle Art von Nachricht direkt an das Konto eines anderen OpenChat-Benutzers senden. Sie können auch den ICP (bald CHAT) in Ihrem Wallet verwenden, um ein Upgrade auf die Diamond-Mitgliedschaft durchzuführen. Wenn Sie die Token an ein anderes Konto senden möchten, können Sie dies tun, indem Sie Ihr Wallet im Hauptmenü öffnen und auf den Link „Senden“ klicken. Auf diesem Bildschirm haben Sie die Möglichkeit, Ihre Token an eine beliebige andere Adresse Ihrer Wahl zu senden.", - "diamond_q": "Was ist die 💎 Diamond-Mitgliedschaft?", "diamond_a": "Sie können auf die Diamond-Mitgliedschaft upgraden, um Zugriff auf zusätzliche oder erweiterte Funktionen in OpenChat zu erhalten. Hier finden Sie einen Funktionsvergleich und ein Upgrade.", "info_q": "Wo kann ich mehr über OpenChat erfahren?", "info_a": "Gehen Sie zu unserer Homepage und finden Sie Links zu OpenChat Funktionen, Roadmap, Whitepaper, Architektur und Blog", "referral_rewards_q": "Was sind Empfehlungsprämien?", "referral_rewards_a": "Als Diamond-Mitglied können Sie Belohnungen für jeden von Ihnen geworbenen Benutzer erhalten, der selbst Diamond-Mitglied wird. Sie erhalten die Hälfte aller Diamond-Zahlungen, die sie innerhalb des ersten Jahres nach Ihrem Beitritt zu OpenChat leisten. Sie finden Ihren Empfehlungslink im Abschnitt „Freunde/Familie einladen“ Ihrer „Profileinstellungen“ im Hauptmenü.

Wie könnten Sie das tun? Es gibt viele Möglichkeiten. Sie können Freunden und Familie davon erzählen, über OpenChat twittern, ein Video über OpenChat erstellen, in dem Sie beschreiben, was Sie daran lieben, und es auf TikTok oder YouTube teilen. Sie könnten auf Twitch streamen, einen Blog-Beitrag schreiben, einen Podcast erstellen, einen Twitter-Bereich hosten oder sogar ein echtes Treffen veranstalten. Wir freuen uns darauf, all die kreativen Ideen zu sehen, die Sie haben, um die Nachricht bekannt zu machen!

In Zukunft bieten wir Empfehlungsprämien für alle von Ihnen geworbenen Benutzer an, unabhängig davon, ob sie Diamond-Mitglieder werden. Da dies jedoch leicht auszunutzen ist, müssen wir zunächst eine Art „Beweis für die einzigartige Persönlichkeit“ einführen.", - "buychat_a": "Im Moment müssen Sie zunächst ICP kaufen , das Sie dann an verschiedenen dezentralen Börsen (DEXes) gegen CHAT eintauschen können. DFINITY fügt Unterstützung für die Rosetta-API für ICRC1-Tokens (einschließlich CHAT) hinzu, wodurch die Integration in Centralized Exchanges (CEXes) erheblich vereinfacht wird. Sobald dies geschehen ist, ist es wahrscheinlich, dass CHAT für den Handel an CEXes für ICP oder USDT verfügbar sein wird." + "buychat_a": "Im Moment müssen Sie zunächst ICP kaufen , das Sie dann an verschiedenen dezentralen Börsen (DEXes) gegen CHAT eintauschen können. DFINITY fügt Unterstützung für die Rosetta-API für ICRC1-Tokens (einschließlich CHAT) hinzu, wodurch die Integration in Centralized Exchanges (CEXes) erheblich vereinfacht wird. Sobald dies geschehen ist, ist es wahrscheinlich, dass CHAT für den Handel an CEXes für ICP oder USDT verfügbar sein wird.", + "diamond_q": "Was ist eine Diamond-Mitgliedschaft?" }, "roadmap": "Fahrplan", "here": "Hier", @@ -787,7 +787,6 @@ "blurb": "Einige OpenChat-Funktionen sind nur für Premium-Konten verfügbar. Sie können Ihr Konto upgraden, indem Sie entweder eine Telefonnummer per SMS verifizieren oder eine kleine ICP-Gebühr zahlen.", "congratulations": "Sie können jetzt unsere Premium-Funktionen genießen!", "button": "Aktualisierung", - "featuresTitle": "💎 Upgrade auf Diamond", "paymentTitle": "Zahlung", "feature": "Besonderheit", "features": "Merkmale", @@ -835,8 +834,6 @@ "chatAndIcp": "ICP & CHAT", "paymentFailed": "Die Zahlung für die Diamond-Mitgliedschaft ist fehlgeschlagen", "upgradeSuccess": "Ihre Zahlung wurde erfolgreich durchgeführt", - "benefits": "💎 Vorteile von Diamant", - "message": "💎 Erzählen Sie mir von der Diamond-Mitgliedschaft", "extend": "Diamant verlängern", "extendShort": "Erweitern", "membership": "Mitgliedschaft", @@ -850,7 +847,14 @@ "notifications": "Benachrichtigungen", "freeNotifications": "iOS, Android und Desktop", "diamondNotifications": "iOS, Android und Desktop", - "createCommunities": "Erstellen Sie Communities" + "createCommunities": "Erstellen Sie Communities", + "featuresTitle": "Upgrade auf Diamond", + "forDisplayName": "Aktualisieren Sie, um den Anzeigenamen festzulegen", + "displayNames": "Anzeigenamen", + "benefits": "Vorteile von Diamant", + "message": "Erzählen Sie mir von der Diamond-Mitgliedschaft", + "lifetimeMessage": "Sie haben eine lebenslange Diamond-Mitgliedschaft.", + "lifetime": "Lebensdauer" }, "currentUsage": "Aktuelle Speichernutzung", "promoteTo": "Heraufstufen zu {role}", diff --git a/frontend/app/src/i18n/en.json b/frontend/app/src/i18n/en.json index 94d5503648..0f7dee8d58 100644 --- a/frontend/app/src/i18n/en.json +++ b/frontend/app/src/i18n/en.json @@ -356,7 +356,7 @@ "voting_a": "Join the OpenChat proposals group and read this blog post which gives an overview of OpenChat governance and has a step-by-step guide on voting.", "buychat_q": "How do I buy CHAT tokens?", "buychat_a": "Right now you must first buy ICP which you can then swap for CHAT on various Decentralised Exchanges (DEXes).DFINITY are adding support for the Rosetta API for ICRC1 tokens (which includes CHAT) making them much easier to integrate with Centralised Exchanges (CEXes). Once this has happened it is likely CHAT will be available to trade on CEXes for ICP or USDT.", - "diamond_q": "What is 💎 Diamond Membership?", + "diamond_q": "What is Diamond Membership?", "diamond_a": "You can upgrade to Diamond membership to have access to extra or enhanced features within OpenChat. See here for a feature comparison and to upgrade.", "content_q": "What kind of content is permissible?", "content_a": "For full details of our content and moderation guidelines see here.", @@ -875,14 +875,17 @@ "button": "Upgrade", "groupMsg": "To create up to 10 public groups and to control group access, you must be a Diamond user", "gatingMsg": "To create groups with gated access, you must be a Diamond user", - "featuresTitle": "💎 Upgrade to Diamond", + "featuresTitle": "Upgrade to Diamond", "extend": "Extend Diamond", "extendShort": "Extend", + "forDisplayName": "Upgrade to set display name", + "displayNames": "Display names", "membership": "Membership", - "benefits": "💎 Benefits of Diamond", - "message": "💎 Tell me about Diamond membership", + "benefits": "Benefits of Diamond", + "message": "Tell me about Diamond membership", "paymentTitle": "Payment", "congratulations": "You can now enjoy our Diamond features!", + "lifetimeMessage": "You have lifetime Diamond membership.", "feature": "Feature", "features": "Features", "free": "Free", @@ -924,6 +927,7 @@ "oneMonth": "One month", "threeMonths": "Three months", "oneYear": "One year", + "lifetime": "Lifetime", "insufficientFunds": "Please ensure that your ICP account contains at least {amount}", "confirm": "Confirm", "allSupportedTokens": "All supported tokens", diff --git a/frontend/app/src/i18n/es.json b/frontend/app/src/i18n/es.json index 3a08dc0b0b..5b6abcd6a0 100644 --- a/frontend/app/src/i18n/es.json +++ b/frontend/app/src/i18n/es.json @@ -209,13 +209,13 @@ "airdrop_a": "En abril hubo un airdrop de 1 millón de tokens de CHAT para los usuarios Diamond elegibles. Consulte esta propuesta de movimiento para obtener detalles.

Más adelante este año, el equipo de desarrollo de OpenChat hará una propuesta para implementar un esquema de recompensas para el usuario. Los usuarios Diamond podrán ganar tokens de CHAT en función de su puntuación de reputación en OpenChat. No se ha decidido exactamente cómo se calcula la reputación, pero se harán grandes esfuerzos para garantizar que se castigue a los spammers. Queremos fomentar el chat genuino y no brindar una oportunidad para que los bots o los usuarios sin escrúpulos \"cultiven\" tokens.", "wallet_q": "¿Cómo funciona mi billetera?", "wallet_a": "Cuando se crea un usuario de OpenChat, automáticamente tiene una cuenta para varios tokens (ICP, CHAT, ckBTC, SNS1). Esta cuenta inicialmente estará vacía, pero puede recargarla transfiriendo tokens a su dirección. Una vez que tenga algunos tokens en su billetera de OpenChat, podrá enviarlos directamente a la cuenta de cualquier otro usuario de OpenChat a través de un tipo especial de mensaje. También puede usar el ICP (pronto CHAT) en su billetera para actualizar a la membresía Diamond. Si desea enviar los tokens a otra cuenta, puede hacerlo abriendo su billetera desde el menú principal y haciendo clic en el enlace \"enviar\". Esta pantalla le da la opción de enviar sus tokens a cualquier otra dirección de su elección.", - "diamond_q": "¿Qué es la 💎 Membresía Diamante?", "diamond_a": "Puede actualizar a la membresía Diamond para tener acceso a funciones adicionales o mejoradas dentro de OpenChat. Ver aquí para una comparación de características y para actualizar.", "info_q": "¿Dónde puedo obtener más información sobre OpenChat?", "info_a": "Vaya a nuestra página de inicio y busque enlaces a las características de OpenChat, hoja de ruta, informe, arquitectura y blog", "referral_rewards_q": "¿Qué son las recompensas por recomendación?", "referral_rewards_a": "Como miembro Diamond, puede ganar recompensas por cada usuario que recomiende y que luego se convierta en miembro Diamond. Recibirá la mitad de los pagos Diamante que realicen durante el primer año después de unirse a OpenChat. Puede encontrar su enlace de referencia en la sección "invitar amigos/familiares" de su "configuración de perfil" en el menú principal.

¿Cómo podrías hacer esto? Hay muchas maneras. Puedes contarles a tus amigos y familiares, twittear sobre OpenChat, crear un video sobre OpenChat que describa lo que te gusta de él y compartirlo en TikTok o YouTube. Puedes transmitir en Twitch o escribir una publicación en un blog, crear un podcast, organizar un espacio en Twitter o incluso realizar una reunión en la vida real. ¡Esperamos ver todas las ideas creativas que se te ocurran para correr la voz!

En el futuro, abriremos recompensas por referencias para todos los usuarios que recomiendes, independientemente de si se convierten en miembros Diamante. Sin embargo, debido a que esto es fácilmente explotable, primero necesitaremos introducir algún tipo de "prueba de personalidad única".", - "buychat_a": "En este momento, primero debe comprar ICP , que luego puede cambiar por CHAT en varios intercambios descentralizados (DEX). DFINITY está agregando soporte para la API de Rosetta para tokens ICRC1 (que incluye CHAT), lo que los hace mucho más fáciles de integrar con los intercambios centralizados (CEX). Una vez que esto haya sucedido, es probable que CHAT esté disponible para operar en CEX por ICP o USDT." + "buychat_a": "En este momento, primero debe comprar ICP , que luego puede cambiar por CHAT en varios intercambios descentralizados (DEX). DFINITY está agregando soporte para la API de Rosetta para tokens ICRC1 (que incluye CHAT), lo que los hace mucho más fáciles de integrar con los intercambios centralizados (CEX). Una vez que esto haya sucedido, es probable que CHAT esté disponible para operar en CEX por ICP o USDT.", + "diamond_q": "¿Qué es la membresía Diamante?" }, "roadmap": "Roadmap", "here": "aquí", @@ -787,7 +787,6 @@ "blurb": "Algunas funciones de OpenChat solo están disponibles para cuentas premium. Puede actualizar su cuenta verificando un número de teléfono a través de SMS o pagando una pequeña tarifa de ICP.", "congratulations": "¡Ya puedes disfrutar de nuestras funciones premium!", "button": "Mejora", - "featuresTitle": "💎 Mejora a Diamante", "paymentTitle": "Pago", "feature": "Característica", "features": "Características", @@ -835,8 +834,6 @@ "chatAndIcp": "ICP Y CHAT", "paymentFailed": "Error en el pago de la membresía Diamond", "upgradeSuccess": "Su pago se ha realizado con éxito", - "benefits": "💎 Beneficios del Diamante", - "message": "💎 Cuéntame sobre la membresía Diamante", "extend": "Extender diamante", "extendShort": "Extender", "membership": "Afiliación", @@ -850,7 +847,14 @@ "notifications": "Notificaciones", "freeNotifications": "iOS, Android y escritorio", "diamondNotifications": "iOS, Android y escritorio", - "createCommunities": "Crear comunidades" + "createCommunities": "Crear comunidades", + "featuresTitle": "Actualizar a Diamante", + "forDisplayName": "Actualizar para configurar el nombre para mostrar", + "displayNames": "Nombres para mostrar", + "benefits": "Beneficios del diamante", + "message": "Cuéntame sobre la membresía Diamante", + "lifetimeMessage": "Tienes membresía Diamond vitalicia.", + "lifetime": "Toda la vida" }, "currentUsage": "Uso de almacenamiento actual", "promoteTo": "Promocionar a {role}", diff --git a/frontend/app/src/i18n/fr.json b/frontend/app/src/i18n/fr.json index 2a5e49765c..d53091e128 100644 --- a/frontend/app/src/i18n/fr.json +++ b/frontend/app/src/i18n/fr.json @@ -213,13 +213,13 @@ "airdrop_a": "En avril, il y a eu un airdrop de 1 million de jetons CHAT pour les utilisateurs Diamond éligibles. Voir cette proposition de motion pour plus de détails.

Plus tard cette année, l'équipe de développement d'OpenChat fera une proposition pour mettre en œuvre une programme de récompenses pour les utilisateurs. Les utilisateurs Diamond pourront gagner des jetons CHAT en fonction de leur score de réputation dans OpenChat. La manière exacte dont la réputation est calculée n'a pas été décidée, mais de grands efforts seront déployés pour s'assurer que les spammeurs sont pénalisés. Nous voulons encourager le chat authentique et ne pas donner l'opportunité aux bots ou aux utilisateurs peu scrupuleux de \"farmer\" des jetons.", "wallet_q": "Comment fonctionne mon portefeuille ?", "wallet_a": "Lorsqu'un utilisateur OpenChat est créé, il a automatiquement un compte pour un tas de jetons (ICP, CHAT, ckBTC, SNS1). Ce compte sera initialement vide mais vous pouvez recharger le compte en transférant des jetons à son adresse. Une fois que vous avez des jetons dans votre portefeuille OpenChat, vous pourrez les envoyer directement sur le compte de tout autre utilisateur OpenChat via un type de message spécial. Vous pouvez également utiliser l'ICP (bientôt CHAT) dans votre portefeuille pour passer à l'adhésion Diamond. Si vous souhaitez envoyer les jetons à un autre compte, vous pouvez le faire en ouvrant votre portefeuille depuis le menu principal et en cliquant sur le lien \"envoyer\". Cet écran vous donne la possibilité d'envoyer vos jetons à toute autre adresse de votre choix.", - "diamond_q": "Qu'est-ce que l'💎 Adhésion Diamant ?", "diamond_a": "Vous pouvez passer à l'adhésion Diamond pour avoir accès à des fonctionnalités supplémentaires ou améliorées dans OpenChat. Voir ici pour une comparaison des fonctionnalités et pour mettre à niveau.", "info_q": "Où puis-je en savoir plus sur OpenChat ?", "info_a": "Accédez à notre page d'accueil et recherchez des liens vers les fonctionnalités d'OpenChat, feuille de route, livre blanc, architecture et blog", "referral_rewards_q": "Que sont les récompenses de parrainage ?", "referral_rewards_a": "En tant que membre Diamond, vous pouvez gagner des récompenses pour chaque utilisateur que vous parrainez et qui devient lui-même membre Diamond. Vous recevrez la moitié de tous les paiements Diamond effectués au cours de la première année suivant votre adhésion à OpenChat. Vous pouvez trouver votre lien de parrainage dans la section « inviter des amis/famille » de vos « paramètres de profil » dans le menu principal.

Comment pourriez-vous faire cela ? Il y a beaucoup de façons. Vous pouvez en parler à vos amis et à votre famille, tweeter à propos d'OpenChat, créer une vidéo sur OpenChat décrivant ce que vous aimez et la partager sur TikTok ou YouTube. Vous pouvez diffuser sur Twitch, rédiger un article de blog, créer un podcast, héberger un espace Twitter ou même organiser une véritable rencontre. Nous avons hâte de voir toutes les idées créatives que vous proposerez pour faire passer le message !

À l'avenir, nous offrirons des récompenses de parrainage à tous les utilisateurs que vous parrainez, qu'ils deviennent ou non membres Diamond. Cependant, comme cela est facilement exploitable, nous devrons d'abord introduire une sorte de « preuve de personnalité unique ».", - "buychat_a": "À l'heure actuelle, vous devez d'abord acheter de l'ICP que vous pouvez ensuite échanger contre du CHAT sur divers échanges décentralisés (DEX). DFINITY ajoute la prise en charge de l' API Rosetta pour les jetons ICRC1 (qui inclut CHAT), ce qui les rend beaucoup plus faciles à intégrer aux échanges centralisés (CEX). Une fois que cela se sera produit, il est probable que CHAT soit disponible pour échanger sur des CEX contre des ICP ou des USDT." + "buychat_a": "À l'heure actuelle, vous devez d'abord acheter de l'ICP que vous pouvez ensuite échanger contre du CHAT sur divers échanges décentralisés (DEX). DFINITY ajoute la prise en charge de l' API Rosetta pour les jetons ICRC1 (qui inclut CHAT), ce qui les rend beaucoup plus faciles à intégrer aux échanges centralisés (CEX). Une fois que cela se sera produit, il est probable que CHAT soit disponible pour échanger sur des CEX contre des ICP ou des USDT.", + "diamond_q": "Qu’est-ce que l’adhésion Diamant ?" }, "roadmap": "Feuille de route", "here": "ici", @@ -785,7 +785,6 @@ "blurb": "Certaines fonctionnalités d'OpenChat ne sont disponibles que pour les comptes premium. Vous pouvez mettre à niveau votre compte soit en vérifiant un numéro de téléphone par SMS, soit en payant une petite redevance ICP.", "congratulations": "Vous pouvez désormais profiter de nos fonctionnalités premium !", "button": "Améliorer", - "featuresTitle": "💎 Mise à niveau vers Diamant", "paymentTitle": "Paiement", "feature": "Fonctionnalité", "features": "Caractéristiques", @@ -833,8 +832,6 @@ "chatAndIcp": "PCI & CHAT", "paymentFailed": "Échec du paiement de l'abonnement Diamond", "upgradeSuccess": "Votre paiement a été effectué avec succès", - "benefits": "💎 Avantages du diamant", - "message": "💎 Parlez-moi de l'adhésion Diamond", "extend": "Étendre le diamant", "extendShort": "Étendre", "membership": "Adhésion", @@ -848,7 +845,14 @@ "notifications": "Avis", "freeNotifications": "iOS, Android et ordinateur de bureau", "diamondNotifications": "iOS, Android et ordinateur de bureau", - "createCommunities": "Créer des communautés" + "createCommunities": "Créer des communautés", + "featuresTitle": "Mise à niveau vers Diamant", + "forDisplayName": "Mettre à niveau pour définir le nom d'affichage", + "displayNames": "Noms d'affichage", + "benefits": "Avantages du diamant", + "message": "Parlez-moi de l'adhésion Diamond", + "lifetimeMessage": "Vous êtes membre Diamond à vie.", + "lifetime": "Durée de vie" }, "currentUsage": "Utilisation actuelle du stockage", "promoteTo": "Promouvoir à {role}", diff --git a/frontend/app/src/i18n/it.json b/frontend/app/src/i18n/it.json index 5047f48cb2..95e7a119c1 100644 --- a/frontend/app/src/i18n/it.json +++ b/frontend/app/src/i18n/it.json @@ -214,13 +214,13 @@ "airdrop_a": "Ad aprile c'è stato un airdrop di 1 milione di token CHAT per qualificare gli utenti Diamond. Vedi questa proposta di mozione per i dettagli.

Più tardi quest'anno il team di sviluppo di OpenChat presenterà una proposta per implementare un sistema di premi per gli utenti. Gli utenti Diamond potranno guadagnare token CHAT in base al loro punteggio di reputazione all'interno di OpenChat. Non è stato deciso esattamente come viene calcolata la reputazione, ma saranno compiuti grandi sforzi per garantire che gli spammer vengano penalizzati. Vogliamo incoraggiare la chat autentica e non fornire l'opportunità a bot o utenti senza scrupoli di \"coltivare\" token.", "wallet_q": "Come funziona il mio portafoglio?", "wallet_a": "Quando viene creato un utente OpenChat, ha automaticamente un account per una serie di token (ICP, CHAT, ckBTC, SNS1). Questo account sarà inizialmente vuoto ma puoi ricaricare l'account trasferendo i token al suo indirizzo. Una volta che avrai alcuni token nel tuo portafoglio OpenChat, potrai inviarli direttamente all'account di qualsiasi altro utente OpenChat tramite un tipo speciale di messaggio. Puoi anche utilizzare l'ICP (presto CHAT) nel tuo portafoglio per passare all'abbonamento Diamond. Se desideri inviare i token a un altro account, puoi farlo aprendo il tuo portafoglio dal menu principale e facendo clic sul link \"invia\". Questa schermata ti dà la possibilità di inviare i tuoi token a qualsiasi altro indirizzo di tua scelta.", - "diamond_q": "Cos'è 💎 Abbonamento Diamond?", "diamond_a": "Puoi passare all'abbonamento Diamond per avere accesso a funzionalità extra o migliorate all'interno di OpenChat. Vedi qui per un confronto delle funzioni e per l'aggiornamento.", "info_q": "Dove posso trovare ulteriori informazioni su OpenChat?", "info_a": "Vai alla nostra home page e trova i link alle funzionalità di OpenChat, roadmap, whitepaper, architettura e blog", "referral_rewards_q": "Cosa sono i premi di segnalazione?", "referral_rewards_a": "Come membro Diamond puoi guadagnare premi per ogni utente che inviti e che diventerà a sua volta un membro Diamond. Riceverai la metà dei pagamenti Diamond effettuati entro il primo anno dall'adesione a OpenChat. Puoi trovare il tuo link di riferimento nella sezione "invita amici/familiari" delle "impostazioni del profilo" dal menu principale.

Come potresti farlo? Ci sono molti modi. Puoi dirlo ad amici e parenti, twittare su OpenChat, creare un video su OpenChat descrivendo ciò che ti piace di esso e condividerlo su TikTok o YouTube. Potresti trasmettere in streaming su Twitch o scrivere un post sul blog o creare un podcast o ospitare uno spazio Twitter o persino organizzare un incontro nella vita reale. Non vediamo l'ora di vedere tutte le idee creative che ti verranno in mente per spargere la voce!

In futuro offriremo premi per i referral a tutti gli utenti che inviti, indipendentemente dal fatto che diventino membri Diamond. Tuttavia, poiché questo è facilmente sfruttabile, dovremo prima introdurre un qualche tipo di "prova della personalità unica".", - "buychat_a": "In questo momento devi prima acquistare ICP che potrai poi scambiare con CHAT su vari scambi decentralizzati (DEX). DFINITY sta aggiungendo il supporto per l' API Rosetta per i token ICRC1 (che include CHAT) rendendoli molto più facili da integrare con gli scambi centralizzati (CEX). Una volta che ciò sarà accaduto, è probabile che CHAT sarà disponibile per fare trading su CEX per ICP o USDT." + "buychat_a": "In questo momento devi prima acquistare ICP che potrai poi scambiare con CHAT su vari scambi decentralizzati (DEX). DFINITY sta aggiungendo il supporto per l' API Rosetta per i token ICRC1 (che include CHAT) rendendoli molto più facili da integrare con gli scambi centralizzati (CEX). Una volta che ciò sarà accaduto, è probabile che CHAT sarà disponibile per fare trading su CEX per ICP o USDT.", + "diamond_q": "Cos'è l'abbonamento Diamond?" }, "roadmap": "Tabella di marcia", "here": "qui", @@ -785,7 +785,6 @@ "blurb": "Alcune funzionalità di OpenChat sono disponibili solo per gli account premium. Puoi aggiornare il tuo account verificando un numero di telefono tramite SMS o pagando una piccola commissione ICP.", "congratulations": "Ora puoi goderti le nostre funzionalità premium!", "button": "Aggiornamento", - "featuresTitle": "💎 Passa a Diamante", "paymentTitle": "Pagamento", "feature": "Caratteristica", "features": "Caratteristiche", @@ -833,8 +832,6 @@ "chatAndIcp": "PIC & CHAT", "paymentFailed": "Il pagamento per l'abbonamento Diamond non è andato a buon fine", "upgradeSuccess": "Il tuo pagamento è stato effettuato con successo", - "benefits": "💎 Vantaggi del diamante", - "message": "💎 Parlami dell'abbonamento Diamond", "extend": "Estendi Diamante", "extendShort": "Estendere", "membership": "Adesione", @@ -848,7 +845,14 @@ "notifications": "Notifiche", "freeNotifications": "iOS, Android e Desktop", "diamondNotifications": "iOS, Android e Desktop", - "createCommunities": "Crea comunità" + "createCommunities": "Crea comunità", + "featuresTitle": "Passa a Diamante", + "forDisplayName": "Esegui l'upgrade per impostare il nome visualizzato", + "displayNames": "Visualizza nomi", + "benefits": "Vantaggi del diamante", + "message": "Parlami dell'abbonamento Diamond", + "lifetimeMessage": "Hai un abbonamento Diamond a vita.", + "lifetime": "Tutta la vita" }, "currentUsage": "Utilizzo corrente dello spazio di archiviazione", "promoteTo": "Promuovi a {role}", diff --git a/frontend/app/src/i18n/iw.json b/frontend/app/src/i18n/iw.json index 587d2c2784..70093af74c 100644 --- a/frontend/app/src/i18n/iw.json +++ b/frontend/app/src/i18n/iw.json @@ -280,13 +280,13 @@ "airdrop_a": "באפריל נרשמה ירידה של 1M אסימוני CHAT למשתמשי Diamond. ראה הצעה זו לפרטים.

בהמשך השנה צוות המפתחים של OpenChat יציע הצעה ליישום ערכת תגמולים למשתמש. משתמשי יהלום יוכלו לזכות באסימוני CHAT בהתבסס על ציון המוניטין שלהם בתוך OpenChat. איך בדיוק מחושב המוניטין לא הוחלט, אך יינקטו מאמצים רבים כדי להבטיח שספאמרי ספאם ייענשו. אנחנו רוצים לעודד צ'אט אמיתי ולא לספק הזדמנות לבוטים או למשתמשים חסרי מצפון \"לחווה\" אסימונים.", "wallet_q": "איך עובד הארנק שלי?", "wallet_a": "כאשר משתמש OpenChat נוצר, יש לו אוטומטית חשבון עבור חבורה של אסימונים (ICP, CHAT, ckBTC, SNS1). חשבון זה יהיה ריק בהתחלה, אך ניתן להטעין את החשבון על ידי העברת אסימונים לכתובתו. ברגע שיש לך כמה אסימונים בארנק ה-OpenChat שלך, תוכל לשלוח אותם ישירות לחשבון של כל משתמש OpenChat אחר באמצעות סוג מיוחד של הודעה. אתה יכול גם להשתמש ב-ICP (בקרוב CHAT) בארנק שלך כדי לשדרג לחברות Diamond. אם תרצו לשלוח את האסימונים לחשבון אחר, תוכלו לעשות זאת על ידי פתיחת הארנק מהתפריט הראשי ולחיצה על הקישור \"שלח\". מסך זה נותן לך את האפשרות לשלוח את האסימונים שלך לכל כתובת אחרת שתבחר.", - "diamond_q": "מה זה 💎 חברות יהלום?", "diamond_a": "אתה יכול לשדרג לחברות Diamond כדי לקבל גישה לתכונות נוספות או משופרות בתוך OpenChat. ראה כאן להשוואת תכונות ולשדרוג.", "info_q": "היכן אוכל לקבל מידע נוסף על OpenChat?", "info_a": "עבור אל דף הבית שלנו ומצא קישורים לתכונות של OpenChat, מפת דרכים a>, whitepaper, ארכיטקטורה ובלוג", "referral_rewards_q": "מהם תגמולי הפניה?", "referral_rewards_a": "כחבר Diamond אתה יכול לזכות בפרסים עבור כל משתמש שאתה מפנה שימשיך להיות חבר בעצמו. תקבל מחצית מתשלומי היהלומים שהם ישלמו במהלך השנה הראשונה להצטרפות ל-OpenChat. אתה יכול למצוא את קישור ההפניה שלך בקטע "הזמן חברים/משפחה" ב"הגדרות הפרופיל" שלך מהתפריט הראשי.

איך אתה יכול לעשות את זה? יש המון דרכים. אתה יכול לספר לחברים ובני משפחה, לצייץ על OpenChat, ליצור סרטון על OpenChat שמתאר מה אתה אוהב בו ולשתף אותו ב-TikTok או YouTube. אתה יכול להזרים ב-Twitch או לכתוב פוסט בבלוג או ליצור פודקאסט או לארח חלל טוויטר או אפילו לקיים מפגש אמיתי. אנו מצפים לראות את כל הרעיונות היצירתיים שאתה מעלה כדי להפיץ את הבשורה!

בעתיד נפתח תגמולי הפניה לכל המשתמשים שאתה מפנה ללא קשר אם הם הופכים לחברי Diamond. עם זאת, מכיוון שזה קל לניצול, נצטרך קודם כל להציג סוג כלשהו של "הוכחה לאישיות ייחודית".", - "buychat_a": "כרגע עליך לקנות תחילה ICP שאותו תוכל להחליף ב-CHAT בבורסה מבוזרת (DEXes) שונות. DFINITY מוסיפה תמיכה עבור ה- API של Rosetta עבור אסימוני ICRC1 (הכוללים CHAT) מה שהופך אותם להרבה יותר קלים לשילוב עם חילופים מרכזיים (CEXes). ברגע שזה קרה, סביר להניח ש-CHAT יהיה זמין למסחר ב-CEXes עבור ICP או USDT." + "buychat_a": "כרגע עליך לקנות תחילה ICP שאותו תוכל להחליף ב-CHAT בבורסה מבוזרת (DEXes) שונות. DFINITY מוסיפה תמיכה עבור ה- API של Rosetta עבור אסימוני ICRC1 (הכוללים CHAT) מה שהופך אותם להרבה יותר קלים לשילוב עם חילופים מרכזיים (CEXes). ברגע שזה קרה, סביר להניח ש-CHAT יהיה זמין למסחר ב-CEXes עבור ICP או USDT.", + "diamond_q": "מהי חברות יהלום?" }, "showPinned": "הצג הודעות מוצמדות", "pinnedMessages": "הודעות מוצמדות", @@ -783,7 +783,6 @@ "blurb": "חלק מתכונות OpenChat זמינות רק לחשבונות פרימיום. אתה יכול לשדרג את חשבונך על ידי אימות מספר טלפון באמצעות SMS או על ידי תשלום עמלת ICP קטנה.", "congratulations": "עכשיו אתה יכול ליהנות מתכונות הפרימיום שלנו!", "button": "שדרוג", - "featuresTitle": "💎 שדרג ליהלום", "paymentTitle": "תַשְׁלוּם", "feature": "תכונה", "features": "מאפיינים", @@ -831,8 +830,6 @@ "chatAndIcp": "ICP וצ'אט", "paymentFailed": "התשלום עבור חברות Diamond נכשל", "upgradeSuccess": "התשלום שלך בוצע בהצלחה", - "benefits": "💎 היתרונות של יהלום", - "message": "💎 ספר לי על חברות Diamond", "extend": "הארכת יהלום", "extendShort": "לְהַאֲרִיך", "membership": "חֲבֵרוּת", @@ -846,7 +843,14 @@ "notifications": "התראות", "freeNotifications": "iOS, אנדרואיד ושולחן עבודה", "diamondNotifications": "iOS, אנדרואיד ושולחן עבודה", - "createCommunities": "צור קהילות" + "createCommunities": "צור קהילות", + "featuresTitle": "שדרג ליהלום", + "forDisplayName": "שדרג כדי להגדיר שם תצוגה", + "displayNames": "שמות תצוגה", + "benefits": "היתרונות של יהלום", + "message": "ספר לי על חברות ב-Diamond", + "lifetimeMessage": "יש לך חברות Diamond לכל החיים.", + "lifetime": "לכל החיים" }, "currentUsage": "שימוש נוכחי באחסון", "promoteTo": "קדם ל-{role}", diff --git a/frontend/app/src/i18n/jp.json b/frontend/app/src/i18n/jp.json index 800f999255..932f9927d9 100644 --- a/frontend/app/src/i18n/jp.json +++ b/frontend/app/src/i18n/jp.json @@ -269,13 +269,13 @@ "airdrop_a": "4 月には、適格なダイヤモンド ユーザーに 100 万の CHAT トークンがエアドロップされました。詳細については、この動議提案をご覧ください。

今年後半、OpenChat 開発チームは、ユーザー報酬制度。ダイヤモンド ユーザーは、OpenChat 内の評判スコアに基づいて CHAT トークンを獲得できます。評判の正確な計算方法はまだ決まっていませんが、スパマーが確実に処罰されるよう多大な努力が払われるでしょう。私たちは本物のチャットを奨励し、ボットや悪意のあるユーザーがトークンを「ファーム」する機会を提供しないようにしたいと考えています。", "wallet_q": "私の財布はどのように機能しますか?", "wallet_a": "OpenChat ユーザーが作成されると、一連のトークン (ICP、CHAT、ckBTC、SNS1) のアカウントが自動的に作成されます。このアカウントは最初は空ですが、そのアドレスにトークンを転送することでアカウントを補充できます。 OpenChat ウォレットにトークンをいくつか取得すると、特別な種類のメッセージを介して他の OpenChat ユーザーのアカウントにトークンを直接送信できるようになります。ウォレット内の ICP (近日中に CHAT) を使用してダイヤモンド メンバーシップにアップグレードすることもできます。トークンを別のアカウントに送信したい場合は、メインメニューからウォレットを開いて「送信」リンクをクリックすることで送信できます。この画面では、選択した他のアドレスにトークンを送信するオプションが表示されます。", - "diamond_q": "💎ダイヤモンドメンバーシップとは何ですか?", "diamond_a": "ダイヤモンド メンバーシップにアップグレードすると、OpenChat 内の追加または拡張機能にアクセスできるようになります。機能の比較とアップグレードについては、ここを参照してください。", "info_q": "OpenChat について詳しくはどこで確認できますか?", "info_a": "ホームページにアクセスし、OpenChatの機能ロードマップへのリンクを見つけてください。 a>、ホワイトペーパーアーキテクチャ、およびブログ", "referral_rewards_q": "紹介報酬とは何ですか?", "referral_rewards_a": "ダイヤモンド メンバーは、紹介したユーザーがダイヤモンド メンバーになるたびに特典を獲得できます。 OpenChat に参加してから 1 年以内にダイヤモンドの支払いの半分を受け取ります。紹介リンクは、メインメニューの「プロフィール設定」の「友達/家族を招待」セクションにあります。

どうすればこんなことができるでしょうか?たくさんの方法があります。友達や家族に伝えたり、OpenChat についてツイートしたり、OpenChat の気に入っている点を説明するビデオを作成して、TikTok や YouTube に共有したりできます。 Twitch でストリーミングしたり、ブログ投稿を書いたり、ポッドキャストを作成したり、Twitter スペースをホストしたり、実際の交流会を開催したりすることもできます。この情報を広めるために皆さんが思いついた創造的なアイデアをお待ちしています。

将来的には、ダイヤモンド メンバーになるかどうかに関係なく、紹介したすべてのユーザーに紹介報酬を提供できるようになります。ただし、これは簡単に悪用できるため、最初に何らかの「ユニークな人格の証明」を導入する必要があります。", - "buychat_a": "現時点では、まずICP を購入する必要があります。その後、さまざまな分散型取引所 (DEX) で CHAT と交換できます。DFINITY は、ICRC1 トークン (CHAT を含む) のRosetta APIのサポートを追加し、集中型取引所 (CEX) との統合をより簡単にします。これが実現すると、CHAT で ICP または USDT の CEX を取引できるようになる可能性があります。" + "buychat_a": "現時点では、まずICP を購入する必要があります。その後、さまざまな分散型取引所 (DEX) で CHAT と交換できます。DFINITY は、ICRC1 トークン (CHAT を含む) のRosetta APIのサポートを追加し、集中型取引所 (CEX) との統合をより簡単にします。これが実現すると、CHAT で ICP または USDT の CEX を取引できるようになる可能性があります。", + "diamond_q": "ダイヤモンド会員とは何ですか?" }, "roleChanged": "{changedBy}は、{changed}の役割を{oldRole}から{newRole}に変更しました", "yourRoleChanged": "{changedBy}が役割を{oldRole}から{newRole}に変更しました", @@ -786,7 +786,6 @@ "blurb": "一部のOpenChat機能は、プレミアムアカウントでのみ利用できます。 SMSで電話番号を確認するか、少額のICP料金を支払うことで、アカウントをアップグレードできます。", "congratulations": "プレミアム機能をお楽しみいただけます!", "button": "アップグレード", - "featuresTitle": "💎 ダイヤモンドにアップグレード", "paymentTitle": "支払い", "feature": "特徴", "features": "特徴", @@ -834,8 +833,6 @@ "chatAndIcp": "ICP & chat", "paymentFailed": "ダイヤモンド メンバーシップの支払いに失敗しました", "upgradeSuccess": "お支払いは正常に完了しました", - "benefits": "💎 ダイヤモンドのメリット", - "message": "💎 ダイヤモンド会員について教えてください", "extend": "ダイヤモンドを拡張", "extendShort": "拡張する", "membership": "メンバーシップ", @@ -849,7 +846,14 @@ "notifications": "通知", "freeNotifications": "iOS、Android、デスクトップ", "diamondNotifications": "iOS、Android、デスクトップ", - "createCommunities": "コミュニティを作成する" + "createCommunities": "コミュニティを作成する", + "featuresTitle": "ダイヤモンドにアップグレード", + "forDisplayName": "表示名を設定するようにアップグレードする", + "displayNames": "表示名", + "benefits": "ダイヤモンドの利点", + "message": "ダイヤモンド会員について教えてください", + "lifetimeMessage": "あなたは生涯ダイヤモンド会員権を持っています。", + "lifetime": "一生" }, "currentUsage": "現在のストレージ使用量", "promoteTo": "{role} に昇格", diff --git a/frontend/app/src/i18n/ru.json b/frontend/app/src/i18n/ru.json index 287bcb49c1..756fc75c87 100644 --- a/frontend/app/src/i18n/ru.json +++ b/frontend/app/src/i18n/ru.json @@ -214,13 +214,13 @@ "airdrop_a": "В апреле был проведен аирдроп 1 млн токенов CHAT для обладателей статуса Diamond. Подробности см. в этом предложении.

В конце этого года команда разработчиков OpenChat внесет предложение по реализации схема поощрения пользователей. Пользователи со статусом Diamond смогут зарабатывать токены CHAT в зависимости от своей репутации в OpenChat. Как именно рассчитывается репутация, еще не решено, но будут предприняты большие усилия для обеспечения наказания спамеров. Мы хотим поощрять искренний чат и не давать возможность ботам или недобросовестным пользователям «фармить» токены.", "wallet_q": "Как работает мой кошелек?", "wallet_a": "Когда создается пользователь OpenChat, у него автоматически создается учетная запись для набора токенов (ICP, CHAT, ckBTC, SNS1). Эта учетная запись изначально будет пустой, но вы можете пополнить ее, переведя токены на ее адрес. Как только у вас будет несколько токенов в вашем кошельке OpenChat, вы сможете отправить их непосредственно на счет любого другого пользователя OpenChat с помощью специального сообщения. Вы также можете использовать ICP (скоро ЧАТ) в своем кошельке, чтобы перейти на статус Diamond. Если вы хотите отправить токены на другую учетную запись, вы можете сделать это, открыв свой кошелек в главном меню и щелкнув ссылку «отправить». Этот экран дает вам возможность отправить ваши токены на любой другой адрес по вашему выбору.", - "diamond_q": "Что такое 💎 Алмазное членство?", "diamond_a": "Вы можете перейти на статус Diamond, чтобы получить доступ к дополнительным или улучшенным функциям OpenChat. См. здесь сравнение функций и обновление.", "info_q": "Где я могу узнать больше об OpenChat?", "info_a": "Перейдите на нашу главную страницу и найдите ссылки на функции OpenChat, дорожную карту, технический документ, архитектура и блог", "referral_rewards_q": "Что такое реферальные вознаграждения?", "referral_rewards_a": "Будучи участником уровня Diamond, вы можете получать вознаграждения за каждого привлеченного вами пользователя, который впоследствии сам станет участником уровня Diamond. Вы получите половину всех платежей Diamond, которые они сделают в течение первого года после присоединения к OpenChat. Вы можете найти свою реферальную ссылку в разделе «пригласить друзей/родственников» в «настройках профиля» главного меню.

Как вы могли бы это сделать? Есть много способов. Вы можете рассказать друзьям и родственникам, написать в Твиттере об OpenChat, создать видео об OpenChat, описывающее, что вам в нем нравится, и поделиться им в TikTok или YouTube. Вы можете вести трансляцию на Twitch, написать сообщение в блоге, создать подкаст, разместить пространство в Твиттере или даже провести реальную встречу. Мы с нетерпением ждем возможности увидеть все творческие идеи, которые вы придумаете, чтобы донести до общественности!

В будущем мы откроем реферальные вознаграждения для всех привлеченных вами пользователей, независимо от того, станут ли они участниками уровня Diamond. Однако, поскольку этим легко воспользоваться, нам сначала нужно будет ввести некое «доказательство уникальной личности».", - "buychat_a": "Прямо сейчас вы должны сначала купить ICP , который затем можно обменять на CHAT на различных децентрализованных биржах (DEX). DFINITY добавляет поддержку Rosetta API для токенов ICRC1 (включая CHAT), что значительно упрощает их интеграцию с централизованными биржами (CEX). Как только это произойдет, вполне вероятно, что CHAT станет доступен для торговли на CEX за ICP или USDT." + "buychat_a": "Прямо сейчас вы должны сначала купить ICP , который затем можно обменять на CHAT на различных децентрализованных биржах (DEX). DFINITY добавляет поддержку Rosetta API для токенов ICRC1 (включая CHAT), что значительно упрощает их интеграцию с централизованными биржами (CEX). Как только это произойдет, вполне вероятно, что CHAT станет доступен для торговли на CEX за ICP или USDT.", + "diamond_q": "Что такое Бриллиантовое членство?" }, "roadmap": "Дорожная карта", "here": "здесь", @@ -785,7 +785,6 @@ "blurb": "Некоторые функции OpenChat доступны только для премиум-аккаунтов. Вы можете обновить свою учетную запись, либо подтвердив номер телефона с помощью SMS, либо заплатив небольшую комиссию ICP.", "congratulations": "Теперь вы можете пользоваться нашими премиальными функциями!", "button": "Обновление", - "featuresTitle": "💎 Обновите до Алмаза", "paymentTitle": "Оплата", "feature": "Особенность", "features": "Функции", @@ -833,8 +832,6 @@ "chatAndIcp": "ICP и чат", "paymentFailed": "Не удалось оплатить статус Diamond", "upgradeSuccess": "Ваш платеж успешно произведен", - "benefits": "💎 Преимущества Алмаза", - "message": "💎 Расскажите мне о статусе Diamond", "extend": "Продлить алмаз", "extendShort": "Продлевать", "membership": "Членство", @@ -848,7 +845,14 @@ "notifications": "Уведомления", "freeNotifications": "iOS, Android и рабочий стол", "diamondNotifications": "iOS, Android и рабочий стол", - "createCommunities": "Создавайте сообщества" + "createCommunities": "Создавайте сообщества", + "featuresTitle": "Обновите до Diamond", + "forDisplayName": "Обновите, чтобы установить отображаемое имя", + "displayNames": "Отображаемые имена", + "benefits": "Преимущества бриллианта", + "message": "Расскажите мне о членстве Diamond", + "lifetimeMessage": "У вас есть пожизненное членство Diamond.", + "lifetime": "Продолжительность жизни" }, "currentUsage": "Текущее использование хранилища", "promoteTo": "Повысить до {role}", diff --git a/frontend/app/src/i18n/vi.json b/frontend/app/src/i18n/vi.json index bc50054d1f..c1ef4dc3f2 100644 --- a/frontend/app/src/i18n/vi.json +++ b/frontend/app/src/i18n/vi.json @@ -210,13 +210,13 @@ "airdrop_a": "Vào tháng 4, đã có đợt airdrop 1 triệu mã thông báo CHAT cho người dùng Kim cương đủ điều kiện. Xem đề xuất chuyển động này để biết chi tiết.

Cuối năm nay, nhóm nhà phát triển OpenChat sẽ đưa ra đề xuất triển khai một chương trình phần thưởng người dùng. Người dùng kim cương sẽ có thể kiếm được mã thông báo CHAT dựa trên điểm danh tiếng của họ trong OpenChat. Chính xác cách tính danh tiếng vẫn chưa được quyết định nhưng sẽ có những nỗ lực lớn để đảm bảo những kẻ gửi thư rác bị trừng phạt. Chúng tôi muốn khuyến khích trò chuyện thực sự và không tạo cơ hội cho bot hoặc người dùng vô đạo đức \"kiếm\" mã thông báo.", "wallet_q": "Ví của tôi hoạt động như thế nào?", "wallet_a": "Khi người dùng OpenChat được tạo, họ sẽ tự động có tài khoản cho một loạt mã thông báo (ICP, CHAT, ckBTC, SNS1). Tài khoản này ban đầu sẽ trống nhưng bạn có thể nạp tiền vào tài khoản bằng cách chuyển mã thông báo đến địa chỉ của tài khoản. Khi bạn có một số mã thông báo trong ví OpenChat của mình, bạn sẽ có thể gửi chúng trực tiếp đến tài khoản của bất kỳ người dùng OpenChat nào khác thông qua một loại tin nhắn đặc biệt. Bạn cũng có thể sử dụng ICP (sắp CHAT) trong ví của mình để nâng cấp lên thành viên Kim cương. Nếu bạn muốn gửi mã thông báo đến một tài khoản khác, bạn có thể làm như vậy bằng cách mở ví của mình từ menu chính và nhấp vào liên kết \"gửi\". Màn hình này cung cấp cho bạn tùy chọn gửi mã thông báo của bạn đến bất kỳ địa chỉ nào khác mà bạn chọn.", - "diamond_q": "💎 Tư cách thành viên kim cương là gì?", "diamond_a": "Bạn có thể nâng cấp lên thành viên Kim cương để có quyền truy cập vào các tính năng bổ sung hoặc nâng cao trong OpenChat. Xem tại đây để so sánh tính năng và nâng cấp.", "info_q": "Tôi có thể tìm hiểu thêm về OpenChat ở đâu?", "info_a": "Truy cập trang chủ của chúng tôi và tìm các liên kết đến các tính năng, lộ trình của OpenChat a>, sách trắng, kiến trúcblog", "referral_rewards_q": "Phần thưởng giới thiệu là gì?", "referral_rewards_a": "Là thành viên Kim cương, bạn có thể kiếm được phần thưởng cho mỗi người dùng mà bạn giới thiệu, những người sẽ trở thành thành viên Kim cương. Bạn sẽ nhận được một nửa số khoản thanh toán Kim cương mà họ thực hiện trong năm đầu tiên tham gia OpenChat. Bạn có thể tìm thấy liên kết giới thiệu của mình trong phần "mời bạn bè/gia đình" trong "cài đặt hồ sơ" từ menu chính.

Làm thế nào bạn có thể làm điều này? Có rất nhiều cách. Bạn có thể kể với bạn bè và gia đình, tweet về OpenChat, tạo video về OpenChat mô tả những điều bạn yêu thích về nó và chia sẻ nó lên TikTok hoặc YouTube. Bạn có thể phát trực tiếp trên Twitch hoặc viết một bài đăng trên blog hoặc tạo một podcast hoặc tổ chức một không gian trên Twitter hoặc thậm chí tổ chức một buổi gặp mặt ngoài đời thực. Chúng tôi mong được nhìn thấy tất cả những ý tưởng sáng tạo mà bạn nghĩ ra để truyền tải thông tin!

Trong tương lai, chúng tôi sẽ mở rộng phần thưởng giới thiệu cho tất cả người dùng mà bạn giới thiệu bất kể họ có trở thành thành viên Kim cương hay không. Tuy nhiên, vì điều này có thể dễ dàng khai thác nên trước tiên chúng tôi cần giới thiệu một số loại "bằng chứng về tư cách cá nhân độc nhất".", - "buychat_a": "Ngay bây giờ, trước tiên bạn phải mua ICP , sau đó bạn có thể đổi lấy CHAT trên các Sàn giao dịch phi tập trung (DEX) khác nhau. DFINITY đang bổ sung hỗ trợ cho API Rosetta cho mã thông báo ICRC1 (bao gồm CHAT) giúp chúng tích hợp dễ dàng hơn nhiều với Sàn giao dịch tập trung (CEX). Một khi điều này xảy ra, rất có thể CHAT sẽ có sẵn để giao dịch trên CEX lấy ICP hoặc USDT." + "buychat_a": "Ngay bây giờ, trước tiên bạn phải mua ICP , sau đó bạn có thể đổi lấy CHAT trên các Sàn giao dịch phi tập trung (DEX) khác nhau. DFINITY đang bổ sung hỗ trợ cho API Rosetta cho mã thông báo ICRC1 (bao gồm CHAT) giúp chúng tích hợp dễ dàng hơn nhiều với Sàn giao dịch tập trung (CEX). Một khi điều này xảy ra, rất có thể CHAT sẽ có sẵn để giao dịch trên CEX lấy ICP hoặc USDT.", + "diamond_q": "Thành viên kim cương là gì?" }, "roadmap": "Lộ trình", "here": "tại đây", @@ -786,7 +786,6 @@ "blurb": "Một số tính năng của OpenChat chỉ có sẵn cho các tài khoản trả phí. Bạn có thể nâng cấp tài khoản của mình bằng cách xác minh số điện thoại qua SMS hoặc trả một khoản phí ICP nhỏ.", "congratulations": "Bây giờ bạn có thể tận hưởng các tính năng cao cấp của chúng tôi!", "button": "Nâng cấp", - "featuresTitle": "💎 Nâng cấp lên Kim cương", "paymentTitle": "Sự chi trả", "feature": "Tính năng", "features": "Đặc trưng", @@ -834,8 +833,6 @@ "chatAndIcp": "ICP & TRÒ CHUYỆN", "paymentFailed": "Thanh toán cho thành viên Diamond không thành công", "upgradeSuccess": "Thanh toán của bạn đã được thực hiện thành công", - "benefits": "💎 Lợi ích của Kim cương", - "message": "💎 Hãy cho tôi biết về tư cách thành viên Kim cương", "extend": "mở rộng kim cương", "extendShort": "Mở rộng", "membership": "Tư cách thành viên", @@ -849,7 +846,14 @@ "notifications": "thông báo", "freeNotifications": "iOS, Android & Máy tính để bàn", "diamondNotifications": "iOS, Android & Máy tính để bàn", - "createCommunities": "Tạo cộng đồng" + "createCommunities": "Tạo cộng đồng", + "featuresTitle": "Nâng cấp lên Kim cương", + "forDisplayName": "Nâng cấp để đặt tên hiển thị", + "displayNames": "Tên hiển thị", + "benefits": "Lợi ích của kim cương", + "message": "Cho tôi biết về tư cách thành viên Diamond", + "lifetimeMessage": "Bạn có tư cách thành viên Kim cương trọn đời.", + "lifetime": "Cả đời" }, "currentUsage": "Mức sử dụng bộ nhớ hiện tại", "promoteTo": "Quảng cáo lên {role}", diff --git a/frontend/app/src/styles/mixins.scss b/frontend/app/src/styles/mixins.scss index 4b514f088b..cbf5b99e00 100644 --- a/frontend/app/src/styles/mixins.scss +++ b/frontend/app/src/styles/mixins.scss @@ -492,14 +492,6 @@ $shadow-level-3: 2px 6px 12px 0 rgba(25, 25, 25, 0.55); text-overflow: ellipsis; } -@mixin diamond() { - &::after { - content: "💎"; - margin-inline-start: 5px; - @include font-size(fs-80); - } -} - @mixin bullet_list() { text-align: left; list-style: none; diff --git a/frontend/openchat-agent/src/services/community/candid/types.d.ts b/frontend/openchat-agent/src/services/community/candid/types.d.ts index 9db27c7ffd..10a3768afd 100644 --- a/frontend/openchat-agent/src/services/community/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/community/candid/types.d.ts @@ -577,13 +577,25 @@ export type DeletedMessageResponse = { 'UserNotInChannel' : null } | { 'MessageHardDeleted' : null } | { 'MessageNotDeleted' : null }; export interface DiamondMembershipDetails { - 'recurring' : [] | [DiamondMembershipPlanDuration], + 'pay_in_chat' : boolean, + 'subscription' : DiamondMembershipSubscription, + 'recurring' : [] | [DiamondMembershipSubscription], 'expires_at' : TimestampMillis, } export type DiamondMembershipPlanDuration = { 'OneYear' : null } | { 'Lifetime' : null } | { 'ThreeMonths' : null } | { 'OneMonth' : null }; +export type DiamondMembershipStatus = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : null }; +export type DiamondMembershipStatusFull = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : DiamondMembershipDetails }; +export type DiamondMembershipSubscription = { 'OneYear' : null } | + { 'ThreeMonths' : null } | + { 'Disabled' : null } | + { 'OneMonth' : null }; export type DirectChatCreated = {}; export interface DirectChatSummary { 'read_by_them_up_to' : [] | [MessageIndex], @@ -1355,14 +1367,6 @@ export interface OptionalMessagePermissions { export type OptionalMessagePermissionsUpdate = { 'NoChange' : null } | { 'SetToNone' : null } | { 'SetToSome' : OptionalMessagePermissions }; -export interface PartialUserSummary { - 'username' : [] | [string], - 'diamond_member' : boolean, - 'user_id' : UserId, - 'is_bot' : boolean, - 'avatar_id' : [] | [bigint], - 'suspended' : boolean, -} export interface Participant { 'role' : GroupRole, 'user_id' : UserId, @@ -2022,6 +2026,7 @@ export type UserId = CanisterId; export interface UserSummary { 'username' : string, 'diamond_member' : boolean, + 'diamond_membership_status' : DiamondMembershipStatus, 'user_id' : UserId, 'is_bot' : boolean, 'display_name' : [] | [string], diff --git a/frontend/openchat-agent/src/services/group/candid/types.d.ts b/frontend/openchat-agent/src/services/group/candid/types.d.ts index 18d4c7da12..d770e84999 100644 --- a/frontend/openchat-agent/src/services/group/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/group/candid/types.d.ts @@ -463,13 +463,25 @@ export type DeletedMessageResponse = { 'MessageNotFound' : null } | { 'MessageHardDeleted' : null } | { 'MessageNotDeleted' : null }; export interface DiamondMembershipDetails { - 'recurring' : [] | [DiamondMembershipPlanDuration], + 'pay_in_chat' : boolean, + 'subscription' : DiamondMembershipSubscription, + 'recurring' : [] | [DiamondMembershipSubscription], 'expires_at' : TimestampMillis, } export type DiamondMembershipPlanDuration = { 'OneYear' : null } | { 'Lifetime' : null } | { 'ThreeMonths' : null } | { 'OneMonth' : null }; +export type DiamondMembershipStatus = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : null }; +export type DiamondMembershipStatusFull = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : DiamondMembershipDetails }; +export type DiamondMembershipSubscription = { 'OneYear' : null } | + { 'ThreeMonths' : null } | + { 'Disabled' : null } | + { 'OneMonth' : null }; export type DirectChatCreated = {}; export interface DirectChatSummary { 'read_by_them_up_to' : [] | [MessageIndex], @@ -1200,14 +1212,6 @@ export interface OptionalMessagePermissions { export type OptionalMessagePermissionsUpdate = { 'NoChange' : null } | { 'SetToNone' : null } | { 'SetToSome' : OptionalMessagePermissions }; -export interface PartialUserSummary { - 'username' : [] | [string], - 'diamond_member' : boolean, - 'user_id' : UserId, - 'is_bot' : boolean, - 'avatar_id' : [] | [bigint], - 'suspended' : boolean, -} export interface Participant { 'role' : GroupRole, 'user_id' : UserId, @@ -1735,6 +1739,7 @@ export type UserId = CanisterId; export interface UserSummary { 'username' : string, 'diamond_member' : boolean, + 'diamond_membership_status' : DiamondMembershipStatus, 'user_id' : UserId, 'is_bot' : boolean, 'display_name' : [] | [string], diff --git a/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts index e0e8c7c9f7..d7bd59f133 100644 --- a/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts @@ -410,13 +410,25 @@ export interface DeletedGroupInfo { 'group_name' : string, } export interface DiamondMembershipDetails { - 'recurring' : [] | [DiamondMembershipPlanDuration], + 'pay_in_chat' : boolean, + 'subscription' : DiamondMembershipSubscription, + 'recurring' : [] | [DiamondMembershipSubscription], 'expires_at' : TimestampMillis, } export type DiamondMembershipPlanDuration = { 'OneYear' : null } | { 'Lifetime' : null } | { 'ThreeMonths' : null } | { 'OneMonth' : null }; +export type DiamondMembershipStatus = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : null }; +export type DiamondMembershipStatusFull = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : DiamondMembershipDetails }; +export type DiamondMembershipSubscription = { 'OneYear' : null } | + { 'ThreeMonths' : null } | + { 'Disabled' : null } | + { 'OneMonth' : null }; export type DirectChatCreated = {}; export interface DirectChatSummary { 'read_by_them_up_to' : [] | [MessageIndex], @@ -1151,14 +1163,6 @@ export interface OptionalMessagePermissions { export type OptionalMessagePermissionsUpdate = { 'NoChange' : null } | { 'SetToNone' : null } | { 'SetToSome' : OptionalMessagePermissions }; -export interface PartialUserSummary { - 'username' : [] | [string], - 'diamond_member' : boolean, - 'user_id' : UserId, - 'is_bot' : boolean, - 'avatar_id' : [] | [bigint], - 'suspended' : boolean, -} export interface Participant { 'role' : GroupRole, 'user_id' : UserId, @@ -1503,6 +1507,7 @@ export type UserId = CanisterId; export interface UserSummary { 'username' : string, 'diamond_member' : boolean, + 'diamond_membership_status' : DiamondMembershipStatus, 'user_id' : UserId, 'is_bot' : boolean, 'display_name' : [] | [string], diff --git a/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts index 1b538a6ab7..6f81dda4f8 100644 --- a/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts @@ -369,13 +369,25 @@ export interface DeletedContent { 'deleted_by' : UserId, } export interface DiamondMembershipDetails { - 'recurring' : [] | [DiamondMembershipPlanDuration], + 'pay_in_chat' : boolean, + 'subscription' : DiamondMembershipSubscription, + 'recurring' : [] | [DiamondMembershipSubscription], 'expires_at' : TimestampMillis, } export type DiamondMembershipPlanDuration = { 'OneYear' : null } | { 'Lifetime' : null } | { 'ThreeMonths' : null } | { 'OneMonth' : null }; +export type DiamondMembershipStatus = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : null }; +export type DiamondMembershipStatusFull = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : DiamondMembershipDetails }; +export type DiamondMembershipSubscription = { 'OneYear' : null } | + { 'ThreeMonths' : null } | + { 'Disabled' : null } | + { 'OneMonth' : null }; export type DirectChatCreated = {}; export interface DirectChatSummary { 'read_by_them_up_to' : [] | [MessageIndex], @@ -1133,14 +1145,6 @@ export interface OptionalMessagePermissions { export type OptionalMessagePermissionsUpdate = { 'NoChange' : null } | { 'SetToNone' : null } | { 'SetToSome' : OptionalMessagePermissions }; -export interface PartialUserSummary { - 'username' : [] | [string], - 'diamond_member' : boolean, - 'user_id' : UserId, - 'is_bot' : boolean, - 'avatar_id' : [] | [bigint], - 'suspended' : boolean, -} export interface Participant { 'role' : GroupRole, 'user_id' : UserId, @@ -1449,6 +1453,7 @@ export type UserId = CanisterId; export interface UserSummary { 'username' : string, 'diamond_member' : boolean, + 'diamond_membership_status' : DiamondMembershipStatus, 'user_id' : UserId, 'is_bot' : boolean, 'display_name' : [] | [string], diff --git a/frontend/openchat-agent/src/services/localUserIndex/localUserIndex.client.ts b/frontend/openchat-agent/src/services/localUserIndex/localUserIndex.client.ts index 9d8c32f976..1a6fb0d72a 100644 --- a/frontend/openchat-agent/src/services/localUserIndex/localUserIndex.client.ts +++ b/frontend/openchat-agent/src/services/localUserIndex/localUserIndex.client.ts @@ -29,44 +29,43 @@ export class LocalUserIndexClient extends CandidService { this.localUserIndexService = this.createServiceClient( idlFactory, canisterId, - config + config, ); } static create( identity: Identity, config: AgentConfig, - canisterId: string + canisterId: string, ): LocalUserIndexClient { return new LocalUserIndexClient(identity, config, canisterId); } registerUser( username: string, - displayName: string | undefined, - referralCode: string | undefined + referralCode: string | undefined, ): Promise { return this.handleResponse( this.localUserIndexService.register_user({ username, - display_name: apiOptional(identity, displayName), + display_name: [], referral_code: apiOptional(identity, referralCode), public_key: new Uint8Array((this.identity as SignIdentity).getPublicKey().toDer()), }), - registerUserResponse + registerUserResponse, ); } joinCommunity( communityId: string, - inviteCode: string | undefined + inviteCode: string | undefined, ): Promise { return this.handleResponse( this.localUserIndexService.join_community({ community_id: Principal.fromText(communityId), invite_code: apiOptional(textToCode, inviteCode), }), - joinCommunityResponse + joinCommunityResponse, ); } @@ -77,7 +76,7 @@ export class LocalUserIndexClient extends CandidService { invite_code: apiOptional(textToCode, inviteCode), correlation_id: BigInt(0), }), - joinGroupResponse + joinGroupResponse, ); } @@ -88,7 +87,7 @@ export class LocalUserIndexClient extends CandidService { channel_id: BigInt(id.channelId), invite_code: apiOptional(textToCode, inviteCode), }), - (resp) => joinChannelResponse(resp, id.communityId) + (resp) => joinChannelResponse(resp, id.communityId), ); } @@ -98,7 +97,7 @@ export class LocalUserIndexClient extends CandidService { community_id: Principal.fromText(communityId), user_ids: userIds.map((u) => Principal.fromText(u)), }), - inviteUsersResponse + inviteUsersResponse, ); } @@ -109,14 +108,14 @@ export class LocalUserIndexClient extends CandidService { user_ids: userIds.map((u) => Principal.fromText(u)), correlation_id: BigInt(0), }), - inviteUsersResponse + inviteUsersResponse, ); } inviteUsersToChannel( communityId: string, channelId: string, - userIds: string[] + userIds: string[], ): Promise { return this.handleResponse( this.localUserIndexService.invite_users_to_channel({ @@ -124,7 +123,7 @@ export class LocalUserIndexClient extends CandidService { channel_id: BigInt(channelId), user_ids: userIds.map((u) => Principal.fromText(u)), }), - inviteUsersResponse + inviteUsersResponse, ); } } diff --git a/frontend/openchat-agent/src/services/notifications/candid/types.d.ts b/frontend/openchat-agent/src/services/notifications/candid/types.d.ts index ac12859500..9898b14391 100644 --- a/frontend/openchat-agent/src/services/notifications/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/notifications/candid/types.d.ts @@ -369,13 +369,25 @@ export interface DeletedContent { 'deleted_by' : UserId, } export interface DiamondMembershipDetails { - 'recurring' : [] | [DiamondMembershipPlanDuration], + 'pay_in_chat' : boolean, + 'subscription' : DiamondMembershipSubscription, + 'recurring' : [] | [DiamondMembershipSubscription], 'expires_at' : TimestampMillis, } export type DiamondMembershipPlanDuration = { 'OneYear' : null } | { 'Lifetime' : null } | { 'ThreeMonths' : null } | { 'OneMonth' : null }; +export type DiamondMembershipStatus = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : null }; +export type DiamondMembershipStatusFull = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : DiamondMembershipDetails }; +export type DiamondMembershipSubscription = { 'OneYear' : null } | + { 'ThreeMonths' : null } | + { 'Disabled' : null } | + { 'OneMonth' : null }; export type DirectChatCreated = {}; export interface DirectChatSummary { 'read_by_them_up_to' : [] | [MessageIndex], @@ -1038,14 +1050,6 @@ export interface OptionalMessagePermissions { export type OptionalMessagePermissionsUpdate = { 'NoChange' : null } | { 'SetToNone' : null } | { 'SetToSome' : OptionalMessagePermissions }; -export interface PartialUserSummary { - 'username' : [] | [string], - 'diamond_member' : boolean, - 'user_id' : UserId, - 'is_bot' : boolean, - 'avatar_id' : [] | [bigint], - 'suspended' : boolean, -} export interface Participant { 'role' : GroupRole, 'user_id' : UserId, @@ -1334,6 +1338,7 @@ export type UserId = CanisterId; export interface UserSummary { 'username' : string, 'diamond_member' : boolean, + 'diamond_membership_status' : DiamondMembershipStatus, 'user_id' : UserId, 'is_bot' : boolean, 'display_name' : [] | [string], diff --git a/frontend/openchat-agent/src/services/online/candid/types.d.ts b/frontend/openchat-agent/src/services/online/candid/types.d.ts index 6d83c632f0..5187144c61 100644 --- a/frontend/openchat-agent/src/services/online/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/online/candid/types.d.ts @@ -369,13 +369,25 @@ export interface DeletedContent { 'deleted_by' : UserId, } export interface DiamondMembershipDetails { - 'recurring' : [] | [DiamondMembershipPlanDuration], + 'pay_in_chat' : boolean, + 'subscription' : DiamondMembershipSubscription, + 'recurring' : [] | [DiamondMembershipSubscription], 'expires_at' : TimestampMillis, } export type DiamondMembershipPlanDuration = { 'OneYear' : null } | { 'Lifetime' : null } | { 'ThreeMonths' : null } | { 'OneMonth' : null }; +export type DiamondMembershipStatus = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : null }; +export type DiamondMembershipStatusFull = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : DiamondMembershipDetails }; +export type DiamondMembershipSubscription = { 'OneYear' : null } | + { 'ThreeMonths' : null } | + { 'Disabled' : null } | + { 'OneMonth' : null }; export type DirectChatCreated = {}; export interface DirectChatSummary { 'read_by_them_up_to' : [] | [MessageIndex], @@ -1048,14 +1060,6 @@ export interface OptionalMessagePermissions { export type OptionalMessagePermissionsUpdate = { 'NoChange' : null } | { 'SetToNone' : null } | { 'SetToSome' : OptionalMessagePermissions }; -export interface PartialUserSummary { - 'username' : [] | [string], - 'diamond_member' : boolean, - 'user_id' : UserId, - 'is_bot' : boolean, - 'avatar_id' : [] | [bigint], - 'suspended' : boolean, -} export interface Participant { 'role' : GroupRole, 'user_id' : UserId, @@ -1334,6 +1338,7 @@ export type UserId = CanisterId; export interface UserSummary { 'username' : string, 'diamond_member' : boolean, + 'diamond_membership_status' : DiamondMembershipStatus, 'user_id' : UserId, 'is_bot' : boolean, 'display_name' : [] | [string], diff --git a/frontend/openchat-agent/src/services/openchatAgent.ts b/frontend/openchat-agent/src/services/openchatAgent.ts index 94465a6f29..bf6edaa3ca 100644 --- a/frontend/openchat-agent/src/services/openchatAgent.ts +++ b/frontend/openchat-agent/src/services/openchatAgent.ts @@ -2126,17 +2126,12 @@ export class OpenChatAgent extends EventTarget { async registerUser( username: string, - displayName: string | undefined, referralCode: string | undefined, ): Promise { if (offline()) return Promise.resolve(CommonResponses.offline()); const localUserIndex = await this._userIndexClient.userRegistrationCanister(); - return this.createLocalUserIndexClient(localUserIndex).registerUser( - username, - displayName, - referralCode, - ); + return this.createLocalUserIndexClient(localUserIndex).registerUser(username, referralCode); } getUserStorageLimits(): Promise { diff --git a/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts b/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts index 52823c5163..b2b6e761b8 100644 --- a/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts @@ -369,13 +369,25 @@ export interface DeletedContent { 'deleted_by' : UserId, } export interface DiamondMembershipDetails { - 'recurring' : [] | [DiamondMembershipPlanDuration], + 'pay_in_chat' : boolean, + 'subscription' : DiamondMembershipSubscription, + 'recurring' : [] | [DiamondMembershipSubscription], 'expires_at' : TimestampMillis, } export type DiamondMembershipPlanDuration = { 'OneYear' : null } | { 'Lifetime' : null } | { 'ThreeMonths' : null } | { 'OneMonth' : null }; +export type DiamondMembershipStatus = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : null }; +export type DiamondMembershipStatusFull = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : DiamondMembershipDetails }; +export type DiamondMembershipSubscription = { 'OneYear' : null } | + { 'ThreeMonths' : null } | + { 'Disabled' : null } | + { 'OneMonth' : null }; export type DirectChatCreated = {}; export interface DirectChatSummary { 'read_by_them_up_to' : [] | [MessageIndex], @@ -1038,14 +1050,6 @@ export interface OptionalMessagePermissions { export type OptionalMessagePermissionsUpdate = { 'NoChange' : null } | { 'SetToNone' : null } | { 'SetToSome' : OptionalMessagePermissions }; -export interface PartialUserSummary { - 'username' : [] | [string], - 'diamond_member' : boolean, - 'user_id' : UserId, - 'is_bot' : boolean, - 'avatar_id' : [] | [bigint], - 'suspended' : boolean, -} export interface Participant { 'role' : GroupRole, 'user_id' : UserId, @@ -1337,6 +1341,7 @@ export type UserId = CanisterId; export interface UserSummary { 'username' : string, 'diamond_member' : boolean, + 'diamond_membership_status' : DiamondMembershipStatus, 'user_id' : UserId, 'is_bot' : boolean, 'display_name' : [] | [string], diff --git a/frontend/openchat-agent/src/services/registry/candid/types.d.ts b/frontend/openchat-agent/src/services/registry/candid/types.d.ts index c8158979f2..8cc2583fc2 100644 --- a/frontend/openchat-agent/src/services/registry/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/registry/candid/types.d.ts @@ -369,13 +369,25 @@ export interface DeletedContent { 'deleted_by' : UserId, } export interface DiamondMembershipDetails { - 'recurring' : [] | [DiamondMembershipPlanDuration], + 'pay_in_chat' : boolean, + 'subscription' : DiamondMembershipSubscription, + 'recurring' : [] | [DiamondMembershipSubscription], 'expires_at' : TimestampMillis, } export type DiamondMembershipPlanDuration = { 'OneYear' : null } | { 'Lifetime' : null } | { 'ThreeMonths' : null } | { 'OneMonth' : null }; +export type DiamondMembershipStatus = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : null }; +export type DiamondMembershipStatusFull = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : DiamondMembershipDetails }; +export type DiamondMembershipSubscription = { 'OneYear' : null } | + { 'ThreeMonths' : null } | + { 'Disabled' : null } | + { 'OneMonth' : null }; export type DirectChatCreated = {}; export interface DirectChatSummary { 'read_by_them_up_to' : [] | [MessageIndex], @@ -1047,14 +1059,6 @@ export interface OptionalMessagePermissions { export type OptionalMessagePermissionsUpdate = { 'NoChange' : null } | { 'SetToNone' : null } | { 'SetToSome' : OptionalMessagePermissions }; -export interface PartialUserSummary { - 'username' : [] | [string], - 'diamond_member' : boolean, - 'user_id' : UserId, - 'is_bot' : boolean, - 'avatar_id' : [] | [bigint], - 'suspended' : boolean, -} export interface Participant { 'role' : GroupRole, 'user_id' : UserId, @@ -1356,6 +1360,7 @@ export type UserId = CanisterId; export interface UserSummary { 'username' : string, 'diamond_member' : boolean, + 'diamond_membership_status' : DiamondMembershipStatus, 'user_id' : UserId, 'is_bot' : boolean, 'display_name' : [] | [string], diff --git a/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts b/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts index b708803d3c..01237ace20 100644 --- a/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts @@ -384,13 +384,25 @@ export interface DeletedContent { 'deleted_by' : UserId, } export interface DiamondMembershipDetails { - 'recurring' : [] | [DiamondMembershipPlanDuration], + 'pay_in_chat' : boolean, + 'subscription' : DiamondMembershipSubscription, + 'recurring' : [] | [DiamondMembershipSubscription], 'expires_at' : TimestampMillis, } export type DiamondMembershipPlanDuration = { 'OneYear' : null } | { 'Lifetime' : null } | { 'ThreeMonths' : null } | { 'OneMonth' : null }; +export type DiamondMembershipStatus = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : null }; +export type DiamondMembershipStatusFull = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : DiamondMembershipDetails }; +export type DiamondMembershipSubscription = { 'OneYear' : null } | + { 'ThreeMonths' : null } | + { 'Disabled' : null } | + { 'OneMonth' : null }; export type DirectChatCreated = {}; export interface DirectChatSummary { 'read_by_them_up_to' : [] | [MessageIndex], @@ -1068,14 +1080,6 @@ export interface OptionalMessagePermissions { export type OptionalMessagePermissionsUpdate = { 'NoChange' : null } | { 'SetToNone' : null } | { 'SetToSome' : OptionalMessagePermissions }; -export interface PartialUserSummary { - 'username' : [] | [string], - 'diamond_member' : boolean, - 'user_id' : UserId, - 'is_bot' : boolean, - 'avatar_id' : [] | [bigint], - 'suspended' : boolean, -} export interface Participant { 'role' : GroupRole, 'user_id' : UserId, @@ -1377,6 +1381,7 @@ export type UserId = CanisterId; export interface UserSummary { 'username' : string, 'diamond_member' : boolean, + 'diamond_membership_status' : DiamondMembershipStatus, 'user_id' : UserId, 'is_bot' : boolean, 'display_name' : [] | [string], diff --git a/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts index 1c859c28e9..2efda037fa 100644 --- a/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts @@ -399,13 +399,25 @@ export interface DeletedContent { 'deleted_by' : UserId, } export interface DiamondMembershipDetails { - 'recurring' : [] | [DiamondMembershipPlanDuration], + 'pay_in_chat' : boolean, + 'subscription' : DiamondMembershipSubscription, + 'recurring' : [] | [DiamondMembershipSubscription], 'expires_at' : TimestampMillis, } export type DiamondMembershipPlanDuration = { 'OneYear' : null } | { 'Lifetime' : null } | { 'ThreeMonths' : null } | { 'OneMonth' : null }; +export type DiamondMembershipStatus = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : null }; +export type DiamondMembershipStatusFull = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : DiamondMembershipDetails }; +export type DiamondMembershipSubscription = { 'OneYear' : null } | + { 'ThreeMonths' : null } | + { 'Disabled' : null } | + { 'OneMonth' : null }; export type DirectChatCreated = {}; export interface DirectChatSummary { 'read_by_them_up_to' : [] | [MessageIndex], @@ -1068,14 +1080,6 @@ export interface OptionalMessagePermissions { export type OptionalMessagePermissionsUpdate = { 'NoChange' : null } | { 'SetToNone' : null } | { 'SetToSome' : OptionalMessagePermissions }; -export interface PartialUserSummary { - 'username' : [] | [string], - 'diamond_member' : boolean, - 'user_id' : UserId, - 'is_bot' : boolean, - 'avatar_id' : [] | [bigint], - 'suspended' : boolean, -} export interface Participant { 'role' : GroupRole, 'user_id' : UserId, @@ -1378,6 +1382,7 @@ export type UserResponse = { 'Success' : UserRecord } | export interface UserSummary { 'username' : string, 'diamond_member' : boolean, + 'diamond_membership_status' : DiamondMembershipStatus, 'user_id' : UserId, 'is_bot' : boolean, 'display_name' : [] | [string], diff --git a/frontend/openchat-agent/src/services/user/candid/types.d.ts b/frontend/openchat-agent/src/services/user/candid/types.d.ts index 77c7acdf50..1cbd2860ad 100644 --- a/frontend/openchat-agent/src/services/user/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/user/candid/types.d.ts @@ -543,13 +543,25 @@ export type DeletedMessageResponse = { 'MessageNotFound' : null } | { 'MessageHardDeleted' : null } | { 'MessageNotDeleted' : null }; export interface DiamondMembershipDetails { - 'recurring' : [] | [DiamondMembershipPlanDuration], + 'pay_in_chat' : boolean, + 'subscription' : DiamondMembershipSubscription, + 'recurring' : [] | [DiamondMembershipSubscription], 'expires_at' : TimestampMillis, } export type DiamondMembershipPlanDuration = { 'OneYear' : null } | { 'Lifetime' : null } | { 'ThreeMonths' : null } | { 'OneMonth' : null }; +export type DiamondMembershipStatus = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : null }; +export type DiamondMembershipStatusFull = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : DiamondMembershipDetails }; +export type DiamondMembershipSubscription = { 'OneYear' : null } | + { 'ThreeMonths' : null } | + { 'Disabled' : null } | + { 'OneMonth' : null }; export type DirectChatCreated = {}; export interface DirectChatSummary { 'read_by_them_up_to' : [] | [MessageIndex], @@ -1349,14 +1361,6 @@ export interface OptionalMessagePermissions { export type OptionalMessagePermissionsUpdate = { 'NoChange' : null } | { 'SetToNone' : null } | { 'SetToSome' : OptionalMessagePermissions }; -export interface PartialUserSummary { - 'username' : [] | [string], - 'diamond_member' : boolean, - 'user_id' : UserId, - 'is_bot' : boolean, - 'avatar_id' : [] | [bigint], - 'suspended' : boolean, -} export interface Participant { 'role' : GroupRole, 'user_id' : UserId, @@ -2014,6 +2018,7 @@ export type UserId = CanisterId; export interface UserSummary { 'username' : string, 'diamond_member' : boolean, + 'diamond_membership_status' : DiamondMembershipStatus, 'user_id' : UserId, 'is_bot' : boolean, 'display_name' : [] | [string], diff --git a/frontend/openchat-agent/src/services/userIndex/candid/idl.d.ts b/frontend/openchat-agent/src/services/userIndex/candid/idl.d.ts index da406dac0c..2a258821b7 100644 --- a/frontend/openchat-agent/src/services/userIndex/candid/idl.d.ts +++ b/frontend/openchat-agent/src/services/userIndex/candid/idl.d.ts @@ -25,6 +25,9 @@ import { ReferralLeaderboardResponse, ReferralStats, SetModerationFlagsResponse, + DiamondMembershipStatus, + DiamondMembershipStatusFull, + DiamondMembershipSubscription } from "./types"; export { _SERVICE as UserIndexService, @@ -52,6 +55,9 @@ export { ReferralLeaderboardResponse as ApiReferralLeaderboardResponse, ReferralStats as ApiReferralStats, SetModerationFlagsResponse as ApiSetModerationFlagsResponse, + DiamondMembershipStatus as ApiDiamondMembershipStatus, + DiamondMembershipStatusFull as ApiDiamondMembershipStatusFull, + DiamondMembershipSubscription as ApiDiamondMembershipSubscription }; export const idlFactory: IDL.InterfaceFactory; diff --git a/frontend/openchat-agent/src/services/userIndex/candid/idl.js b/frontend/openchat-agent/src/services/userIndex/candid/idl.js index 3480be36cb..f381bb8f6b 100644 --- a/frontend/openchat-agent/src/services/userIndex/candid/idl.js +++ b/frontend/openchat-agent/src/services/userIndex/candid/idl.js @@ -34,6 +34,23 @@ export const idlFactory = ({ IDL }) => { 'Success' : IDL.Null, }); const EmptyArgs = IDL.Record({}); + const DiamondMembershipSubscription = IDL.Variant({ + 'OneYear' : IDL.Null, + 'ThreeMonths' : IDL.Null, + 'Disabled' : IDL.Null, + 'OneMonth' : IDL.Null, + }); + const DiamondMembershipDetails = IDL.Record({ + 'pay_in_chat' : IDL.Bool, + 'subscription' : DiamondMembershipSubscription, + 'recurring' : IDL.Opt(DiamondMembershipSubscription), + 'expires_at' : TimestampMillis, + }); + const DiamondMembershipStatusFull = IDL.Variant({ + 'Inactive' : IDL.Null, + 'Lifetime' : IDL.Null, + 'Active' : DiamondMembershipDetails, + }); const BuildVersion = IDL.Record({ 'major' : IDL.Nat32, 'minor' : IDL.Nat32, @@ -53,19 +70,10 @@ export const idlFactory = ({ IDL }) => { 'suspended_by' : UserId, 'reason' : IDL.Text, }); - const DiamondMembershipPlanDuration = IDL.Variant({ - 'OneYear' : IDL.Null, - 'Lifetime' : IDL.Null, - 'ThreeMonths' : IDL.Null, - 'OneMonth' : IDL.Null, - }); - const DiamondMembershipDetails = IDL.Record({ - 'recurring' : IDL.Opt(DiamondMembershipPlanDuration), - 'expires_at' : TimestampMillis, - }); const CurrentUserResponse = IDL.Variant({ 'Success' : IDL.Record({ 'username' : IDL.Text, + 'diamond_membership_status' : DiamondMembershipStatusFull, 'wasm_version' : BuildVersion, 'icp_account' : AccountIdentifier, 'referrals' : IDL.Vec(UserId), @@ -102,6 +110,12 @@ export const idlFactory = ({ IDL }) => { }); const MarkSuspectedBotArgs = IDL.Record({}); const MarkSuspectedBotResponse = IDL.Variant({ 'Success' : IDL.Null }); + const DiamondMembershipPlanDuration = IDL.Variant({ + 'OneYear' : IDL.Null, + 'Lifetime' : IDL.Null, + 'ThreeMonths' : IDL.Null, + 'OneMonth' : IDL.Null, + }); const PayForDiamondMembershipArgs = IDL.Record({ 'token' : Cryptocurrency, 'duration' : DiamondMembershipPlanDuration, @@ -178,9 +192,15 @@ export const idlFactory = ({ IDL }) => { 'max_results' : IDL.Nat8, 'search_term' : IDL.Text, }); + const DiamondMembershipStatus = IDL.Variant({ + 'Inactive' : IDL.Null, + 'Lifetime' : IDL.Null, + 'Active' : IDL.Null, + }); const UserSummary = IDL.Record({ 'username' : IDL.Text, 'diamond_member' : IDL.Bool, + 'diamond_membership_status' : DiamondMembershipStatus, 'user_id' : UserId, 'is_bot' : IDL.Bool, 'display_name' : IDL.Opt(IDL.Text), @@ -244,6 +264,15 @@ export const idlFactory = ({ IDL }) => { 'InternalError' : IDL.Text, 'UserNotFound' : IDL.Null, }); + const UpdateDiamondMembershipSubscriptionArgs = IDL.Record({ + 'pay_in_chat' : IDL.Opt(IDL.Bool), + 'subscription' : IDL.Opt(DiamondMembershipSubscription), + }); + const UpdateDiamondMembershipSubscriptionResponse = IDL.Variant({ + 'NotDiamondMember' : IDL.Null, + 'Success' : IDL.Null, + 'AlreadyLifetimeDiamondMember' : IDL.Null, + }); const UserArgs = IDL.Record({ 'username' : IDL.Opt(IDL.Text), 'user_id' : IDL.Opt(UserId), @@ -256,28 +285,6 @@ export const idlFactory = ({ IDL }) => { 'Success' : CanisterId, 'NewRegistrationsClosed' : IDL.Null, }); - const UsersArgs = IDL.Record({ - 'user_groups' : IDL.Vec( - IDL.Record({ - 'users' : IDL.Vec(UserId), - 'updated_since' : TimestampMillis, - }) - ), - }); - const PartialUserSummary = IDL.Record({ - 'username' : IDL.Opt(IDL.Text), - 'diamond_member' : IDL.Bool, - 'user_id' : UserId, - 'is_bot' : IDL.Bool, - 'avatar_id' : IDL.Opt(IDL.Nat), - 'suspended' : IDL.Bool, - }); - const UsersResponse = IDL.Variant({ - 'Success' : IDL.Record({ - 'timestamp' : TimestampMillis, - 'users' : IDL.Vec(PartialUserSummary), - }), - }); const UsersV2Args = IDL.Record({ 'user_groups' : IDL.Vec( IDL.Record({ @@ -397,13 +404,17 @@ export const idlFactory = ({ IDL }) => { [UnsuspendUserResponse], [], ), + 'update_diamond_membership_subscription' : IDL.Func( + [UpdateDiamondMembershipSubscriptionArgs], + [UpdateDiamondMembershipSubscriptionResponse], + [], + ), 'user' : IDL.Func([UserArgs], [UserResponse], ['query']), 'user_registration_canister' : IDL.Func( [EmptyArgs], [UserRegistrationCanisterResponse], ['query'], ), - 'users' : IDL.Func([UsersArgs], [UsersResponse], ['query']), 'users_v2' : IDL.Func([UsersV2Args], [UsersV2Response], ['query']), }); }; diff --git a/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts index 358fc8a2c3..29ea4ac10f 100644 --- a/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts @@ -375,6 +375,7 @@ export type Cryptocurrency = { 'InternetComputer' : null } | export type CurrentUserResponse = { 'Success' : { 'username' : string, + 'diamond_membership_status' : DiamondMembershipStatusFull, 'wasm_version' : BuildVersion, 'icp_account' : AccountIdentifier, 'referrals' : Array, @@ -409,7 +410,9 @@ export interface DeletedContent { 'deleted_by' : UserId, } export interface DiamondMembershipDetails { - 'recurring' : [] | [DiamondMembershipPlanDuration], + 'pay_in_chat' : boolean, + 'subscription' : DiamondMembershipSubscription, + 'recurring' : [] | [DiamondMembershipSubscription], 'expires_at' : TimestampMillis, } export type DiamondMembershipFeesResponse = { @@ -427,6 +430,16 @@ export type DiamondMembershipPlanDuration = { 'OneYear' : null } | { 'Lifetime' : null } | { 'ThreeMonths' : null } | { 'OneMonth' : null }; +export type DiamondMembershipStatus = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : null }; +export type DiamondMembershipStatusFull = { 'Inactive' : null } | + { 'Lifetime' : null } | + { 'Active' : DiamondMembershipDetails }; +export type DiamondMembershipSubscription = { 'OneYear' : null } | + { 'ThreeMonths' : null } | + { 'Disabled' : null } | + { 'OneMonth' : null }; export type DirectChatCreated = {}; export interface DirectChatSummary { 'read_by_them_up_to' : [] | [MessageIndex], @@ -1091,14 +1104,6 @@ export interface OptionalMessagePermissions { export type OptionalMessagePermissionsUpdate = { 'NoChange' : null } | { 'SetToNone' : null } | { 'SetToSome' : OptionalMessagePermissions }; -export interface PartialUserSummary { - 'username' : [] | [string], - 'diamond_member' : boolean, - 'user_id' : UserId, - 'is_bot' : boolean, - 'avatar_id' : [] | [bigint], - 'suspended' : boolean, -} export interface Participant { 'role' : GroupRole, 'user_id' : UserId, @@ -1478,6 +1483,15 @@ export type UnsuspendUserResponse = { 'UserNotSuspended' : null } | { 'Success' : null } | { 'InternalError' : string } | { 'UserNotFound' : null }; +export interface UpdateDiamondMembershipSubscriptionArgs { + 'pay_in_chat' : [] | [boolean], + 'subscription' : [] | [DiamondMembershipSubscription], +} +export type UpdateDiamondMembershipSubscriptionResponse = { + 'NotDiamondMember' : null + } | + { 'Success' : null } | + { 'AlreadyLifetimeDiamondMember' : null }; export interface UpdatedRules { 'new_version' : boolean, 'text' : string, @@ -1501,17 +1515,13 @@ export type UserResponse = { 'Success' : UserSummary } | export interface UserSummary { 'username' : string, 'diamond_member' : boolean, + 'diamond_membership_status' : DiamondMembershipStatus, 'user_id' : UserId, 'is_bot' : boolean, 'display_name' : [] | [string], 'avatar_id' : [] | [bigint], 'suspended' : boolean, } -export interface UsersArgs { - 'user_groups' : Array< - { 'users' : Array, 'updated_since' : TimestampMillis } - >, -} export interface UsersBlocked { 'user_ids' : Array, 'blocked_by' : UserId, @@ -1520,12 +1530,6 @@ export interface UsersInvited { 'user_ids' : Array, 'invited_by' : UserId, } -export type UsersResponse = { - 'Success' : { - 'timestamp' : TimestampMillis, - 'users' : Array, - } - }; export interface UsersUnblocked { 'user_ids' : Array, 'unblocked_by' : UserId, @@ -1633,11 +1637,14 @@ export interface _SERVICE { 'suspected_bots' : ActorMethod<[SuspectedBotsArgs], SuspectedBotsResponse>, 'suspend_user' : ActorMethod<[SuspendUserArgs], SuspendUserResponse>, 'unsuspend_user' : ActorMethod<[UnsuspendUserArgs], UnsuspendUserResponse>, + 'update_diamond_membership_subscription' : ActorMethod< + [UpdateDiamondMembershipSubscriptionArgs], + UpdateDiamondMembershipSubscriptionResponse + >, 'user' : ActorMethod<[UserArgs], UserResponse>, 'user_registration_canister' : ActorMethod< [EmptyArgs], UserRegistrationCanisterResponse >, - 'users' : ActorMethod<[UsersArgs], UsersResponse>, 'users_v2' : ActorMethod<[UsersV2Args], UsersV2Response>, } diff --git a/frontend/openchat-agent/src/services/userIndex/mappers.ts b/frontend/openchat-agent/src/services/userIndex/mappers.ts index 12db8d1018..371d08b197 100644 --- a/frontend/openchat-agent/src/services/userIndex/mappers.ts +++ b/frontend/openchat-agent/src/services/userIndex/mappers.ts @@ -15,6 +15,8 @@ import type { ReferralLeaderboardResponse, ReferralStats, SetDisplayNameResponse, + DiamondMembershipSubscription, + DiamondMembershipStatus, } from "openchat-shared"; import { UnsupportedValueError } from "openchat-shared"; import type { @@ -23,6 +25,9 @@ import type { ApiDiamondMembershipDetails, ApiDiamondMembershipFeesResponse, ApiDiamondMembershipPlanDuration, + ApiDiamondMembershipStatus, + ApiDiamondMembershipStatusFull, + ApiDiamondMembershipSubscription, ApiPayForDiamondMembershipResponse, ApiReferralLeaderboardResponse, ApiReferralStats, @@ -71,10 +76,23 @@ export function userSummary(candid: ApiUserSummary, timestamp: bigint): UserSumm })), updated: timestamp, suspended: candid.suspended, - diamond: candid.diamond_member, + diamondStatus: diamondStatus(candid.diamond_membership_status), }; } +export function diamondStatus(candid: ApiDiamondMembershipStatus): DiamondMembershipStatus["kind"] { + if ("Inactive" in candid) { + return "inactive"; + } + if ("Active" in candid) { + return "active"; + } + if ("Lifetime" in candid) { + return "lifetime"; + } + throw new UnsupportedValueError("Unexpected ApiDiamondMembershipStatus type received", candid); +} + export function userRegistrationCanisterResponse( candid: ApiUserRegistrationCanisterResponse, ): string { @@ -105,7 +123,7 @@ export function currentUserResponse(candid: ApiCurrentUserResponse): CurrentUser isPlatformModerator: r.is_platform_moderator, suspensionDetails: optional(r.suspension_details, suspensionDetails), isSuspectedBot: r.is_suspected_bot, - diamondMembership: optional(r.diamond_membership_details, diamondMembership), + diamondStatus: diamondMembershipStatus(r.diamond_membership_status), moderationFlagsEnabled: r.moderation_flags_enabled, }; } @@ -117,16 +135,36 @@ export function currentUserResponse(candid: ApiCurrentUserResponse): CurrentUser throw new Error(`Unexpected ApiCurrentUserResponse type received: ${candid}`); } +function diamondMembershipStatus(candid: ApiDiamondMembershipStatusFull): DiamondMembershipStatus { + if ("Inactive" in candid) { + return { kind: "inactive" }; + } + if ("Lifetime" in candid) { + return { kind: "lifetime" }; + } + if ("Active" in candid) { + return { + kind: "active", + ...diamondMembership(candid.Active), + }; + } + throw new UnsupportedValueError( + "Unexpected ApiDiamondMembershipStatusFull type received", + candid, + ); +} + function diamondMembership(candid: ApiDiamondMembershipDetails): DiamondMembershipDetails { return { expiresAt: candid.expires_at, - recurring: optional(candid.recurring, diamondMembershipDuration), + subscription: diamondMembershipSubscription(candid.subscription), + payInChat: candid.pay_in_chat, }; } -function diamondMembershipDuration( - candid: ApiDiamondMembershipPlanDuration, -): DiamondMembershipDuration { +function diamondMembershipSubscription( + candid: ApiDiamondMembershipSubscription, +): DiamondMembershipSubscription { if ("OneMonth" in candid) { return "one_month"; } @@ -136,7 +174,13 @@ function diamondMembershipDuration( if ("OneYear" in candid) { return "one_year"; } - throw new Error(`Unexpected ApiDiamondMembershipPlanDuration type received: ${candid}`); + if ("Disabled" in candid) { + return "disabled"; + } + throw new UnsupportedValueError( + "Unexpected ApiDiamondMembershipSubscription type received", + candid, + ); } function suspensionDetails(candid: ApiSuspensionDetails): SuspensionDetails { @@ -286,6 +330,7 @@ export function referralLeaderboardResponse( } export function payForDiamondMembershipResponse( + duration: DiamondMembershipDuration, candid: ApiPayForDiamondMembershipResponse, ): PayForDiamondMembershipResponse { if ("PaymentAlreadyInProgress" in candid) { @@ -295,7 +340,13 @@ export function payForDiamondMembershipResponse( return { kind: "currency_not_supported" }; } if ("Success" in candid) { - return { kind: "success", details: diamondMembership(candid.Success) }; + return { + kind: "success", + status: + duration === "lifetime" + ? { kind: "lifetime" } + : { kind: "active", ...diamondMembership(candid.Success) }, + }; } if ("PriceMismatch" in candid) { return { kind: "price_mismatch" }; @@ -333,6 +384,9 @@ export function apiDiamondDuration( if (domain === "one_year") { return { OneYear: null }; } + if (domain === "lifetime") { + return { Lifetime: null }; + } throw new UnsupportedValueError("Unexpected DiamondMembershipDuration type received", domain); } diff --git a/frontend/openchat-agent/src/services/userIndex/userIndex.client.ts b/frontend/openchat-agent/src/services/userIndex/userIndex.client.ts index 25b81524bf..c1207870d5 100644 --- a/frontend/openchat-agent/src/services/userIndex/userIndex.client.ts +++ b/frontend/openchat-agent/src/services/userIndex/userIndex.client.ts @@ -42,11 +42,15 @@ import { getCachedUsers, setCachedUsers, setDisplayNameInCache, - setUserDiamondStatusToTrueInCache, + setUserDiamondStatusInCache, setUsernameInCache, } from "../../utils/userCache"; import { identity } from "../../utils/mapping"; -import { getCachedCurrentUser, setCachedCurrentUser } from "../../utils/caching"; +import { + getCachedCurrentUser, + setCachedCurrentUser, + setCurrentUserDiamondStatusInCache, +} from "../../utils/caching"; export class UserIndexClient extends CandidService { private userIndexService: UserIndexService; @@ -319,10 +323,12 @@ export class UserIndexClient extends CandidService { recurring, expected_price_e8s: expectedPriceE8s, }), - payForDiamondMembershipResponse, + (res) => payForDiamondMembershipResponse(duration, res), ).then((res) => { if (res.kind === "success") { - setUserDiamondStatusToTrueInCache(userId); + const principal = this.identity.getPrincipal().toString(); + setUserDiamondStatusInCache(userId, res.status); + setCurrentUserDiamondStatusInCache(principal, res.status); } return res; }); diff --git a/frontend/openchat-agent/src/utils/caching.ts b/frontend/openchat-agent/src/utils/caching.ts index 850e0d924d..7d37906822 100644 --- a/frontend/openchat-agent/src/utils/caching.ts +++ b/frontend/openchat-agent/src/utils/caching.ts @@ -29,6 +29,7 @@ import type { CommunitySummary, DataContent, CreatedUser, + DiamondMembershipStatus, } from "openchat-shared"; import { chatIdentifiersEqual, @@ -1128,3 +1129,19 @@ export async function setCachedCurrentUser(principal: string, user: CreatedUser) if (db === undefined) return; (await db).put("currentUser", user, principal); } + +export async function setCurrentUserDiamondStatusInCache( + principal: string, + diamondStatus: DiamondMembershipStatus, +): Promise { + const user = await getCachedCurrentUser(principal); + if (user === undefined || db === undefined) return; + (await db).put( + "currentUser", + { + ...user, + diamondStatus, + }, + principal, + ); +} diff --git a/frontend/openchat-agent/src/utils/userCache.ts b/frontend/openchat-agent/src/utils/userCache.ts index 8cad1ff48e..0668114ade 100644 --- a/frontend/openchat-agent/src/utils/userCache.ts +++ b/frontend/openchat-agent/src/utils/userCache.ts @@ -1,7 +1,7 @@ import { openDB, type DBSchema, type IDBPDatabase } from "idb"; -import type { UserSummary } from "openchat-shared"; +import type { DiamondMembershipStatus, UserSummary } from "openchat-shared"; -const CACHE_VERSION = 3; +const CACHE_VERSION = 4; let db: UserDatabase | undefined; @@ -54,7 +54,7 @@ export async function setCachedUsers(users: UserSummary[]): Promise { export async function writeCachedUsersToDatabase( db: UserDatabase, - users: UserSummary[] + users: UserSummary[], ): Promise { // in this one case we will open the db every time because we expect this to be done from the service worker const tx = (await db).transaction("users", "readwrite", { @@ -78,7 +78,10 @@ export async function setUsernameInCache(userId: string, username: string): Prom await tx.done; } -export async function setDisplayNameInCache(userId: string, displayName: string | undefined): Promise { +export async function setDisplayNameInCache( + userId: string, + displayName: string | undefined, +): Promise { const tx = (await lazyOpenUserCache()).transaction("users", "readwrite", { durability: "relaxed", }); @@ -91,14 +94,17 @@ export async function setDisplayNameInCache(userId: string, displayName: string await tx.done; } -export async function setUserDiamondStatusToTrueInCache(userId: string): Promise { +export async function setUserDiamondStatusInCache( + userId: string, + status: DiamondMembershipStatus, +): Promise { const tx = (await lazyOpenUserCache()).transaction("users", "readwrite", { durability: "relaxed", }); const store = tx.objectStore("users"); const user = await store.get(userId); if (user !== undefined) { - user.diamond = true; + user.diamondStatus = status.kind; await store.put(user, userId); } await tx.done; diff --git a/frontend/openchat-client/src/liveState.ts b/frontend/openchat-client/src/liveState.ts index 86060c0924..eb26bfdc0a 100644 --- a/frontend/openchat-client/src/liveState.ts +++ b/frontend/openchat-client/src/liveState.ts @@ -6,7 +6,6 @@ import type { ChatSummary, CommunitySummary, CommunityMap, - DiamondMembershipDetails, DirectChatSummary, EnhancedReplyContext, EventWrapper, @@ -19,6 +18,7 @@ import type { Member, VersionedRules, CreatedUser, + DiamondMembershipStatus, } from "openchat-shared"; import { selectedAuthProviderStore } from "./stores/authProviders"; import { @@ -54,7 +54,7 @@ import { remainingStorage } from "./stores/storage"; import { userCreatedStore } from "./stores/userCreated"; import { anonUser, currentUser, platformModerator, suspendedUser, userStore } from "./stores/user"; import { blockedUsers } from "./stores/blockedUsers"; -import { diamondMembership, isDiamond } from "./stores/diamond"; +import { diamondStatus, isDiamond, isLifetimeDiamond } from "./stores/diamond"; import type DRange from "drange"; import { communities, @@ -103,8 +103,9 @@ export class LiveState { chatsInitialised!: boolean; chatsLoading!: boolean; blockedUsers!: Set; - diamondMembership!: DiamondMembershipDetails | undefined; + diamondStatus!: DiamondMembershipStatus; isDiamond!: boolean; + isLifetimeDiamond!: boolean; confirmedThreadEventIndexesLoaded!: DRange; communities!: CommunityMap; chatListScope!: ChatListScope; @@ -162,8 +163,9 @@ export class LiveState { chatsInitialised.subscribe((data) => (this.chatsInitialised = data)); chatsLoading.subscribe((data) => (this.chatsLoading = data)); blockedUsers.subscribe((data) => (this.blockedUsers = data)); - diamondMembership.subscribe((data) => (this.diamondMembership = data)); + diamondStatus.subscribe((data) => (this.diamondStatus = data)); isDiamond.subscribe((data) => (this.isDiamond = data)); + isLifetimeDiamond.subscribe((data) => (this.isDiamond = data)); communities.subscribe((data) => (this.communities = data)); chatListScopeStore.subscribe((scope) => (this.chatListScope = scope)); globalStateStore.subscribe((data) => (this.globalState = data)); diff --git a/frontend/openchat-client/src/openchat.ts b/frontend/openchat-client/src/openchat.ts index de88a64c32..4a55a0115e 100644 --- a/frontend/openchat-client/src/openchat.ts +++ b/frontend/openchat-client/src/openchat.ts @@ -287,7 +287,6 @@ import type { UserStatus, ThreadRead, DiamondMembershipDuration, - DiamondMembershipDetails, DiamondMembershipFees, UpdateMarketMakerConfigArgs, UpdateMarketMakerConfigResponse, @@ -351,6 +350,7 @@ import type { Member, Level, VersionedRules, + DiamondMembershipStatus, } from "openchat-shared"; import { AuthProvider, @@ -385,13 +385,16 @@ import { anonymousUser, ANON_USER_ID, isPaymentGate, + ONE_MINUTE_MILLIS, + ONE_HOUR, } from "openchat-shared"; import { failedMessagesStore } from "./stores/failedMessages"; import { canExtendDiamond, - diamondMembership, + diamondStatus, isDiamond, diamondDurationToMs, + isLifetimeDiamond, } from "./stores/diamond"; import { addCommunityPreview, @@ -433,13 +436,11 @@ import { offlineStore } from "./stores/network"; const UPGRADE_POLL_INTERVAL = 1000; const MARK_ONLINE_INTERVAL = 61 * 1000; const SESSION_TIMEOUT_NANOS = BigInt(30 * 24 * 60 * 60 * 1000 * 1000 * 1000); // 30 days -const ONE_MINUTE_MILLIS = 60 * 1000; const MAX_TIMEOUT_MS = Math.pow(2, 31) - 1; const CHAT_UPDATE_INTERVAL = 5000; const CHAT_UPDATE_IDLE_INTERVAL = ONE_MINUTE_MILLIS; const USER_UPDATE_INTERVAL = ONE_MINUTE_MILLIS; const REGISTRY_UPDATE_INTERVAL = 30 * ONE_MINUTE_MILLIS; -const ONE_HOUR = 60 * ONE_MINUTE_MILLIS; const MAX_USERS_TO_UPDATE_PER_BATCH = 500; const MAX_INT32 = Math.pow(2, 31) - 1; @@ -693,12 +694,21 @@ export class OpenChat extends OpenChatAgentWorker { if (userId === this._liveState.user.userId) return this._liveState.isDiamond; - return user.diamond; + return user.diamondStatus !== "inactive"; + } + + userIsLifetimeDiamond(userId: string): boolean { + const user = this._liveState.userStore[userId]; + if (user === undefined || user.kind === "bot") return false; + + if (userId === this._liveState.user.userId) return this._liveState.isLifetimeDiamond; + + return user.diamondStatus === "lifetime"; } diamondExpiresIn(now: number, locale: string | null | undefined): string | undefined { - if (this._liveState.diamondMembership !== undefined) { - return formatRelativeTime(now, locale, this._liveState.diamondMembership.expiresAt); + if (this._liveState.diamondStatus.kind === "active") { + return formatRelativeTime(now, locale, this._liveState.diamondStatus.expiresAt); } } @@ -711,7 +721,7 @@ export class OpenChat extends OpenChatAgentWorker { throw new Error("onCreatedUser called before the user's identity has been established"); } this.user.set(user); - this.setDiamondMembership(user.diamondMembership); + this.setDiamondStatus(user.diamondStatus); const id = this._identity; // TODO remove this once the principal migration can be done via the UI const principalMigrationNewPrincipal = localStorage.getItem( @@ -1349,6 +1359,7 @@ export class OpenChat extends OpenChatAgentWorker { userOrUserGroupId = userOrUserGroupId; extractUserIdsFromMentions = extractUserIdsFromMentions; toRecord2 = toRecord2; + toRecord = toRecord; toDatetimeString = toDatetimeString; groupBySender = groupBySender; groupBy = groupBy; @@ -3811,11 +3822,10 @@ export class OpenChat extends OpenChatAgentWorker { return captured; } - registerUser(username: string, displayName: string | undefined): Promise { + registerUser(username: string): Promise { return this.sendRequest({ kind: "registerUser", username, - displayName, referralCode: this._referralCode, }).then((res) => { console.log("register user response: ", res); @@ -3835,7 +3845,7 @@ export class OpenChat extends OpenChatAgentWorker { userCreatedStore.set(true); selectedAuthProviderStore.init(AuthProvider.II); this.user.set(user); - this.setDiamondMembership(user.diamondMembership); + this.setDiamondStatus(user.diamondStatus); } if (!resolved) { // we want to resolve the promise with the first response from the stream so that @@ -4876,18 +4886,18 @@ export class OpenChat extends OpenChatAgentWorker { } } - private updateDiamondStatusInUserStore(now: number, details?: DiamondMembershipDetails): void { - const diamond = details !== undefined && Number(details.expiresAt) > now; - this.overwriteUserInStore(this._liveState.user.userId, (user) => - user.diamond !== diamond ? { ...user, diamond } : undefined, - ); + private updateDiamondStatusInUserStore(status: DiamondMembershipStatus): void { + this.overwriteUserInStore(this._liveState.user.userId, (user) => { + const changed = status.kind !== user.diamondStatus; + return changed ? { ...user, diamondStatus: status.kind } : undefined; + }); } - private setDiamondMembership(details?: DiamondMembershipDetails): void { + private setDiamondStatus(status: DiamondMembershipStatus): void { const now = Date.now(); - this.updateDiamondStatusInUserStore(now, details); - if (details !== undefined) { - const expiry = Number(details.expiresAt); + this.updateDiamondStatusInUserStore(status); + if (status.kind === "active") { + const expiry = Number(status.expiresAt); if (expiry > now) { if (this._membershipCheck !== undefined) { window.clearTimeout(this._membershipCheck); @@ -4936,9 +4946,9 @@ export class OpenChat extends OpenChatAgentWorker { } else { this.user.update((user) => ({ ...user, - diamondMembership: resp.details, + diamondStatus: resp.status, })); - this.setDiamondMembership(resp.details); + this.setDiamondStatus(resp.status); return true; } }) @@ -5026,9 +5036,9 @@ export class OpenChat extends OpenChatAgentWorker { return this.sendRequest({ kind: "getReferralLeaderboard", args }); } - displayNameAndIcon(user?: UserSummary): string { + displayName(user?: UserSummary): string { return user !== undefined - ? `${user?.displayName ?? user?.username} ${user?.diamond ? "💎" : ""}` + ? `${user?.displayName ?? user?.username}` : this.config.i18nFormatter("unknownUser"); } @@ -5799,8 +5809,9 @@ export class OpenChat extends OpenChatAgentWorker { userMetrics = userMetrics; threadEvents = threadEvents; isDiamond = isDiamond; + isLifetimeDiamond = isLifetimeDiamond; canExtendDiamond = canExtendDiamond; - diamondMembership = diamondMembership; + diamondStatus = diamondStatus; selectedThreadRootEvent = selectedThreadRootEvent; selectedThreadRootMessageIndex = selectedThreadRootMessageIndex; selectedMessageContext = selectedMessageContext; diff --git a/frontend/openchat-client/src/stores/diamond.ts b/frontend/openchat-client/src/stores/diamond.ts index 3fee4c4526..b4db546c5c 100644 --- a/frontend/openchat-client/src/stores/diamond.ts +++ b/frontend/openchat-client/src/stores/diamond.ts @@ -2,27 +2,25 @@ import { type DiamondMembershipDuration, UnsupportedValueError } from "openchat- import { derived } from "svelte/store"; import { currentUser } from "./user"; -export const diamondMembership = derived( - currentUser, - ($currentUser) => $currentUser.diamondMembership, -); +export const diamondStatus = derived(currentUser, ($currentUser) => $currentUser.diamondStatus); -export const isDiamond = derived(diamondMembership, ($diamondMembership) => { - return $diamondMembership !== undefined && $diamondMembership.expiresAt > Date.now(); +export const isDiamond = derived(diamondStatus, ($diamondStatus) => { + return ( + $diamondStatus.kind === "lifetime" || + ($diamondStatus.kind === "active" && $diamondStatus.expiresAt > Date.now()) + ); +}); + +export const isLifetimeDiamond = derived(diamondStatus, ($diamondStatus) => { + return $diamondStatus.kind === "lifetime"; }); const MONTH_IN_MS: number = ((4 * 365 + 1) * 24 * 60 * 60 * 1000) / (4 * 12); const THREE_MONTH_IN_MS: number = 3 * MONTH_IN_MS; const YEAR_IN_MS: number = 12 * MONTH_IN_MS; -export const canExtendDiamond = derived(diamondMembership, ($diamondMembership) => { - const now = Date.now(); - const threeMonths = now + THREE_MONTH_IN_MS; - return ( - $diamondMembership !== undefined && - $diamondMembership.expiresAt > now && - $diamondMembership.expiresAt < threeMonths - ); +export const canExtendDiamond = derived(diamondStatus, ($diamondStatus) => { + return $diamondStatus.kind === "active"; }); export function diamondDurationToMs(duration: DiamondMembershipDuration): number { @@ -35,5 +33,8 @@ export function diamondDurationToMs(duration: DiamondMembershipDuration): number if (duration === "one_year") { return YEAR_IN_MS; } + if (duration === "lifetime") { + return YEAR_IN_MS * 1000; + } throw new UnsupportedValueError("Unknown diamond membership duration supplied", duration); } diff --git a/frontend/openchat-client/src/stores/user.ts b/frontend/openchat-client/src/stores/user.ts index 4f0a3cfad0..e2ea50da08 100644 --- a/frontend/openchat-client/src/stores/user.ts +++ b/frontend/openchat-client/src/stores/user.ts @@ -23,7 +23,7 @@ export const openChatBotUser: UserSummary = { updated: BigInt(0), suspended: false, blobUrl: OPENCHAT_BOT_AVATAR_URL, - diamond: false, + diamondStatus: "inactive", }; export const anonymousUserSummary: UserSummary = { @@ -34,7 +34,7 @@ export const anonymousUserSummary: UserSummary = { updated: BigInt(0), suspended: false, blobUrl: ANON_AVATAR_URL, - diamond: false, + diamondStatus: "inactive", }; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -50,7 +50,7 @@ export function proposalsBotUser(userId: string): UserSummary { updated: BigInt(0), suspended: false, blobUrl: PROPOSALS_BOT_AVATAR_URL, - diamond: false, + diamondStatus: "inactive", }; } diff --git a/frontend/openchat-client/src/utils/chat.spec.ts b/frontend/openchat-client/src/utils/chat.spec.ts index 04635bd2ff..0f2f155c85 100644 --- a/frontend/openchat-client/src/utils/chat.spec.ts +++ b/frontend/openchat-client/src/utils/chat.spec.ts @@ -88,7 +88,7 @@ function createUser(userId: string, username: string): UserSummary { displayName: undefined, updated: BigInt(0), suspended: false, - diamond: false, + diamondStatus: "inactive", }; } diff --git a/frontend/openchat-client/src/utils/user.spec.ts b/frontend/openchat-client/src/utils/user.spec.ts index 5fb1ec81a3..1b22bea662 100644 --- a/frontend/openchat-client/src/utils/user.spec.ts +++ b/frontend/openchat-client/src/utils/user.spec.ts @@ -25,7 +25,7 @@ const lookup: UserLookup = { displayName: undefined, updated: BigInt(0), suspended: false, - diamond: false, + diamondStatus: "inactive", }, b: { kind: "user", @@ -34,7 +34,7 @@ const lookup: UserLookup = { displayName: undefined, updated: BigInt(0), suspended: false, - diamond: false, + diamondStatus: "inactive", }, xyz: { kind: "user", @@ -43,7 +43,7 @@ const lookup: UserLookup = { displayName: undefined, updated: BigInt(0), suspended: false, - diamond: false, + diamondStatus: "inactive", }, alpha: { kind: "user", @@ -52,7 +52,7 @@ const lookup: UserLookup = { displayName: undefined, updated: BigInt(0), suspended: false, - diamond: false, + diamondStatus: "inactive", }, }; @@ -101,7 +101,7 @@ describe("compare username", () => { displayName: undefined, updated: BigInt(0), suspended: false, - diamond: false, + diamondStatus: "inactive", }; } test("works with non-null usernames", () => { diff --git a/frontend/openchat-client/src/utils/user.ts b/frontend/openchat-client/src/utils/user.ts index 57822d421d..574db8b3a5 100644 --- a/frontend/openchat-client/src/utils/user.ts +++ b/frontend/openchat-client/src/utils/user.ts @@ -86,7 +86,7 @@ export function nullUser(username: string): UserSummary { displayName: undefined, updated: BigInt(0), suspended: false, - diamond: false, + diamondStatus: "inactive", }; } diff --git a/frontend/openchat-shared/src/constants.ts b/frontend/openchat-shared/src/constants.ts index c46a6c9c7d..a6338acbb5 100644 --- a/frontend/openchat-shared/src/constants.ts +++ b/frontend/openchat-shared/src/constants.ts @@ -8,3 +8,7 @@ export const OPENCHAT_BOT_AVATAR_URL = "assets/robot.svg"; // downlink is the effective bandwidth estimate in megabits per second, rounded to the nearest multiple of 25 kilobits per seconds. export const MIN_DOWNLINK = 0.05; +export const ONE_MINUTE_MILLIS = 60 * 1000; +export const ONE_HOUR = 60 * ONE_MINUTE_MILLIS; +export const ONE_DAY = 24 * ONE_HOUR; +export const ONE_YEAR = 365 * ONE_DAY; diff --git a/frontend/openchat-shared/src/domain/user/user.ts b/frontend/openchat-shared/src/domain/user/user.ts index 1d44b8f718..671cc64b23 100644 --- a/frontend/openchat-shared/src/domain/user/user.ts +++ b/frontend/openchat-shared/src/domain/user/user.ts @@ -18,7 +18,7 @@ export type UserSummary = DataContent & { displayName: string | undefined; updated: bigint; suspended: boolean; - diamond: boolean; + diamondStatus: DiamondMembershipStatus["kind"]; }; export type UserGroupSummary = { @@ -116,7 +116,7 @@ export function anonymousUser(): CreatedUser { isPlatformModerator: false, suspensionDetails: undefined, isSuspectedBot: false, - diamondMembership: undefined, + diamondStatus: { kind: "inactive" }, moderationFlagsEnabled: 0, }; } @@ -132,16 +132,24 @@ export type CreatedUser = { isPlatformModerator: boolean; suspensionDetails: SuspensionDetails | undefined; isSuspectedBot: boolean; - diamondMembership?: DiamondMembershipDetails; + diamondStatus: DiamondMembershipStatus; moderationFlagsEnabled: number; }; +export type DiamondMembershipStatus = + | { kind: "inactive" } + | { kind: "lifetime" } + | ({ kind: "active" } & DiamondMembershipDetails); + export type DiamondMembershipDetails = { - recurring?: DiamondMembershipDuration; + payInChat: boolean; + subscription: DiamondMembershipSubscription; expiresAt: bigint; }; -export type DiamondMembershipDuration = "one_month" | "three_months" | "one_year"; +export type DiamondMembershipDuration = "one_month" | "three_months" | "one_year" | "lifetime"; + +export type DiamondMembershipSubscription = "one_month" | "three_months" | "one_year" | "disabled"; export type SuspensionDetails = { reason: string; @@ -251,7 +259,7 @@ export type UnsuspendUserResponse = export type PayForDiamondMembershipResponse = | { kind: "payment_already_in_progress" } | { kind: "currency_not_supported" } - | { kind: "success"; details: DiamondMembershipDetails } + | { kind: "success"; status: DiamondMembershipStatus } | { kind: "price_mismatch" } | { kind: "transfer_failed" } | { kind: "internal_error" } diff --git a/frontend/openchat-shared/src/domain/user/user.utils.spec.ts b/frontend/openchat-shared/src/domain/user/user.utils.spec.ts index 96291377d5..8e33fb6370 100644 --- a/frontend/openchat-shared/src/domain/user/user.utils.spec.ts +++ b/frontend/openchat-shared/src/domain/user/user.utils.spec.ts @@ -12,7 +12,7 @@ const lookup: UserLookup = { displayName: undefined, updated: BigInt(0), suspended: false, - diamond: false, + diamondStatus: "inactive", }, b: { kind: "user", @@ -21,7 +21,7 @@ const lookup: UserLookup = { displayName: undefined, updated: BigInt(0), suspended: false, - diamond: false, + diamondStatus: "inactive", }, xyz: { kind: "user", @@ -30,7 +30,7 @@ const lookup: UserLookup = { displayName: undefined, updated: BigInt(0), suspended: false, - diamond: false, + diamondStatus: "inactive", }, alpha: { kind: "user", @@ -39,7 +39,7 @@ const lookup: UserLookup = { displayName: undefined, updated: BigInt(0), suspended: false, - diamond: false, + diamondStatus: "inactive", }, }; @@ -50,7 +50,7 @@ describe("extract user ids from mentions", () => { }); test("extract multiple user ids", () => { const parsed = extractUserIdsFromMentions( - "hello there @UserId(xyz) and hello @UserId(abc), how are you?" + "hello there @UserId(xyz) and hello @UserId(abc), how are you?", ); expect(parsed).toEqual(["xyz", "abc"]); }); diff --git a/frontend/openchat-shared/src/domain/worker.ts b/frontend/openchat-shared/src/domain/worker.ts index 1ccb0533b3..81be1d0725 100644 --- a/frontend/openchat-shared/src/domain/worker.ts +++ b/frontend/openchat-shared/src/domain/worker.ts @@ -617,7 +617,6 @@ type SubscriptionExists = { type RegisterUser = { username: string; - displayName: string | undefined; referralCode: string | undefined; kind: "registerUser"; }; diff --git a/frontend/openchat-worker/src/worker.ts b/frontend/openchat-worker/src/worker.ts index 888a763495..3e340a0f69 100644 --- a/frontend/openchat-worker/src/worker.ts +++ b/frontend/openchat-worker/src/worker.ts @@ -569,7 +569,7 @@ self.addEventListener("message", (msg: MessageEvent) => executeThenReply( payload, correlationId, - agent.registerUser(payload.username, payload.displayName, payload.referralCode), + agent.registerUser(payload.username, payload.referralCode), ); break; From f512acfbd5263d76cebe4474695775c89a094557 Mon Sep 17 00:00:00 2001 From: Julian Jelfs Date: Mon, 4 Dec 2023 12:47:52 +0000 Subject: [PATCH 12/14] fix emoji picker (#4914) --- .../src/components/home/EmojiPicker.svelte | 26 +------------------ .../app/src/components/home/Footer.svelte | 4 +++ .../app/src/theme/community/solarizeddark.ts | 1 + 3 files changed, 6 insertions(+), 25 deletions(-) diff --git a/frontend/app/src/components/home/EmojiPicker.svelte b/frontend/app/src/components/home/EmojiPicker.svelte index a01dbc4422..ab52d491ae 100644 --- a/frontend/app/src/components/home/EmojiPicker.svelte +++ b/frontend/app/src/components/home/EmojiPicker.svelte @@ -24,7 +24,6 @@ diff --git a/frontend/app/src/components/home/Footer.svelte b/frontend/app/src/components/home/Footer.svelte index 5d8570bfc3..9b12fe3ec9 100644 --- a/frontend/app/src/components/home/Footer.svelte +++ b/frontend/app/src/components/home/Footer.svelte @@ -169,6 +169,10 @@ @include z-index("footer"); } + :global(.emoji-overlay .modal-content) { + border: var(--bw) solid var(--bd); + } + .footer { position: relative; flex: 0 0 toRem(60); diff --git a/frontend/app/src/theme/community/solarizeddark.ts b/frontend/app/src/theme/community/solarizeddark.ts index d00740e142..c912eedeb5 100644 --- a/frontend/app/src/theme/community/solarizeddark.ts +++ b/frontend/app/src/theme/community/solarizeddark.ts @@ -31,6 +31,7 @@ export function getTheme(base: Theme): Theme { base.collapsible.closed.header.txt = txt; base.collapsible.open.header.arrow = olive; base.accent = olive; + base.panel.nav.bg = veryDarkCyan; base.panel.left.bg = hexPercent(darkCyan, 40); base.panel.right.modal = veryDarkCyan; base.modal.bd = base.bd; From 6a6d3856a56d7a67e862e317e65d83557b644ea0 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 4 Dec 2023 15:00:36 +0000 Subject: [PATCH 13/14] Fix filter of which tokens can view transactions (#4916) --- .../app/src/components/home/profile/AccountTransactions.svelte | 3 ++- frontend/app/src/components/home/profile/Accounts.svelte | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/app/src/components/home/profile/AccountTransactions.svelte b/frontend/app/src/components/home/profile/AccountTransactions.svelte index 0837505e33..0f6b8dbe49 100644 --- a/frontend/app/src/components/home/profile/AccountTransactions.svelte +++ b/frontend/app/src/components/home/profile/AccountTransactions.svelte @@ -34,6 +34,7 @@ $: cryptoLookup = client.cryptoLookup; $: tokenDetails = $cryptoLookup[ledger]; $: nervousSystemLookup = client.nervousSystemLookup; + $: snsLedgers = new Set(Object.values($nervousSystemLookup).filter((ns) => !ns.isNns).map((ns) => ns.ledgerCanisterId)); $: moreAvailable = moreTransactionsAvailable(transationData); $: loading = transationData.kind === "loading" || transationData.kind === "loading_more"; @@ -146,7 +147,7 @@
{$_("cryptoAccount.transactions")}
!["ckbtc", "icp"].includes(t.symbol.toLowerCase())} + filter={(t) => snsLedgers.has(t.ledger)} on:select={ledgerSelected} {ledger} />
diff --git a/frontend/app/src/components/home/profile/Accounts.svelte b/frontend/app/src/components/home/profile/Accounts.svelte index e246f6bf7e..0ad7f8089f 100644 --- a/frontend/app/src/components/home/profile/Accounts.svelte +++ b/frontend/app/src/components/home/profile/Accounts.svelte @@ -36,7 +36,7 @@ $: cryptoBalance = client.cryptoBalance; $: accounts = buildAccountsList($cryptoLookup, $cryptoBalance); $: nervousSystemLookup = client.nervousSystemLookup; - $: snsLedgers = new Set(Object.values($nervousSystemLookup).map((ns) => ns.ledgerCanisterId)); + $: snsLedgers = new Set(Object.values($nervousSystemLookup).filter((ns) => !ns.isNns).map((ns) => ns.ledgerCanisterId)); $: { zeroCount = accounts.filter((a) => a.zero).length; From e49decc79640d5172491cab6933adc53dd88de38 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 4 Dec 2023 15:04:39 +0000 Subject: [PATCH 14/14] Fix `earliestAvailableEventIndex` to work with channels (#4912) --- frontend/openchat-client/src/openchat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/openchat-client/src/openchat.ts b/frontend/openchat-client/src/openchat.ts index 4a55a0115e..433ade6946 100644 --- a/frontend/openchat-client/src/openchat.ts +++ b/frontend/openchat-client/src/openchat.ts @@ -2523,7 +2523,7 @@ export class OpenChat extends OpenChatAgentWorker { } earliestAvailableEventIndex(chat: ChatSummary): number { - return chat.kind === "group_chat" ? chat.minVisibleEventIndex : 0; + return chat.kind === "direct_chat" ? 0 : chat.minVisibleEventIndex; } private earliestLoadedIndex(chatId: ChatIdentifier): number | undefined {