diff --git a/backend/canisters/user/CHANGELOG.md b/backend/canisters/user/CHANGELOG.md index dde09c0dc1..5570def127 100644 --- a/backend/canisters/user/CHANGELOG.md +++ b/backend/canisters/user/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Implement tipping messages ([#4420](https://github.com/open-chat-labs/open-chat/pull/4420)) - Implement notifications for message tips ([#4427](https://github.com/open-chat-labs/open-chat/pull/4427)) - Add `followed_by_me` to the thread summary returned in GroupChatSummary ([#4431](https://github.com/open-chat-labs/open-chat/pull/4431)) +- Allow users to save named cryptocurrency accounts ([#4434](https://github.com/open-chat-labs/open-chat/pull/4434)) ## [[2.0.852](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.852-user)] - 2023-09-18 diff --git a/backend/canisters/user/api/can.did b/backend/canisters/user/api/can.did index f13538a7e7..01c942c23b 100644 --- a/backend/canisters/user/api/can.did +++ b/backend/canisters/user/api/can.did @@ -545,6 +545,22 @@ type ArchiveUnarchiveChatsResponse = variant { UserSuspended; }; +type NamedAccount = record { + name : text; + account : text; +}; + +type SaveCryptoAccountResponse = variant { + Success; + Invalid; + NameTaken; + UserSuspended; +}; + +type SavedCryptoAccountsResponse = variant { + Success : vec NamedAccount; +}; + type InitUserPrincipalMigrationArgs = record { new_principal : principal; }; @@ -851,6 +867,7 @@ service : { unpin_chat_v2 : (UnpinChatV2Request) -> (UnpinChatV2Response); manage_favourite_chats : (ManageFavouriteChatsArgs) -> (ManageFavouriteChatsResponse); archive_unarchive_chats : (ArchiveUnarchiveChatsArgs) -> (ArchiveUnarchiveChatsResponse); + save_crypto_account : (NamedAccount) -> (SaveCryptoAccountResponse); init_user_principal_migration : (InitUserPrincipalMigrationArgs) -> (InitUserPrincipalMigrationResponse); migrate_user_principal : (MigrateUserPrincipalArgs) -> (MigrateUserPrincipalResponse); @@ -868,4 +885,5 @@ service : { contacts : (ContactsArgs) -> (ContactsResponse) query; public_profile : (PublicProfileArgs) -> (PublicProfileResponse) query; hot_group_exclusions : (HotGroupExclusionsArgs) -> (HotGroupExclusionsResponse) query; + saved_crypto_accounts : (EmptyArgs) -> (SavedCryptoAccountsResponse) query; }; diff --git a/backend/canisters/user/api/src/lib.rs b/backend/canisters/user/api/src/lib.rs index 0e3bc0c433..b28904471a 100644 --- a/backend/canisters/user/api/src/lib.rs +++ b/backend/canisters/user/api/src/lib.rs @@ -172,3 +172,9 @@ pub enum ChatInList { Favourite(Chat), Community(CommunityId, ChannelId), } + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub struct NamedAccount { + pub name: String, + pub account: String, +} diff --git a/backend/canisters/user/api/src/main.rs b/backend/canisters/user/api/src/main.rs index 6af0cf47ea..80398af7da 100644 --- a/backend/canisters/user/api/src/main.rs +++ b/backend/canisters/user/api/src/main.rs @@ -13,6 +13,7 @@ fn main() { generate_candid_method!(user, messages_by_message_index, query); generate_candid_method!(user, public_profile, query); generate_candid_method!(user, search_messages, query); + generate_candid_method!(user, saved_crypto_accounts, query); generate_candid_method!(user, updates, query); generate_candid_method!(user, add_hot_group_exclusions, update); @@ -35,6 +36,7 @@ fn main() { generate_candid_method!(user, mute_notifications, update); generate_candid_method!(user, pin_chat_v2, update); generate_candid_method!(user, remove_reaction, update); + generate_candid_method!(user, save_crypto_account, update); generate_candid_method!(user, send_message_with_transfer_to_channel, update); generate_candid_method!(user, send_message_with_transfer_to_group, update); generate_candid_method!(user, send_message_v2, update); diff --git a/backend/canisters/user/api/src/queries/mod.rs b/backend/canisters/user/api/src/queries/mod.rs index 0ca0ca2168..5bd0fb9640 100644 --- a/backend/canisters/user/api/src/queries/mod.rs +++ b/backend/canisters/user/api/src/queries/mod.rs @@ -8,5 +8,6 @@ pub mod hot_group_exclusions; pub mod initial_state; pub mod messages_by_message_index; pub mod public_profile; +pub mod saved_crypto_accounts; pub mod search_messages; pub mod updates; diff --git a/backend/canisters/user/api/src/queries/saved_crypto_accounts.rs b/backend/canisters/user/api/src/queries/saved_crypto_accounts.rs new file mode 100644 index 0000000000..d3c4d2c839 --- /dev/null +++ b/backend/canisters/user/api/src/queries/saved_crypto_accounts.rs @@ -0,0 +1,11 @@ +use crate::NamedAccount; +use candid::CandidType; +use serde::{Deserialize, Serialize}; +use types::Empty; + +pub type Args = Empty; + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub enum Response { + Success(Vec), +} diff --git a/backend/canisters/user/api/src/updates/mod.rs b/backend/canisters/user/api/src/updates/mod.rs index db5b6c3de0..654cd90c0a 100644 --- a/backend/canisters/user/api/src/updates/mod.rs +++ b/backend/canisters/user/api/src/updates/mod.rs @@ -39,6 +39,7 @@ pub mod migrate_user_principal; pub mod mute_notifications; pub mod pin_chat_v2; pub mod remove_reaction; +pub mod save_crypto_account; pub mod send_message_v2; pub mod send_message_with_transfer_to_channel; pub mod send_message_with_transfer_to_group; diff --git a/backend/canisters/user/api/src/updates/save_crypto_account.rs b/backend/canisters/user/api/src/updates/save_crypto_account.rs new file mode 100644 index 0000000000..58ee18dae5 --- /dev/null +++ b/backend/canisters/user/api/src/updates/save_crypto_account.rs @@ -0,0 +1,13 @@ +use crate::NamedAccount; +use candid::CandidType; +use serde::{Deserialize, Serialize}; + +pub type Args = NamedAccount; + +#[derive(CandidType, Serialize, Deserialize, Debug)] +pub enum Response { + Success, + Invalid, + NameTaken, + UserSuspended, +} diff --git a/backend/canisters/user/impl/src/lib.rs b/backend/canisters/user/impl/src/lib.rs index 9ab065c3f8..5c432ec974 100644 --- a/backend/canisters/user/impl/src/lib.rs +++ b/backend/canisters/user/impl/src/lib.rs @@ -24,6 +24,7 @@ use types::{ BuildVersion, CanisterId, Chat, ChatId, ChatMetrics, CommunityId, Cryptocurrency, Cycles, Document, Notification, TimestampMillis, Timestamped, UserId, }; +use user_canister::NamedAccount; use utils::env::Environment; use utils::regular_jobs::RegularJobs; @@ -152,6 +153,8 @@ struct Data { pub contacts: Contacts, pub diamond_membership_expires_at: Option, pub fire_and_forget_handler: FireAndForgetHandler, + #[serde(default)] + pub saved_crypto_accounts: Vec, } impl Data { @@ -195,6 +198,7 @@ impl Data { contacts: Contacts::default(), diamond_membership_expires_at: None, fire_and_forget_handler: FireAndForgetHandler::default(), + saved_crypto_accounts: Vec::new(), } } diff --git a/backend/canisters/user/impl/src/queries/mod.rs b/backend/canisters/user/impl/src/queries/mod.rs index a57d9ab19a..b2ccf06205 100644 --- a/backend/canisters/user/impl/src/queries/mod.rs +++ b/backend/canisters/user/impl/src/queries/mod.rs @@ -9,5 +9,6 @@ pub mod http_request; pub mod initial_state; pub mod messages_by_message_index; pub mod public_profile; +pub mod saved_crypto_accounts; pub mod search_messages; pub mod updates; diff --git a/backend/canisters/user/impl/src/queries/saved_crypto_accounts.rs b/backend/canisters/user/impl/src/queries/saved_crypto_accounts.rs new file mode 100644 index 0000000000..2d39d99a91 --- /dev/null +++ b/backend/canisters/user/impl/src/queries/saved_crypto_accounts.rs @@ -0,0 +1,12 @@ +use crate::{read_state, RuntimeState}; +use ic_cdk_macros::query; +use user_canister::saved_crypto_accounts::{Response::*, *}; + +#[query] +fn saved_crypto_accounts(_args: Args) -> Response { + read_state(saved_crypto_accounts_impl) +} + +fn saved_crypto_accounts_impl(state: &RuntimeState) -> Response { + Success(state.data.saved_crypto_accounts.clone()) +} diff --git a/backend/canisters/user/impl/src/updates/mod.rs b/backend/canisters/user/impl/src/updates/mod.rs index 29dc9d9379..a28685aa4d 100644 --- a/backend/canisters/user/impl/src/updates/mod.rs +++ b/backend/canisters/user/impl/src/updates/mod.rs @@ -37,6 +37,7 @@ pub mod migrate_user_principal; pub mod mute_notifications; pub mod pin_chat_v2; pub mod remove_reaction; +pub mod save_crypto_account; pub mod send_message; pub mod send_message_with_transfer; pub mod set_avatar; diff --git a/backend/canisters/user/impl/src/updates/save_crypto_account.rs b/backend/canisters/user/impl/src/updates/save_crypto_account.rs new file mode 100644 index 0000000000..7b9d7fb829 --- /dev/null +++ b/backend/canisters/user/impl/src/updates/save_crypto_account.rs @@ -0,0 +1,43 @@ +use crate::guards::caller_is_owner; +use crate::{mutate_state, run_regular_jobs, RuntimeState}; +use candid::Principal; +use canister_tracing_macros::trace; +use ic_cdk_macros::update; +use ic_ledger_types::AccountIdentifier; +use user_canister::save_crypto_account::{Response::*, *}; + +#[update(guard = "caller_is_owner")] +#[trace] +fn save_crypto_account(args: Args) -> Response { + run_regular_jobs(); + + mutate_state(|state| save_crypto_account_impl(args, state)) +} + +fn save_crypto_account_impl(mut args: Args, state: &mut RuntimeState) -> Response { + if state.data.suspended.value { + return UserSuspended; + } + + args.name = args.name.trim().to_string(); + args.name.truncate(25); + args.account = args.account.trim().to_string(); + + let valid = Principal::from_text(&args.account).is_ok() || AccountIdentifier::from_hex(&args.account).is_ok(); + + if valid { + for named_account in state.data.saved_crypto_accounts.iter_mut() { + if named_account.account == args.account { + named_account.name = args.name; + return Success; + } + if named_account.name == args.name { + return NameTaken; + } + } + state.data.saved_crypto_accounts.push(args); + Success + } else { + Invalid + } +} diff --git a/backend/integration_tests/src/client/user.rs b/backend/integration_tests/src/client/user.rs index 2a09efe7e4..42b3a30e93 100644 --- a/backend/integration_tests/src/client/user.rs +++ b/backend/integration_tests/src/client/user.rs @@ -5,6 +5,7 @@ use user_canister::*; generate_query_call!(events); generate_query_call!(events_by_index); generate_query_call!(initial_state); +generate_query_call!(saved_crypto_accounts); generate_query_call!(updates); // Updates @@ -22,6 +23,7 @@ generate_update_call!(leave_group); generate_update_call!(mark_read); generate_update_call!(mute_notifications); generate_update_call!(remove_reaction); +generate_update_call!(save_crypto_account); generate_update_call!(send_message_v2); generate_update_call!(send_message_with_transfer_to_channel); generate_update_call!(send_message_with_transfer_to_group); diff --git a/backend/integration_tests/src/lib.rs b/backend/integration_tests/src/lib.rs index 4baff9f4bb..bf29c6af3c 100644 --- a/backend/integration_tests/src/lib.rs +++ b/backend/integration_tests/src/lib.rs @@ -26,6 +26,7 @@ mod register_user_tests; mod registry_tests; mod remove_from_group_tests; mod rng; +mod save_crypto_account_tests; mod send_crypto_tests; mod send_direct_message_tests; mod set_message_reminder_tests; diff --git a/backend/integration_tests/src/save_crypto_account_tests.rs b/backend/integration_tests/src/save_crypto_account_tests.rs new file mode 100644 index 0000000000..9405350eb4 --- /dev/null +++ b/backend/integration_tests/src/save_crypto_account_tests.rs @@ -0,0 +1,26 @@ +use crate::env::ENV; +use crate::rng::{random_principal, random_string}; +use crate::{client, TestEnv}; +use std::ops::Deref; +use types::Empty; +use user_canister::NamedAccount; + +#[test] +fn save_crypto_account_succeeds() { + 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); + let name = random_string(); + let account = random_principal().to_string(); + + let named_account = NamedAccount { name, account }; + + let response = client::user::save_crypto_account(env, user.principal, user.canister(), &named_account); + assert!(matches!(response, user_canister::save_crypto_account::Response::Success)); + + let user_canister::saved_crypto_accounts::Response::Success(accounts) = + client::user::saved_crypto_accounts(env, user.principal, user.canister(), &Empty {}); + + assert_eq!(accounts, vec![named_account]); +}