diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 716947789d010412f329d410f6822bb9ca0b0bb3..267a0c26bc870482f0c246fab2ba48eb20c2ce4e 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -1,9 +1,13 @@ use crate::commands::cesium::compute_g1v1_public_key; use crate::entities::vault_account; -use crate::entities::vault_account::{AccountTreeNode, DbAccountId}; +use crate::entities::vault_account::{ + fetch_base_account_tree_node_hierarchy_unwrapped, get_account_tree_node_for_address, + get_base_parent_hierarchy_account_tree_node, AccountTreeNode, ActiveModel, DbAccountId, +}; use crate::*; use age::secrecy::Secret; use comfy_table::{Cell, Table}; +use sea_orm::ActiveValue::Set; use sea_orm::ModelTrait; use sea_orm::{ConnectionTrait, TransactionTrait}; use sp_core::crypto::AddressUri; @@ -66,6 +70,11 @@ pub enum Subcommand { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, }, + /// Inspect a vault entry, retrieving its Substrate URI (will provide more data in a future version) + Inspect { + #[clap(flatten)] + address_or_vault_name: AddressOrVaultNameGroup, + }, /// (deprecated)List available key files (needs to be migrated with command `vault migrate` in order to use them) ListFiles, /// (deprecated)Migrate old key files into db (will have to provide password for each key) @@ -206,53 +215,16 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE } } - let address_to_import = vault_data_for_import.key_pair.address(); - - println!("Trying to import for SS58 address :'{}'", address_to_import); - - if let Some(check_account) = - vault_account::find_by_id(db, &DbAccountId::from(address_to_import)).await? - { - println!( - "Vault entry already exists for that address: {}", - check_account - ); - - let account_tree_node_hierarchy = - vault_account::fetch_base_account_tree_node_hierarchy_unwrapped( - db, - &check_account.address.to_string(), - ) - .await?; - - println!("Here are all the SS58 Addresses linked to it in the vault:"); - - let table = compute_vault_accounts_table(&[account_tree_node_hierarchy])?; - println!("{table}"); - - return Ok(()); - //TODO For later, possibly allow to replace the entry - } - - println!("Enter password to protect the key"); - let password = inputs::prompt_password_confirm()?; - - println!("(Optional) Enter a name for the vault entry"); - let name = inputs::prompt_vault_name()?; - let txn = db.begin().await?; - let _account = create_account_for_vault_data_to_import( - &txn, - &vault_data_for_import, - &password, - name.as_ref(), - ) - .await?; + println!(); + let _account = + create_base_account_for_vault_data_to_import(&txn, &vault_data_for_import, None) + .await?; txn.commit().await?; - println!("Import done"); + println!("Change done"); } Subcommand::Derive { address_or_vault_name, @@ -289,6 +261,19 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE println!("Adding derivation to: {account_to_derive}"); + let base_parent_hierarchy_account_tree_node_to_derive = + get_base_parent_hierarchy_account_tree_node(&account_tree_node_to_derive); + + let parent_hierarchy_table_account_to_derive = + compute_vault_accounts_table(&[base_parent_hierarchy_account_tree_node_to_derive])?; + + println!(); + println!("It's parent hierarchy is this:"); + println!("{parent_hierarchy_table_account_to_derive}"); + println!(); + + println!("The linked <Base> account is {base_account}"); + println!("Enter password to decrypt the <Base> account key"); let password = inputs::prompt_password()?; @@ -297,6 +282,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE password, )?; + println!(); let derivation_path = inputs::prompt_vault_derivation_path()?; let derivation_secret_suri = @@ -307,28 +293,19 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let derivation_address: String = derivation_keypair.address().to_string(); - let check_derivation = - vault_account::find_by_id(db, &DbAccountId::from_str(&derivation_address)?).await?; - - //TODO For later, possibly allow to replace the entry - if check_derivation.is_some() { - println!("Derivation already exists for address:'{derivation_address}'"); - return Ok(()); - } - - println!("(Optional) Enter a name for the new derivation"); - let name = inputs::prompt_vault_name()?; + let txn = db.begin().await?; - let _derivation = vault_account::create_derivation_account( - db, + println!(); + let _derivation = create_derivation_account( + &txn, &derivation_address, - name.as_ref(), &derivation_path, &account_to_derive.address.to_string(), ) .await?; - println!("Derive done"); + txn.commit().await?; + println!("Change done"); } Subcommand::Rename { address } => { let account = @@ -348,7 +325,8 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE ); println!("Enter new name for address (leave empty to remove the name)"); - let name = inputs::prompt_vault_name()?; + let name = + inputs::prompt_vault_name_and_check_availability(db, account.name.as_ref()).await?; let _account = vault_account::update_account_name(db, account, name.as_ref()).await?; @@ -408,6 +386,22 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE println!("Done removing address:'{address_to_delete}'"); } + Subcommand::Inspect { + address_or_vault_name, + } => { + let account_tree_node_to_derive = + retrieve_account_tree_node(&data, address_or_vault_name).await?; + + println!("Enter password to decrypt the <Base> account key"); + let password = inputs::prompt_password()?; + + let account_to_derive_secret_suri = vault_account::compute_suri_account_tree_node( + &account_tree_node_to_derive, + password, + )?; + + println!("Substrate URI: '{account_to_derive_secret_suri}'") + } Subcommand::Migrate => { println!("Migrating existing key files to db"); @@ -450,16 +444,23 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let txn = db.begin().await?; - let account = create_account_for_vault_data_to_import( + let account = create_base_account_for_vault_data_to_import( &txn, &vault_data_to_import, - &vault_data_from_file.password, - None, + Some(&vault_data_from_file.password), ) - .await?; + .await; - txn.commit().await?; - println!("Import done: {}", account); + match account { + Ok(_account) => { + txn.commit().await?; + println!("Change done"); + } + Err(error) => { + println!("Error occurred: {error}"); + println!("Continuing to next key"); + } + } } println!("Migration done"); @@ -568,7 +569,7 @@ async fn compute_vault_key_files_table(vault_key_addresses: &[String]) -> Result Ok(table) } -fn compute_vault_accounts_table( +pub fn compute_vault_accounts_table( account_tree_nodes: &[Rc<RefCell<AccountTreeNode>>], ) -> Result<Table, GcliError> { let mut table = Table::new(); @@ -767,51 +768,252 @@ fn prompt_secret_and_compute_vault_data_to_import( } } -/// Creates an account for the vault data to import +/// Creates a `base` vault account for vault_data provided and returns it +/// +/// Does extra checks and asks for user input in case the address is already present in the vault. /// -/// Does it all using "db" parameter that should better be a transaction since multiple operations can be done -pub async fn create_account_for_vault_data_to_import<C>( - db: &C, +/// Can request password and (optional) name to the user at the proper time +/// +/// Typically used for `vault import|migrate` commands +pub async fn create_base_account_for_vault_data_to_import<C>( + db_tx: &C, vault_data: &VaultDataToImport, - password: &str, - name: Option<&String>, + password: Option<&String>, ) -> Result<vault_account::Model, GcliError> where C: ConnectionTrait, { let address_to_import = vault_data.key_pair.address().to_string(); - //To be safe - if vault_account::find_by_id(db, &DbAccountId::from_str(&address_to_import)?) - .await? - .is_some() + println!("Trying to import for SS58 address :'{}'", address_to_import); + println!(); + + let vault_account = if let Some(existing_vault_account) = + vault_account::find_by_id(db_tx, &DbAccountId::from(address_to_import.clone())).await? { - //TODO Later possibly allow to replace the entry - return Err(GcliError::Input(format!( - "Vault entry already exists for address {}", - &address_to_import - ))); - } + if existing_vault_account.is_base_account() { + println!("You are trying to add {address_to_import} as a <Base> account while it already exists as a <Base> account."); + println!(); + println!("Do you want to:"); + println!("1. keep the existing <Base> account and cancel import"); + println!("2. overwrite existing <Base> account with the new encrypted key (children will be re-parented)"); + } else { + // Existing derivation account + let account_tree_node_hierarchy = + fetch_base_account_tree_node_hierarchy_unwrapped(db_tx, &address_to_import).await?; + let account_tree_node_for_address = + get_account_tree_node_for_address(&account_tree_node_hierarchy, &address_to_import); + + let base_parent_hierarchy_account_tree_node = + get_base_parent_hierarchy_account_tree_node(&account_tree_node_for_address); + + let parent_hierarchy_table = + compute_vault_accounts_table(&[base_parent_hierarchy_account_tree_node])?; + + println!("You are trying to add {address_to_import} as a <Base> account"); + println!( + "but it is already present as `{}` derivation of {} account.", + existing_vault_account.path.clone().unwrap(), + existing_vault_account.parent.clone().unwrap() + ); + println!(); + println!("It's parent hierarchy is this:"); + println!("{parent_hierarchy_table}"); + println!(); + println!("Do you want to:"); + println!("1. keep the existing derivation and cancel import"); + println!("2. delete the derivation account and replace it with the new <Base> account (children will be re-parented)"); + } - let secret_format = vault_data.secret_format; + let result = inputs::select_action("Your choice?", vec!["1", "2"])?; + match result { + "2" => { + let encrypted_suri = + compute_encrypted_suri(password, vault_data.secret_suri.clone())?; - let encrypted_suri = encrypt( - vault_data.secret_suri.clone().as_bytes(), - password.to_string(), - ) - .map_err(|e| anyhow!(e))?; + println!( + "(Optional) Enter a name for the vault entry (leave empty to remove the name)" + ); + let name = inputs::prompt_vault_name_and_check_availability( + db_tx, + existing_vault_account.name.as_ref(), + ) + .await?; - let crypto_scheme = map_secret_format_to_crypto_scheme(secret_format); + // Since links are made based on address / parent(address) we can just edit the existing entry and it should be fine + let mut vault_account: ActiveModel = existing_vault_account.into(); + vault_account.path = Set(None); + vault_account.parent = Set(None); + vault_account.crypto_scheme = Set(Some( + map_secret_format_to_crypto_scheme(vault_data.secret_format).into(), + )); + vault_account.encrypted_suri = Set(Some(encrypted_suri)); + vault_account.name = Set(name.clone()); + let updated_vault_account = + vault_account::update_account(db_tx, vault_account).await?; + + println!("Updating vault account {updated_vault_account}"); + updated_vault_account + } + _ => { + return Err(GcliError::Input("import canceled".into())); + } + } + } else { + //New entry + let secret_format = vault_data.secret_format; - let base_account = vault_account::create_base_account( - db, - &address_to_import, - name, - crypto_scheme, - encrypted_suri, - ) - .await?; + let encrypted_suri = compute_encrypted_suri(password, vault_data.secret_suri.clone())?; + + println!("(Optional) Enter a name for the vault entry"); + let name = inputs::prompt_vault_name_and_check_availability(db_tx, None).await?; + + let crypto_scheme = map_secret_format_to_crypto_scheme(secret_format); + + let base_account = vault_account::create_base_account( + db_tx, + &address_to_import, + name.as_ref(), + crypto_scheme, + encrypted_suri, + ) + .await?; + println!("Creating <Base> account {base_account}"); + + base_account + }; + + Ok(vault_account) +} + +/// Creates a `derivation` vault account for data provided and returns it +/// +/// Does extra checks and asks for user input in case the address is already present in the vault. +/// +/// Can request (optional) name to the user at the proper time +/// +/// Typically used for `vault derive` command +pub async fn create_derivation_account<C>( + db_tx: &C, + derivation_address: &String, + derivation_path: &String, + parent_address: &String, +) -> Result<vault_account::Model, GcliError> +where + C: ConnectionTrait, +{ + println!("Trying to create derivation with address '{derivation_address}'"); + println!(); + let vault_account = if let Some(existing_vault_account) = + vault_account::find_by_id(db_tx, &DbAccountId::from(derivation_address.clone())).await? + { + // Existing account + println!("You are trying to derive '{derivation_path}' from parent '{parent_address}'"); + + if existing_vault_account.is_base_account() { + println!( + "but it is already present as a direct <Base> account '{}'", + existing_vault_account.address + ); + + println!("Do you want to:"); + println!("1. keep the existing <Base> account and cancel import"); + println!("2. delete the existing <Base> account and associated key and replace it with the new derivation account (children will be re-parented)"); + } else { + //Existing derivation + let existing_account_tree_node_hierarchy = + fetch_base_account_tree_node_hierarchy_unwrapped(db_tx, derivation_address).await?; + + let existing_account_tree_node_for_address = get_account_tree_node_for_address( + &existing_account_tree_node_hierarchy, + derivation_address, + ); + + let base_parent_hierarchy_existing_account_tree_node = + get_base_parent_hierarchy_account_tree_node( + &existing_account_tree_node_for_address, + ); + + let parent_hierarchy_table_existing_account = + compute_vault_accounts_table(&[base_parent_hierarchy_existing_account_tree_node])?; + + println!( + "but it is already present as `{}` derivation of '{}' account.", + existing_vault_account.path.clone().unwrap(), + existing_vault_account.parent.clone().unwrap() + ); + + println!(); + println!("It's parent hierarchy is this:"); + println!("{parent_hierarchy_table_existing_account}"); + println!(); + println!("Do you want to:"); + println!("1. keep the existing derivation and cancel import"); + println!("2. delete the derivation account and replace it with this new derivation (children will be re-parented)"); + } + + let result = inputs::select_action("Your choice?", vec!["1", "2"])?; + match result { + "2" => { + println!( + "(Optional) Enter a name for the vault entry (leave empty to remove the name)" + ); + let name = inputs::prompt_vault_name_and_check_availability( + db_tx, + existing_vault_account.name.as_ref(), + ) + .await?; + + // Since links are made based on address / parent(address) we can just edit the existing entry and it should be fine + let mut vault_account: ActiveModel = existing_vault_account.into(); + vault_account.path = Set(Some(derivation_path.clone())); + vault_account.parent = Set(Some(DbAccountId::from(parent_address.clone()))); + vault_account.crypto_scheme = Set(None); + vault_account.encrypted_suri = Set(None); + vault_account.name = Set(name.clone()); + let updated_vault_account = + vault_account::update_account(db_tx, vault_account).await?; + + println!("Updating vault account {updated_vault_account}"); + updated_vault_account + } + _ => { + return Err(GcliError::Input("derive canceled".into())); + } + } + } else { + println!("(Optional) Enter a name for the vault entry"); + let name = inputs::prompt_vault_name_and_check_availability(db_tx, None).await?; + + let derivation = vault_account::create_derivation_account( + db_tx, + derivation_address, + name.as_ref(), + derivation_path, + parent_address, + ) + .await?; + println!("Creating derivation account {derivation}"); + + derivation + }; + + Ok(vault_account) +} + +/// Function will ask for password if not present and compute the encrypted suri +fn compute_encrypted_suri( + password: Option<&String>, + secret_suri: String, +) -> Result<Vec<u8>, GcliError> { + let password = match password.cloned() { + Some(password) => password, + _ => { + println!("Enter password to protect the key"); + inputs::prompt_password_confirm()? + } + }; - Ok(base_account) + Ok(encrypt(secret_suri.as_bytes(), password).map_err(|e| anyhow!(e))?) } fn get_vault_key_path(data: &Data, vault_filename: &str) -> PathBuf { diff --git a/src/entities/vault_account.rs b/src/entities/vault_account.rs index 71905ec368d410630e4c5bedb9560e072b1098b9..797a50e4bad9bd368de882d6783e55bd368895b9 100644 --- a/src/entities/vault_account.rs +++ b/src/entities/vault_account.rs @@ -4,7 +4,6 @@ use anyhow::anyhow; use sea_orm::prelude::async_trait::async_trait; use sea_orm::prelude::StringLen; use sea_orm::ActiveValue::Set; -use sea_orm::FromJsonQueryResult; use sea_orm::QueryFilter; use sea_orm::{ ActiveModelBehavior, ColumnTrait, DbErr, DeriveEntityModel, DerivePrimaryKey, EnumIter, Linked, @@ -12,6 +11,7 @@ use sea_orm::{ }; use sea_orm::{ActiveModelTrait, ConnectionTrait, PrimaryKeyTrait}; use sea_orm::{DeriveActiveEnum, EntityTrait}; +use sea_orm::{FromJsonQueryResult, PaginatorTrait}; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::HashMap; @@ -228,8 +228,8 @@ impl ActiveModelBehavior for ActiveModel { && self.encrypted_suri.try_as_ref().unwrap().is_some())) { return Err(DbErr::Custom( - "A \"base\" vault account must have path:None, parent:None, crypto_scheme:Some(_), encrypted_suri:Some(_)".into(), - )); + "A \"base\" vault account must have path:None, parent:None, crypto_scheme:Some(_), encrypted_suri:Some(_)".into(), + )); } } else if !((self.path.is_set() && self.path.try_as_ref().unwrap().is_some()) && (self.parent.is_set() && self.parent.try_as_ref().unwrap().is_some()) @@ -239,20 +239,53 @@ impl ActiveModelBehavior for ActiveModel { || self.encrypted_suri.try_as_ref().unwrap().is_none())) { return Err(DbErr::Custom( - "A \"derivation\" vault account must have path:Some(_), parent:Some(_), crypto_scheme:None, encrypted_suri:None".into(), - )); + "A \"derivation\" vault account must have path:Some(_), parent:Some(_), crypto_scheme:None, encrypted_suri:None".into(), + )); } } else { - //Update checks - if !(self.crypto_scheme.is_unchanged() - && self.encrypted_suri.is_unchanged() - && self.path.is_unchanged() - && self.parent.is_unchanged() - && self.address.is_unchanged()) + //Updates to accept: + // * Name Only + // * Overriding Base with Base account => only changing encrypted_suri + // * Should also support changing name at the same time + // * Overriding Derivation with Base account => upd Path=>None Parent=>None crypto=>Some enc_suri=>Some + // * Should also support changing name at the same time + // * Overriding Derivation with Derivation account => upd (Path=>Some) (Parent=>Some) + // * Should also support changing name at the same time + // * Overriding Base with Derivation account => upd Path=>Some Parent=>Some crypto=>None enc_suri=>None + // * Should also support changing name at the same time + + // If updating all path, parent, crypto_scheme, encrypted_suri + if self.path.is_set() + && self.parent.is_set() + && self.crypto_scheme.is_set() + && self.encrypted_suri.is_set() + { + if self.parent.try_as_ref().unwrap().is_some() { + if !(self.path.try_as_ref().unwrap().is_some() + && self.crypto_scheme.try_as_ref().unwrap().is_none() + && self.encrypted_suri.try_as_ref().unwrap().is_none()) + { + return Err(DbErr::Custom( + "An update to \"derivation\" vault account must have path:Some(_), parent:Some(_), crypto_scheme:None, encrypted_suri:None".into(), + )); + } + } else if !(self.path.try_as_ref().unwrap().is_none() + && self.crypto_scheme.try_as_ref().unwrap().is_some() + && self.encrypted_suri.try_as_ref().unwrap().is_some()) + { + return Err(DbErr::Custom( + "An update to \"base\" vault account must have path:None, parent:None, crypto_scheme:Some(_), encrypted_suri:Some(_)".into(), + )); + } + } + // Else if updating path || parent both needs to have Some(_) value (update of Derivation) + else if (self.path.is_set() || self.parent.is_set()) + && !(self.parent.try_as_ref().unwrap().is_some() + && self.path.try_as_ref().unwrap().is_some()) { return Err(DbErr::Custom( - "Only the name can be updated for a vault account".into(), - )); + "An update of \"derivation\" parent/path must have both path:Some(_), parent:Some(_)".into(), + )); } } @@ -384,6 +417,42 @@ pub fn get_account_tree_node_for_address( Rc::clone(&account_tree_node_for_address) } +/// Returns a new (limited) `AccountTreeNode` hierarchy including the selected account_tree_node and all it's parents. +/// +/// The base of the new hierarchy will be returned +pub fn get_base_parent_hierarchy_account_tree_node( + account_tree_node: &Rc<RefCell<AccountTreeNode>>, +) -> Rc<RefCell<AccountTreeNode>> { + // Clone the current node to start the new hierarchy + let new_node = Rc::new(RefCell::new(AccountTreeNode { + account: account_tree_node.borrow().account.clone(), + children: Vec::new(), + parent: None, + })); + + // Traverse up to the base node, creating new nodes for each parent + let mut current_new_node = Rc::clone(&new_node); + let mut current_node = Rc::clone(account_tree_node); + + while let Some(parent_node) = { + let borrowed_node = current_node.borrow(); + borrowed_node.parent.as_ref().map(Rc::clone) + } { + let new_parent_node = Rc::new(RefCell::new(AccountTreeNode { + account: parent_node.borrow().account.clone(), + children: vec![Rc::clone(¤t_new_node)], + parent: None, + })); + + current_new_node.borrow_mut().parent = Some(Rc::clone(&new_parent_node)); + current_new_node = new_parent_node; + current_node = parent_node; + } + + // Return the base of the new hierarchy + current_new_node +} + /// Returns a vec of all the accounts starting from `account_tree_node` and all its children; depth first /// /// Can be used to delete all the accounts in the hierarchy in the proper order @@ -557,7 +626,6 @@ where /// This one unwraps the Option and gives a proper error message in case of None pub async fn fetch_base_account_tree_node_hierarchy_unwrapped<C>( db: &C, - //FIXME Ripple parameter type to &AccountId in all methods (instead of &str) address: &str, ) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError> where @@ -574,7 +642,6 @@ where /// Fetches the `base` account tree node hierarchy for the given address using db pub async fn fetch_base_account_tree_node_hierarchy<C>( db: &C, - //FIXME Ripple parameter type to &AccountId in all methods (instead of &str) address: &str, ) -> Result<Option<Rc<RefCell<AccountTreeNode>>>, GcliError> where @@ -634,7 +701,8 @@ where } /// To make clippy happy... "warning: very complex type used. Consider factoring parts into `type` definitions" -type AccountTreeNodeResult<'c> = Pin<Box<dyn Future<Output = Result<Rc<RefCell<AccountTreeNode>>, GcliError>> + 'c>>; +type AccountTreeNodeResult<'c> = + Pin<Box<dyn Future<Output = Result<Rc<RefCell<AccountTreeNode>>, GcliError>> + 'c>>; /// This one seems necessary in order to handle async + recursion issue /// @@ -688,7 +756,38 @@ where Ok(current_node) } -/// Creates a `base` vault account (if it doesn't exist) and returns it +pub async fn check_name_available<C>( + db: &C, + old_name: Option<&String>, + new_name: Option<&String>, +) -> Result<bool, GcliError> +where + C: ConnectionTrait, +{ + if old_name == new_name { + return Ok(true); + } + + if let Some(new_name) = new_name { + let name_usage_count = Entity::find() + .filter(Column::Name.eq(Some(new_name.clone()))) + .count(db) + .await?; + + Ok(name_usage_count == 0) + } else { + Ok(true) + } +} + +pub async fn update_account<C>(db: &C, vault_account: ActiveModel) -> Result<Model, GcliError> +where + C: ConnectionTrait, +{ + Ok(vault_account.update(db).await?) +} + +/// Creates a `base` vault account and returns it /// /// Typically used for `vault import|migrate` commands pub async fn create_base_account<C>( @@ -702,27 +801,14 @@ where C: ConnectionTrait, { let account_id = DbAccountId::from_str(address)?; - let vault_account = Entity::find_by_id(account_id.clone()).one(db).await?; + let existing_vault_account = Entity::find_by_id(account_id.clone()).one(db).await?; - Ok(match vault_account { - Some(vault_account) => { - //TODO Later possibly allow to replace the entry + Ok(match existing_vault_account { + Some(existing_vault_account) => { + // To be safe return Err(GcliError::Input(format!( - "Already existing vault account {vault_account}" + "Already existing vault account {existing_vault_account}" ))); - - // let overwrite_key = - // inputs::confirm_action("Do you want to overwrite with the new encrypted key ?")?; - // if overwrite_key { - // let mut vault_account: ActiveModel = vault_account.into(); - // vault_account.encrypted_suri = Set(Some(encrypted_suri)); - // let vault_account = vault_account.update(db).await?; - // println!("Updated vault account {vault_account}"); - // - // vault_account - // } else { - // vault_account - // } } None => { let vault_account = ActiveModel { @@ -733,14 +819,12 @@ where encrypted_suri: Set(Some(encrypted_suri)), parent: Default::default(), }; - let vault_account = vault_account.insert(db).await?; - println!("Created base account {}", vault_account); - vault_account + vault_account.insert(db).await? } }) } -/// Creates a `derivation` vault account (if it doesn't exist) and returns it +/// Creates a `derivation` vault account and returns it /// /// Typically used for `vault derive` command pub async fn create_derivation_account<C>( @@ -758,7 +842,7 @@ where Ok(match vault_account { Some(vault_account) => { - //TODO Later possibly allow to replace the entry + // To be safe return Err(GcliError::Input(format!( "Already existing vault account {vault_account}" ))); @@ -772,9 +856,7 @@ where encrypted_suri: Set(None), parent: Set(Some(parent_address.to_string().into())), }; - let vault_account = vault_account.insert(db).await?; - println!("Created derivation account {}", vault_account); - vault_account + vault_account.insert(db).await? } }) } @@ -925,6 +1007,44 @@ pub mod tests { ); } + #[test] + fn test_get_base_parent_hierarchy_account_tree_node() { + let mother = mother_account_tree_node(); + let child1 = mother.borrow().children[0].clone(); + + let new_mother = get_base_parent_hierarchy_account_tree_node(&child1); + + // Check if the base of the new hierarchy is the mother node + assert_eq!( + new_mother.borrow().account.address.to_string(), + mother.borrow().account.address.to_string() + ); + assert_eq!(new_mother.borrow().children.len(), 1); + + // Check if the child1 node is correctly linked in the new hierarchy + let new_child1 = new_mother.borrow().children[0].clone(); + assert_eq!( + new_child1.borrow().account.address.to_string(), + child1.borrow().account.address.to_string() + ); + assert_eq!(new_child1.borrow().children.len(), 0); + + // Check if the parent references are correctly set + assert!(new_mother.borrow().parent.is_none()); + assert_eq!( + new_child1 + .borrow() + .parent + .as_ref() + .unwrap() + .borrow() + .account + .address + .to_string(), + new_mother.borrow().account.address.to_string() + ); + } + #[test] fn test_retrieve_accounts_depth_first_from_account_tree_node() { let mother = mother_account_tree_node(); diff --git a/src/inputs.rs b/src/inputs.rs index 103ba3b293194937810b44ce9ef05ed7b506e8f2..082313029575334b85d44ce9392121bb406ccd13 100644 --- a/src/inputs.rs +++ b/src/inputs.rs @@ -1,6 +1,8 @@ use crate::commands::vault; +use crate::entities::vault_account; use crate::utils::GcliError; use inquire::validator::{ErrorMessage, Validation}; +use sea_orm::ConnectionTrait; pub fn prompt_password() -> Result<String, GcliError> { prompt_password_query("Password") @@ -46,25 +48,52 @@ pub fn prompt_password_query_confirm(query: impl ToString) -> Result<String, Gcl /// Prompt for a (direct) vault name (cannot contain derivation path) /// /// Also preventing to use '<' and '>' as those are used in the display -pub fn prompt_vault_name() -> Result<Option<String>, GcliError> { - let name = inquire::Text::new("Name:") - .with_validator(|input: &str| { - if input.contains('<') || input.contains('>') || input.contains('/') { - Ok(Validation::Invalid( - "Name cannot contain characters '<', '>', '/'".into(), - )) - } else { +pub async fn prompt_vault_name_and_check_availability<C>( + db: &C, + initial_name: Option<&String>, +) -> Result<Option<String>, GcliError> +where + C: ConnectionTrait, +{ + loop { + let mut text_inquire = inquire::Text::new("Name:").with_validator({ + |input: &str| { + if input.contains('<') || input.contains('>') || input.contains('/') { + return Ok(Validation::Invalid( + "Name cannot contain characters '<', '>', '/'".into(), + )); + } + Ok(Validation::Valid) } - }) - .prompt() - .map_err(|e| GcliError::Input(e.to_string()))?; + }); + + if let Some(initial_name) = initial_name { + text_inquire = text_inquire.with_initial_value(initial_name); + } - Ok(if name.trim().is_empty() { - None - } else { - Some(name.trim().to_string()) - }) + let name = text_inquire + .prompt() + .map_err(|e| GcliError::Input(e.to_string()))?; + + let name = if name.trim().is_empty() { + None + } else { + Some(name.trim().to_string()) + }; + + let available = + vault_account::check_name_available(db, initial_name, name.as_ref()).await?; + + if available { + return Ok(name); + } + + println!( + "Name '{}' is already in use in the vault. Please choose another name.", + name.unwrap() + ); + } } /// Prompt for a derivation path @@ -97,3 +126,9 @@ pub fn confirm_action(query: impl ToString) -> Result<bool, GcliError> { .prompt() .map_err(|e| GcliError::Input(e.to_string())) } + +pub fn select_action(query: impl ToString, choices: Vec<&str>) -> Result<&str, GcliError> { + inquire::Select::new(query.to_string().as_str(), choices) + .prompt() + .map_err(|e| GcliError::Input(e.to_string())) +}