Skip to content
Snippets Groups Projects
Select Git revision
  • nostr
  • json-output
  • master default protected
  • 48-error-base-58-requirement-is-violated
  • no-rename
  • hugo/tx-comments
  • poka/dev
  • hugo/dev
  • tuxmain/mail
  • 0.4.3-RC1
  • 0.4.2
  • 0.4.1
  • 0.4.0
  • 0.3.0
  • 0.2.17
  • 0.2.16
  • 0.2.15
  • 0.2.14
  • 0.2.13
  • 0.2.12
  • 0.2.10
  • 0.2.9
  • 0.2.8
  • 0.2.7
  • 0.2.6
  • 0.2.5
  • 0.2.4
  • 0.2.3
  • 0.2.1
29 results

vault.rs

Blame
    • Nicolas80's avatar
      da7c790d
      * Added some logic between arguments of `vault import`; can't provide both... · da7c790d
      Nicolas80 authored
      * Added some logic between arguments of `vault import`; can't provide both `password` and `no-password`
      ** Added extra validation of non-interactive `name` argument value (same validation as when interactive: no '<', '>', '/' characters)
      * Added possibility to make a non-interactive derivation (given proper arguments are given and there is no issue found during the process)
      ** Added same validation for non-interactive `derivation_path` argument as when interactive
      ** If the same resulting address is already in the vault; interaction is still mandatory to make a choice
      * Had to change the name of argument AddressOrVaultNameGroup.name => vault_name to avoid conflict in `vault derive`
      ** Not changing the `-v` shortcut so no impact on existing commands
      * Allowing to pass "" empty string as non-interactive `name` argument and considering it as None (does a trim before checking empty; so only spaces will be considered as None as well)
      da7c790d
      History
      * Added some logic between arguments of `vault import`; can't provide both...
      Nicolas80 authored
      * Added some logic between arguments of `vault import`; can't provide both `password` and `no-password`
      ** Added extra validation of non-interactive `name` argument value (same validation as when interactive: no '<', '>', '/' characters)
      * Added possibility to make a non-interactive derivation (given proper arguments are given and there is no issue found during the process)
      ** Added same validation for non-interactive `derivation_path` argument as when interactive
      ** If the same resulting address is already in the vault; interaction is still mandatory to make a choice
      * Had to change the name of argument AddressOrVaultNameGroup.name => vault_name to avoid conflict in `vault derive`
      ** Not changing the `-v` shortcut so no impact on existing commands
      * Allowing to pass "" empty string as non-interactive `name` argument and considering it as None (does a trim before checking empty; so only spaces will be considered as None as well)
    vault.rs 41.34 KiB
    mod display;
    
    use crate::commands::cesium::compute_g1v1_public_key;
    use crate::entities::vault_account;
    use crate::entities::vault_account::{AccountTreeNode, ActiveModel, DbAccountId};
    use crate::inputs::{trim_and_reduce_empty_as_none, validate_derivation_path, validate_vault_name};
    use crate::keys::seed_from_cesium;
    use crate::*;
    use age::secrecy::Secret;
    use sea_orm::ActiveValue::Set;
    use sea_orm::ModelTrait;
    use sea_orm::{ConnectionTrait, TransactionTrait};
    use sp_core::crypto::AddressUri;
    use std::cell::RefCell;
    use std::io::{Read, Write};
    use std::path::PathBuf;
    use std::rc::Rc;
    
    /// vault subcommands
    #[derive(Clone, Debug, clap::Parser)]
    pub enum Subcommand {
    	/// List available SS58 Addresses in the vault
    	#[clap(subcommand)]
    	List(ListChoice),
    	/// Use specific SS58 Address (changes the config Address)
    	Use {
    		#[clap(flatten)]
    		address_or_vault_name: AddressOrVaultNameGroup,
    	},
    	/// Generate a mnemonic
    	Generate,
    	/// Import key from (substrate uri) or other format with interactive prompt
    	#[clap(
    		long_about = "Import key from (substrate uri) or other format with interactive prompt.\n\
    		\n\
    		This will create a <Base> account in the vault for the provided/computed Substrate URI \n\
    		and associated SS58 Address.\n\
    		\n\
    		If using default format (or specifically \"substrate\") a derivation path is supported\n\
    		in the substrate uri value."
    	)]
    	Import {
    		/// Secret key format (substrate, seed, g1v1)
    		#[clap(short = 'S', long, required = false, default_value = SecretFormat::Substrate)]
    		secret_format: SecretFormat,
    
    		/// Crypto scheme to use (sr25519, ed25519)
    		#[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)]
    		crypto_scheme: CryptoScheme,
    
    		/// Substrate URI to import (non-interactive mode)
    		#[clap(short = 'u', long, required = false)]
    		uri: Option<String>,
    
    		/// G1v1 ID (non-interactive mode for g1v1 format)
    		#[clap(long, required = false)]
    		g1v1_id: Option<String>,
    
    		/// G1v1 password (non-interactive mode for g1v1 format)
    		#[clap(long, required = false)]
    		g1v1_password: Option<String>,
    
    		/// Password for encrypting the key (non-interactive mode)
    		#[clap(short = 'p', long, required = false, conflicts_with_all=["no_password"])]
    		password: Option<String>,
    
    		/// Use empty password for encrypting the key (non-interactive mode)
    		#[clap(long, required = false)]
    		no_password: bool,
    
    		/// Name for the wallet entry (non-interactive mode) - "" empty string will be considered as None
    		#[clap(short = 'n', long, required = false)]
    		name: Option<String>,
    	},
    	/// Add a derivation to an existing SS58 Address
    	#[clap(long_about = "Add a derivation to an existing SS58 Address.\n\
    		\n\
    		Both \"sr25519\" and \"ed25519\" crypto schemes are supported
    		\n\
    		Use command `vault list base` to see available <Base> account and their crypto scheme\n\
    		And then use command 'vault list for' to find all accounts linked to that <Base> account")]
    	#[clap(alias = "deriv")]
    	#[clap(alias = "derivation")]
    	Derive {
    		#[clap(flatten)]
    		address_or_vault_name: AddressOrVaultNameGroup,
    
    		/// Derivation path (non-interactive mode)
    		#[clap(short = 'd', long, required = false)]
    		derivation_path: Option<String>,
    
    		/// Password to decrypt the <Base> account key (non-interactive mode)
    		#[clap(short = 'p', long, required = false, requires = "derivation_path", conflicts_with_all=["no_password"])]
    		password: Option<String>,
    
    		/// Use empty password to decrypt the <Base> account key (non-interactive mode)
    		#[clap(long, required = false, requires = "derivation_path")]
    		no_password: bool,
    
    		/// Name for the wallet entry (non-interactive mode) - "" empty string will be considered as None
    		#[clap(short = 'n', long, required = false, requires = "derivation_path")]
    		name: Option<String>,
    	},
    	/// Give a meaningful name to an SS58 Address in the vault
    	Rename {
    		/// SS58 Address
    		address: AccountId,
    	},
    	/// Remove an SS58 Address from the vault together with its linked derivations
    	#[clap(long_about = "Remove an SS58 Address from the vault\n\
    		\n\
    		If a <Base> Address is given it will also remove the saved key")]
    	Remove {
    		#[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)
    	#[deprecated(
    		note = "Should be removed in a future version when db persistence of vault is present for a while"
    	)]
    	ListFiles,
    	/// (deprecated) Migrate old key files into db (will have to provide password for each key)
    	#[deprecated(
    		note = "Should be removed in a future version when db persistence of vault is present for a while"
    	)]
    	Migrate,
    	/// Show where vault db (or old keys) is stored
    	Where,
    }
    
    /// List subcommands
    #[derive(Clone, Debug, clap::Subcommand)]
    pub enum ListChoice {
    	/// List all accounts
    	#[clap(alias = "a")]
    	All {
    		/// Show G1v1 public key for ed25519 keys
    		#[clap(long)]
    		show_g1v1: bool,
    		/// Show wallet type (g1v1 or mnemonic)
    		#[clap(long)]
    		show_type: bool,
    	},
    	/// List only base accounts
    	#[clap(alias = "b")]
    	Base {
    		/// Show G1v1 public key for ed25519 keys
    		#[clap(long)]
    		show_g1v1: bool,
    		/// Show wallet type (g1v1 or mnemonic)
    		#[clap(long)]
    		show_type: bool,
    	},
    	/// List accounts for a specific address
    	#[clap(alias = "f")]
    	For {
    		#[clap(flatten)]
    		address_or_vault_name: AddressOrVaultNameGroup,
    		/// Show G1v1 public key for ed25519 keys
    		#[clap(long)]
    		show_g1v1: bool,
    		/// Show wallet type (g1v1 or mnemonic)
    		#[clap(long)]
    		show_type: bool,
    	},
    }
    
    impl Default for ListChoice {
    	fn default() -> Self {
    		ListChoice::All {
    			show_g1v1: false,
    			show_type: false,
    		}
    	}
    }
    
    #[derive(Debug, clap::Args, Clone)]
    #[group(required = true, multiple = false)]
    pub struct AddressOrVaultNameGroup {
    	/// SS58 Address
    	#[clap(short)]
    	address: Option<AccountId>,
    	/// Name of an SS58 Address in the vault
    	#[clap(short = 'v')]
    	vault_name: Option<String>,
    }
    
    pub struct VaultDataToImport {
    	secret_format: SecretFormat,
    	secret_suri: String,
    	key_pair: KeyPair,
    }
    
    // encrypt input with passphrase
    pub fn encrypt(input: &[u8], passphrase: String) -> Result<Vec<u8>, age::EncryptError> {
    	let encryptor = age::Encryptor::with_user_passphrase(Secret::new(passphrase));
    	let mut encrypted = vec![];
    	let mut writer = encryptor.wrap_output(age::armor::ArmoredWriter::wrap_output(
    		&mut encrypted,
    		age::armor::Format::AsciiArmor,
    	)?)?;
    	writer.write_all(input)?;
    	writer.finish().and_then(|armor| armor.finish())?;
    	Ok(encrypted)
    }
    
    // decrypt cypher with passphrase
    pub fn decrypt(input: &[u8], passphrase: String) -> Result<Vec<u8>, age::DecryptError> {
    	let age::Decryptor::Passphrase(decryptor) =
    		age::Decryptor::new(age::armor::ArmoredReader::new(input))?
    	else {
    		unimplemented!()
    	};
    	let mut decrypted = vec![];
    	let mut reader = decryptor.decrypt(&Secret::new(passphrase.to_owned()), None)?;
    	reader.read_to_end(&mut decrypted)?;
    	Ok(decrypted)
    }
    
    /// handle vault commands
    pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
    	let db = data.connect_db();
    
    	// match subcommand
    	match command {
    		Subcommand::List(choice) => match choice {
    			ListChoice::All {
    				show_g1v1,
    				show_type,
    			} => {
    				let all_account_tree_node_hierarchies =
    					vault_account::fetch_all_base_account_tree_node_hierarchies(db).await?;
    
    				let table = display::compute_vault_accounts_table_with_g1v1(
    					&all_account_tree_node_hierarchies,
    					show_g1v1,
    					show_type,
    				)?;
    
    				println!("available SS58 Addresses:");
    				println!("{table}");
    			}
    			ListChoice::Base {
    				show_g1v1,
    				show_type,
    			} => {
    				let base_account_tree_nodes =
    					vault_account::fetch_only_base_account_tree_nodes(db).await?;
    
    				let table = display::compute_vault_accounts_table_with_g1v1(
    					&base_account_tree_nodes,
    					show_g1v1,
    					show_type,
    				)?;
    
    				println!("available <Base> SS58 Addresses:");
    				println!("{table}");
    			}
    			ListChoice::For {
    				address_or_vault_name,
    				show_g1v1,
    				show_type,
    			} => {
    				let account_tree_node =
    					retrieve_account_tree_node(db, address_or_vault_name).await?;
    
    				let base_account_tree_node =
    					vault_account::get_base_account_tree_node(&account_tree_node);
    
    				let table = display::compute_vault_accounts_table_with_g1v1(
    					&[base_account_tree_node],
    					show_g1v1,
    					show_type,
    				)?;
    
    				println!(
    					"available SS58 Addresses linked to {}:",
    					account_tree_node.borrow().account
    				);
    				println!("{table}");
    			}
    		},
    		Subcommand::ListFiles => {
    			let vault_key_addresses = fetch_vault_key_addresses(&data).await?;
    
    			let table = display::compute_vault_key_files_table(&vault_key_addresses)?;
    
    			println!("available key files (needs to be migrated with command `vault migrate` in order to use them):");
    			println!("{table}");
    		}
    		Subcommand::Use {
    			address_or_vault_name,
    		} => {
    			let account = retrieve_vault_account(db, address_or_vault_name).await?;
    
    			println!("Using: {}", account);
    
    			let updated_cfg = conf::Config {
    				address: Some(account.address.0),
    				..data.cfg
    			};
    
    			//This updated configuration will be picked up with next GCli execution
    			conf::save(&updated_cfg);
    		}
    		Subcommand::Generate => {
    			// TODO allow custom word count
    			let mnemonic = bip39::Mnemonic::generate(12).unwrap();
    			println!("{mnemonic}");
    		}
    		Subcommand::Import {
    			secret_format,
    			crypto_scheme,
    			uri,
    			g1v1_id,
    			g1v1_password,
    			password,
    			no_password,
    			name,
    		} => {
    			let vault_data_for_import = if let Some(uri_str) = uri {
    				// Non-interactive mode with provided URI
    				if secret_format != SecretFormat::Substrate {
    					return Err(GcliError::Input(format!(
    						"URI can only be provided directly with secret_format=substrate, got: {:?}",
    						secret_format
    					)));
    				}
    
    				// Create keypair from provided URI
    				let key_pair = compute_keypair(crypto_scheme, &uri_str)?;
    
    				VaultDataToImport {
    					secret_format,
    					secret_suri: uri_str,
    					key_pair,
    				}
    			} else if let (Some(id), Some(pwd)) = (&g1v1_id, &g1v1_password) {
    				// Non-interactive mode with provided G1v1 ID and password
    				if secret_format != SecretFormat::G1v1 {
    					return Err(GcliError::Input(format!(
    						"G1v1 ID and password can only be provided directly with secret_format=g1v1, got: {:?}", 
    						secret_format
    					)));
    				}
    
    				// Create keypair from provided G1v1 ID and password
    				let seed = seed_from_cesium(id, pwd);
    				let secret_suri = format!("0x{}", hex::encode(seed));
    
    				// G1v1 always uses Ed25519
    				let key_pair = compute_keypair(CryptoScheme::Ed25519, &secret_suri)?;
    
    				VaultDataToImport {
    					secret_format,
    					secret_suri,
    					key_pair,
    				}
    			} else {
    				// Interactive mode
    				prompt_secret_and_compute_vault_data_to_import(secret_format, crypto_scheme)?
    			};
    
    			//Extra check for SecretFormat::G1v1 (old cesium) - showing the G1v1 cesium public key for confirmation
    			if secret_format == SecretFormat::G1v1 {
    				println!(
    					"The G1v1 public key for the provided secret is: '{}'",
    					compute_g1v1_public_key(&vault_data_for_import.key_pair)?
    				);
    
    				// Skip confirmation in non-interactive mode
    				let is_non_interactive_g1v1 = g1v1_id.is_some() && g1v1_password.is_some();
    				if !is_non_interactive_g1v1 {
    					let confirmed = inputs::confirm_action("Is it the correct one (if not, you should try again to input G1v1 id/password) ?".to_string())?;
    					if !confirmed {
    						return Ok(());
    					}
    				}
    			}
    
    			let txn = db.begin().await?;
    
    			// Handle password in non-interactive mode
    			let provided_password = if no_password {
    				Some(String::new()) // Empty password
    			} else {
    				password
    			};
    
    			let _account = create_base_account_for_vault_data_to_import(
    				&txn,
    				&vault_data_for_import,
    				provided_password.as_ref(),
    				Some(crypto_scheme),
    				name,
    			)
    			.await?;
    
    			txn.commit().await?;
    
    			println!("Change done");
    		}
    		Subcommand::Derive {
    			address_or_vault_name,
    			derivation_path,
    			password,
    			no_password,
    			name,
    		} => {
    			let account_tree_node_to_derive =
    				retrieve_account_tree_node(db, address_or_vault_name).await?;
    
    			let account_to_derive = account_tree_node_to_derive.borrow().account.clone();
    
    			let base_account_tree_node =
    				vault_account::get_base_account_tree_node(&account_tree_node_to_derive);
    
    			let base_account = &base_account_tree_node.borrow().account.clone();
    
    			println!("Adding derivation to: {account_to_derive}");
    
    			let base_parent_hierarchy_account_tree_node_to_derive =
    				vault_account::get_base_parent_hierarchy_account_tree_node(
    					&account_tree_node_to_derive,
    				);
    
    			let parent_hierarchy_table_account_to_derive =
    				display::compute_vault_accounts_table(&[
    					base_parent_hierarchy_account_tree_node_to_derive,
    				])?;
    
    			println!();
    			println!("Its parent hierarchy is this:");
    			println!("{parent_hierarchy_table_account_to_derive}");
    			println!();
    
    			println!("The linked <Base> account is {base_account}");
    
    			// Handle password from non-interactive mode or ask for it
    			let password = if no_password {
    				String::new()
    			} else if let Some(password) = password {
    				password
    			} else {
    				println!("Enter password to decrypt the <Base> account key");
    				inputs::prompt_password()?
    			};
    
    			let account_to_derive_secret_suri = vault_account::compute_suri_account_tree_node(
    				&account_tree_node_to_derive,
    				password,
    			)?;
    
    			println!();
    
    			// Handle derivation_path from non-interactive mode or ask for it
    			let derivation_path = if let Some(derivation_path) = derivation_path {
    				validate_derivation_path(derivation_path.clone())?;
    				derivation_path
    			} else {
    				inputs::prompt_vault_derivation_path()?
    			};
    
    			let derivation_secret_suri =
    				format!("{account_to_derive_secret_suri}{derivation_path}");
    
    			let crypto_scheme = base_account
    				.crypto_scheme
    				.map(CryptoScheme::from)
    				.unwrap_or(CryptoScheme::Ed25519); // Fallback to Ed25519 if not defined (should never happen)
    
    			let derivation_keypair = compute_keypair(crypto_scheme, &derivation_secret_suri)?;
    
    			let derivation_address: String = derivation_keypair.address().to_string();
    
    			let txn = db.begin().await?;
    
    			println!();
    			let _derivation = create_derivation_account(
    				&txn,
    				&derivation_address,
    				&derivation_path,
    				&account_to_derive.address.to_string(),
    				name,
    			)
    			.await?;
    
    			txn.commit().await?;
    			println!("Change done");
    		}
    		Subcommand::Rename { address } => {
    			let account =
    				vault_account::find_by_id(db, &DbAccountId::from(address.clone())).await?;
    
    			if account.is_none() {
    				println!("No vault entry found for address:'{address}'");
    				println!("You might want to import it first with 'vault import'");
    				return Ok(());
    			}
    
    			let account = account.unwrap();
    
    			println!(
    				"Current name for address:'{address}' is {:?}",
    				&account.name
    			);
    
    			println!("Enter new name for address (leave empty to remove the 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?;
    
    			println!("Rename done");
    		}
    		Subcommand::Remove {
    			address_or_vault_name,
    		} => {
    			let account_tree_node_to_delete =
    				retrieve_account_tree_node(db, address_or_vault_name).await?;
    
    			let txn = db.begin().await?;
    
    			let account_to_delete = account_tree_node_to_delete.borrow().account.clone();
    			let address_to_delete = account_tree_node_to_delete.borrow().account.address.clone();
    
    			//If account to delete has children; also delete all linked derivations
    			if !account_tree_node_to_delete.borrow().children.is_empty() {
    				let table =
    					display::compute_vault_accounts_table(&[account_tree_node_to_delete.clone()])?;
    
    				println!("All addresses linked to: {account_to_delete}");
    				println!("{table}");
    
    				println!(
    					"This {} account has {} addresses in total",
    					account_to_delete.account_type(),
    					vault_account::count_accounts_in_account_tree_node_and_children(
    						&account_tree_node_to_delete
    					)
    				);
    
    				let confirmation_message = if account_to_delete.is_base_account() {
    					"Are you sure you want to delete it along with the saved key?"
    				} else {
    					"Are you sure you want to delete it?"
    				};
    
    				let confirmed = inputs::confirm_action(confirmation_message.to_string())?;
    
    				if !confirmed {
    					return Ok(());
    				}
    
    				for account_to_delete in
    					vault_account::extract_accounts_depth_first_from_account_tree_node(
    						&account_tree_node_to_delete,
    					)? {
    					let delete_result = account_to_delete.delete(&txn).await?;
    					println!("Deleting {} address", delete_result.rows_affected);
    				}
    			} else {
    				let delete_result = account_to_delete.delete(&txn).await?;
    				println!("Deleting {} address", delete_result.rows_affected);
    			}
    
    			txn.commit().await?;
    
    			println!("Done removing address:'{address_to_delete}'");
    		}
    		Subcommand::Inspect {
    			address_or_vault_name,
    		} => {
    			let account_tree_node_to_derive =
    				retrieve_account_tree_node(db, 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");
    
    			let vault_key_addresses = fetch_vault_key_addresses(&data).await?;
    
    			let table = display::compute_vault_key_files_table(&vault_key_addresses)?;
    			println!("available key files to possibly migrate:");
    			println!("{table}");
    
    			for address in vault_key_addresses {
    				//Check if we already have a vault_derivation for that address
    				let existing_account =
    					vault_account::find_by_id(db, &DbAccountId::from_str(&address)?).await?;
    
    				if existing_account.is_some() {
    					//Already migrated
    					continue;
    				}
    
    				println!();
    				println!("Trying to migrate key {address}");
    				let vault_data_from_file = match try_fetch_vault_data_from_file(&data, &address) {
    					Ok(Some(vault_data)) => vault_data,
    					Ok(None) => {
    						println!("No vault entry file found for address {address}");
    						continue;
    					}
    					Err(e) => {
    						println!("Error while fetching vault data for address {address}: {e}");
    						println!("Continuing to next one");
    						continue;
    					}
    				};
    
    				let vault_data_to_import = VaultDataToImport {
    					secret_format: vault_data_from_file.secret_format,
    					secret_suri: vault_data_from_file.secret,
    					key_pair: vault_data_from_file.key_pair,
    				};
    
    				let txn = db.begin().await?;
    
    				// Old key files were in Sr25519 format (and had the Address associated to that scheme)
    				let account = create_base_account_for_vault_data_to_import(
    					&txn,
    					&vault_data_to_import,
    					Some(&vault_data_from_file.password),
    					Some(CryptoScheme::Sr25519),
    					None,
    				)
    				.await;
    
    				match account {
    					Ok(_account) => {
    						txn.commit().await?;
    						println!("Change done");
    					}
    					Err(error) => {
    						println!("Error occurred: {error}");
    						println!("Continuing to next key");
    					}
    				}
    			}
    
    			println!("Migration done");
    		}
    		Subcommand::Where => {
    			println!("{}", data.project_dir.data_dir().to_str().unwrap());
    		}
    	};
    
    	Ok(())
    }
    
    /// Method used to separate vault `name` part from optional `derivation` part in computed names that can be provided by users in the different `vault` commands using `AddressOrVaultNameGroup`
    fn parse_vault_name_and_derivation_path_from_user_input(
    	user_input_name: String,
    ) -> Result<(String, Option<String>), GcliError> {
    	if user_input_name.contains("/") {
    		user_input_name.find("/").map_or(
    			Err(GcliError::Input("Invalid format".to_string())),
    			|idx| {
    				let (prefix, derivation_path) = user_input_name.split_at(idx);
    				Ok((prefix.to_string(), Some(derivation_path.to_string())))
    			},
    		)
    	} else {
    		Ok((user_input_name, None))
    	}
    }
    
    /// Method that can be used to parse a Substrate URI (which can also be only a derivation path)
    ///
    /// Does some internal verification (done by sp_core::address_uri::AddressUri)
    ///
    /// It extracts the (optional) `phrase` and the (optional) recomposed full `derivation path`
    ///
    /// It also checks if a derivation `password` was provided and returns an error if one was found
    pub fn parse_prefix_and_derivation_path_from_suri(
    	raw_string: String,
    ) -> Result<(Option<String>, Option<String>), GcliError> {
    	let address_uri =
    		AddressUri::parse(&raw_string).map_err(|e| GcliError::Input(e.to_string()))?;
    
    	if let Some(pass) = address_uri.pass {
    		return Err(GcliError::Input(format!(
    			"Having a password in the derivation path is not supported (password:'{}')",
    			pass
    		)));
    	}
    
    	let full_path = if address_uri.paths.is_empty() {
    		None
    	} else {
    		Some("/".to_owned() + &address_uri.paths.into_iter().collect::<Vec<_>>().join("/"))
    	};
    
    	Ok((address_uri.phrase.map(|s| s.to_string()), full_path))
    }
    
    fn map_secret_format_to_crypto_scheme(
    	secret_format: SecretFormat,
    	override_crypto_scheme: Option<CryptoScheme>,
    ) -> CryptoScheme {
    	// If a crypto_scheme is explicitly specified, use it except for G1v1 which must always use Ed25519
    	if let Some(scheme) = override_crypto_scheme {
    		if secret_format == SecretFormat::G1v1 {
    			// G1v1 must always use Ed25519
    			CryptoScheme::Ed25519
    		} else {
    			scheme
    		}
    	} else {
    		// Default behavior if no crypto_scheme is specified
    		match secret_format {
    			// All formats use Ed25519 by default
    			SecretFormat::Seed => CryptoScheme::Ed25519,
    			SecretFormat::Substrate => CryptoScheme::Ed25519,
    			SecretFormat::Predefined => CryptoScheme::Ed25519,
    			SecretFormat::G1v1 => CryptoScheme::Ed25519,
    		}
    	}
    }
    
    /// This method will scan files in the data directory and return the addresses of the vault keys found
    #[deprecated(
    	note = "Should be removed in a future version when db persistence of vault is present for a while"
    )]
    async fn fetch_vault_key_addresses(data: &Data) -> Result<Vec<String>, GcliError> {
    	let mut entries = std::fs::read_dir(data.project_dir.data_dir())?
    		.map(|res| res.map(|e| e.path()))
    		.collect::<Result<Vec<_>, std::io::Error>>()?;
    
    	// To have consistent ordering
    	entries.sort();
    
    	let mut vault_key_addresses: Vec<String> = vec![];
    	entries.iter().for_each(|dir_path| {
    		let filename = dir_path.file_name().unwrap().to_str().unwrap();
    		// If potential_address is a valid AccountId
    		if AccountId::from_str(filename).is_ok() {
    			vault_key_addresses.push(filename.to_string());
    		}
    	});
    
    	Ok(vault_key_addresses)
    }
    
    pub async fn retrieve_vault_account_for_name<C>(
    	db: &C,
    	name_input: &String,
    ) -> Result<vault_account::Model, GcliError>
    where
    	C: ConnectionTrait,
    {
    	let account_tree_node = retrieve_account_tree_node_for_name(db, name_input).await?;
    
    	//Need this extra step to avoid borrowing issues
    	let account = account_tree_node.borrow().account.clone();
    	Ok(account)
    }
    
    pub async fn retrieve_account_tree_node<C>(
    	db: &C,
    	address_or_vault_name: AddressOrVaultNameGroup,
    ) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError>
    where
    	C: ConnectionTrait,
    {
    	let account_tree_node = if let Some(name_input) = &address_or_vault_name.vault_name {
    		retrieve_account_tree_node_for_name(db, name_input).await?
    	} else if let Some(address) = &address_or_vault_name.address {
    		let base_account_tree_node =
    			vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
    				db,
    				&address.to_string(),
    			)
    			.await?;
    
    		let account_tree_node_for_address = vault_account::get_account_tree_node_for_address(
    			&base_account_tree_node,
    			&address.to_string(),
    		);
    
    		Rc::clone(&account_tree_node_for_address)
    	} else {
    		//Should never happen since clap enforces exactly one of the 2 options
    		return Err(GcliError::Input("No address or name provided".to_string()));
    	};
    	Ok(account_tree_node)
    }
    
    pub async fn retrieve_account_tree_node_for_name<C>(
    	db: &C,
    	name_input: &String,
    ) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError>
    where
    	C: ConnectionTrait,
    {
    	let (name, derivation_path_opt) =
    		parse_vault_name_and_derivation_path_from_user_input(name_input.to_string())?;
    
    	let account_for_name = vault_account::find_by_name(db, &name).await?;
    
    	let account_for_name = account_for_name.ok_or(GcliError::Input(format!(
    		"No account found with name:'{name}'"
    	)))?;
    
    	let base_account_tree_node = vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
    		db,
    		&account_for_name.address.to_string(),
    	)
    	.await?;
    
    	let account_tree_node_for_name = vault_account::get_account_tree_node_for_address(
    		&base_account_tree_node,
    		&account_for_name.address.to_string(),
    	);
    
    	Ok(match derivation_path_opt {
    		None => Rc::clone(&account_tree_node_for_name),
    		Some(path) => {
    			let account_tree_node_for_name_input =
    				vault_account::compute_name_map_for_account_tree_node(&account_tree_node_for_name)?
    					.get(name_input)
    					.cloned()
    					.ok_or(GcliError::Input(format!(
    						"No account found with name:'{name}' and path:'{path}'"
    					)))?;
    
    			Rc::clone(&account_tree_node_for_name_input)
    		}
    	})
    }
    
    pub async fn retrieve_vault_account<C>(
    	db: &C,
    	address_or_vault_name: AddressOrVaultNameGroup,
    ) -> Result<vault_account::Model, GcliError>
    where
    	C: ConnectionTrait,
    {
    	let account_tree_node = retrieve_account_tree_node(db, address_or_vault_name).await?;
    
    	//Need this extra step to avoid borrowing issues
    	let account = account_tree_node.borrow().account.clone();
    	Ok(account)
    }
    
    fn create_vault_data_to_import<F, P>(
    	secret_format: SecretFormat,
    	crypto_scheme: CryptoScheme,
    	prompt_fn: F,
    ) -> Result<VaultDataToImport, GcliError>
    where
    	F: Fn(CryptoScheme) -> (String, P),
    	P: Into<KeyPair>,
    {
    	let (secret, pair) = prompt_fn(crypto_scheme);
    	let key_pair = pair.into();
    	Ok(VaultDataToImport {
    		secret_format,
    		secret_suri: secret,
    		key_pair,
    	})
    }
    
    fn prompt_secret_and_compute_vault_data_to_import(
    	secret_format: SecretFormat,
    	crypto_scheme: CryptoScheme,
    ) -> Result<VaultDataToImport, GcliError> {
    	match secret_format {
    		SecretFormat::Substrate => create_vault_data_to_import(
    			secret_format,
    			crypto_scheme,
    			prompt_secret_substrate_and_compute_keypair,
    		),
    		SecretFormat::Seed => create_vault_data_to_import(
    			secret_format,
    			crypto_scheme,
    			prompt_seed_and_compute_keypair,
    		),
    		SecretFormat::G1v1 => {
    			// G1v1 always uses Ed25519, ignore crypto_scheme
    			create_vault_data_to_import(
    				secret_format,
    				CryptoScheme::Ed25519,
    				prompt_secret_cesium_and_compute_keypair,
    			)
    		}
    		SecretFormat::Predefined => create_vault_data_to_import(
    			secret_format,
    			crypto_scheme,
    			prompt_predefined_and_compute_keypair,
    		),
    	}
    }
    
    /// 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.
    ///
    /// 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_opt: Option<&String>,
    	crypto_scheme: Option<CryptoScheme>,
    	name_opt: Option<String>,
    ) -> Result<vault_account::Model, GcliError>
    where
    	C: ConnectionTrait,
    {
    	let address = vault_data.key_pair.address().to_string();
    
    	// Check if the account already exists
    	let existing_vault_account =
    		vault_account::find_by_id(db_tx, &DbAccountId(vault_data.key_pair.address())).await?;
    
    	let password = match password_opt {
    		Some(password) => password.clone(),
    		None => inputs::prompt_password_query("Enter password to encrypt the key: ")?,
    	};
    
    	let encrypted_suri = compute_encrypted_suri(password.clone(), vault_data.secret_suri.clone())?;
    
    	if let Some(existing_vault_account) = existing_vault_account {
    		// Existing account
    		match inputs::confirm_action(format!(
    			"Account {} already exists. Do you want to update it?",
    			existing_vault_account
    		))? {
    			true => {
    				let name = if let Some(name) = name_opt {
    					validate_vault_name(&name)?;
    					trim_and_reduce_empty_as_none(name)
    				} else {
    					println!("(Optional) Enter a name for the vault entry (leave empty to remove the 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(None);
    				vault_account.parent = Set(None);
    				vault_account.crypto_scheme = Set(Some(
    					map_secret_format_to_crypto_scheme(vault_data.secret_format, crypto_scheme)
    						.into(),
    				));
    				vault_account.encrypted_suri = Set(Some(encrypted_suri));
    				vault_account.name = Set(name);
    				vault_account.secret_format = Set(Some(vault_data.secret_format.into()));
    				let updated_vault_account =
    					vault_account::update_account(db_tx, vault_account).await?;
    
    				println!("Updating vault account {updated_vault_account}");
    				Ok(updated_vault_account)
    			}
    			_ => Err(GcliError::Input("import canceled".into())),
    		}
    	} else {
    		//New entry
    		let secret_format = vault_data.secret_format;
    
    		let encrypted_suri = compute_encrypted_suri(password, vault_data.secret_suri.clone())?;
    
    		let name = if let Some(name) = name_opt {
    			validate_vault_name(&name)?;
    			trim_and_reduce_empty_as_none(name)
    		} else {
    			println!("(Optional) Enter a name for the vault entry");
    			inputs::prompt_vault_name_and_check_availability(db_tx, None).await?
    		};
    
    		let crypto_scheme = map_secret_format_to_crypto_scheme(secret_format, crypto_scheme);
    
    		let account = vault_account::create_base_account(
    			db_tx,
    			&address,
    			name.as_ref(),
    			crypto_scheme,
    			encrypted_suri,
    			secret_format,
    		)
    		.await?;
    
    		println!("Creating <Base> account {account}");
    		Ok(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,
    	name_opt: Option<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 =
    				vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
    					db_tx,
    					derivation_address,
    				)
    				.await?;
    
    			let existing_account_tree_node_for_address =
    				vault_account::get_account_tree_node_for_address(
    					&existing_account_tree_node_hierarchy,
    					derivation_address,
    				);
    
    			let base_parent_hierarchy_existing_account_tree_node =
    				vault_account::get_base_parent_hierarchy_account_tree_node(
    					&existing_account_tree_node_for_address,
    				);
    
    			let parent_hierarchy_table_existing_account =
    				display::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!("Its 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" => {
    				let name = if let Some(name) = name_opt {
    					validate_vault_name(&name)?;
    					trim_and_reduce_empty_as_none(name)
    				} else {
    					println!("(Optional) Enter a name for the vault entry (leave empty to remove the 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 {
    		let name = if let Some(name) = name_opt {
    			validate_vault_name(&name)?;
    			trim_and_reduce_empty_as_none(name)
    		} else {
    			println!("(Optional) Enter a name for the vault entry");
    			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 compute the encrypted suri
    fn compute_encrypted_suri(password: String, secret_suri: String) -> Result<Vec<u8>, GcliError> {
    	encrypt(secret_suri.as_bytes(), password).map_err(|e| GcliError::Input(e.to_string()))
    }
    
    fn get_vault_key_path(data: &Data, vault_filename: &str) -> PathBuf {
    	data.project_dir.data_dir().join(vault_filename)
    }
    
    /// look for different possible paths for vault keys and return both format and path
    fn find_substrate_vault_key_file(data: &Data, address: &str) -> Result<Option<PathBuf>, GcliError> {
    	let path = get_vault_key_path(data, address);
    	if path.exists() {
    		return Ok(Some(path));
    	}
    
    	Ok(None)
    }
    
    /// try to get secret in keystore, prompt for the password and compute the keypair
    pub async fn try_fetch_key_pair(
    	data: &Data,
    	address: AccountId,
    ) -> Result<Option<KeyPair>, GcliError> {
    	if let Some(account_tree_node_hierarchy) =
    		vault_account::fetch_base_account_tree_node_hierarchy(
    			data.connect_db(),
    			&address.to_string(),
    		)
    		.await?
    	{
    		let account_tree_node = vault_account::get_account_tree_node_for_address(
    			&account_tree_node_hierarchy,
    			&address.to_string(),
    		);
    
    		println!("(Vault: {})", account_tree_node.borrow().account);
    
    		let password = inputs::prompt_password()?;
    		let secret_suri =
    			vault_account::compute_suri_account_tree_node(&account_tree_node, password)?;
    
    		let base_account_tree_node = vault_account::get_base_account_tree_node(&account_tree_node);
    
    		let base_account = &base_account_tree_node.borrow().account.clone();
    
    		let key_pair = compute_keypair(base_account.crypto_scheme.unwrap().into(), &secret_suri)?;
    
    		//To be safe
    		if address != key_pair.address() {
    			return Err(GcliError::Input(format!(
    				"Computed address {} does not match the expected address {}",
    				key_pair.address(),
    				address
    			)));
    		}
    
    		Ok(Some(key_pair))
    	} else {
    		Ok(None)
    	}
    }
    
    pub fn compute_keypair(
    	crypto_scheme: CryptoScheme,
    	secret_suri: &str,
    ) -> Result<KeyPair, GcliError> {
    	let key_pair = match crypto_scheme {
    		CryptoScheme::Sr25519 => pair_from_sr25519_str(secret_suri)?.into(),
    		CryptoScheme::Ed25519 => pair_from_ed25519_str(secret_suri)?.into(),
    	};
    	Ok(key_pair)
    }
    
    pub struct VaultDataFromFile {
    	secret_format: SecretFormat,
    	secret: String,
    	#[allow(dead_code)]
    	path: PathBuf,
    	password: String,
    	key_pair: KeyPair,
    }
    
    /// try to get secret in keystore, prompt for the password and compute the keypair
    #[deprecated(
    	note = "Should be removed in a future version when db persistence of vault is present for a while"
    )]
    pub fn try_fetch_vault_data_from_file(
    	data: &Data,
    	address: &str,
    ) -> Result<Option<VaultDataFromFile>, GcliError> {
    	if let Some(path) = find_substrate_vault_key_file(data, address)? {
    		println!("Enter password to unlock account {address}");
    		let password = inputs::prompt_password()?;
    		let mut file = std::fs::OpenOptions::new().read(true).open(path.clone())?;
    		let mut cypher = vec![];
    		file.read_to_end(&mut cypher)?;
    		let secret_vec =
    			decrypt(&cypher, password.clone()).map_err(|e| GcliError::Input(e.to_string()))?;
    		let secret = String::from_utf8(secret_vec).map_err(|e| anyhow!(e))?;
    
    		let key_pair = pair_from_sr25519_str(&secret)?.into();
    
    		Ok(Some(VaultDataFromFile {
    			secret_format: SecretFormat::Substrate,
    			secret,
    			path,
    			password,
    			key_pair,
    		}))
    	} else {
    		Ok(None)
    	}
    }
    
    #[cfg(test)]
    mod tests {
    	use super::*;
    	use rstest::rstest;
    
    	/// test that armored encryption/decryption work as intended
    	#[test]
    	fn test_encrypt_decrypt() {
    		let plaintext = b"Hello world!";
    		let passphrase = "this is not a good passphrase".to_string();
    		let encrypted = encrypt(plaintext, passphrase.clone()).unwrap();
    		let decrypted = decrypt(&encrypted, passphrase).unwrap();
    		assert_eq!(decrypted, plaintext);
    	}
    
    	#[rstest]
    	#[case(
    		String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0"),
    		Some(String::from(
    			"bottom drive obey lake curtain smoke basket hold race lonely fit walk"
    		)),
    		Some(String::from("//0"))
    	)]
    	#[case(
    		String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0"),
    		Some(String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")),
    		Some(String::from("//0"))
    	)]
    	#[case(
    		String::from(
    			"bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice"
    		),
    		Some(String::from(
    			"bottom drive obey lake curtain smoke basket hold race lonely fit walk"
    		)),
    		Some(String::from("//Alice"))
    	)]
    	#[case(
    		String::from(
    			"bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Bob/soft1/soft2"
    		),
    		Some(String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk")),
    		Some(String::from("//Alice//Bob/soft1/soft2"))
    	)]
    	#[case(
    		String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"),
    		Some(String::from(
    			"bottom drive obey lake curtain smoke basket hold race lonely fit walk"
    		)),
    		None
    	)]
    	#[case(
    		String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"),
    		Some(String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")),
    		None
    	)]
    	#[case(
    		String::from("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"),
    		Some(String::from("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")),
    		None
    	)]
    	#[case(
    		String::from("someVaultName//Alice"),
    		Some(String::from("someVaultName")),
    		Some(String::from("//Alice"))
    	)]
    	#[case(
    		String::from("someVaultName"),
    		Some(String::from("someVaultName")),
    		None
    	)]
    	fn test_parse_prefix_and_derivation_path_from_suri(
    		#[case] raw_string: String,
    		#[case] expected_prefix: Option<String>,
    		#[case] expected_derivation_path: Option<String>,
    	) {
    		let (root_secret, derivation_path) =
    			parse_prefix_and_derivation_path_from_suri(raw_string).unwrap();
    		assert_eq!(expected_prefix, root_secret);
    		assert_eq!(expected_derivation_path, derivation_path);
    	}
    
    	#[rstest]
    	#[case(
    		String::from("//Alice//Bob/soft1/soft2"),
    		None,
    		Some(String::from("//Alice//Bob/soft1/soft2"))
    	)]
    	#[case(String::from(""), None, None)]
    	#[case(String::from("//0"), None, Some(String::from("//0")))]
    	fn test_parse_prefix_and_derivation_path_from_suri_works_with_empty_prefix_phrase(
    		#[case] raw_string: String,
    		#[case] expected_prefix: Option<String>,
    		#[case] expected_derivation_path: Option<String>,
    	) {
    		let (root_secret, derivation_path) =
    			parse_prefix_and_derivation_path_from_suri(raw_string).unwrap();
    		assert_eq!(expected_prefix, root_secret);
    		assert_eq!(expected_derivation_path, derivation_path);
    	}
    
    	#[rstest]
    	#[case(
            String::from(
                "bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Bob/soft1/soft2///password"
            ),
        )]
    	#[case(String::from(
    		"bottom drive obey lake curtain smoke basket hold race lonely fit walk///password"
    	))]
    	#[case(String::from(
    		"bottom drive obey lake curtain smoke basket hold race lonely fit walk///"
    	))]
    	#[case(
            String::from(
                "bottom drive obey lake curtain smoke basket hold race lonely fit walk///password//NotDerivations//Still/password/part"
            ),
        )]
    	fn test_parse_prefix_and_derivation_path_from_suri_does_not_allow_password(
    		#[case] raw_string: String,
    	) {
    		let result = parse_prefix_and_derivation_path_from_suri(raw_string);
    		match result.unwrap_err() {
    			GcliError::Input(err) => {
    				println!("Error message: {}", err);
    				assert!(
    					err.starts_with("Having a password in the derivation path is not supported")
    				);
    			}
    			other => panic!("Should have been an Input error; got: {:?}", other),
    		}
    	}
    }