diff --git a/Cargo.lock b/Cargo.lock index 2005b13471f27ced27db06cde75c85bf65b62268..43d375be8b97d9a7d24339affec5e94f481b14d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2353,11 +2353,11 @@ dependencies = [ "futures", "graphql_client", "hex", + "indoc", "inquire", "log", "parity-scale-codec", "reqwest", - "rpassword", "rstest", "scrypt", "sea-orm", @@ -2911,6 +2911,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "inherent" version = "1.0.11" @@ -4516,17 +4522,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "rpassword" -version = "7.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" -dependencies = [ - "libc", - "rtoolbox", - "windows-sys 0.48.0", -] - [[package]] name = "rsa" version = "0.9.7" @@ -4577,16 +4572,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "rtoolbox" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" -dependencies = [ - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "rust-embed" version = "8.5.0" diff --git a/Cargo.toml b/Cargo.toml index 48423dd41bee2516e85d8e27a96147fbc49e125b..31314799a82564928e1cbcf8fecb1e104ee31819 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,6 @@ reqwest = { version = "^0.11.27", default-features = false, features = [ "rustls-tls", ] } inquire = "^0.7.5" -rpassword = "^7.3.1" serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0.128" tokio = { version = "^1.40.0", features = ["macros"] } @@ -56,6 +55,7 @@ colored = "2.1.0" # Tests rstest = "0.23.0" +indoc = "2.0.5" # allows to build gcli for different runtimes and with different predefined networks [features] diff --git a/doc/config.md b/doc/config.md index 5c8d5919043c04eb92fd8f32fee9ea02b6e0a5ff..fbe59f562c861634503e3cec9054e0936ed2c972 100644 --- a/doc/config.md +++ b/doc/config.md @@ -36,6 +36,8 @@ gcli account transfer 1 5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n # no need for password to sign transaction ``` +`FIXME` Need to update documentation when vault changes are stable. + but in general usage, you want to store your secret in the local vault. This goes like this: ```sh diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 8c8fc31dd69be7fb818815bbfef15308ea12151b..716947789d010412f329d410f6822bb9ca0b0bb3 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -1,15 +1,16 @@ use crate::commands::cesium::compute_g1v1_public_key; -use crate::entities::vault_account::CryptoType; -use crate::entities::{vault_account, vault_derivation}; +use crate::entities::vault_account; +use crate::entities::vault_account::{AccountTreeNode, DbAccountId}; use crate::*; use age::secrecy::Secret; use comfy_table::{Cell, Table}; -use sea_orm::ActiveValue::Set; -use sea_orm::{ActiveModelTrait, EntityTrait, ModelTrait}; -use sea_orm::{ColumnTrait, QueryFilter}; +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)] @@ -24,27 +25,31 @@ pub enum Subcommand { }, /// Generate a mnemonic Generate, - /// Import key from (substrate)mnemonic or other format with interactive prompt + /// Import key from (substrate uri) or other format with interactive prompt #[clap( - long_about = "Import key from (substrate)mnemonic or other format with interactive prompt\n\ + long_about = "Import key from (substrate uri) or other format with interactive prompt\n\ \n\ - If a (substrate)mnemonic is provided with a derivation path, it will ensure the base <Account>\n\ - and associated SS58 Address exists before creating the derivation; but please use command \n\ - `vault derivation|derive|deriv` to add a derivation to an existing <Account> instead." + 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, cesium) #[clap(short = 'S', long, required = false, default_value = SecretFormat::Substrate)] secret_format: SecretFormat, }, - /// Add a derivation to an existing <Account> - #[clap(long_about = "Add a derivation to an existing <Account>\n\ + /// Add a derivation to an existing account + #[clap(long_about = "Add a derivation to an existing account\n\ \n\ Only \"substrate\" and \"seed\" format are supported for derivations\n\ - Use command `vault list account` to see available <Account> and their format")] + \n\ + Use command `vault list base` to see available <Base> account and their format\n\ + And then use command 'vault list for' to find all accounts linked to that <Base> account")] #[clap(alias = "deriv")] - #[clap(alias = "derive")] - Derivation { + #[clap(alias = "derivation")] + Derive { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, }, @@ -53,10 +58,10 @@ pub enum Subcommand { /// SS58 Address address: AccountId, }, - /// Remove an SS58 Address from the vault + /// Remove an SS58 Address from the vault together with it's linked derivations #[clap(long_about = "Remove an SS58 Address from the vault\n\ \n\ - If an <Account> Address is given it will also remove all linked derivations")] + If a <Base> Address is given it will also remove the saved key")] Remove { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, @@ -71,16 +76,16 @@ pub enum Subcommand { #[derive(Clone, Default, Debug, clap::Parser)] pub enum ListChoice { - /// List all <Account> and their linked derivations SS58 Addresses in the vault + /// List all <Base> accounts and their linked derivations SS58 Addresses in the vault #[default] All, - /// List <Account> and derivations SS58 Addresses linked to the selected one + /// List <Base> and Derivations SS58 Addresses linked to the selected one For { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, }, - /// List all <Account> SS58 Addresses in the vault - Account, + /// List all <Base> SS58 Addresses in the vault + Base, } pub struct VaultDataToImport { @@ -90,7 +95,7 @@ pub struct VaultDataToImport { } // encrypt input with passphrase -fn encrypt(input: &[u8], passphrase: String) -> Result<Vec<u8>, age::EncryptError> { +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( @@ -103,7 +108,7 @@ fn encrypt(input: &[u8], passphrase: String) -> Result<Vec<u8>, age::EncryptErro } // decrypt cypher with passphrase -fn decrypt(input: &[u8], passphrase: String) -> Result<Vec<u8>, age::DecryptError> { +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 { @@ -123,36 +128,37 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE match command { Subcommand::List(choice) => match choice { ListChoice::All => { - let derivations = vault_derivation::list_all_derivations_in_order(db).await?; - - let table = compute_vault_derivations_table(db, &derivations).await?; + let all_account_tree_node_hierarchies = + vault_account::fetch_all_base_account_tree_node_hierarchies(db).await?; + let table = compute_vault_accounts_table(&all_account_tree_node_hierarchies)?; println!("available SS58 Addresses:"); println!("{table}"); } - ListChoice::Account => { - let derivations = vault_derivation::list_all_root_derivations_in_order(db).await?; + ListChoice::Base => { + let base_account_tree_nodes = + vault_account::fetch_only_base_account_tree_nodes(db).await?; - let table = compute_vault_derivations_table(db, &derivations).await?; + let table = compute_vault_accounts_table(&base_account_tree_nodes)?; - println!("available <Account> SS58 Addresses:"); + println!("available <Base> SS58 Addresses:"); println!("{table}"); } ListChoice::For { address_or_vault_name, } => { - let selected_derivation = - retrieve_vault_derivation(&data, address_or_vault_name).await?; + let account = retrieve_vault_account(&data, address_or_vault_name).await?; - let linked_derivations = vault_derivation::fetch_all_linked_derivations_in_order( - db, - &selected_derivation.root_address, - ) - .await?; + let account_tree_node_hierarchy = + vault_account::fetch_base_account_tree_node_hierarchy_unwrapped( + db, + &account.address.to_string(), + ) + .await?; - let table = compute_vault_derivations_table(db, &linked_derivations).await?; + let table = compute_vault_accounts_table(&[account_tree_node_hierarchy])?; - println!("available SS58 Addresses linked to {selected_derivation}:"); + println!("available SS58 Addresses linked to {account}:"); println!("{table}"); } }, @@ -167,12 +173,12 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE Subcommand::Use { address_or_vault_name, } => { - let derivation = retrieve_vault_derivation(&data, address_or_vault_name).await?; + let account = retrieve_vault_account(&data, address_or_vault_name).await?; - println!("Using: {}", derivation); + println!("Using: {}", account); let updated_cfg = conf::Config { - address: Some(AccountId::from_str(&derivation.address).expect("invalid address")), + address: Some(account.address.0), ..data.cfg }; @@ -200,30 +206,32 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE } } - let address_to_import = vault_data_for_import.key_pair.address().to_string(); + let address_to_import = vault_data_for_import.key_pair.address(); println!("Trying to import for SS58 address :'{}'", address_to_import); - if let Some(derivation) = vault_derivation::Entity::find_by_id(&address_to_import) - .one(db) - .await? + 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: {}", - derivation + check_account ); - let linked_derivations = vault_derivation::fetch_all_linked_derivations_in_order( - db, - &derivation.root_address.clone(), - ) - .await?; + 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_derivations_table(db, &linked_derivations).await?; + 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"); @@ -234,7 +242,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let txn = db.begin().await?; - let _derivation = create_derivation_for_vault_data_to_import( + let _account = create_account_for_vault_data_to_import( &txn, &vault_data_for_import, &password, @@ -246,60 +254,63 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE println!("Import done"); } - Subcommand::Derivation { + Subcommand::Derive { address_or_vault_name, } => { - let root_derivation = retrieve_vault_derivation(&data, address_or_vault_name).await?; + let account_tree_node_to_derive = + retrieve_account_tree_node(&data, address_or_vault_name).await?; - if root_derivation.path.is_some() { - println!("Can only add derivation on an <Account>"); - println!( - "The selected address:'{}' already has an <Account> with address:'{}'", - root_derivation.address, root_derivation.root_address - ); - println!("You can check for available <Account> addresses with command 'vault list account'"); - return Ok(()); - } + let account_to_derive = account_tree_node_to_derive.borrow().account.clone(); - let vault_account = vault_account::Entity::find_by_id(&root_derivation.address) - .one(db) - .await? - .ok_or(GcliError::Input(format!( - "Could not find vault_account for address:'{}'", - root_derivation.address - )))?; + let base_account_tree_node = + vault_account::get_base_account_tree_node(&account_tree_node_to_derive); - if vault_account.crypto_type == CryptoType::G1v1Seed { - println!( - "Only \"{}\" and \"{}\" format are supported for derivations", - Into::<&str>::into(SecretFormat::Substrate), - Into::<&str>::into(SecretFormat::Seed) - ); - println!( - "Use command `vault list account` to see available <Account> and their format" - ); - return Ok(()); + let base_account = &base_account_tree_node.borrow().account.clone(); + + if base_account.crypto_scheme.is_none() { + panic!("Crypto scheme is not set for the base account:{base_account} - should not happen"); + } + + if let Some(crypto_scheme) = base_account.crypto_scheme { + if CryptoScheme::from(crypto_scheme) == CryptoScheme::Ed25519 { + println!( + "Only \"{}\" and \"{}\" format are supported for derivations", + Into::<&str>::into(SecretFormat::Substrate), + Into::<&str>::into(SecretFormat::Seed) + ); + println!(); + println!( + "Use command `vault list base` to see available <Base> account and their format\n\ + And then use command 'vault list for' to find all accounts linked to that <Base> account" + ); + return Ok(()); + } } - println!("Adding derivation to: {root_derivation}"); + println!("Adding derivation to: {account_to_derive}"); - println!("Enter password to decrypt the <Account> key"); + println!("Enter password to decrypt the <Base> account key"); + let password = inputs::prompt_password()?; - let root_secret_suri = retrieve_suri_from_vault_account(&vault_account)?; + let account_to_derive_secret_suri = vault_account::compute_suri_account_tree_node( + &account_tree_node_to_derive, + password, + )?; let derivation_path = inputs::prompt_vault_derivation_path()?; - let derivation_secret_suri = format!("{root_secret_suri}{derivation_path}"); + let derivation_secret_suri = + format!("{account_to_derive_secret_suri}{derivation_path}"); let derivation_keypair = - compute_keypair(vault_account.crypto_type, &derivation_secret_suri)?; + compute_keypair(CryptoScheme::Sr25519, &derivation_secret_suri)?; let derivation_address: String = derivation_keypair.address().to_string(); - let check_derivation = vault_derivation::Entity::find_by_id(&derivation_address) - .one(db) - .await?; + 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(()); @@ -308,91 +319,88 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE println!("(Optional) Enter a name for the new derivation"); let name = inputs::prompt_vault_name()?; - let derivation = vault_derivation::ActiveModel { - address: Set(derivation_address), - name: Set(name), - path: Set(Some(derivation_path)), - root_address: Set(root_derivation.root_address.clone()), - }; - let derivation = derivation.insert(db).await?; - println!("Created: {}", derivation); + let _derivation = vault_account::create_derivation_account( + db, + &derivation_address, + name.as_ref(), + &derivation_path, + &account_to_derive.address.to_string(), + ) + .await?; + + println!("Derive done"); } Subcommand::Rename { address } => { - let derivation = vault_derivation::Entity::find_by_id(address.to_string()) - .one(db) - .await?; + let account = + vault_account::find_by_id(db, &DbAccountId::from(address.clone())).await?; - if derivation.is_none() { + 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 derivation = derivation.unwrap(); + let account = account.unwrap(); println!( "Current name for address:'{address}' is {:?}", - derivation.name + &account.name ); println!("Enter new name for address (leave empty to remove the name)"); let name = inputs::prompt_vault_name()?; - let old_name = derivation.name.clone(); - let mut derivation: vault_derivation::ActiveModel = derivation.into(); - derivation.name = Set(name.clone()); - let _derivation = derivation.update(db).await?; - println!( - "Renamed address:'{address}' from {:?} to {:?}", - old_name, name - ); + let _account = vault_account::update_account_name(db, account, name.as_ref()).await?; + + println!("Rename done"); } Subcommand::Remove { address_or_vault_name, } => { - let derivation = retrieve_vault_derivation(&data, address_or_vault_name).await?; - let address_to_delete = derivation.address.clone(); + let account_tree_node_to_delete = + retrieve_account_tree_node(&data, address_or_vault_name).await?; let txn = db.begin().await?; - //If deleting a root derivation; also delete the vault account and all linked derivations - if derivation.path.is_none() { - let all_derivations_to_delete = - vault_derivation::fetch_all_linked_derivations_in_order( - &txn, - &address_to_delete, - ) - .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(); - let table = - compute_vault_derivations_table(&txn, &all_derivations_to_delete).await?; + //If account to delete has children; also delete all linked derivations + if !account_tree_node_to_delete.borrow().children.is_empty() { + let table = compute_vault_accounts_table(&[account_tree_node_to_delete.clone()])?; - println!("All addresses linked to: {derivation}"); + println!("All addresses linked to: {account_to_delete}"); println!("{table}"); println!( - "This <Account> has {} addresses in total", - all_derivations_to_delete.len() + "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 confirmed = inputs::confirm_action( - "Are you sure you want to delete it along with the saved key ?".to_string(), - )?; + + 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 derivation_to_delete in all_derivations_to_delete { - let delete_result = derivation_to_delete.delete(&txn).await?; + 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!("Deleted {} address", delete_result.rows_affected); } - - let delete_result = vault_account::Entity::delete_by_id(&address_to_delete) - .exec(&txn) - .await?; - println!("Deleted {} vault account", delete_result.rows_affected); } else { - let delete_result = derivation.delete(&txn).await?; + let delete_result = account_to_delete.delete(&txn).await?; println!("Deleted {} address", delete_result.rows_affected); } @@ -411,11 +419,10 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE for address in vault_key_addresses { //Check if we already have a vault_derivation for that address - let derivation = vault_derivation::Entity::find_by_id(&address) - .one(db) - .await?; + let existing_account = + vault_account::find_by_id(db, &DbAccountId::from_str(&address)?).await?; - if derivation.is_some() { + if existing_account.is_some() { //Already migrated continue; } @@ -443,7 +450,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let txn = db.begin().await?; - let derivation = create_derivation_for_vault_data_to_import( + let account = create_account_for_vault_data_to_import( &txn, &vault_data_to_import, &vault_data_from_file.password, @@ -452,7 +459,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE .await?; txn.commit().await?; - println!("Import done: {}", derivation); + println!("Import done: {}", account); } println!("Migration done"); @@ -465,7 +472,8 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE Ok(()) } -fn parse_prefix_and_derivation_path_from_string( +/// Method used to separate `name` part from optional `derivation` part in computed names +fn parse_prefix_and_derivation_path_from_string_for_vault_name( raw_string: String, ) -> Result<(String, Option<String>), GcliError> { if raw_string.contains("/") { @@ -480,12 +488,48 @@ fn parse_prefix_and_derivation_path_from_string( } } -fn map_secret_format_to_crypto_type(secret_format: SecretFormat) -> CryptoType { +/// 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( + address_uri + .paths + .iter() + .map(|s| "/".to_string() + s) + .collect::<Vec<_>>() + .join(""), + ) + }; + + Ok((address_uri.phrase.map(|s| s.to_string()), full_path)) +} + +fn map_secret_format_to_crypto_scheme(secret_format: SecretFormat) -> CryptoScheme { match secret_format { - SecretFormat::Seed => vault_account::CryptoType::EntropyKdfSeed, - SecretFormat::Substrate => vault_account::CryptoType::Bip39Mnemonic, - SecretFormat::Predefined => vault_account::CryptoType::Bip39Mnemonic, - SecretFormat::Cesium => vault_account::CryptoType::G1v1Seed, + SecretFormat::Seed => CryptoScheme::Sr25519, + SecretFormat::Substrate => CryptoScheme::Sr25519, + SecretFormat::Predefined => CryptoScheme::Sr25519, + SecretFormat::Cesium => CryptoScheme::Ed25519, } } @@ -524,85 +568,76 @@ async fn compute_vault_key_files_table(vault_key_addresses: &[String]) -> Result Ok(table) } -async fn compute_vault_derivations_table<C>( - db: &C, - derivations_ordered: &[vault_derivation::Model], -) -> Result<Table, GcliError> -where - C: ConnectionTrait, -{ +fn compute_vault_accounts_table( + account_tree_nodes: &[Rc<RefCell<AccountTreeNode>>], +) -> Result<Table, GcliError> { let mut table = Table::new(); table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY); table.set_header(vec!["SS58 Address", "Format", "Account/Path", "Name"]); + + for account_tree_node in account_tree_nodes { + add_account_tree_node_to_table(&mut table, account_tree_node); + } + + Ok(table) +} + +fn add_account_tree_node_to_table( + table: &mut Table, + account_tree_node: &Rc<RefCell<AccountTreeNode>>, +) { + let row = compute_vault_accounts_row(account_tree_node); + table.add_row(row); + + for child in &account_tree_node.borrow().children { + add_account_tree_node_to_table(table, child); + } +} + +pub fn compute_vault_accounts_row(account_tree_node: &Rc<RefCell<AccountTreeNode>>) -> Vec<Cell> { let empty_string = "".to_string(); - let root_path = "<Account>".to_string(); - - let mut current_root_address = "".to_string(); - let mut current_root_name: Option<String> = None; - let mut current_vault_format: Option<&str> = None; - - for derivation in derivations_ordered { - if derivation.root_address != current_root_address { - // First entry when changing root address should be an account ("root" derivation) - if derivation.path.is_some() { - return Err(GcliError::Input( - "Order of derivations parameter is wrong".to_string(), - )); - } - current_root_address = derivation.root_address.clone(); - current_root_name = derivation.name.clone(); - let vault_account = vault_account::Entity::find_by_id(current_root_address.clone()) - .one(db) - .await? - .ok_or(GcliError::Input(format!( - "No vault <Account> found with address:'{current_root_address}'" - )))?; + let depth_account_tree_node = vault_account::count_depth_account_tree_node(account_tree_node); - current_vault_format = match vault_account.crypto_type { - CryptoType::Bip39Mnemonic => Some(SecretFormat::Substrate.into()), - CryptoType::EntropyKdfSeed => Some(SecretFormat::Seed.into()), - CryptoType::G1v1Seed => Some(SecretFormat::Cesium.into()), - }; - } + let name = if let Some(name) = account_tree_node.borrow().account.name.clone() { + name + } else if let Some(computed_name) = + vault_account::compute_name_account_tree_node(account_tree_node) + { + format!("<{}>", computed_name) + } else { + empty_string.clone() + }; - let address = if derivation.path.is_none() { - derivation.address.clone() - } else { - " ".to_string() + &derivation.address - }; + let account_tree_node = account_tree_node.borrow(); - let (path, format) = if derivation.path.is_none() { - (root_path.clone(), current_vault_format.unwrap()) - } else { - (derivation.path.clone().unwrap(), empty_string.as_str()) - }; + let address = if depth_account_tree_node > 0 { + let ancestors = "│ ".repeat(depth_account_tree_node - 1); + format!("{}├─{}", ancestors, account_tree_node.account.address) + } else { + account_tree_node.account.address.to_string() + }; - let name = if derivation.name.is_none() { - if derivation.path.is_none() { - empty_string.clone() - } else if let Some(current_root_name) = ¤t_root_name { - format!( - "<{}{}>", - current_root_name, - derivation.path.clone().unwrap() - ) - } else { - empty_string.clone() - } - } else { - derivation.name.clone().unwrap() + let (path, format) = if let Some(path) = account_tree_node.account.path.clone() { + (path, empty_string.clone()) + } else { + let secret_format = match account_tree_node.account.crypto_scheme.unwrap().into() { + CryptoScheme::Sr25519 => SecretFormat::Substrate, + CryptoScheme::Ed25519 => SecretFormat::Cesium, }; + let secret_format_str: &str = secret_format.into(); + ( + format!("<{}>", account_tree_node.account.account_type()), + secret_format_str.to_string(), + ) + }; - table.add_row(vec![ - Cell::new(&address), - Cell::new(format), - Cell::new(&path), - Cell::new(&name), - ]); - } - - Ok(table) + vec![ + Cell::new(&address), + Cell::new(format), + Cell::new(&path), + Cell::new(&name), + ] } pub async fn retrieve_address_string<T: AddressOrVaultName>( @@ -613,57 +648,87 @@ pub async fn retrieve_address_string<T: AddressOrVaultName>( return Ok(address.to_string()); } - let derivation = retrieve_vault_derivation(data, address_or_vault_name).await?; + let account = retrieve_vault_account(data, address_or_vault_name).await?; - Ok(derivation.address) + Ok(account.address.to_string()) } -pub async fn retrieve_vault_derivation<T: AddressOrVaultName>( +pub async fn retrieve_account_tree_node<T: AddressOrVaultName>( data: &Data, address_or_vault_name: T, -) -> Result<vault_derivation::Model, GcliError> { - let derivation = if let Some(name) = address_or_vault_name.name() { +) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError> { + //FIXME Should do the inverse as we do potentially several times same operation + let account = retrieve_vault_account(data, address_or_vault_name).await?; + + let account_tree_node = vault_account::fetch_base_account_tree_node_hierarchy_unwrapped( + data.connect_db(), + &account.address.to_string(), + ) + .await?; + + Ok(vault_account::get_account_tree_node_for_address( + &account_tree_node, + &account.address.to_string(), + )) +} + +pub async fn retrieve_vault_account<T: AddressOrVaultName>( + data: &Data, + address_or_vault_name: T, +) -> Result<vault_account::Model, GcliError> { + let account = if let Some(name_input) = address_or_vault_name.name() { let (name, derivation_path_opt) = - parse_prefix_and_derivation_path_from_string(name.to_string())?; + parse_prefix_and_derivation_path_from_string_for_vault_name(name_input.to_string())?; - let derivation = vault_derivation::Entity::find() - .filter(vault_derivation::Column::Name.eq(Some(name.clone()))) - .one(data.connect_db()) - .await?; + let account = vault_account::find_by_name(data.connect_db(), &name).await?; - let derivation = derivation.ok_or(GcliError::Input(format!( - "No vault SS58 Address found with name:'{name}'" + let account = account.ok_or(GcliError::Input(format!( + "No account found with name:'{name}'" )))?; match derivation_path_opt { - None => derivation, + None => account, Some(path) => { - let sub_derivation = vault_derivation::Entity::find() - .filter( - vault_derivation::Column::RootAddress.eq(derivation.root_address.clone()), + let account_tree_node_hierarchy = + vault_account::fetch_base_account_tree_node_hierarchy_unwrapped( + data.connect_db(), + &account.address.to_string(), ) - .filter(vault_derivation::Column::Path.eq(Some(path.clone()))) - .one(data.connect_db()) .await?; - sub_derivation.ok_or(GcliError::Input(format!( - "No vault derivation found with <Account> name:'{name}' and path:'{path}'" - )))? + let account_tree_node_hierarchy = vault_account::get_account_tree_node_for_address( + &account_tree_node_hierarchy, + &account.address.to_string(), + ); + + let account_tree_node = vault_account::compute_name_map_for_account_tree_node( + &account_tree_node_hierarchy, + )? + .get(name_input) + .cloned() + .ok_or(GcliError::Input(format!( + "No account found with name:'{name}' and path:'{path}'" + )))?; + + //Need this extra step to avoid borrowing issues + let account = account_tree_node.borrow().account.clone(); + + account } } } else if let Some(address) = address_or_vault_name.address() { - let derivation = vault_derivation::Entity::find_by_id(address.to_string()) - .one(data.connect_db()) - .await?; + let account = + vault_account::find_by_id(data.connect_db(), &DbAccountId::from(address.clone())) + .await?; - derivation.ok_or(GcliError::Input(format!( + account.ok_or(GcliError::Input(format!( "No vault entry found with Address:'{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(derivation) + Ok(account) } fn create_vault_data_to_import<F, P>( @@ -702,25 +767,25 @@ fn prompt_secret_and_compute_vault_data_to_import( } } -/// Creates derivation and if necessary root vault account and root derivation +/// Creates an account for the vault data to import /// /// Does it all using "db" parameter that should better be a transaction since multiple operations can be done -pub async fn create_derivation_for_vault_data_to_import<C>( +pub async fn create_account_for_vault_data_to_import<C>( db: &C, vault_data: &VaultDataToImport, password: &str, name: Option<&String>, -) -> Result<vault_derivation::Model, GcliError> +) -> Result<vault_account::Model, GcliError> where C: ConnectionTrait, { let address_to_import = vault_data.key_pair.address().to_string(); //To be safe - if vault_derivation::Entity::find_by_id(&address_to_import) - .one(db) + if vault_account::find_by_id(db, &DbAccountId::from_str(&address_to_import)?) .await? .is_some() { + //TODO Later possibly allow to replace the entry return Err(GcliError::Input(format!( "Vault entry already exists for address {}", &address_to_import @@ -729,136 +794,24 @@ where let secret_format = vault_data.secret_format; - let (root_secret_suri, derivation_path_opt, root_address, derivation_address) = - compute_root_and_derivation_data(&secret_format, vault_data.secret_suri.clone())?; - - // Making sure the computed address is the same as the address to import - if let Some(derivation_address) = &derivation_address { - if *derivation_address != address_to_import { - return Err(GcliError::Input(format!( - "Derivation address {} does not match the expected address {}", - derivation_address, address_to_import - ))); - } - } else if root_address != address_to_import { - return Err(GcliError::Input(format!( - "Derivation address {} does not match the expected address {}", - root_address, address_to_import - ))); - } - - let encrypted_suri = - encrypt(root_secret_suri.as_bytes(), password.to_string()).map_err(|e| anyhow!(e))?; - - let crypto_type = map_secret_format_to_crypto_type(secret_format); - - let _root_account = - vault_account::create_vault_account(db, &root_address, crypto_type, encrypted_suri).await?; - - let derivation = if let Some(derivation_path) = derivation_path_opt { - let derivation_address = derivation_address.unwrap(); - - // Extra check of derivation path to make sure it's not linking to the same SS58 Address as root - if root_address == derivation_address { - println!("Derivation path provided:'{derivation_path}' linked to the same SS58 Address than the base suri without derivation"); - - let root_derivation = - vault_derivation::create_root_vault_derivation(db, &root_address, name).await?; - - println!("For that reason only the base suri was imported"); - println!("Created: {}", root_derivation); + let encrypted_suri = encrypt( + vault_data.secret_suri.clone().as_bytes(), + password.to_string(), + ) + .map_err(|e| anyhow!(e))?; - root_derivation - } else { - let _root_derivation = - vault_derivation::create_root_vault_derivation(db, &root_address, None).await?; - - // Compute derivation ! - let derivation = vault_derivation::ActiveModel { - address: Set(derivation_address.clone()), - name: Set(name.cloned()), - path: Set(Some(derivation_path)), - root_address: Set(root_address.clone()), - }; - - let derivation = derivation.insert(db).await?; - - println!("Created: {}", derivation); - derivation - } - } else { - let derivation = - vault_derivation::create_root_vault_derivation(db, &root_address, name).await?; - println!("Created: {}", derivation); - derivation - }; + let crypto_scheme = map_secret_format_to_crypto_scheme(secret_format); - Ok(derivation) -} + let base_account = vault_account::create_base_account( + db, + &address_to_import, + name, + crypto_scheme, + encrypted_suri, + ) + .await?; -fn compute_root_and_derivation_data( - secret_format: &SecretFormat, - secret_suri: String, -) -> Result<(String, Option<String>, String, Option<String>), GcliError> { - let (root_secret_suri, derivation_path_opt) = - parse_prefix_and_derivation_path_from_string(secret_suri)?; - - let (root_address, derivation_address_opt) = match &secret_format { - SecretFormat::Cesium => match &derivation_path_opt { - None => { - let root_suri = &root_secret_suri; - let root_pair = pair_from_ed25519_str(root_suri)?; - let root_address: AccountId = root_pair.public().into(); - - (root_address.to_string(), None) - } - Some(derivation_path) => { - let root_suri = &root_secret_suri; - let root_pair = pair_from_ed25519_str(root_suri)?; - let root_address: AccountId = root_pair.public().into(); - - let derivation_suri = root_suri.clone() + derivation_path; - let derivation_pair = pair_from_ed25519_str(&derivation_suri)?; - let derivation_address: AccountId = derivation_pair.public().into(); - - ( - root_address.to_string(), - Some(derivation_address.to_string()), - ) - } - }, - SecretFormat::Substrate | SecretFormat::Seed | SecretFormat::Predefined => { - match &derivation_path_opt { - None => { - let root_suri = &root_secret_suri; - let root_pair = pair_from_sr25519_str(root_suri)?; - let root_address: AccountId = root_pair.public().into(); - - (root_address.to_string(), None) - } - Some(derivation_path) => { - let root_suri = &root_secret_suri; - let root_pair = pair_from_sr25519_str(root_suri)?; - let root_address: AccountId = root_pair.public().into(); - - let derivation_suri = root_suri.clone() + derivation_path; - let derivation_pair = pair_from_sr25519_str(&derivation_suri)?; - let derivation_address: AccountId = derivation_pair.public().into(); - - ( - root_address.to_string(), - Some(derivation_address.to_string()), - ) - } - } - } - }; - Ok(( - root_secret_suri, - derivation_path_opt, - root_address, - derivation_address_opt, - )) + Ok(base_account) } fn get_vault_key_path(data: &Data, vault_filename: &str) -> PathBuf { @@ -880,62 +833,50 @@ pub async fn try_fetch_key_pair( data: &Data, address: AccountId, ) -> Result<Option<KeyPair>, GcliError> { - if let Some(derivation) = vault_derivation::Entity::find_by_id(address.to_string()) - .one(data.connect_db()) + if let Some(account_tree_node_hierarchy) = + vault_account::fetch_base_account_tree_node_hierarchy( + data.connect_db(), + &address.to_string(), + ) .await? { - if let Some(vault_account) = - vault_account::Entity::find_by_id(derivation.root_address.clone()) - .one(data.connect_db()) - .await? - { - let root_secret_suri = retrieve_suri_from_vault_account(&vault_account)?; - - let secret_suri = if let Some(derivation_path) = derivation.path { - format!("{root_secret_suri}{derivation_path}") - } else { - root_secret_suri - }; + let account_tree_node = vault_account::get_account_tree_node_for_address( + &account_tree_node_hierarchy, + &address.to_string(), + ); - let key_pair = compute_keypair(vault_account.crypto_type, &secret_suri)?; + let password = inputs::prompt_password()?; + let secret_suri = + vault_account::compute_suri_account_tree_node(&account_tree_node, password)?; - //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 - ))); - } + let base_account_tree_node = vault_account::get_base_account_tree_node(&account_tree_node); - Ok(Some(key_pair)) - } else { - Ok(None) + 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 retrieve_suri_from_vault_account( - vault_account: &vault_account::Model, -) -> Result<String, GcliError> { - let password = inputs::prompt_password()?; - - let cypher = &vault_account.encrypted_suri; - let secret_vec = - decrypt(cypher, password.clone()).map_err(|e| GcliError::Input(e.to_string()))?; - let secret_suri = String::from_utf8(secret_vec).map_err(|e| anyhow!(e))?; - - Ok(secret_suri) -} - -pub fn compute_keypair(crypto_type: CryptoType, secret_suri: &str) -> Result<KeyPair, GcliError> { - let key_pair = match crypto_type { - CryptoType::Bip39Mnemonic | CryptoType::EntropyKdfSeed => { - pair_from_sr25519_str(secret_suri)?.into() - } - CryptoType::G1v1Seed => pair_from_ed25519_str(secret_suri)?.into(), +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) } @@ -956,7 +897,7 @@ pub fn try_fetch_vault_data_from_file( ) -> Result<Option<VaultDataFromFile>, GcliError> { if let Some(path) = find_substrate_vault_key_file(data, address)? { println!("Enter password to unlock account {address}"); - let password = rpassword::prompt_password("Password: ")?; + 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)?; @@ -996,50 +937,178 @@ mod tests { #[rstest] #[case( String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0"), - 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" + )), Some(String::from("//0")) )] #[case( String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0"), - String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"), + Some(String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")), Some(String::from("//0")) )] #[case( String::from( "bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice" ), - 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" + )), 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"), - 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"), - String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"), + Some(String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")), None )] #[case( String::from("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"), - String::from("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"), + Some(String::from("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")), None )] #[case( String::from("someVaultName//Alice"), - String::from("someVaultName"), + Some(String::from("someVaultName")), Some(String::from("//Alice")) )] - #[case(String::from("someVaultName"), String::from("someVaultName"), None)] - fn test_parse_prefix_and_derivation_path_from_string( + #[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: String, + #[case] expected_prefix: Option<String>, #[case] expected_derivation_path: Option<String>, ) { let (root_secret, derivation_path) = - parse_prefix_and_derivation_path_from_string(raw_string).unwrap(); + 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), + } + } + + mod vault_accounts_table_tests { + use crate::commands::vault::compute_vault_accounts_table; + use crate::entities::vault_account::tests::account_tree_node_tests::mother_account_tree_node; + use indoc::indoc; + + #[test] + fn test_compute_vault_accounts_table_empty() { + let table = compute_vault_accounts_table(&[]).unwrap(); + + let expected_table = indoc! {r#" + ┌─────────────────────────────────────────────┠+ │ SS58 Address Format Account/Path Name │ + ╞â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•¡ + └─────────────────────────────────────────────┘"# + }; + + assert_eq!(table.to_string(), expected_table); + } + + #[test] + fn test_compute_vault_accounts_table() { + let account_tree_node = mother_account_tree_node(); + + let table = compute_vault_accounts_table(&[account_tree_node]).unwrap(); + + let expected_table = indoc! {r#" + ┌──────────────────────────────────────────────────────────────────────────────────────────────────┠+ │ SS58 Address Format Account/Path Name │ + ╞â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•¡ + │ 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV substrate <Base> Mother │ + │ ├─5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH //0 Child 1 │ + │ │ ├─5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d //0 Grandchild 1 │ + │ ├─5GBNeWRhZc2jXu7D55rBimKYDk8PGk8itRYFTPfC8RJLKG5o //1 <Mother//1> │ + │ │ ├─5CvdJuB9HLXSi5FS9LW57cyHF13iCv5HDimo2C45KxnxriCT //1 <Mother//1//1> │ + └──────────────────────────────────────────────────────────────────────────────────────────────────┘"# + }; + + assert_eq!(table.to_string(), expected_table); + } + + #[test] + fn test_compute_vault_accounts_table_partial() { + let mother = mother_account_tree_node(); + let child1 = mother.borrow().children[0].clone(); + + let table = compute_vault_accounts_table(&[child1]).unwrap(); + + let expected_table = indoc! {r#" + ┌─────────────────────────────────────────────────────────────────────────────────────────────┠+ │ SS58 Address Format Account/Path Name │ + ╞â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•¡ + │ ├─5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH //0 Child 1 │ + │ │ ├─5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d //0 Grandchild 1 │ + └─────────────────────────────────────────────────────────────────────────────────────────────┘"# + }; + + assert_eq!(table.to_string(), expected_table); + } + } } diff --git a/src/conf.rs b/src/conf.rs index 207e7c68b7ac8998e3c3240224eae52b01312b97..509e0c5bb9dfe36fb572a65eb6f0fff823b28546 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -1,4 +1,5 @@ -use crate::entities::vault_derivation; +use crate::entities::vault_account; +use crate::entities::vault_account::DbAccountId; use crate::*; use serde::{Deserialize, Serialize}; @@ -85,13 +86,13 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE Subcommand::Show => { println!("{}", data.cfg); if let Some(ref account_id) = data.cfg.address { - if let Some(derivation) = vault_derivation::fetch_vault_derivation( + if let Some(account) = vault_account::find_by_id( data.connect_db(), - account_id.to_string().as_str(), + &DbAccountId::from(account_id.clone()), ) .await? { - println!("(Vault: {})", derivation); + println!("(Vault: {})", account); } } } diff --git a/src/database.rs b/src/database.rs index fad4f71741268c3e75d24406fc388d37f127c47f..01ef735bf4e05a0e9b2f02e3f3e923d76860c76c 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,4 +1,4 @@ -use crate::entities::{vault_account, vault_derivation}; +use crate::entities::vault_account; use crate::utils::GcliError; use sea_orm::sea_query::IndexCreateStatement; use sea_orm::{ConnectionTrait, Database, DatabaseConnection, Schema}; @@ -32,7 +32,6 @@ pub async fn initialize_db(db_url: &str) -> Result<DatabaseConnection, GcliError let schema = Schema::new(db.get_database_backend()); create_table_if_not_exists(&db, &schema, vault_account::Entity).await?; - create_table_if_not_exists(&db, &schema, vault_derivation::Entity).await?; Ok(db) } diff --git a/src/entities.rs b/src/entities.rs index 2de801c7093606cdc3cdf62a8d85fe2b60955309..bd92a81f604c6e8f5756ee435b59dc62af75b50e 100644 --- a/src/entities.rs +++ b/src/entities.rs @@ -1,2 +1 @@ pub mod vault_account; -pub mod vault_derivation; diff --git a/src/entities/vault_account.rs b/src/entities/vault_account.rs index 76d34635165606c7c3d4522aa974d4ffe6b1af46..71905ec368d410630e4c5bedb9560e072b1098b9 100644 --- a/src/entities/vault_account.rs +++ b/src/entities/vault_account.rs @@ -1,106 +1,1225 @@ -use crate::inputs; +use crate::commands::vault; use crate::utils::GcliError; +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, DeriveEntityModel, DerivePrimaryKey, EnumIter, Related, RelationDef, - RelationTrait, + ActiveModelBehavior, ColumnTrait, DbErr, DeriveEntityModel, DerivePrimaryKey, EnumIter, Linked, + ModelTrait, QueryOrder, RelationDef, RelationTrait, TryFromU64, }; use sea_orm::{ActiveModelTrait, ConnectionTrait, PrimaryKeyTrait}; use sea_orm::{DeriveActiveEnum, EntityTrait}; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::collections::HashMap; use std::fmt::Display; +use std::future::Future; +use std::pin::Pin; +use std::rc::Rc; +use std::str::FromStr; +use subxt::utils::AccountId32; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "vault_account")] pub struct Model { - /// SS58 Address of (root) account + /// SS58 Address of account #[sea_orm(primary_key, auto_increment = false)] - pub address: String, - pub crypto_type: CryptoType, - pub encrypted_suri: Vec<u8>, + pub address: DbAccountId, + /// Optional name for the account + #[sea_orm(unique)] + pub name: Option<String>, + /// derivation path - None if for a "base" account that has `crypto_scheme` and `encrypted_suri` set and no `parent` + pub path: Option<String>, + /// Crypto scheme used for the account - Only set for "base" accounts + pub crypto_scheme: Option<DbCryptoScheme>, + /// Encrypted SURI for the account - Only set for "base" accounts + pub encrypted_suri: Option<Vec<u8>>, + /// ForeignKey to parent vault_account SS58 Address - None if for a "base" account + pub parent: Option<DbAccountId>, +} + +impl Model { + pub fn is_base_account(&self) -> bool { + self.parent.is_none() + } + + #[allow(unused)] + pub fn is_derivation_account(&self) -> bool { + self.parent.is_some() + } + + pub fn account_type(&self) -> String { + if self.is_base_account() { + "Base".to_string() + } else { + "Derivation".to_string() + } + } } impl Display for Model { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "[address:\"{}\"]", self.address) + if self.is_base_account() { + write!( + f, + "{}[address:{}, name:{:?}, crypto_scheme:{:?}]", + self.account_type(), + self.address, + self.name, + self.crypto_scheme + ) + } else { + fn get_parent_name(parent: &Option<DbAccountId>) -> String { + if let Some(parent) = parent { + format!("Some(\"{parent}\")") + } else { + "None".to_string() + } + } + + write!( + f, + "{}[address:{}, name:{:?}, path:{:?}, parent:{}]", + self.account_type(), + self.address, + self.name, + self.path, + get_parent_name(&self.parent) + ) + } } } -#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +/// Necessary to create a wrapper over AccountId32 to implement sea-orm traits +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)] +pub struct DbAccountId(pub AccountId32); + +impl FromStr for DbAccountId { + type Err = GcliError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + AccountId32::from_str(s) + .map(DbAccountId) + .map_err(|_| GcliError::Input("Invalid AccountId32 format".to_string())) + } +} + +impl Display for DbAccountId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From<AccountId32> for DbAccountId { + fn from(account_id32: AccountId32) -> Self { + DbAccountId(account_id32) + } +} + +impl From<DbAccountId> for AccountId32 { + fn from(db_account_id: DbAccountId) -> Self { + db_account_id.0 + } +} + +impl From<String> for DbAccountId { + fn from(s: String) -> Self { + DbAccountId(AccountId32::from_str(&s).expect("Invalid AccountId32 format")) + } +} + +/// sea-orm forces us to implement this one; but since we map from/to string, we can't convert from a u64 +impl TryFromU64 for DbAccountId { + fn try_from_u64(_v: u64) -> Result<Self, DbErr> { + Err(DbErr::Custom( + "AccountIdWrapper cannot be created from U64".to_owned(), + )) + } +} + +/// Didn't want to pollute the keys::CryptoScheme enum with sea-orm specific derivations +/// +/// created a separate enum for the database with conversions between the two +#[derive(Debug, Copy, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[sea_orm( rs_type = "String", db_type = "String(StringLen::None)", rename_all = "PascalCase" )] -pub enum CryptoType { - /// The BIP39 mnemonic phrase (?12 words) (SR25519) - Bip39Mnemonic, - /// The 32B hexadecimal seed with "0x" prefix (64+2 characters when unencrypted) (SR25519) - EntropyKdfSeed, - /// The 32B hexadecimal seed with "0x" prefix for cesium v1 (64+2 characters when unencrypted) (ED25519) - G1v1Seed, +pub enum DbCryptoScheme { + Ed25519, + Sr25519, +} + +impl From<crate::keys::CryptoScheme> for DbCryptoScheme { + fn from(scheme: crate::keys::CryptoScheme) -> Self { + match scheme { + crate::keys::CryptoScheme::Ed25519 => DbCryptoScheme::Ed25519, + crate::keys::CryptoScheme::Sr25519 => DbCryptoScheme::Sr25519, + } + } +} + +impl From<DbCryptoScheme> for crate::keys::CryptoScheme { + fn from(scheme: DbCryptoScheme) -> Self { + match scheme { + DbCryptoScheme::Ed25519 => crate::keys::CryptoScheme::Ed25519, + DbCryptoScheme::Sr25519 => crate::keys::CryptoScheme::Sr25519, + } + } } #[derive(Copy, Clone, Debug, EnumIter)] pub enum Relation { - Derivation, + ParentAccount, } impl RelationTrait for Relation { fn def(&self) -> RelationDef { match self { - Self::Derivation => Entity::has_many(super::vault_derivation::Entity).into(), + Self::ParentAccount => Entity::belongs_to(Entity) + .from(Column::Parent) + .to(Column::Address) + .into(), + } + } +} + +pub struct ParentAccountLink; + +impl Linked for ParentAccountLink { + type FromEntity = Entity; + + type ToEntity = Entity; + + fn link(&self) -> Vec<RelationDef> { + vec![Relation::ParentAccount.def()] + } +} + +#[async_trait] +impl ActiveModelBehavior for ActiveModel { + /// This method is called before saving or updating the model to the database. + /// It ensures that the model is valid according to the following constraints: + /// + /// - A "base" vault account must have path:None, parent:None, crypto_scheme:Some(_), encrypted_suri:Some(_) + /// - A "derivation" vault account must have path:Some(_), parent:Some(_), crypto_scheme:None, encrypted_suri:None + async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr> + where + C: ConnectionTrait, + { + if insert { + // If one of the elements of a "base" account is seen, all must be correctly filled + if (self.path.is_not_set() || self.path.try_as_ref().unwrap().is_none()) + || (self.parent.is_not_set() || self.parent.try_as_ref().unwrap().is_none()) + || (self.crypto_scheme.is_set() + && self.crypto_scheme.try_as_ref().unwrap().is_some()) + || (self.encrypted_suri.is_set() + && self.encrypted_suri.try_as_ref().unwrap().is_some()) + { + if !((self.path.is_not_set() || self.path.try_as_ref().unwrap().is_none()) + && (self.parent.is_not_set() || self.parent.try_as_ref().unwrap().is_none()) + && (self.crypto_scheme.is_set() + && self.crypto_scheme.try_as_ref().unwrap().is_some()) + && (self.encrypted_suri.is_set() + && 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(), + )); + } + } 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()) + && (self.crypto_scheme.is_not_set() + || self.crypto_scheme.try_as_ref().unwrap().is_none()) + && (self.encrypted_suri.is_not_set() + || 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(), + )); + } + } 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()) + { + return Err(DbErr::Custom( + "Only the name can be updated for a vault account".into(), + )); + } + } + + Ok(self) + } +} + +pub async fn find_by_id<C>(db: &C, address: &DbAccountId) -> Result<Option<Model>, GcliError> +where + C: ConnectionTrait, +{ + Entity::find_by_id(address.clone()) + .one(db) + .await + .map_err(GcliError::from) +} + +pub async fn find_by_name<C>(db: &C, name: &str) -> Result<Option<Model>, GcliError> +where + C: ConnectionTrait, +{ + Entity::find() + .filter(Column::Name.eq(Some(name.to_string()))) + .one(db) + .await + .map_err(GcliError::from) +} + +pub async fn find_base_accounts<C>(db: &C) -> Result<Vec<Model>, GcliError> +where + C: ConnectionTrait, +{ + Entity::find() + .filter(Column::Path.is_null()) + .order_by_asc(Column::Address) + .all(db) + .await + .map_err(GcliError::from) +} + +/// Represents a node in the hierarchy of accounts +pub struct AccountTreeNode { + pub account: Model, + pub children: Vec<Rc<RefCell<AccountTreeNode>>>, + pub parent: Option<Rc<RefCell<AccountTreeNode>>>, +} + +/// Counts the depth of an `AccountTreeNode` in the hierarchy. +pub fn count_depth_account_tree_node(account_tree_node: &Rc<RefCell<AccountTreeNode>>) -> usize { + let mut depth = 0; + 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) + } { + depth += 1; + current_node = parent_node; + } + + depth +} + +/// Counts number of accounts in an `AccountTreeNode` hierarchy, starting from account_tree_node and only visiting children. +pub fn count_accounts_in_account_tree_node_and_children( + node: &Rc<RefCell<AccountTreeNode>>, +) -> usize { + let borrowed_node = node.borrow(); + let mut count = 1; // Count the current node + + for child in &borrowed_node.children { + count += count_accounts_in_account_tree_node_and_children(child); + } + + count +} + +/// Gets the base account tree node of the `AccountTreeNode` hierarchy. +pub fn get_base_account_tree_node( + account_tree_node: &Rc<RefCell<AccountTreeNode>>, +) -> Rc<RefCell<AccountTreeNode>> { + //Move up to the base node + let mut base_node = Rc::clone(account_tree_node); + while let Some(parent_node) = { + let borrowed_node = base_node.borrow(); + borrowed_node.parent.as_ref().map(Rc::clone) + } { + base_node = parent_node; + } + + Rc::clone(&base_node) +} + +/// Gets the account tree node for given address from the `AccountTreeNode` hierarchy. +pub fn get_account_tree_node_for_address( + account_tree_node: &Rc<RefCell<AccountTreeNode>>, + address: &str, +) -> Rc<RefCell<AccountTreeNode>> { + fn find_address_recursive( + node: &Rc<RefCell<AccountTreeNode>>, + address: &str, + ) -> Option<Rc<RefCell<AccountTreeNode>>> { + let borrowed_node = node.borrow(); + + if borrowed_node.account.address.to_string() == address { + return Some(Rc::clone(node)); + } + + for child in &borrowed_node.children { + if let Some(found) = find_address_recursive(child, address) { + return Some(found); + } + } + + None + } + + //Move up to the base node + let base_account_tree_node = get_base_account_tree_node(account_tree_node); + + let account_tree_node_for_address = find_address_recursive(&base_account_tree_node, address) + .unwrap_or_else(|| { + panic!( + "Could not find account with address:{} in the hierarchy", + address + ) + }); + + Rc::clone(&account_tree_node_for_address) +} + +/// 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 +pub fn extract_accounts_depth_first_from_account_tree_node( + account_tree_node: &Rc<RefCell<AccountTreeNode>>, +) -> Result<Vec<Model>, GcliError> { + fn retrieve_recursive_depth_first( + node: &Rc<RefCell<AccountTreeNode>>, + accounts: &mut Vec<Model>, + ) -> Result<(), GcliError> { + let borrowed_node = node.borrow(); + + for child in &borrowed_node.children { + retrieve_recursive_depth_first(child, accounts)?; + } + + accounts.push(borrowed_node.account.clone()); + + Ok(()) + } + + let mut accounts = Vec::new(); + retrieve_recursive_depth_first(account_tree_node, &mut accounts)?; + + Ok(accounts) +} + +/// Computes the name to reference the `AccountTreeNode` in the hierarchy if we can find/compute one. +/// +/// Returns `None` otherwise. +pub fn compute_name_account_tree_node( + account_tree_node: &Rc<RefCell<AccountTreeNode>>, +) -> Option<String> { + let mut name = String::new(); + let mut current_node = Rc::clone(account_tree_node); + + while let Some(parent_node) = { + let borrowed_node = current_node.borrow(); + if let Some(account_name) = &borrowed_node.account.name { + name.insert_str(0, account_name); + return Some(name); + } else if let Some(account_path) = &borrowed_node.account.path { + name.insert_str(0, account_path); + } else { + return None; + } + borrowed_node.parent.as_ref().map(Rc::clone) + } { + current_node = parent_node; + } + + Some(name) +} + +/// Computes a map of names to reference of `AccountTreeNodes` in the hierarchy of its children. +pub fn compute_name_map_for_account_tree_node( + account_tree_node: &Rc<RefCell<AccountTreeNode>>, +) -> Result<HashMap<String, Rc<RefCell<AccountTreeNode>>>, GcliError> { + let mut names_to_ref_map = HashMap::<String, Rc<RefCell<AccountTreeNode>>>::new(); + + fn compute_recursive_name_map( + node: &Rc<RefCell<AccountTreeNode>>, + current_name: Option<String>, + names_to_ref_map: &mut HashMap<String, Rc<RefCell<AccountTreeNode>>>, + ) -> Result<(), GcliError> { + let borrowed_node = node.borrow(); + + let current_name = match &borrowed_node.account.name { + Some(name) => Some(name.clone()), + None => match &borrowed_node.account.path { + Some(path) => current_name + .as_ref() + .map(|name| format!("{}{}", name, path)), + None => None, + }, + }; + + if let Some(name) = ¤t_name { + names_to_ref_map.insert(name.clone(), Rc::clone(node)); } + + for child in &borrowed_node.children { + compute_recursive_name_map(child, current_name.clone(), names_to_ref_map)?; + } + + Ok(()) + } + + compute_recursive_name_map(account_tree_node, None, &mut names_to_ref_map)?; + + Ok(names_to_ref_map) +} + +/// Computes the SURI of the `AccountTreeNode` in the hierarchy; using the password to decrypt the encrypted SURI of Base account. +pub fn compute_suri_account_tree_node( + account_tree_node: &Rc<RefCell<AccountTreeNode>>, + password: String, +) -> Result<String, GcliError> { + let mut suri = String::new(); + let mut current_node = Rc::clone(account_tree_node); + + while let Some(parent_node) = { + let borrowed_node = current_node.borrow(); + + if let Some(account_path) = &borrowed_node.account.path { + suri.insert_str(0, account_path); + } else if let Some(encrypted_suri) = &borrowed_node.account.encrypted_suri { + let decrypted_suri = vault::decrypt(encrypted_suri, password.clone()).unwrap(); + let secret_suri = String::from_utf8(decrypted_suri).map_err(|e| anyhow!(e))?; + suri.insert_str(0, &secret_suri); + } else { + return Err(GcliError::Input("No encrypted SURI found".to_string())); + } + + borrowed_node.parent.as_ref().map(Rc::clone) + } { + current_node = parent_node; + } + + Ok(suri) +} + +/// Fetches all the `base` account tree nodes with their hierarchies +pub async fn fetch_all_base_account_tree_node_hierarchies<C>( + db: &C, +) -> Result<Vec<Rc<RefCell<AccountTreeNode>>>, GcliError> +where + C: ConnectionTrait, +{ + let base_accounts = find_base_accounts(db).await?; + + let mut account_tree_nodes = Vec::new(); + + for base_account in base_accounts { + let base_account_tree_node = + fetch_children_account_tree_nodes_boxed(db, base_account, None).await?; + account_tree_nodes.push(base_account_tree_node); + } + + Ok(account_tree_nodes) +} + +/// Only returns the `base` accounts without their children +/// +/// To be used in compute_vault_accounts_table to display only the base accounts +pub async fn fetch_only_base_account_tree_nodes<C>( + db: &C, +) -> Result<Vec<Rc<RefCell<AccountTreeNode>>>, GcliError> +where + C: ConnectionTrait, +{ + let base_accounts = find_base_accounts(db).await?; + + let mut account_tree_nodes = Vec::new(); + + for base_account in base_accounts { + let current_node = Rc::new(RefCell::new(AccountTreeNode { + account: base_account.clone(), + children: Vec::new(), + parent: None, + })); + + account_tree_nodes.push(current_node); } + + Ok(account_tree_nodes) } -// `Related` trait has to be implemented by hand -impl Related<super::vault_derivation::Entity> for Entity { - fn to() -> RelationDef { - Relation::Derivation.def() +/// Fetches the `base` account tree node hierarchy for the given address +/// +/// 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 + C: ConnectionTrait, +{ + fetch_base_account_tree_node_hierarchy(db, address) + .await? + .ok_or(GcliError::Input(format!( + "Could not compute tree of accounts for address:'{}'", + address + ))) +} + +/// 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 + C: ConnectionTrait, +{ + if let Some(base_parent_account) = find_base_parent_account(db, address).await? { + let base_account_tree_node = + fetch_children_account_tree_nodes_boxed(db, base_parent_account, None).await?; + Ok(Some(base_account_tree_node)) + } else { + Ok(None) + } +} + +/// Finds the `base` account in db for the given address +async fn find_base_parent_account<C>(db: &C, address: &str) -> Result<Option<Model>, GcliError> +where + C: ConnectionTrait, +{ + let account = find_by_id( + db, + &DbAccountId::from_str(address).expect("invalid address"), + ) + .await?; + + if account.is_none() { + return Ok(None); + } + + let mut base_parent_account = account.unwrap(); + + while let Some(parent_account) = base_parent_account + .find_linked(ParentAccountLink) + .one(db) + .await + .map_err(GcliError::from)? + { + base_parent_account = parent_account; } + + Ok(Some(base_parent_account)) } -impl ActiveModelBehavior for ActiveModel {} +async fn find_direct_children_accounts<C>( + db: &C, + current_account: &Model, +) -> Result<Vec<Model>, GcliError> +where + C: ConnectionTrait, +{ + Entity::find() + .filter(Column::Parent.eq(current_account.address.clone())) + .order_by_asc(Column::Address) + .all(db) + .await + .map_err(GcliError::from) +} + +/// 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>>; + +/// This one seems necessary in order to handle async + recursion issue +/// +/// Was suggested by AI and seems to work (might be improved) +fn fetch_children_account_tree_nodes_boxed<'c, C>( + db: &'c C, + current_account: Model, + parent_node: Option<Rc<RefCell<AccountTreeNode>>>, +) -> AccountTreeNodeResult<'c> +where + C: ConnectionTrait + 'c, +{ + Box::pin(fetch_children_account_tree_nodes( + db, + current_account, + parent_node, + )) +} -/// Creates a (root) vault account (if it doesn't exist) and returns it -pub async fn create_vault_account<C>( +/// Fetches the children account tree nodes for the given account and parent node +async fn fetch_children_account_tree_nodes<C>( + db: &C, + current_account: Model, + parent_node: Option<Rc<RefCell<AccountTreeNode>>>, +) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError> +where + C: ConnectionTrait, +{ + let children_accounts = find_direct_children_accounts(db, ¤t_account).await?; + + let current_node = Rc::new(RefCell::new(AccountTreeNode { + account: current_account.clone(), + children: Vec::new(), + parent: parent_node, + })); + + let mut children = Vec::new(); + + for child_account in children_accounts { + let child_node = fetch_children_account_tree_nodes_boxed( + db, + child_account, + Some(Rc::clone(¤t_node)), + ) + .await?; + children.push(child_node); + } + + current_node.borrow_mut().children = children; + + Ok(current_node) +} + +/// Creates a `base` vault account (if it doesn't exist) and returns it +/// +/// Typically used for `vault import|migrate` commands +pub async fn create_base_account<C>( db: &C, address: &str, - crypto_type: CryptoType, + name: Option<&String>, + crypto_scheme: crate::keys::CryptoScheme, encrypted_suri: Vec<u8>, ) -> Result<Model, GcliError> where C: ConnectionTrait, { - let vault_account = Entity::find_by_id(address.to_owned()).one(db).await?; + let account_id = DbAccountId::from_str(address)?; + let vault_account = Entity::find_by_id(account_id.clone()).one(db).await?; Ok(match vault_account { Some(vault_account) => { - println!("Already existing vault account {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(encrypted_suri); - let vault_account = vault_account.update(db).await?; - println!("Updated vault account {vault_account}"); - - vault_account - } else { - vault_account - } + //TODO Later possibly allow to replace the entry + return Err(GcliError::Input(format!( + "Already existing vault account {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 { + address: Set(account_id), + name: Set(name.cloned()), + path: Set(None), + crypto_scheme: Set(Some(crypto_scheme.into())), + 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 + } + }) +} + +/// Creates a `derivation` vault account (if it doesn't exist) and returns it +/// +/// Typically used for `vault derive` command +pub async fn create_derivation_account<C>( + db: &C, + address: &str, + name: Option<&String>, + derivation_path: &str, + parent_address: &str, +) -> Result<Model, GcliError> +where + C: ConnectionTrait, +{ + let account_id = DbAccountId::from_str(address)?; + let 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 + return Err(GcliError::Input(format!( + "Already existing vault account {vault_account}" + ))); } None => { let vault_account = ActiveModel { - address: Set(address.to_owned()), - crypto_type: Set(crypto_type), - encrypted_suri: Set(encrypted_suri), + address: Set(address.to_string().into()), + name: Set(name.cloned()), + path: Set(Some(derivation_path.to_string())), + crypto_scheme: Set(None), + encrypted_suri: Set(None), + parent: Set(Some(parent_address.to_string().into())), }; let vault_account = vault_account.insert(db).await?; - println!("Created vault account {}", vault_account); + println!("Created derivation account {}", vault_account); vault_account } }) } + +pub async fn update_account_name<C>( + db: &C, + account: Model, + new_name: Option<&String>, +) -> Result<Model, GcliError> +where + C: ConnectionTrait, +{ + let old_name = account.name.clone(); + let mut account: ActiveModel = account.into(); + account.name = Set(new_name.cloned()); + let account = account.update(db).await?; + println!( + "Renamed address:'{}' from {:?} to {:?}", + &account.address, old_name, new_name + ); + + Ok(account) +} + +// Unit tests +#[cfg(test)] +pub mod tests { + use super::*; + pub mod account_tree_node_tests { + use super::*; + use crate::commands::vault; + use crate::keys::SUBSTRATE_MNEMONIC; + + pub fn mother_account_tree_node() -> Rc<RefCell<AccountTreeNode>> { + let mother_address = + DbAccountId::from_str("5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV").unwrap(); + let child1_address = + DbAccountId::from_str("5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH").unwrap(); + let grandchild1_address = + DbAccountId::from_str("5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d").unwrap(); + let child2_address = + DbAccountId::from_str("5GBNeWRhZc2jXu7D55rBimKYDk8PGk8itRYFTPfC8RJLKG5o").unwrap(); + let grandchild2_address = + DbAccountId::from_str("5CvdJuB9HLXSi5FS9LW57cyHF13iCv5HDimo2C45KxnxriCT").unwrap(); + + let grandchild1 = Rc::new(RefCell::new(AccountTreeNode { + account: Model { + address: grandchild1_address.clone(), + name: Some("Grandchild 1".to_string()), + path: Some("//0".to_string()), + crypto_scheme: None, + encrypted_suri: None, + parent: Some(child1_address.clone()), + }, + children: vec![], + parent: None, + })); + + let grandchild2 = Rc::new(RefCell::new(AccountTreeNode { + account: Model { + address: grandchild2_address.clone(), + // name: Some("Grandchild 2".to_string()), + name: None, + path: Some("//1".to_string()), + crypto_scheme: None, + encrypted_suri: None, + parent: Some(child2_address.clone()), + }, + children: vec![], + parent: None, + })); + + let child1 = Rc::new(RefCell::new(AccountTreeNode { + account: Model { + address: child1_address.clone(), + name: Some("Child 1".to_string()), + path: Some("//0".to_string()), + crypto_scheme: None, + encrypted_suri: None, + parent: Some(mother_address.clone()), + }, + children: vec![grandchild1.clone()], + parent: None, + })); + + let child2 = Rc::new(RefCell::new(AccountTreeNode { + account: Model { + address: child2_address.clone(), + // name: Some("Child 2".to_string()), + name: None, + path: Some("//1".to_string()), + crypto_scheme: None, + encrypted_suri: None, + parent: Some(mother_address.clone()), + }, + children: vec![grandchild2.clone()], + parent: None, + })); + + let mother = Rc::new(RefCell::new(AccountTreeNode { + account: Model { + address: mother_address.clone(), + name: Some("Mother".to_string()), + path: None, + crypto_scheme: Some(DbCryptoScheme::Sr25519), + encrypted_suri: Some( + vault::encrypt(SUBSTRATE_MNEMONIC.as_bytes(), "".to_string()).unwrap(), + ), + parent: None, + }, + children: vec![child1.clone(), child2.clone()], + parent: None, + })); + + // Set parent references + grandchild1.borrow_mut().parent = Some(child1.clone()); + grandchild2.borrow_mut().parent = Some(child2.clone()); + child1.borrow_mut().parent = Some(mother.clone()); + child2.borrow_mut().parent = Some(mother.clone()); + + mother + } + + #[test] + fn test_count_depth_account_tree_node() { + let mother = mother_account_tree_node(); + assert_eq!(count_depth_account_tree_node(&mother), 0); + + let child1 = mother.borrow().children[0].clone(); + assert_eq!(count_depth_account_tree_node(&child1), 1); + + let grandchild1 = child1.borrow().children[0].clone(); + assert_eq!(count_depth_account_tree_node(&grandchild1), 2); + } + + #[test] + fn test_count_accounts_in_account_tree_node_and_children() { + let mother = mother_account_tree_node(); + assert_eq!(count_accounts_in_account_tree_node_and_children(&mother), 5); + + let child1 = mother.borrow().children[0].clone(); + assert_eq!(count_accounts_in_account_tree_node_and_children(&child1), 2); + + let grandchild1 = child1.borrow().children[0].clone(); + assert_eq!( + count_accounts_in_account_tree_node_and_children(&grandchild1), + 1 + ); + } + + #[test] + fn test_retrieve_accounts_depth_first_from_account_tree_node() { + let mother = mother_account_tree_node(); + let accounts = extract_accounts_depth_first_from_account_tree_node(&mother).unwrap(); + assert_eq!(accounts.len(), 5); + assert_eq!( + accounts[0].address.to_string(), + mother.borrow().children[0].borrow().children[0] + .borrow() + .account + .address + .to_string() + ); + assert_eq!( + accounts[1].address.to_string(), + mother.borrow().children[0] + .borrow() + .account + .address + .to_string() + ); + assert_eq!( + accounts[2].address.to_string(), + mother.borrow().children[1].borrow().children[0] + .borrow() + .account + .address + .to_string() + ); + assert_eq!( + accounts[3].address.to_string(), + mother.borrow().children[1] + .borrow() + .account + .address + .to_string() + ); + assert_eq!( + accounts[4].address.to_string(), + mother.borrow().account.address.to_string() + ); + + let child1 = mother.borrow().children[0].clone(); + let accounts = extract_accounts_depth_first_from_account_tree_node(&child1).unwrap(); + assert_eq!(accounts.len(), 2); + assert_eq!( + accounts[0].address.to_string(), + mother.borrow().children[0].borrow().children[0] + .borrow() + .account + .address + .to_string() + ); + assert_eq!( + accounts[1].address.to_string(), + mother.borrow().children[0] + .borrow() + .account + .address + .to_string() + ); + + let grandchild1 = child1.borrow().children[0].clone(); + let accounts = + extract_accounts_depth_first_from_account_tree_node(&grandchild1).unwrap(); + assert_eq!(accounts.len(), 1); + assert_eq!( + accounts[0].address.to_string(), + mother.borrow().children[0].borrow().children[0] + .borrow() + .account + .address + .to_string() + ); + } + + #[test] + fn test_compute_name_account_tree_node() { + let mother = mother_account_tree_node(); + assert_eq!( + compute_name_account_tree_node(&mother), + Some("Mother".to_string()) + ); + + let child1 = mother.borrow().children[0].clone(); + assert_eq!( + compute_name_account_tree_node(&child1), + Some("Child 1".to_string()) + ); + + let grandchild1 = child1.borrow().children[0].clone(); + assert_eq!( + compute_name_account_tree_node(&grandchild1), + Some("Grandchild 1".to_string()) + ); + + let child2 = mother.borrow().children[1].clone(); + assert_eq!( + compute_name_account_tree_node(&child2), + Some("Mother//1".to_string()) + ); + + let grandchild2 = child2.borrow().children[0].clone(); + assert_eq!( + compute_name_account_tree_node(&grandchild2), + Some("Mother//1//1".to_string()) + ); + } + + #[test] + fn test_compute_name_account_tree_node_mother_without_name() { + let mother = mother_account_tree_node(); + mother.borrow_mut().account.name = None; + assert_eq!(compute_name_account_tree_node(&mother), None); + + let child1 = mother.borrow().children[0].clone(); + assert_eq!( + compute_name_account_tree_node(&child1), + Some("Child 1".to_string()) + ); + + let grandchild1 = child1.borrow().children[0].clone(); + assert_eq!( + compute_name_account_tree_node(&grandchild1), + Some("Grandchild 1".to_string()) + ); + + let child2 = mother.borrow().children[1].clone(); + assert_eq!(compute_name_account_tree_node(&child2), None); + + let grandchild2 = child2.borrow().children[0].clone(); + assert_eq!(compute_name_account_tree_node(&grandchild2), None); + } + + #[test] + fn test_compute_name_account_tree_node_grandchild1_without_name() { + let mother = mother_account_tree_node(); + mother.borrow().children[0].borrow().children[0] + .borrow_mut() + .account + .name = None; + + assert_eq!( + compute_name_account_tree_node(&mother), + Some("Mother".to_string()) + ); + + let child1 = mother.borrow().children[0].clone(); + assert_eq!( + compute_name_account_tree_node(&child1), + Some("Child 1".to_string()) + ); + + let grandchild1 = child1.borrow().children[0].clone(); + assert_eq!( + compute_name_account_tree_node(&grandchild1), + Some("Child 1//0".to_string()) + ); + } + + #[test] + fn test_compute_name_map_for_account_tree_node() { + let mother = mother_account_tree_node(); + let name_map = compute_name_map_for_account_tree_node(&mother).unwrap(); + assert_eq!(name_map.len(), 5); + assert_eq!( + name_map + .get("Mother") + .unwrap() + .borrow() + .account + .address + .to_string(), + mother.borrow().account.address.to_string() + ); + assert_eq!( + name_map + .get("Child 1") + .unwrap() + .borrow() + .account + .address + .to_string(), + mother.borrow().children[0] + .borrow() + .account + .address + .to_string() + ); + assert_eq!( + name_map + .get("Grandchild 1") + .unwrap() + .borrow() + .account + .address + .to_string(), + mother.borrow().children[0].borrow().children[0] + .borrow() + .account + .address + .to_string() + ); + assert_eq!( + name_map + .get("Mother//1") + .unwrap() + .borrow() + .account + .address + .to_string(), + mother.borrow().children[1] + .borrow() + .account + .address + .to_string() + ); + assert_eq!( + name_map + .get("Mother//1//1") + .unwrap() + .borrow() + .account + .address + .to_string(), + mother.borrow().children[1].borrow().children[0] + .borrow() + .account + .address + .to_string() + ); + } + + #[test] + fn test_get_base_account_tree_node() { + let mother = mother_account_tree_node(); + let child1 = mother.borrow().children[0].clone(); + let grandchild1 = child1.borrow().children[0].clone(); + assert_eq!( + get_base_account_tree_node(&grandchild1) + .borrow() + .account + .address + .to_string(), + mother.borrow().account.address.to_string() + ); + } + + #[test] + fn test_get_account_tree_node_for_address() { + let mother = mother_account_tree_node(); + let grandchild1 = mother.borrow().children[0].borrow().children[0].clone(); + let grandchild1_address = &grandchild1.borrow().account.address.to_string(); + assert_eq!( + get_account_tree_node_for_address(&mother, grandchild1_address) + .borrow() + .account + .address + .to_string(), + grandchild1_address.to_string() + ); + } + + #[test] + fn test_compute_suri_account_tree_node() { + let mother = mother_account_tree_node(); + let password = "".to_string(); + assert_eq!( + compute_suri_account_tree_node(&mother, password.clone()).unwrap(), + SUBSTRATE_MNEMONIC + ); + + let child1 = mother.borrow().children[0].clone(); + assert_eq!( + compute_suri_account_tree_node(&child1, password.clone()).unwrap(), + SUBSTRATE_MNEMONIC.to_string() + "//0" + ); + + let grandchild1 = child1.borrow().children[0].clone(); + assert_eq!( + compute_suri_account_tree_node(&grandchild1, password.clone()).unwrap(), + SUBSTRATE_MNEMONIC.to_string() + "//0//0" + ); + + let child2 = mother.borrow().children[1].clone(); + assert_eq!( + compute_suri_account_tree_node(&child2, password.clone()).unwrap(), + SUBSTRATE_MNEMONIC.to_string() + "//1" + ); + + let grandchild2 = child2.borrow().children[0].clone(); + assert_eq!( + compute_suri_account_tree_node(&grandchild2, password.clone()).unwrap(), + SUBSTRATE_MNEMONIC.to_string() + "//1//1" + ); + } + } +} diff --git a/src/entities/vault_derivation.rs b/src/entities/vault_derivation.rs deleted file mode 100644 index 6851b151a13f3ae05b6ace9135bbedbe5de96697..0000000000000000000000000000000000000000 --- a/src/entities/vault_derivation.rs +++ /dev/null @@ -1,158 +0,0 @@ -use crate::utils::GcliError; -use sea_orm::sea_query::NullOrdering; -use sea_orm::ActiveValue::Set; -use sea_orm::PrimaryKeyTrait; -use sea_orm::QueryFilter; -use sea_orm::{ - ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, EnumIter, Related, RelationDef, - RelationTrait, -}; -use sea_orm::{ActiveModelTrait, ColumnTrait}; -use sea_orm::{ConnectionTrait, EntityTrait, Order, QueryOrder}; -use std::fmt::Display; - -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] -#[sea_orm(table_name = "vault_derivation")] -pub struct Model { - /// SS58 Address - #[sea_orm(primary_key, auto_increment = false)] - pub address: String, - /// Optional name for the derivation - #[sea_orm(unique)] - pub name: Option<String>, - /// derivation path - None if for the root account - pub path: Option<String>, - /// ForeignKey to root vault_account SS58 Address - pub root_address: String, -} - -impl Display for Model { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.path.is_none() { - write!( - f, - "Account[address:\"{}\", name:{:?}]", - self.address, self.name - ) - } else { - write!( - f, - "Derivation[address:\"{}\", name:{:?}, path:{:?}, account_address:\"{}\"]", - self.address, self.name, self.path, self.root_address - ) - } - } -} - -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation { - RootAccount, -} - -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - match self { - Self::RootAccount => Entity::belongs_to(super::vault_account::Entity) - .from(Column::RootAddress) - .to(super::vault_account::Column::Address) - .into(), - } - } -} - -// `Related` trait has to be implemented by hand -impl Related<super::vault_account::Entity> for Entity { - fn to() -> RelationDef { - Relation::RootAccount.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Creates a root derivation (if it doesn't exist) and returns it -pub async fn create_root_vault_derivation<C>( - db: &C, - root_address: &str, - root_name: Option<&String>, -) -> Result<Model, GcliError> -where - C: ConnectionTrait, -{ - create_vault_derivation(db, root_address, root_address, root_name, None).await -} - -/// Creates a derivation (if it doesn't exist) and returns it -pub async fn create_vault_derivation<C>( - db: &C, - address: &str, - root_address: &str, - name: Option<&String>, - path: Option<&String>, -) -> Result<Model, GcliError> -where - C: ConnectionTrait, -{ - let derivation = Entity::find_by_id(root_address.to_owned()).one(db).await?; - - Ok(match derivation { - Some(derivation) => derivation, - None => { - let derivation = ActiveModel { - address: Set(address.to_owned()), - name: Set(name.cloned()), - path: Set(path.cloned()), - root_address: Set(root_address.to_owned()), - }; - derivation.insert(db).await? - } - }) -} - -pub async fn fetch_vault_derivation<C>(db: &C, address: &str) -> Result<Option<Model>, GcliError> -where - C: ConnectionTrait, -{ - Entity::find_by_id(address.to_owned()) - .one(db) - .await - .map_err(GcliError::from) -} - -pub async fn fetch_all_linked_derivations_in_order<C>( - db: &C, - root_address: &str, -) -> Result<Vec<Model>, GcliError> -where - C: ConnectionTrait, -{ - Entity::find() - .filter(Column::RootAddress.eq(root_address.to_owned())) - .order_by_with_nulls(Column::Path, Order::Asc, NullOrdering::First) - .all(db) - .await - .map_err(GcliError::from) -} - -pub async fn list_all_derivations_in_order<C>(db: &C) -> Result<Vec<Model>, GcliError> -where - C: ConnectionTrait, -{ - Entity::find() - .order_by_asc(Column::RootAddress) - .order_by_with_nulls(Column::Path, Order::Asc, NullOrdering::First) - .all(db) - .await - .map_err(GcliError::from) -} - -pub async fn list_all_root_derivations_in_order<C>(db: &C) -> Result<Vec<Model>, GcliError> -where - C: ConnectionTrait, -{ - Entity::find() - .filter(Column::Path.is_null()) - .order_by_asc(Column::RootAddress) - .all(db) - .await - .map_err(GcliError::from) -} diff --git a/src/inputs.rs b/src/inputs.rs index 31c53925bbbb21a55577eee5bc4d134265a1dd92..103ba3b293194937810b44ce9ef05ed7b506e8f2 100644 --- a/src/inputs.rs +++ b/src/inputs.rs @@ -1,5 +1,6 @@ +use crate::commands::vault; use crate::utils::GcliError; -use inquire::validator::Validation; +use inquire::validator::{ErrorMessage, Validation}; pub fn prompt_password() -> Result<String, GcliError> { prompt_password_query("Password") @@ -75,7 +76,16 @@ pub fn prompt_vault_derivation_path() -> Result<String, GcliError> { "derivation path needs to start with one or more '/'".into(), )) } else { - Ok(Validation::Valid) + match vault::parse_prefix_and_derivation_path_from_suri(input.to_string()) { + Ok(_) => Ok(Validation::Valid), + Err(error) => { + if let GcliError::Input(message) = error { + Ok(Validation::Invalid(ErrorMessage::from(message))) + } else { + Ok(Validation::Invalid("Unknown error".into())) + } + } + } } }) .prompt() diff --git a/src/keys.rs b/src/keys.rs index 73c230d6fb9ccc6903ee74814c548bb015384e2a..63ffbbf0dec322423ef629c563dad77be4e296fd 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -49,10 +49,29 @@ impl From<SecretFormat> for OsStr { } } +/// The crypto scheme to use - partial copy from sc_cli::arg_enums::CryptoScheme +/// +/// Preferred making a copy since adding the dependency to sc-cli brings more than 300 dependencies +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CryptoScheme { + /// Use ed25519 - used for SecretFormat::Cesium + Ed25519, + /// Use sr25519. + Sr25519, +} + /// wrapper type for keys + signature //FIXME check if it's ok to keep large enum variant // Sr25519 second-largest variant contains at least 256 bytes // Ed25519 largest variant contains at least 480 bytes +//FIXME +// Replace by CryptoScheme from sc_cli?? or CryptoType from sp-core ? +// sc_cli::arg_enums::CryptoScheme (enum) +// CryptoScheme::Ed25519 +// CryptoScheme::Sr25519 +// CryptoScheme::Ecdsa +// sp_core::crypto::CryptoType (trait) +// /home/valpha/.cargo/git/checkouts/duniter-polkadot-sdk-0889f590ced4a269/bcc60f3/substrate/primitives/core/src/crypto.rs:1012 #[allow(clippy::large_enum_variant)] pub enum KeyPair { Sr25519(Sr25519Pair), @@ -178,9 +197,10 @@ pub fn prompt_secret_substrate() -> Sr25519Pair { pub fn prompt_secret_substrate_and_compute_keypair() -> (String, Sr25519Pair) { loop { - let mnemonic_suri = rpassword::prompt_password("Mnemonic: ").unwrap(); - match pair_from_sr25519_str(&mnemonic_suri) { - Ok(pair) => return (mnemonic_suri, pair), + println!("Substrate URI can be a mnemonic or a mini-secret ('0x' prefixed seed) together with optional derivation path"); + let substrate_suri = inputs::prompt_password_query("Substrate URI: ").unwrap(); + match pair_from_sr25519_str(&substrate_suri) { + Ok(pair) => return (substrate_suri, pair), Err(_) => println!("Invalid secret"), } } @@ -193,8 +213,8 @@ pub fn prompt_secret_cesium() -> Ed25519Pair { } pub fn prompt_secret_cesium_and_compute_keypair() -> (String, Ed25519Pair) { - let id = rpassword::prompt_password("Cesium id: ").unwrap(); - let pwd = rpassword::prompt_password("Cesium password: ").unwrap(); + let id = inputs::prompt_password_query("Cesium id: ").unwrap(); + let pwd = inputs::prompt_password_query("Cesium password: ").unwrap(); let seed = seed_from_cesium(&id, &pwd); let secret_suri = format!("0x{}", hex::encode(seed)); @@ -230,7 +250,7 @@ pub fn prompt_predefined() -> Sr25519Pair { } pub fn prompt_predefined_and_compute_keypair() -> (String, Sr25519Pair) { - let deriv = rpassword::prompt_password("Enter derivation path: ").unwrap(); + let deriv = inputs::prompt_password_query("Enter derivation path: ").unwrap(); ( predefined_mnemonic(&deriv), pair_from_predefined(&deriv).expect("invalid secret"), @@ -285,6 +305,123 @@ fn catch_known(address: &str) -> Option<&str> { #[cfg(test)] mod tests { use super::*; + + mod subkey_like_tests { + use super::keys::SUBSTRATE_MNEMONIC; + + use sp_core::crypto::Ss58Codec; + use sp_core::crypto::{Ss58AddressFormat, Ss58AddressFormatRegistry}; + use sp_core::ByteArray; + use sp_runtime::traits::IdentifyAccount; + use sp_runtime::MultiSigner; + + #[test] + fn test_print_from_suri() { + // sc_cli::CryptoSchemeFlag::augment_args() + + let suri_str = SUBSTRATE_MNEMONIC.to_string(); + print_from_suri::<sp_core::sr25519::Pair>(&suri_str, None, None); + print_from_suri::<sp_core::sr25519::Pair>( + &suri_str, + None, + Some(Ss58AddressFormatRegistry::G1Account.into()), + ); + + // bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice + let suri_str = SUBSTRATE_MNEMONIC.to_string() + "//Alice"; + print_from_suri::<sp_core::sr25519::Pair>(&suri_str, None, None); + print_from_suri::<sp_core::sr25519::Pair>( + &suri_str, + None, + Some(Ss58AddressFormatRegistry::G1Account.into()), + ); + } + + /// print account information from suri - simplification of code from + /// sc_cli::commands::utils::print_from_uri + pub fn print_from_suri<Pair>( + uri: &str, + password: Option<&str>, + network_override: Option<Ss58AddressFormat>, + ) where + Pair: sp_core::Pair, + Pair::Public: Into<MultiSigner>, + { + let network_id = String::from(unwrap_or_default_ss58_version(network_override)); + + if let Ok((pair, seed)) = Pair::from_string_with_seed(uri, password) { + let public_key = pair.public(); + let network_override = unwrap_or_default_ss58_version(network_override); + + println!( + "Secret Key URI `{}` is account:\n \ + Network ID: {}\n \ + Secret seed: {}\n \ + Public key (hex): {}\n \ + Account ID: {}\n \ + Public key (SS58): {}\n \ + SS58 Address: {}", + uri, + network_id, + if let Some(seed) = seed { + // sc_cli::utils::format_seed::<Pair>(seed) + format!("0x{}", hex::encode(seed.as_ref())) + } else { + "n/a".into() + }, + format_public_key::<Pair>(public_key.clone()), + format_account_id::<Pair>(public_key.clone()), + public_key.to_ss58check_with_version(network_override), + pair.public() + .into() + .into_account() + .to_ss58check_with_version(network_override), + ); + } else { + println!("Invalid phrase/URI given"); + } + } + + /// Public key type for Runtime + pub type PublicFor<P> = <P as sp_core::Pair>::Public; + + /// formats public key as hex + fn format_public_key<P: sp_core::Pair>(public_key: PublicFor<P>) -> String { + format!("0x{}", hex::encode((&public_key.as_ref()))) + } + + /// formats public key as accountId as hex + fn format_account_id<P: sp_core::Pair>(public_key: PublicFor<P>) -> String + where + PublicFor<P>: Into<MultiSigner>, + { + format!( + "0x{}", + hex::encode(&public_key.into().into_account().as_slice()) + ) + } + + pub fn unwrap_or_default_ss58_version( + network: Option<Ss58AddressFormat>, + ) -> Ss58AddressFormat { + network.unwrap_or_else(default_ss58_version) + } + + pub fn default_ss58_version() -> Ss58AddressFormat { + DEFAULT_VERSION + .load(core::sync::atomic::Ordering::Relaxed) + .into() + } + + static DEFAULT_VERSION: core::sync::atomic::AtomicU16 = core::sync::atomic::AtomicU16::new( + from_known_address_format(Ss58AddressFormatRegistry::SubstrateAccount), + ); + + pub const fn from_known_address_format(x: Ss58AddressFormatRegistry) -> u16 { + x as u16 + } + } + mod substrate { use super::*;