From 552711c432058fad72d614334dd23423e065abd5 Mon Sep 17 00:00:00 2001 From: FroVolod <36816899+FroVolod@users.noreply.github.com> Date: Mon, 29 May 2023 00:07:33 +0300 Subject: [PATCH] feat: New commands to manage data in SocialDB (#38) Resolves #16 #17 --- Cargo.lock | 68 +++---- Cargo.toml | 6 +- README.md | 6 + src/common.rs | 95 +++++++++ src/components/delete/sign_as/mod.rs | 16 +- src/components/deploy/mod.rs | 4 +- .../deploy/{deploy_args.rs => sign_as.rs} | 73 +------ src/social_db/data/delete/mod.rs | 120 ++++++++++++ src/social_db/data/delete/sign_as.rs | 97 +++++++++ src/social_db/data/mod.rs | 30 +++ src/social_db/data/set/data/mod.rs | 39 ++++ src/social_db/data/set/data/with_json.rs | 33 ++++ src/social_db/data/set/data/with_json_file.rs | 68 +++++++ src/social_db/data/set/data/with_text.rs | 34 ++++ src/social_db/data/set/data/with_text_file.rs | 38 ++++ src/social_db/data/set/mod.rs | 43 ++++ src/social_db/data/set/sign_as.rs | 185 ++++++++++++++++++ src/social_db/data/view/mod.rs | 79 ++++++++ src/social_db/mod.rs | 6 + 19 files changed, 915 insertions(+), 125 deletions(-) rename src/components/deploy/{deploy_args.rs => sign_as.rs} (83%) create mode 100644 src/social_db/data/delete/mod.rs create mode 100644 src/social_db/data/delete/sign_as.rs create mode 100644 src/social_db/data/mod.rs create mode 100644 src/social_db/data/set/data/mod.rs create mode 100644 src/social_db/data/set/data/with_json.rs create mode 100644 src/social_db/data/set/data/with_json_file.rs create mode 100644 src/social_db/data/set/data/with_text.rs create mode 100644 src/social_db/data/set/data/with_text_file.rs create mode 100644 src/social_db/data/set/mod.rs create mode 100644 src/social_db/data/set/sign_as.rs create mode 100644 src/social_db/data/view/mod.rs diff --git a/Cargo.lock b/Cargo.lock index cc6d691..bfb256a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,7 +180,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -191,7 +191,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -243,9 +243,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1e31e207a6b8fb791a38ea3105e6cb541f55e4d029902d3039a4ad07cc4105" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "bip39" @@ -540,7 +540,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -1135,7 +1135,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -1482,9 +1482,9 @@ dependencies = [ [[package]] name = "interactive-clap" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509d9e713266154a28bcd600c238fc5b12de98711cb50d17957c46679659b71b" +checksum = "93acfa09fc1e4fde84612a91e84e41f88b80670a23470e0d92d146cbe968a64c" dependencies = [ "interactive-clap-derive", "strum", @@ -1493,9 +1493,9 @@ dependencies = [ [[package]] name = "interactive-clap-derive" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9cacaf732ed37897d24425d7ce762b40e9d8154d3c20635a78b1cf189da7245" +checksum = "33f503f6fb93383eda5a799dfc0ca59d629764c394b50b4122b4af3b230ca226" dependencies = [ "proc-macro-error", "proc-macro2", @@ -1505,9 +1505,9 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.1", "libc", @@ -1776,9 +1776,9 @@ dependencies = [ [[package]] name = "near-cli-rs" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00603bb7f00787674318e85ad4087b15d8099a0383007162f937997bc52fe173" +checksum = "cab3da8fd9d833f901709afceebbb6627e4f145b2665926d3b9fd8d16a993eda" dependencies = [ "base64 0.13.1", "bip39", @@ -2167,7 +2167,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -2326,7 +2326,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -2412,9 +2412,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] @@ -2495,9 +2495,9 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "quote" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -2728,9 +2728,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.2" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1a59b5d8e97dee33696bf13c5ba8ab85341c002922fba050069326b9c498974" +checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" dependencies = [ "aho-corasick", "memchr", @@ -2764,7 +2764,7 @@ version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ - "base64 0.21.1", + "base64 0.21.2", "bytes", "encoding_rs", "futures-core", @@ -2925,7 +2925,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -2947,7 +2947,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -3202,9 +3202,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", @@ -3267,7 +3267,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -3524,7 +3524,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] [[package]] @@ -3632,9 +3632,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" @@ -3758,7 +3758,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", "wasm-bindgen-shared", ] @@ -3792,7 +3792,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4080,5 +4080,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.18", ] diff --git a/Cargo.toml b/Cargo.toml index 6d7faac..7b48921 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,12 +39,12 @@ near-primitives = "0.16.1" near-jsonrpc-client = "0.5.1" near-jsonrpc-primitives = "0.16.1" -interactive-clap = "0.2.1" -interactive-clap-derive = "0.2.1" +interactive-clap = "0.2.2" +interactive-clap-derive = "0.2.2" console = "0.15.5" -near-cli-rs = { version = "0.4.1", default-features = false } +near-cli-rs = { version = "0.4.2", default-features = false } [target.'cfg(target_os = "macos")'.dependencies] security-framework = "2.7.0" diff --git a/README.md b/README.md index 4155209..1ab76df 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,12 @@ Currently, only two groups of commands are implemented: ### socialdb - SocialDb management +#### data - Data management: viewing, adding, updating, deleting information by a given key + +- `view` allows you to view information by a given key. +- `set` allows you to add or update information by a given key. +- `delete` allows you to delete information by the specified key. + #### prepaid-storage - Storage management: deposit, withdrawal, balance review - `view-balance` allows you to view the storage balance for an account. diff --git a/src/common.rs b/src/common.rs index d467499..e25ac31 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::str::FromStr; use color_eyre::eyre::{ContextCompat, WrapErr}; use console::{style, Style}; @@ -195,6 +196,77 @@ pub fn get_access_key_permission( Ok(permission) } +pub fn get_deposit( + network_config: &near_cli_rs::config::NetworkConfig, + signer_account_id: &near_primitives::types::AccountId, + signer_public_key: &near_crypto::PublicKey, + deploy_to_account_id: &near_primitives::types::AccountId, + key: &str, + near_social_account_id: &near_primitives::types::AccountId, + required_deposit: near_cli_rs::common::NearBalance, +) -> color_eyre::eyre::Result { + let signer_access_key_permission = crate::common::get_access_key_permission( + network_config, + signer_account_id, + signer_public_key, + )?; + + let is_signer_access_key_full_access = matches!( + signer_access_key_permission, + near_primitives::views::AccessKeyPermissionView::FullAccess + ); + + let is_write_permission_granted_to_public_key = crate::common::is_write_permission_granted( + network_config, + near_social_account_id, + signer_public_key.clone(), + format!("{deploy_to_account_id}/{key}"), + )?; + + let is_write_permission_granted_to_signer = crate::common::is_write_permission_granted( + network_config, + near_social_account_id, + signer_account_id.clone(), + format!("{deploy_to_account_id}/{key}"), + )?; + + let deposit = if is_signer_access_key_full_access + || crate::common::is_signer_access_key_function_call_access_can_call_set_on_social_db_account( + near_social_account_id, + &signer_access_key_permission + )? + { + if is_write_permission_granted_to_public_key || is_write_permission_granted_to_signer { + if required_deposit.is_zero() + { + near_cli_rs::common::NearBalance::from_str("0 NEAR").unwrap() + } else if is_signer_access_key_full_access { + required_deposit + } else { + color_eyre::eyre::bail!("ERROR: Social DB requires more storage deposit, but we cannot cover it when signing transaction with a Function Call only access key") + } + } else if signer_account_id == deploy_to_account_id { + if is_signer_access_key_full_access { + if required_deposit.is_zero() + { + near_cli_rs::common::NearBalance::from_str("1 yoctoNEAR").unwrap() + } else { + required_deposit + } + } else { + color_eyre::eyre::bail!("ERROR: Social DB requires more storage deposit, but we cannot cover it when signing transaction with a Function Call only access key") + } + } else { + color_eyre::eyre::bail!( + "ERROR: the signer is not allowed to modify the components of this account_id." + ) + } + } else { + color_eyre::eyre::bail!("ERROR: signer access key cannot be used to sign a transaction to update components in Social DB.") + }; + Ok(deposit) +} + pub fn required_deposit( network_config: &near_cli_rs::config::NetworkConfig, near_social_account_id: &near_primitives::types::AccountId, @@ -306,3 +378,26 @@ fn estimate_data_size(data: &serde_json::Value, prev_data: Option<&serde_json::V } } } + +/// Helper function that marks SocialDB values to be deleted by setting `null` to the values +pub fn mark_leaf_values_as_null(data: &mut serde_json::Value) { + match data { + serde_json::Value::Object(object_data) => { + for value in object_data.values_mut() { + mark_leaf_values_as_null(value); + } + } + data => { + *data = serde_json::Value::Null; + } + } +} + +pub fn social_db_data_from_key(full_key: &str, data_to_set: &mut serde_json::Value) { + if let Some((prefix, key)) = full_key.rsplit_once('/') { + *data_to_set = serde_json::json!({ key: data_to_set }); + social_db_data_from_key(prefix, data_to_set) + } else { + *data_to_set = serde_json::json!({ full_key: data_to_set }); + } +} diff --git a/src/components/delete/sign_as/mod.rs b/src/components/delete/sign_as/mod.rs index 5ea3456..918f9cc 100644 --- a/src/components/delete/sign_as/mod.rs +++ b/src/components/delete/sign_as/mod.rs @@ -82,7 +82,7 @@ impl From for near_cli_rs::commands::ActionContext { actions: vec![], }); } - mark_leaf_values_as_null(&mut social_db_data_to_remove); + crate::common::mark_leaf_values_as_null(&mut social_db_data_to_remove); Ok(near_cli_rs::commands::PrepopulatedTransaction { signer_id: signer_id.clone(), @@ -165,17 +165,3 @@ impl Signer { } } } - -/// Helper function that marks SocialDB values to be deleted by setting `null` to the values -fn mark_leaf_values_as_null(data: &mut serde_json::Value) { - match data { - serde_json::Value::Object(object_data) => { - for value in object_data.values_mut() { - mark_leaf_values_as_null(value); - } - } - data => { - *data = serde_json::Value::Null; - } - } -} diff --git a/src/components/deploy/mod.rs b/src/components/deploy/mod.rs index 7751f3a..9a2da60 100644 --- a/src/components/deploy/mod.rs +++ b/src/components/deploy/mod.rs @@ -1,6 +1,6 @@ use inquire::{CustomType, Select}; -mod deploy_args; +mod sign_as; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TransactionFunctionArgs { @@ -16,7 +16,7 @@ pub struct DeployToAccount { deploy_to_account_id: near_cli_rs::types::account_id::AccountId, #[interactive_clap(named_arg)] /// Specify signer account ID - sign_as: self::deploy_args::Signer, + sign_as: self::sign_as::Signer, } #[derive(Clone)] diff --git a/src/components/deploy/deploy_args.rs b/src/components/deploy/sign_as.rs similarity index 83% rename from src/components/deploy/deploy_args.rs rename to src/components/deploy/sign_as.rs index 096bb12..eabadcc 100644 --- a/src/components/deploy/deploy_args.rs +++ b/src/components/deploy/sign_as.rs @@ -171,11 +171,12 @@ impl From for near_cli_rs::commands::ActionContext { if let near_primitives::transaction::Action::FunctionCall(action) = &mut prepopulated_unsigned_transaction.actions[0] { - action.deposit = get_deposit( + action.deposit = crate::common::get_deposit( network_config, &signer_account_id, &prepopulated_unsigned_transaction.public_key, &deploy_to_account_id, + "widget", &prepopulated_unsigned_transaction.receiver_id, near_cli_rs::common::NearBalance::from_yoctonear(action.deposit), )? @@ -267,76 +268,6 @@ impl Signer { } } -fn get_deposit( - network_config: &near_cli_rs::config::NetworkConfig, - signer_account_id: &near_primitives::types::AccountId, - signer_public_key: &near_crypto::PublicKey, - deploy_to_account_id: &near_primitives::types::AccountId, - near_social_account_id: &near_primitives::types::AccountId, - required_deposit: near_cli_rs::common::NearBalance, -) -> color_eyre::eyre::Result { - let signer_access_key_permission = crate::common::get_access_key_permission( - network_config, - signer_account_id, - signer_public_key, - )?; - - let is_signer_access_key_full_access = matches!( - signer_access_key_permission, - near_primitives::views::AccessKeyPermissionView::FullAccess - ); - - let is_write_permission_granted_to_public_key = crate::common::is_write_permission_granted( - network_config, - near_social_account_id, - signer_public_key.clone(), - format!("{deploy_to_account_id}/widget"), - )?; - - let is_write_permission_granted_to_signer = crate::common::is_write_permission_granted( - network_config, - near_social_account_id, - signer_account_id.clone(), - format!("{deploy_to_account_id}/widget"), - )?; - - let deposit = if is_signer_access_key_full_access - || crate::common::is_signer_access_key_function_call_access_can_call_set_on_social_db_account( - near_social_account_id, - &signer_access_key_permission - )? - { - if is_write_permission_granted_to_public_key || is_write_permission_granted_to_signer { - if required_deposit.is_zero() - { - near_cli_rs::common::NearBalance::from_str("0 NEAR").unwrap() - } else if is_signer_access_key_full_access { - required_deposit - } else { - color_eyre::eyre::bail!("ERROR: Social DB requires more storage deposit, but we cannot cover it when signing transaction with a Function Call only access key") - } - } else if signer_account_id == deploy_to_account_id { - if is_signer_access_key_full_access { - if required_deposit.is_zero() - { - near_cli_rs::common::NearBalance::from_str("1 yoctoNEAR").unwrap() - } else { - required_deposit - } - } else { - color_eyre::eyre::bail!("ERROR: Social DB requires more storage deposit, but we cannot cover it when signing transaction with a Function Call only access key") - } - } else { - color_eyre::eyre::bail!( - "ERROR: signer is not allowed to modify deploy_to_account_id components." - ) - } - } else { - color_eyre::eyre::bail!("ERROR: signer access key cannot be used to sign a transaction to update components in Social DB.") - }; - Ok(deposit) -} - async fn get_components( network_config: &near_cli_rs::config::NetworkConfig, near_social_account_id: &near_primitives::types::AccountId, diff --git a/src/social_db/data/delete/mod.rs b/src/social_db/data/delete/mod.rs new file mode 100644 index 0000000..7d0b70b --- /dev/null +++ b/src/social_db/data/delete/mod.rs @@ -0,0 +1,120 @@ +use std::str::FromStr; + +use color_eyre::eyre::{ContextCompat, WrapErr}; +use near_cli_rs::common::{CallResultExt, JsonRpcClientExt}; + +mod sign_as; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = crate::GlobalContext)] +#[interactive_clap(output_context = DeleteContext)] +pub struct Delete { + /// Enter SocialDB key path to delete data (e.g. root.near/profile/image): + key: String, + #[interactive_clap(named_arg)] + /// Specify signer account ID + sign_as: self::sign_as::Signer, +} + +#[derive(Clone)] +pub struct DeleteContext(self::sign_as::PreparedSignerContext); + +impl DeleteContext { + pub fn from_previous_context( + previous_context: crate::GlobalContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let account_id = near_cli_rs::types::account_id::AccountId::from_str( + scope + .key + .split_once('/') + .wrap_err("Failed to parse account_id from this key")? + .0 + .trim(), + )?; + + let on_after_getting_network_callback: near_cli_rs::commands::OnAfterGettingNetworkCallback = std::sync::Arc::new({ + let signer_id = account_id.clone(); + let key = scope.key.clone(); + + move |network_config| { + let near_social_account_id = crate::consts::NEAR_SOCIAL_ACCOUNT_ID.get(network_config.network_name.as_str()) + .wrap_err_with(|| format!("The <{}> network does not have a near-social contract.", network_config.network_name))?; + + let input_args = serde_json::to_string(&crate::socialdb_types::SocialDbQuery { + keys: vec![format!("{key}")], + }) + .wrap_err("Internal error: could not serialize SocialDB input args")?; + + let mut social_db_data_to_remove: serde_json::Value = network_config + .json_rpc_client() + .blocking_call_view_function( + near_social_account_id, + "get", + input_args.into_bytes(), + near_primitives::types::Finality::Final.into(), + ) + .wrap_err("Failed to fetch the components from SocialDB")? + .parse_result_from_json() + .wrap_err("SocialDB `get` data response cannot be parsed")?; + if social_db_data_to_remove.as_object().map(|result| result.is_empty()).unwrap_or(true) { + println!("No keys to remove. Goodbye."); + return Ok(near_cli_rs::commands::PrepopulatedTransaction { + signer_id: signer_id.clone().into(), + receiver_id: near_social_account_id.clone(), + actions: vec![], + }); + } + crate::common::mark_leaf_values_as_null(&mut social_db_data_to_remove); + Ok(near_cli_rs::commands::PrepopulatedTransaction { + signer_id: signer_id.clone().into(), + receiver_id: near_social_account_id.clone(), + actions: vec![near_primitives::transaction::Action::FunctionCall( + near_primitives::transaction::FunctionCallAction { + method_name: "set".to_string(), + args: serde_json::json!({ + "data": social_db_data_to_remove + }).to_string().into_bytes(), + gas: near_cli_rs::common::NearGas::from_str("300 TeraGas") + .unwrap() + .inner, + deposit: near_cli_rs::common::NearBalance::from_yoctonear(0).to_yoctonear(), + }, + )] + }) + } + }); + + let on_after_sending_transaction_callback: near_cli_rs::transaction_signature_options::OnAfterSendingTransactionCallback = std::sync::Arc::new({ + let account_id = account_id.clone(); + + move |transaction_info, _network_config| { + if let near_primitives::views::FinalExecutionStatus::SuccessValue(_) = transaction_info.status { + println!("Keys successfully removed from <{account_id}>"); + } else { + color_eyre::eyre::bail!("Keys were not successfully removed from <{account_id}>"); + }; + Ok(()) + } + }); + + Ok(Self(self::sign_as::PreparedSignerContext { + config: previous_context.0, + account_id, + on_after_getting_network_callback, + on_before_signing_callback: std::sync::Arc::new( + |_prepolulated_unsinged_transaction, _network_config| Ok(()), + ), + on_before_sending_transaction_callback: std::sync::Arc::new( + |_signed_transaction, _network_config, _message| Ok(()), + ), + on_after_sending_transaction_callback, + })) + } +} + +impl From for self::sign_as::PreparedSignerContext { + fn from(item: DeleteContext) -> Self { + item.0 + } +} diff --git a/src/social_db/data/delete/sign_as.rs b/src/social_db/data/delete/sign_as.rs new file mode 100644 index 0000000..85cf19c --- /dev/null +++ b/src/social_db/data/delete/sign_as.rs @@ -0,0 +1,97 @@ +use inquire::{CustomType, Select}; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = PreparedSignerContext)] +#[interactive_clap(output_context = SignerContext)] +pub struct Signer { + #[interactive_clap(skip_default_input_arg)] + /// What is the signer account ID? + signer_account_id: near_cli_rs::types::account_id::AccountId, + #[interactive_clap(named_arg)] + /// Select network + network_config: near_cli_rs::network_for_transaction::NetworkForTransactionArgs, +} + +#[derive(Clone)] +pub struct SignerContext(near_cli_rs::commands::ActionContext); + +impl SignerContext { + pub fn from_previous_context( + previous_context: PreparedSignerContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let signer_account_id = scope.signer_account_id.clone(); + + let on_after_getting_network_callback: near_cli_rs::commands::OnAfterGettingNetworkCallback = std::sync::Arc::new( + move |network_config| { + let mut prepopulated_transaction = (previous_context.on_after_getting_network_callback)(network_config)?; + prepopulated_transaction.signer_id = signer_account_id.clone().into(); + Ok(prepopulated_transaction) + }); + + Ok(Self(near_cli_rs::commands::ActionContext { + config: previous_context.config, + on_after_getting_network_callback, + on_before_signing_callback: std::sync::Arc::new(|_transaction, _network_config| Ok(())), + on_before_sending_transaction_callback: std::sync::Arc::new( + |_signed_transaction, _network_config, _message| Ok(()), + ), + on_after_sending_transaction_callback: previous_context + .on_after_sending_transaction_callback, + })) + } +} + +impl From for near_cli_rs::commands::ActionContext { + fn from(item: SignerContext) -> Self { + item.0 + } +} + +impl Signer { + fn input_signer_account_id( + context: &PreparedSignerContext, + ) -> color_eyre::eyre::Result> { + loop { + let signer_account_id: near_cli_rs::types::account_id::AccountId = + CustomType::new("What is the signer account ID?") + .with_default(context.account_id.clone()) + .prompt()?; + if !near_cli_rs::common::is_account_exist( + &context.config.network_connection, + signer_account_id.clone().into(), + ) { + println!("\nThe account <{signer_account_id}> does not yet exist."); + #[derive(strum_macros::Display)] + enum ConfirmOptions { + #[strum(to_string = "Yes, I want to enter a new account name.")] + Yes, + #[strum(to_string = "No, I want to use this account name.")] + No, + } + let select_choose_input = Select::new( + "Do you want to enter another signer account id?", + vec![ConfirmOptions::Yes, ConfirmOptions::No], + ) + .prompt()?; + if let ConfirmOptions::No = select_choose_input { + return Ok(Some(signer_account_id)); + } + } else { + return Ok(Some(signer_account_id)); + } + } + } +} + +#[derive(Clone)] +pub struct PreparedSignerContext { + pub config: near_cli_rs::config::Config, + pub account_id: near_cli_rs::types::account_id::AccountId, + pub on_after_getting_network_callback: near_cli_rs::commands::OnAfterGettingNetworkCallback, + pub on_before_signing_callback: near_cli_rs::commands::OnBeforeSigningCallback, + pub on_before_sending_transaction_callback: + near_cli_rs::transaction_signature_options::OnBeforeSendingTransactionCallback, + pub on_after_sending_transaction_callback: + near_cli_rs::transaction_signature_options::OnAfterSendingTransactionCallback, +} diff --git a/src/social_db/data/mod.rs b/src/social_db/data/mod.rs new file mode 100644 index 0000000..d9d28d5 --- /dev/null +++ b/src/social_db/data/mod.rs @@ -0,0 +1,30 @@ +use strum::{EnumDiscriminants, EnumIter, EnumMessage}; + +mod delete; +mod set; +mod view; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(context = crate::GlobalContext)] +pub struct Data { + #[interactive_clap(subcommand)] + data_command: DataCommand, +} + +#[derive(Debug, EnumDiscriminants, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(context = crate::GlobalContext)] +#[strum_discriminants(derive(EnumMessage, EnumIter))] +/// Select the data command: +pub enum DataCommand { + #[strum_discriminants(strum(message = "view - Viewing information by a given key"))] + /// Viewing information by a given key + View(self::view::View), + #[strum_discriminants(strum( + message = "set - Adding or updating information by a given key" + ))] + /// Adding or updating information by a given key + Set(self::set::Set), + #[strum_discriminants(strum(message = "delete - Deleting information by a given key"))] + /// Deleting information by a given key + Delete(self::delete::Delete), +} diff --git a/src/social_db/data/set/data/mod.rs b/src/social_db/data/set/data/mod.rs new file mode 100644 index 0000000..118a4b3 --- /dev/null +++ b/src/social_db/data/set/data/mod.rs @@ -0,0 +1,39 @@ +use strum::{EnumDiscriminants, EnumIter, EnumMessage}; + +mod with_json; +mod with_json_file; +mod with_text; +mod with_text_file; + +#[derive(Debug, EnumDiscriminants, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(context = super::SetContext)] +#[strum_discriminants(derive(EnumMessage, EnumIter))] +/// How would you like to pass the data to set to the key? +pub enum DataType { + #[strum_discriminants(strum( + message = "with-json - Valid JSON arguments (e.g. {\"token_id\": \"42\"})" + ))] + /// Valid JSON arguments (e.g. {"token_id": "42"}) + WithJson(self::with_json::JsonData), + #[strum_discriminants(strum(message = "with-text - Arbitrary text arguments"))] + /// Arbitrary text arguments + WithText(self::with_text::TextData), + #[strum_discriminants(strum( + message = "with-json-file - Reading from a reusable text file" + ))] + /// Reading from a reusable text file + WithJsonFile(self::with_json_file::JsonDataFile), + #[strum_discriminants(strum( + message = "with-text-file - Reading from a reusable JSON file" + ))] + /// Reading from a reusable JSON file + WithTextFile(self::with_text_file::TextDataFile), +} + +#[derive(Clone)] +pub struct DataContext { + pub config: near_cli_rs::config::Config, + pub set_to_account_id: near_cli_rs::types::account_id::AccountId, + pub key: String, + pub value: serde_json::Value, +} diff --git a/src/social_db/data/set/data/with_json.rs b/src/social_db/data/set/data/with_json.rs new file mode 100644 index 0000000..4cd0aff --- /dev/null +++ b/src/social_db/data/set/data/with_json.rs @@ -0,0 +1,33 @@ +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = super::super::SetContext)] +#[interactive_clap(output_context = JsonDataContext)] +pub struct JsonData { + /// Enter the data to set to the key (e.g. {"token_id": "42"}): + args: near_cli_rs::types::json::Json, + #[interactive_clap(named_arg)] + /// Specify signer account ID + sign_as: super::super::sign_as::Signer, +} + +#[derive(Clone)] +pub struct JsonDataContext(super::DataContext); + +impl JsonDataContext { + pub fn from_previous_context( + previous_context: super::super::SetContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + Ok(Self(super::DataContext { + config: previous_context.config, + set_to_account_id: previous_context.set_to_account_id, + key: previous_context.key, + value: scope.args.clone().into(), + })) + } +} + +impl From for super::DataContext { + fn from(item: JsonDataContext) -> Self { + item.0 + } +} diff --git a/src/social_db/data/set/data/with_json_file.rs b/src/social_db/data/set/data/with_json_file.rs new file mode 100644 index 0000000..83dabd0 --- /dev/null +++ b/src/social_db/data/set/data/with_json_file.rs @@ -0,0 +1,68 @@ +use color_eyre::eyre::Context; +use inquire::CustomType; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = super::super::SetContext)] +#[interactive_clap(output_context = JsonDataFileContext)] +pub struct JsonDataFile { + #[interactive_clap(skip_default_input_arg)] + /// Enter the path to the data file: + path: near_cli_rs::types::path_buf::PathBuf, + #[interactive_clap(named_arg)] + /// Specify signer account ID + sign_as: super::super::sign_as::Signer, +} + +#[derive(Clone)] +pub struct JsonDataFileContext(super::DataContext); + +impl JsonDataFileContext { + pub fn from_previous_context( + previous_context: super::super::SetContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let file = std::fs::File::open(&scope.path.0) + .wrap_err_with(|| format!("Access to data file <{:?}> not found!", scope.path))?; + let reader = std::io::BufReader::new(file); + let value: serde_json::Value = + serde_json::from_reader(reader).wrap_err("File data is not in JSON format!")?; + Ok(Self(super::DataContext { + config: previous_context.config, + set_to_account_id: previous_context.set_to_account_id, + key: previous_context.key, + value, + })) + } +} + +impl From for super::DataContext { + fn from(item: JsonDataFileContext) -> Self { + item.0 + } +} + +impl JsonDataFile { + fn input_path( + _context: &super::super::SetContext, + ) -> color_eyre::eyre::Result> { + loop { + let path: near_cli_rs::types::path_buf::PathBuf = + CustomType::new("Enter the path to the arguments file:").prompt()?; + let file_result = std::fs::File::open(&path.0); + if let Ok(file) = file_result { + let reader = std::io::BufReader::new(file); + if serde_json::from_reader::, serde_json::Value>( + reader, + ) + .is_err() + { + println!("File data is not in JSON format!"); + } else { + return Ok(Some(path)); + } + } else { + println!("Access to data file <{:?}> not found!", path) + } + } + } +} diff --git a/src/social_db/data/set/data/with_text.rs b/src/social_db/data/set/data/with_text.rs new file mode 100644 index 0000000..5349ffe --- /dev/null +++ b/src/social_db/data/set/data/with_text.rs @@ -0,0 +1,34 @@ +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = super::super::SetContext)] +#[interactive_clap(output_context = TextDataContext)] +pub struct TextData { + /// Enter the data to set to the key: + args: String, + #[interactive_clap(named_arg)] + /// Specify signer account ID + sign_as: super::super::sign_as::Signer, +} + +#[derive(Clone)] +pub struct TextDataContext(super::DataContext); + +impl TextDataContext { + pub fn from_previous_context( + previous_context: super::super::SetContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let value = serde_json::Value::String(scope.args.clone()); + Ok(Self(super::DataContext { + config: previous_context.config, + set_to_account_id: previous_context.set_to_account_id, + key: previous_context.key, + value, + })) + } +} + +impl From for super::DataContext { + fn from(item: TextDataContext) -> Self { + item.0 + } +} diff --git a/src/social_db/data/set/data/with_text_file.rs b/src/social_db/data/set/data/with_text_file.rs new file mode 100644 index 0000000..bbc4b08 --- /dev/null +++ b/src/social_db/data/set/data/with_text_file.rs @@ -0,0 +1,38 @@ +use color_eyre::eyre::Context; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = super::super::SetContext)] +#[interactive_clap(output_context = TextDataFileContext)] +pub struct TextDataFile { + /// Enter the path to the data file: + path: near_cli_rs::types::path_buf::PathBuf, + #[interactive_clap(named_arg)] + /// Specify signer account ID + sign_as: super::super::sign_as::Signer, +} + +#[derive(Clone)] +pub struct TextDataFileContext(super::DataContext); + +impl TextDataFileContext { + pub fn from_previous_context( + previous_context: super::super::SetContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let data = std::fs::read_to_string(&scope.path.0) + .wrap_err_with(|| format!("Access to data file <{:?}> not found!", scope.path))?; + let value = serde_json::Value::String(data); + Ok(Self(super::DataContext { + config: previous_context.config, + set_to_account_id: previous_context.set_to_account_id, + key: previous_context.key, + value, + })) + } +} + +impl From for super::DataContext { + fn from(item: TextDataFileContext) -> Self { + item.0 + } +} diff --git a/src/social_db/data/set/mod.rs b/src/social_db/data/set/mod.rs new file mode 100644 index 0000000..54b4cbc --- /dev/null +++ b/src/social_db/data/set/mod.rs @@ -0,0 +1,43 @@ +use std::str::FromStr; + +use color_eyre::eyre::ContextCompat; + +mod data; +mod sign_as; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = crate::GlobalContext)] +#[interactive_clap(output_context = SetContext)] +pub struct Set { + /// Enter SocialDB key path to set the value (e.g. root.near/profile/name): + key: String, + #[interactive_clap(subcommand)] + data_type: self::data::DataType, +} + +#[derive(Clone)] +pub struct SetContext { + pub config: near_cli_rs::config::Config, + pub set_to_account_id: near_cli_rs::types::account_id::AccountId, + pub key: String, +} + +impl SetContext { + pub fn from_previous_context( + previous_context: crate::GlobalContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + Ok(Self { + config: previous_context.0, + set_to_account_id: near_cli_rs::types::account_id::AccountId::from_str( + scope + .key + .split_once('/') + .wrap_err("Failed to parse account_id from this key")? + .0 + .trim(), + )?, + key: scope.key.clone(), + }) + } +} diff --git a/src/social_db/data/set/sign_as.rs b/src/social_db/data/set/sign_as.rs new file mode 100644 index 0000000..ab586ca --- /dev/null +++ b/src/social_db/data/set/sign_as.rs @@ -0,0 +1,185 @@ +use std::str::FromStr; + +use color_eyre::eyre::{ContextCompat, WrapErr}; +use inquire::{CustomType, Select}; +use near_cli_rs::common::{CallResultExt, JsonRpcClientExt}; +use std::sync::Arc; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = super::data::DataContext)] +#[interactive_clap(output_context = SignerContext)] +pub struct Signer { + #[interactive_clap(skip_default_input_arg)] + /// What is the signer account ID? + signer_account_id: near_cli_rs::types::account_id::AccountId, + #[interactive_clap(named_arg)] + /// Select network + network_config: near_cli_rs::network_for_transaction::NetworkForTransactionArgs, +} + +#[derive(Clone)] +pub struct SignerContext(near_cli_rs::commands::ActionContext); + +impl SignerContext { + pub fn from_previous_context( + previous_context: super::data::DataContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let set_to_account_id: near_primitives::types::AccountId = + previous_context.set_to_account_id.clone().into(); + let signer_id: near_primitives::types::AccountId = scope.signer_account_id.clone().into(); + let key = previous_context.key.clone(); + + let on_after_getting_network_callback: near_cli_rs::commands::OnAfterGettingNetworkCallback = Arc::new({ + let signer_id = signer_id.clone(); + let set_to_account_id = set_to_account_id.clone(); + + move |network_config| { + let near_social_account_id = crate::consts::NEAR_SOCIAL_ACCOUNT_ID.get(network_config.network_name.as_str()) + .wrap_err_with(|| format!("The <{}> network does not have a near-social contract.", network_config.network_name))?; + let key = previous_context.key.clone(); + let input_args = serde_json::to_string(&crate::socialdb_types::SocialDbQuery { + keys: vec![format!("{key}")], + }) + .wrap_err("Internal error: could not serialize SocialDB input args")?; + + let remote_social_db_data_for_key: serde_json::Value = network_config + .json_rpc_client() + .blocking_call_view_function( + near_social_account_id, + "get", + input_args.into_bytes(), + near_primitives::types::Finality::Final.into(), + ) + .wrap_err("Failed to fetch the components from SocialDB")? + .parse_result_from_json() + .wrap_err("SocialDB `get` data response cannot be parsed")?; + + let optional_remote_social_db_data_for_key = + if remote_social_db_data_for_key.as_object().map(|result| result.is_empty()).unwrap_or(true) { + None + } else { + Some(&remote_social_db_data_for_key) + }; + + let mut social_db_data_to_set = previous_context.value.clone(); + + crate::common::social_db_data_from_key(&key, &mut social_db_data_to_set); + + let deposit = crate::common::required_deposit( + network_config, + near_social_account_id, + &set_to_account_id, + &social_db_data_to_set, + optional_remote_social_db_data_for_key, + )?; + + Ok(near_cli_rs::commands::PrepopulatedTransaction { + signer_id: signer_id.clone(), + receiver_id: near_social_account_id.clone(), + actions: vec![ + near_primitives::transaction::Action::FunctionCall( + near_primitives::transaction::FunctionCallAction { + method_name: "set".to_string(), + args: serde_json::json!({ + "data": social_db_data_to_set + }).to_string().into_bytes(), + gas: near_cli_rs::common::NearGas::from_str("300 TeraGas") + .unwrap() + .inner, + deposit: deposit.to_yoctonear(), + }, + ) + ]}) + } + }); + + let on_before_signing_callback: near_cli_rs::commands::OnBeforeSigningCallback = + Arc::new({ + let set_to_account_id = set_to_account_id.clone(); + + move |prepopulated_unsigned_transaction, network_config| { + if let near_primitives::transaction::Action::FunctionCall(action) = + &mut prepopulated_unsigned_transaction.actions[0] + { + action.deposit = crate::common::get_deposit( + network_config, + &signer_id, + &prepopulated_unsigned_transaction.public_key, + &set_to_account_id, + &key, + &prepopulated_unsigned_transaction.receiver_id, + near_cli_rs::common::NearBalance::from_yoctonear(action.deposit), + )? + .to_yoctonear(); + Ok(()) + } else { + color_eyre::eyre::bail!("Unexpected action to change components",); + } + } + }); + + let on_after_sending_transaction_callback: near_cli_rs::transaction_signature_options::OnAfterSendingTransactionCallback = std::sync::Arc::new({ + move |transaction_info, _network_config| { + if let near_primitives::views::FinalExecutionStatus::SuccessValue(_) = transaction_info.status { + println!("Keys successfully installed on <{set_to_account_id}>"); + } else { + color_eyre::eyre::bail!("Keys were not successfully installed on <{set_to_account_id}>"); + }; + Ok(()) + } + }); + + Ok(Self(near_cli_rs::commands::ActionContext { + config: previous_context.config, + on_after_getting_network_callback, + on_before_signing_callback, + on_before_sending_transaction_callback: std::sync::Arc::new( + |_signed_transaction, _network_config, _message| Ok(()), + ), + on_after_sending_transaction_callback, + })) + } +} + +impl From for near_cli_rs::commands::ActionContext { + fn from(item: SignerContext) -> Self { + item.0 + } +} + +impl Signer { + fn input_signer_account_id( + context: &super::data::DataContext, + ) -> color_eyre::eyre::Result> { + loop { + let signer_account_id: near_cli_rs::types::account_id::AccountId = + CustomType::new("What is the signer account ID?") + .with_default(context.set_to_account_id.clone()) + .prompt()?; + if !near_cli_rs::common::is_account_exist( + &context.config.network_connection, + signer_account_id.clone().into(), + ) { + println!("\nThe account <{signer_account_id}> does not yet exist."); + #[derive(strum_macros::Display)] + enum ConfirmOptions { + #[strum(to_string = "Yes, I want to enter a new account name.")] + Yes, + #[strum(to_string = "No, I want to use this account name.")] + No, + } + let select_choose_input = Select::new( + "Do you want to enter another signer account id?", + vec![ConfirmOptions::Yes, ConfirmOptions::No], + ) + .prompt()?; + if let ConfirmOptions::No = select_choose_input { + return Ok(Some(signer_account_id)); + } + } else { + return Ok(Some(signer_account_id)); + } + } + } +} diff --git a/src/social_db/data/view/mod.rs b/src/social_db/data/view/mod.rs new file mode 100644 index 0000000..d9391a3 --- /dev/null +++ b/src/social_db/data/view/mod.rs @@ -0,0 +1,79 @@ +use color_eyre::eyre::WrapErr; +use near_cli_rs::common::{CallResultExt, JsonRpcClientExt}; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = crate::GlobalContext)] +#[interactive_clap(output_context = ViewContext)] +pub struct View { + /// Enter SocialDB key path to view (e.g. root.near/profile/**): + key: String, + #[interactive_clap(named_arg)] + /// Select network + network_config: near_cli_rs::network::Network, +} + +#[derive(Clone)] +pub struct ViewContext(near_cli_rs::network::NetworkContext); + +impl ViewContext { + pub fn from_previous_context( + previous_context: crate::GlobalContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let on_after_getting_network_callback: near_cli_rs::network::OnAfterGettingNetworkCallback = + std::sync::Arc::new({ + let key = scope.key.clone(); + + move |network_config| { + let near_social_account_id = match crate::consts::NEAR_SOCIAL_ACCOUNT_ID + .get(&network_config.network_name.as_str()) + { + Some(account_id) => account_id, + None => { + return Err(color_eyre::Report::msg(format!( + "The <{}> network does not have a near-social contract.", + network_config.network_name + ))) + } + }; + + let input_args = serde_json::to_string(&crate::socialdb_types::SocialDbQuery { + keys: vec![format!("{key}")], + }) + .wrap_err("Internal error: could not serialize SocialDB input args")?; + + let call_result = network_config + .json_rpc_client() + .blocking_call_view_function( + near_social_account_id, + "get", + input_args.into_bytes(), + near_primitives::types::Finality::Final.into(), + ) + .wrap_err("Failed to fetch the widgets state from SocialDB")?; + if call_result.result.is_empty() { + eprintln!("There is no information for this request"); + } else if let Ok(json_result) = + call_result.parse_result_from_json::() + { + println!("{}", serde_json::to_string_pretty(&json_result)?); + } else if let Ok(string_result) = String::from_utf8(call_result.result) { + println!("{string_result}"); + } else { + eprintln!("The returned value is not printable (binary data)"); + } + Ok(()) + } + }); + Ok(Self(near_cli_rs::network::NetworkContext { + config: previous_context.0, + on_after_getting_network_callback, + })) + } +} + +impl From for near_cli_rs::network::NetworkContext { + fn from(item: ViewContext) -> Self { + item.0 + } +} diff --git a/src/social_db/mod.rs b/src/social_db/mod.rs index f81993a..bcf9f88 100644 --- a/src/social_db/mod.rs +++ b/src/social_db/mod.rs @@ -1,5 +1,6 @@ use strum::{EnumDiscriminants, EnumIter, EnumMessage}; +mod data; mod permissions; mod prepaid_storage; @@ -15,6 +16,11 @@ pub struct SocialDb { #[strum_discriminants(derive(EnumMessage, EnumIter))] /// What are you up to? (select one of the options with the up-down arrows on your keyboard and press Enter) pub enum SocialDbCommand { + #[strum_discriminants(strum( + message = "data - Data management: viewing, adding, updating, deleting information by a given key" + ))] + /// Data management: viewing, adding, updating, deleting information by a given key + Data(self::data::Data), #[strum_discriminants(strum( message = "prepaid-storage - Storage management: deposit, withdrawal, balance review" ))]