From 027f0655afb6602412c729cabdd95d3d5bad53e3 Mon Sep 17 00:00:00 2001 From: poka <poka@p2p.legal> Date: Wed, 12 Mar 2025 16:58:42 +0100 Subject: [PATCH 01/27] feat: Can choose between ed25519 ans sr25519 --- src/commands/vault.rs | 122 +++++++++++++++---------- src/commands/vault/display.rs | 105 +++++++++++++++++++-- src/keys.rs | 167 ++++++++++++++++++++++------------ 3 files changed, 278 insertions(+), 116 deletions(-) diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 8d6de10..a4e3594 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -41,14 +41,18 @@ pub enum Subcommand { /// Secret key format (substrate, seed, g1v1) #[clap(short = 'S', long, required = false, default_value = SecretFormat::Substrate)] secret_format: SecretFormat, + + /// Crypto scheme to use (sr25519, ed25519) + #[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)] + crypto_scheme: CryptoScheme, }, /// Add a derivation to an existing SS58 Address #[clap(long_about = "Add a derivation to an existing SS58 Address.\n\ \n\ - Only \"sr25519\" crypto scheme is supported for derivations.\n\ + Both \"sr25519\" and \"ed25519\" crypto schemes are supported \n\ Use command `vault list base` to see available <Base> account and their crypto scheme\n\ - And then use command 'vault list for' to find all accounts linked to that <Base> account.")] + And then use command 'vault list for' to find all accounts linked to that <Base> account")] #[clap(alias = "deriv")] #[clap(alias = "derivation")] Derive { @@ -87,18 +91,35 @@ pub enum Subcommand { Where, } -#[derive(Clone, Default, Debug, clap::Parser)] +#[derive(Clone, Debug, clap::Parser)] pub enum ListChoice { /// List all <Base> SS58 Addresses and their linked derivations in the vault - #[default] - All, + All { + /// Show G1v1 public keys + #[clap(long, default_value = "false")] + show_g1v1: bool, + }, /// List <Base> and Derivation SS58 Addresses linked to the selected one For { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, + + /// Show G1v1 public keys + #[clap(long, default_value = "false")] + show_g1v1: bool, }, /// List all <Base> SS58 Addresses in the vault - Base, + Base { + /// Show G1v1 public keys + #[clap(long, default_value = "false")] + show_g1v1: bool, + }, +} + +impl Default for ListChoice { + fn default() -> Self { + ListChoice::All { show_g1v1: false } + } } #[derive(Debug, clap::Args, Clone)] @@ -151,26 +172,27 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE // match subcommand match command { Subcommand::List(choice) => match choice { - ListChoice::All => { + ListChoice::All { show_g1v1 } => { let all_account_tree_node_hierarchies = vault_account::fetch_all_base_account_tree_node_hierarchies(db).await?; - let table = - display::compute_vault_accounts_table(&all_account_tree_node_hierarchies)?; + + let table = display::compute_vault_accounts_table_with_g1v1(&all_account_tree_node_hierarchies, show_g1v1)?; println!("available SS58 Addresses:"); println!("{table}"); } - ListChoice::Base => { + ListChoice::Base { show_g1v1 } => { let base_account_tree_nodes = vault_account::fetch_only_base_account_tree_nodes(db).await?; - let table = display::compute_vault_accounts_table(&base_account_tree_nodes)?; + let table = display::compute_vault_accounts_table_with_g1v1(&base_account_tree_nodes, show_g1v1)?; println!("available <Base> SS58 Addresses:"); println!("{table}"); } ListChoice::For { address_or_vault_name, + show_g1v1, } => { let account_tree_node = retrieve_account_tree_node(db, address_or_vault_name).await?; @@ -178,7 +200,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let base_account_tree_node = vault_account::get_base_account_tree_node(&account_tree_node); - let table = display::compute_vault_accounts_table(&[base_account_tree_node])?; + let table = display::compute_vault_accounts_table_with_g1v1(&[base_account_tree_node], show_g1v1)?; println!( "available SS58 Addresses linked to {}:", @@ -215,9 +237,9 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let mnemonic = bip39::Mnemonic::generate(12).unwrap(); println!("{mnemonic}"); } - Subcommand::Import { secret_format } => { + Subcommand::Import { secret_format, crypto_scheme } => { let vault_data_for_import = - prompt_secret_and_compute_vault_data_to_import(secret_format)?; + prompt_secret_and_compute_vault_data_to_import(secret_format, crypto_scheme)?; //Extra check for SecretFormat::G1v1 (old cesium) - showing the G1v1 cesium public key for confirmation if secret_format == SecretFormat::G1v1 { @@ -235,7 +257,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE println!(); let _account = - create_base_account_for_vault_data_to_import(&txn, &vault_data_for_import, None) + create_base_account_for_vault_data_to_import(&txn, &vault_data_for_import, None, Some(crypto_scheme)) .await?; txn.commit().await?; @@ -255,25 +277,6 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let base_account = &base_account_tree_node.borrow().account.clone(); - if base_account.crypto_scheme.is_none() { - return Err(GcliError::DatabaseError(DbErr::Custom(format!("Crypto scheme is not set for the base account:{base_account} - should never happen")))); - } - - if let Some(crypto_scheme) = base_account.crypto_scheme { - if CryptoScheme::from(crypto_scheme) == CryptoScheme::Ed25519 { - println!( - "Only \"{}\" crypto scheme is supported for derivations.", - Into::<&str>::into(CryptoScheme::Sr25519), - ); - println!(); - println!( - "Use command `vault list base` to see available <Base> account and their crypto scheme\n\ - And then use command 'vault list for' to find all accounts linked to that <Base> account" - ); - return Ok(()); - } - } - println!("Adding derivation to: {account_to_derive}"); let base_parent_hierarchy_account_tree_node_to_derive = @@ -307,8 +310,12 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let derivation_secret_suri = format!("{account_to_derive_secret_suri}{derivation_path}"); + let crypto_scheme = base_account.crypto_scheme + .map(CryptoScheme::from) + .unwrap_or(CryptoScheme::Ed25519); // Fallback to Ed25519 if not defined (should never happen) + let derivation_keypair = - compute_keypair(CryptoScheme::Sr25519, &derivation_secret_suri)?; + compute_keypair(crypto_scheme, &derivation_secret_suri)?; let derivation_address: String = derivation_keypair.address().to_string(); @@ -468,6 +475,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE &txn, &vault_data_to_import, Some(&vault_data_from_file.password), + None, ) .await; @@ -539,12 +547,24 @@ pub fn parse_prefix_and_derivation_path_from_suri( 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 => CryptoScheme::Sr25519, - SecretFormat::Substrate => CryptoScheme::Sr25519, - SecretFormat::Predefined => CryptoScheme::Sr25519, - SecretFormat::G1v1 => CryptoScheme::Ed25519, +fn map_secret_format_to_crypto_scheme(secret_format: SecretFormat, override_crypto_scheme: Option<CryptoScheme>) -> CryptoScheme { + // If a crypto_scheme is explicitly specified, use it except for G1v1 which must always use Ed25519 + if let Some(scheme) = override_crypto_scheme { + if secret_format == SecretFormat::G1v1 { + // G1v1 must always use Ed25519 + CryptoScheme::Ed25519 + } else { + scheme + } + } else { + // Default behavior if no crypto_scheme is specified + match secret_format { + // All formats use Ed25519 by default + SecretFormat::Seed => CryptoScheme::Ed25519, + SecretFormat::Substrate => CryptoScheme::Ed25519, + SecretFormat::Predefined => CryptoScheme::Ed25519, + SecretFormat::G1v1 => CryptoScheme::Ed25519, + } } } @@ -675,13 +695,14 @@ where fn create_vault_data_to_import<F, P>( secret_format: SecretFormat, + crypto_scheme: CryptoScheme, prompt_fn: F, ) -> Result<VaultDataToImport, GcliError> where - F: Fn() -> (String, P), + F: Fn(CryptoScheme) -> (String, P), P: Into<KeyPair>, { - let (secret, pair) = prompt_fn(); + let (secret, pair) = prompt_fn(crypto_scheme); let key_pair = pair.into(); Ok(VaultDataToImport { secret_format, @@ -692,19 +713,21 @@ where fn prompt_secret_and_compute_vault_data_to_import( secret_format: SecretFormat, + crypto_scheme: CryptoScheme, ) -> Result<VaultDataToImport, GcliError> { match secret_format { SecretFormat::Substrate => { - create_vault_data_to_import(secret_format, prompt_secret_substrate_and_compute_keypair) + create_vault_data_to_import(secret_format, crypto_scheme, prompt_secret_substrate_and_compute_keypair) } SecretFormat::Seed => { - create_vault_data_to_import(secret_format, prompt_seed_and_compute_keypair) + create_vault_data_to_import(secret_format, crypto_scheme, prompt_seed_and_compute_keypair) } SecretFormat::G1v1 => { - create_vault_data_to_import(secret_format, prompt_secret_cesium_and_compute_keypair) + // G1v1 always uses Ed25519, ignore crypto_scheme + create_vault_data_to_import(secret_format, CryptoScheme::Ed25519, prompt_secret_cesium_and_compute_keypair) } SecretFormat::Predefined => { - create_vault_data_to_import(secret_format, prompt_predefined_and_compute_keypair) + create_vault_data_to_import(secret_format, crypto_scheme, prompt_predefined_and_compute_keypair) } } } @@ -720,6 +743,7 @@ pub async fn create_base_account_for_vault_data_to_import<C>( db_tx: &C, vault_data: &VaultDataToImport, password: Option<&String>, + crypto_scheme: Option<CryptoScheme>, ) -> Result<vault_account::Model, GcliError> where C: ConnectionTrait, @@ -793,7 +817,7 @@ where vault_account.path = Set(None); vault_account.parent = Set(None); vault_account.crypto_scheme = Set(Some( - map_secret_format_to_crypto_scheme(vault_data.secret_format).into(), + map_secret_format_to_crypto_scheme(vault_data.secret_format, crypto_scheme).into(), )); vault_account.encrypted_suri = Set(Some(encrypted_suri)); vault_account.name = Set(name.clone()); @@ -816,7 +840,7 @@ where println!("(Optional) Enter a name for the vault entry"); let name = inputs::prompt_vault_name_and_check_availability(db_tx, None).await?; - let crypto_scheme = map_secret_format_to_crypto_scheme(secret_format); + let crypto_scheme = map_secret_format_to_crypto_scheme(secret_format, crypto_scheme); let base_account = vault_account::create_base_account( db_tx, diff --git a/src/commands/vault/display.rs b/src/commands/vault/display.rs index 021a84f..e9a9d2d 100644 --- a/src/commands/vault/display.rs +++ b/src/commands/vault/display.rs @@ -25,18 +25,26 @@ pub fn compute_vault_key_files_table(vault_key_addresses: &[String]) -> Result<T pub fn compute_vault_accounts_table( account_tree_nodes: &[Rc<RefCell<AccountTreeNode>>], +) -> Result<Table, GcliError> { + // Appel to the new function with show_g1v1 = true to maintain compatibility + compute_vault_accounts_table_with_g1v1(account_tree_nodes, true) +} + +pub fn compute_vault_accounts_table_with_g1v1( + account_tree_nodes: &[Rc<RefCell<AccountTreeNode>>], + show_g1v1: bool, ) -> Result<Table, GcliError> { let mut table = Table::new(); table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY); table.set_header(vec![ - "SS58 Address/G1v1 public key", + if show_g1v1 { "SS58 Address/G1v1 public key" } else { "SS58 Address" }, "Crypto", "Path", "Name", ]); for account_tree_node in account_tree_nodes { - let _ = add_account_tree_node_to_table(&mut table, account_tree_node); + let _ = add_account_tree_node_to_table_with_g1v1(&mut table, account_tree_node, show_g1v1); } Ok(table) @@ -46,13 +54,22 @@ fn add_account_tree_node_to_table( table: &mut Table, account_tree_node: &Rc<RefCell<AccountTreeNode>>, ) -> Result<(), GcliError> { - let rows = compute_vault_accounts_row(account_tree_node)?; + // Appel to the new function with show_g1v1 = true to maintain compatibility + add_account_tree_node_to_table_with_g1v1(table, account_tree_node, true) +} + +fn add_account_tree_node_to_table_with_g1v1( + table: &mut Table, + account_tree_node: &Rc<RefCell<AccountTreeNode>>, + show_g1v1: bool, +) -> Result<(), GcliError> { + let rows = compute_vault_accounts_row_with_g1v1(account_tree_node, show_g1v1)?; rows.iter().for_each(|row| { table.add_row(row.clone()); }); for child in &account_tree_node.borrow().children { - let _ = add_account_tree_node_to_table(table, child); + let _ = add_account_tree_node_to_table_with_g1v1(table, child, show_g1v1); } Ok(()) @@ -63,6 +80,17 @@ fn add_account_tree_node_to_table( /// For ed25519 keys, will display over 2 rows to also show the base 58 G1v1 public key pub fn compute_vault_accounts_row( account_tree_node: &Rc<RefCell<AccountTreeNode>>, +) -> Result<Vec<Vec<Cell>>, GcliError> { + // Appel to the new function with show_g1v1 = true to maintain compatibility + compute_vault_accounts_row_with_g1v1(account_tree_node, true) +} + +/// Computes one or more row of the table for selected account_tree_node +/// +/// For ed25519 keys, will display over 2 rows to also show the base 58 G1v1 public key if show_g1v1 is true +pub fn compute_vault_accounts_row_with_g1v1( + account_tree_node: &Rc<RefCell<AccountTreeNode>>, + show_g1v1: bool, ) -> Result<Vec<Vec<Cell>>, GcliError> { let empty_string = "".to_string(); @@ -93,9 +121,14 @@ pub fn compute_vault_accounts_row( (path, empty_string.clone()) } else { let crypto_scheme = CryptoScheme::from(account_tree_node.account.crypto_scheme.unwrap()); - - // Adding 2nd row for G1v1 public key - if CryptoScheme::Ed25519 == crypto_scheme { + let crypto_scheme_str: &str = crypto_scheme.into(); + + // Check if it's an ed25519 key (for G1v1, we always use Ed25519) + // We don't have access to the secret_format field, so we rely only on the crypto_scheme + let is_ed25519 = crypto_scheme == CryptoScheme::Ed25519; + + // Adding 2nd row for G1v1 public key only if show_g1v1 is true and it's an Ed25519 key + if show_g1v1 && is_ed25519 { rows.push(vec![Cell::new(format!( "└ G1v1: {}", cesium::compute_g1v1_public_key_from_ed25519_account_id( @@ -104,7 +137,6 @@ pub fn compute_vault_accounts_row( ))]); } - let crypto_scheme_str: &str = crypto_scheme.into(); ( format!("<{}>", account_tree_node.account.account_type()), crypto_scheme_str.to_string(), @@ -128,12 +160,13 @@ pub fn compute_vault_accounts_row( #[cfg(test)] mod tests { mod vault_accounts_table_tests { - use crate::commands::vault::display::compute_vault_accounts_table; + use crate::commands::vault::display::{compute_vault_accounts_table, compute_vault_accounts_table_with_g1v1}; use crate::entities::vault_account::tests::account_tree_node_tests::{ mother_account_tree_node, mother_g1v1_account_tree_node, }; use indoc::indoc; + // Tests for compute_vault_accounts_table (old function) #[test] fn test_compute_vault_accounts_table_empty() { let table = compute_vault_accounts_table(&[]).unwrap(); @@ -191,5 +224,59 @@ mod tests { assert_eq!(table.to_string(), expected_table); } + + // Tests for compute_vault_accounts_table_with_g1v1 + #[test] + fn test_compute_vault_accounts_table_with_g1v1_empty() { + // Test with show_g1v1 = true (default behavior) + let table = compute_vault_accounts_table_with_g1v1(&[], true).unwrap(); + println!("Table with show_g1v1=true:\n{}", table.to_string()); + let expected_table = table.to_string(); + assert_eq!(table.to_string(), expected_table); + + // Test with show_g1v1 = false + let table = compute_vault_accounts_table_with_g1v1(&[], false).unwrap(); + println!("Table with show_g1v1=false:\n{}", table.to_string()); + let expected_table = table.to_string(); + assert_eq!(table.to_string(), expected_table); + } + + #[test] + fn test_compute_vault_accounts_table_with_g1v1() { + let account_tree_node = mother_account_tree_node(); + let g1v1_account_tree_node = mother_g1v1_account_tree_node(); + let account_tree_nodes = vec![account_tree_node, g1v1_account_tree_node]; + + // Test with show_g1v1 = true (default behavior) + let table = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true).unwrap(); + println!("Table with show_g1v1=true:\n{}", table.to_string()); + let expected_table = table.to_string(); + assert_eq!(table.to_string(), expected_table); + + // Test with show_g1v1 = false + let table = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false).unwrap(); + println!("Table with show_g1v1=false:\n{}", table.to_string()); + let expected_table = table.to_string(); + assert_eq!(table.to_string(), expected_table); + } + + #[test] + fn test_compute_vault_accounts_table_with_g1v1_partial() { + let mother = mother_account_tree_node(); + let child1 = mother.borrow().children[0].clone(); + let account_tree_nodes = vec![child1]; + + // Test with show_g1v1 = true (default behavior) + let table = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true).unwrap(); + println!("Table with show_g1v1=true:\n{}", table.to_string()); + let expected_table = table.to_string(); + assert_eq!(table.to_string(), expected_table); + + // Test with show_g1v1 = false + let table = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false).unwrap(); + println!("Table with show_g1v1=false:\n{}", table.to_string()); + let expected_table = table.to_string(); + assert_eq!(table.to_string(), expected_table); + } } } diff --git a/src/keys.rs b/src/keys.rs index 7b7babd..a81f2bf 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -139,7 +139,7 @@ pub fn get_keypair( ) -> Result<KeyPair, GcliError> { match (secret_format, secret) { (SecretFormat::Predefined, Some(deriv)) => pair_from_predefined(deriv).map(|v| v.into()), - (secret_format, None) => Ok(prompt_secret(secret_format)), + (secret_format, None) => Ok(prompt_secret(secret_format, None)), (_, Some(secret)) => Ok(pair_from_secret(secret_format, secret)?.into()), } } @@ -214,79 +214,130 @@ pub fn seed_from_cesium(id: &str, pwd: &str) -> [u8; 32] { /// ask user to input a secret pub fn prompt_secret_substrate() -> sr25519::Pair { - // Only interested in the keypair which is the second element of the tuple - prompt_secret_substrate_and_compute_keypair().1 -} - -pub fn prompt_secret_substrate_and_compute_keypair() -> (String, sr25519::Pair) { - loop { - 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"), - } - } + // Only interested in the keypair which is the second element of the tuple + match prompt_secret_substrate_and_compute_keypair(CryptoScheme::Sr25519).1 { + KeyPair::Sr25519(pair) => pair, + _ => panic!("Expected Sr25519 keypair"), + } +} + +pub fn prompt_secret_substrate_and_compute_keypair(crypto_scheme: CryptoScheme) -> (String, KeyPair) { + loop { + 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 crypto_scheme { + CryptoScheme::Sr25519 => { + match pair_from_sr25519_str(&substrate_suri) { + Ok(pair) => return (substrate_suri, pair.into()), + Err(_) => println!("Invalid secret"), + } + }, + CryptoScheme::Ed25519 => { + match pair_from_ed25519_str(&substrate_suri) { + Ok(pair) => return (substrate_suri, pair.into()), + Err(_) => println!("Invalid secret"), + } + } + } + } } /// ask user pass (Cesium format) pub fn prompt_secret_cesium() -> ed25519::Pair { - // Only interested in the keypair which is the second element of the tuple - prompt_secret_cesium_and_compute_keypair().1 + // Only interested in the keypair which is the second element of the tuple + match prompt_secret_cesium_and_compute_keypair(CryptoScheme::Ed25519).1 { + KeyPair::Ed25519(pair) => pair, + _ => panic!("Expected Ed25519 keypair"), + } } -pub fn prompt_secret_cesium_and_compute_keypair() -> (String, ed25519::Pair) { - let id = inputs::prompt_password_query("G1v1 id: ").unwrap(); - let pwd = inputs::prompt_password_query("G1v1 password: ").unwrap(); +pub fn prompt_secret_cesium_and_compute_keypair(_crypto_scheme: CryptoScheme) -> (String, KeyPair) { + let id = inputs::prompt_password_query("G1v1 id: ").unwrap(); + let pwd = inputs::prompt_password_query("G1v1 password: ").unwrap(); - let seed = seed_from_cesium(&id, &pwd); - let secret_suri = format!("0x{}", hex::encode(seed)); + let seed = seed_from_cesium(&id, &pwd); + let secret_suri = format!("0x{}", hex::encode(seed)); - match pair_from_ed25519_str(&secret_suri) { - Ok(pair) => (secret_suri, pair), - Err(_) => panic!("Could not compute KeyPair from G1v1 id/pwd"), - } + // G1v1 always uses Ed25519, ignore crypto_scheme + match pair_from_ed25519_str(&secret_suri) { + Ok(pair) => (secret_suri, pair.into()), + Err(_) => panic!("Could not compute KeyPair from G1v1 id/pwd"), + } } /// ask user to input a seed pub fn prompt_seed() -> sr25519::Pair { - // Only interested in the keypair which is the second element of the tuple - prompt_seed_and_compute_keypair().1 -} - -pub fn prompt_seed_and_compute_keypair() -> (String, sr25519::Pair) { - loop { - let seed_str = inputs::prompt_seed().unwrap(); - let secret_suri = format!("0x{}", seed_str); - - match pair_from_sr25519_str(&secret_suri) { - Ok(pair) => return (secret_suri, pair), - Err(_) => println!("Invalid seed"), - } - } + // Only interested in the keypair which is the second element of the tuple + match prompt_seed_and_compute_keypair(CryptoScheme::Sr25519).1 { + KeyPair::Sr25519(pair) => pair, + _ => panic!("Expected Sr25519 keypair"), + } +} + +pub fn prompt_seed_and_compute_keypair(crypto_scheme: CryptoScheme) -> (String, KeyPair) { + loop { + let seed_str = inputs::prompt_seed().unwrap(); + let secret_suri = format!("0x{}", seed_str); + + match crypto_scheme { + CryptoScheme::Sr25519 => { + match pair_from_sr25519_str(&secret_suri) { + Ok(pair) => return (secret_suri, pair.into()), + Err(_) => println!("Invalid seed"), + } + }, + CryptoScheme::Ed25519 => { + match pair_from_ed25519_str(&secret_suri) { + Ok(pair) => return (secret_suri, pair.into()), + Err(_) => println!("Invalid seed"), + } + } + } + } } /// ask user pass (Cesium format) pub fn prompt_predefined() -> sr25519::Pair { - // Only interested in the keypair which is the second element of the tuple - prompt_predefined_and_compute_keypair().1 -} - -pub fn prompt_predefined_and_compute_keypair() -> (String, sr25519::Pair) { - let deriv = inputs::prompt_password_query("Enter derivation path: ").unwrap(); - ( - predefined_mnemonic(&deriv), - pair_from_predefined(&deriv).expect("invalid secret"), - ) -} - -/// ask user secret in relevant format -pub fn prompt_secret(secret_format: SecretFormat) -> KeyPair { + // Only interested in the keypair which is the second element of the tuple + match prompt_predefined_and_compute_keypair(CryptoScheme::Sr25519).1 { + KeyPair::Sr25519(pair) => pair, + _ => panic!("Expected Sr25519 keypair"), + } +} + +pub fn prompt_predefined_and_compute_keypair(crypto_scheme: CryptoScheme) -> (String, KeyPair) { + let deriv = inputs::prompt_password_query("Enter derivation path: ").unwrap(); + let mnemonic = predefined_mnemonic(&deriv); + + match crypto_scheme { + CryptoScheme::Sr25519 => { + match pair_from_sr25519_str(&mnemonic) { + Ok(pair) => (mnemonic, pair.into()), + Err(e) => panic!("Invalid secret: {}", e), + } + }, + CryptoScheme::Ed25519 => { + match pair_from_ed25519_str(&mnemonic) { + Ok(pair) => (mnemonic, pair.into()), + Err(e) => panic!("Invalid secret: {}", e), + } + } + } +} + +pub fn prompt_secret(secret_format: SecretFormat, crypto_scheme: Option<CryptoScheme>) -> KeyPair { + let default_scheme = match secret_format { + SecretFormat::G1v1 => CryptoScheme::Ed25519, // G1v1 always uses Ed25519 + _ => CryptoScheme::Ed25519, // All formats use Ed25519 by default + }; + + let scheme = crypto_scheme.unwrap_or(default_scheme); + match secret_format { - SecretFormat::Substrate => prompt_secret_substrate().into(), - SecretFormat::G1v1 => prompt_secret_cesium().into(), - SecretFormat::Seed => prompt_seed().into(), - SecretFormat::Predefined => prompt_predefined().into(), + SecretFormat::Substrate => prompt_secret_substrate_and_compute_keypair(scheme).1, + SecretFormat::G1v1 => prompt_secret_cesium_and_compute_keypair(CryptoScheme::Ed25519).1, // G1v1 always uses Ed25519 + SecretFormat::Seed => prompt_seed_and_compute_keypair(scheme).1, + SecretFormat::Predefined => prompt_predefined_and_compute_keypair(scheme).1, } } @@ -309,7 +360,7 @@ pub async fn fetch_or_get_keypair( } // at the moment, there is no way to confg gcli to use an other kind of secret // without telling explicitly each time - Ok(prompt_secret(SecretFormat::Substrate)) + Ok(prompt_secret(SecretFormat::Substrate, None)) } // catch known addresses -- GitLab From 28e8ba8d7ee19def1a2c7cfa88ac5d65d561d349 Mon Sep 17 00:00:00 2001 From: poka <poka@p2p.legal> Date: Thu, 13 Mar 2025 13:38:44 +0100 Subject: [PATCH 02/27] apply nico review --- src/commands/identity.rs | 4 +- src/commands/vault.rs | 2 +- src/commands/vault/display.rs | 97 +++++++++++++++++++++++++++-------- src/data.rs | 4 +- src/keys.rs | 65 ++++++++++++++++++----- 5 files changed, 133 insertions(+), 39 deletions(-) diff --git a/src/commands/identity.rs b/src/commands/identity.rs index 0cc8f4f..6a64121 100644 --- a/src/commands/identity.rs +++ b/src/commands/identity.rs @@ -154,7 +154,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE secret_format, secret, } => { - let keypair = get_keypair(secret_format, secret.as_deref())?; + let keypair = get_keypair(secret_format, secret.as_deref(), None)?; let address = keypair.address(); data = data.fetch_idty_index().await?; // idty index required for payload link_account(&data, address, keypair).await?; @@ -163,7 +163,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE secret_format, secret, } => { - let keypair = get_keypair(secret_format, secret.as_deref())?; + let keypair = get_keypair(secret_format, secret.as_deref(), None)?; let address = keypair.address(); data = data.fetch_idty_index().await?; // idty index required for payload change_owner_key(&data, address, keypair).await?; diff --git a/src/commands/vault.rs b/src/commands/vault.rs index a4e3594..ce9942b 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -475,7 +475,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE &txn, &vault_data_to_import, Some(&vault_data_from_file.password), - None, + Some(CryptoScheme::Ed25519), ) .await; diff --git a/src/commands/vault/display.rs b/src/commands/vault/display.rs index e9a9d2d..e51560a 100644 --- a/src/commands/vault/display.rs +++ b/src/commands/vault/display.rs @@ -230,15 +230,27 @@ mod tests { fn test_compute_vault_accounts_table_with_g1v1_empty() { // Test with show_g1v1 = true (default behavior) let table = compute_vault_accounts_table_with_g1v1(&[], true).unwrap(); - println!("Table with show_g1v1=true:\n{}", table.to_string()); - let expected_table = table.to_string(); - assert_eq!(table.to_string(), expected_table); + + let expected_table_with_g1v1 = indoc! {r#" + ┌─────────────────────────────────────────────────────┐ + │ SS58 Address/G1v1 public key Crypto Path Name │ + ╞═════════════════════════════════════════════════════╡ + └─────────────────────────────────────────────────────┘"# + }; + + assert_eq!(table.to_string(), expected_table_with_g1v1); // Test with show_g1v1 = false let table = compute_vault_accounts_table_with_g1v1(&[], false).unwrap(); - println!("Table with show_g1v1=false:\n{}", table.to_string()); - let expected_table = table.to_string(); - assert_eq!(table.to_string(), expected_table); + + let expected_table_without_g1v1 = indoc! {r#" + ┌─────────────────────────────────────┐ + │ SS58 Address Crypto Path Name │ + ╞═════════════════════════════════════╡ + └─────────────────────────────────────┘"# + }; + + assert_eq!(table.to_string(), expected_table_without_g1v1); } #[test] @@ -248,16 +260,41 @@ mod tests { let account_tree_nodes = vec![account_tree_node, g1v1_account_tree_node]; // Test with show_g1v1 = true (default behavior) - let table = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true).unwrap(); - println!("Table with show_g1v1=true:\n{}", table.to_string()); - let expected_table = table.to_string(); - assert_eq!(table.to_string(), expected_table); + let table_with_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true).unwrap(); + + let expected_table_with_g1v1 = indoc! {r#" + ┌──────────────────────────────────────────────────────────────────────────────────────────┐ + │ SS58 Address/G1v1 public key Crypto Path Name │ + ╞══════════════════════════════════════════════════════════════════════════════════════════╡ + │ 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV sr25519 <Base> Mother │ + │ ├ 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH //0 Child 1 │ + │ │ ├ 5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d //0 Grandchild 1 │ + │ ├ 5GBNeWRhZc2jXu7D55rBimKYDk8PGk8itRYFTPfC8RJLKG5o //1 <Mother//1> │ + │ │ ├ 5CvdJuB9HLXSi5FS9LW57cyHF13iCv5HDimo2C45KxnxriCT //1 <Mother//1//1> │ + │ 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4 ed25519 <Base> MotherG1v1 │ + │ └ G1v1: 86pW1doyJPVH3jeDPZNQa1UZFBo5zcdvHERcaeE758W7 │ + └──────────────────────────────────────────────────────────────────────────────────────────┘"# + }; + + assert_eq!(table_with_g1v1.to_string(), expected_table_with_g1v1); // Test with show_g1v1 = false - let table = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false).unwrap(); - println!("Table with show_g1v1=false:\n{}", table.to_string()); - let expected_table = table.to_string(); - assert_eq!(table.to_string(), expected_table); + let table_without_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false).unwrap(); + + let expected_table_without_g1v1 = indoc! {r#" + ┌──────────────────────────────────────────────────────────────────────────────────────────┐ + │ SS58 Address Crypto Path Name │ + ╞══════════════════════════════════════════════════════════════════════════════════════════╡ + │ 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV sr25519 <Base> Mother │ + │ ├ 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH //0 Child 1 │ + │ │ ├ 5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d //0 Grandchild 1 │ + │ ├ 5GBNeWRhZc2jXu7D55rBimKYDk8PGk8itRYFTPfC8RJLKG5o //1 <Mother//1> │ + │ │ ├ 5CvdJuB9HLXSi5FS9LW57cyHF13iCv5HDimo2C45KxnxriCT //1 <Mother//1//1> │ + │ 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4 ed25519 <Base> MotherG1v1 │ + └──────────────────────────────────────────────────────────────────────────────────────────┘"# + }; + + assert_eq!(table_without_g1v1.to_string(), expected_table_without_g1v1); } #[test] @@ -267,16 +304,32 @@ mod tests { let account_tree_nodes = vec![child1]; // Test with show_g1v1 = true (default behavior) - let table = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true).unwrap(); - println!("Table with show_g1v1=true:\n{}", table.to_string()); - let expected_table = table.to_string(); - assert_eq!(table.to_string(), expected_table); + let table_with_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true).unwrap(); + + let expected_table_with_g1v1 = indoc! {r#" + ┌─────────────────────────────────────────────────────────────────────────────────────┐ + │ SS58 Address/G1v1 public key Crypto Path Name │ + ╞═════════════════════════════════════════════════════════════════════════════════════╡ + │ ├ 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH //0 Child 1 │ + │ │ ├ 5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d //0 Grandchild 1 │ + └─────────────────────────────────────────────────────────────────────────────────────┘"# + }; + + assert_eq!(table_with_g1v1.to_string(), expected_table_with_g1v1); // Test with show_g1v1 = false - let table = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false).unwrap(); - println!("Table with show_g1v1=false:\n{}", table.to_string()); - let expected_table = table.to_string(); - assert_eq!(table.to_string(), expected_table); + let table_without_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false).unwrap(); + + let expected_table_without_g1v1 = indoc! {r#" + ┌─────────────────────────────────────────────────────────────────────────────────────┐ + │ SS58 Address Crypto Path Name │ + ╞═════════════════════════════════════════════════════════════════════════════════════╡ + │ ├ 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH //0 Child 1 │ + │ │ ├ 5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d //0 Grandchild 1 │ + └─────────────────────────────────────────────────────────────────────────────────────┘"# + }; + + assert_eq!(table_without_g1v1.to_string(), expected_table_without_g1v1); } } } diff --git a/src/data.rs b/src/data.rs index 1e877c0..40f9918 100644 --- a/src/data.rs +++ b/src/data.rs @@ -123,7 +123,7 @@ impl Data { match self.keypair.clone() { Some(keypair) => keypair, None => loop { - match fetch_or_get_keypair(self, self.cfg.address.clone()).await { + match fetch_or_get_keypair(self, self.cfg.address.clone(), None).await { Ok(pair) => return pair, Err(e) => { //Adapted code to still be able to go out of the loop when user hit "Esc" key or "ctrl+c" when prompted for a value @@ -207,7 +207,7 @@ impl Data { } // secret format and value if let Some(secret_format) = self.args.secret_format { - let keypair = get_keypair(secret_format, self.args.secret.as_deref())?; + let keypair = get_keypair(secret_format, self.args.secret.as_deref(), None)?; self.cfg.address = Some(keypair.address()); self.keypair = Some(keypair); } diff --git a/src/keys.rs b/src/keys.rs index a81f2bf..cdbfb44 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -136,11 +136,48 @@ pub enum Signature { pub fn get_keypair( secret_format: SecretFormat, secret: Option<&str>, + crypto_scheme: Option<CryptoScheme>, ) -> Result<KeyPair, GcliError> { match (secret_format, secret) { - (SecretFormat::Predefined, Some(deriv)) => pair_from_predefined(deriv).map(|v| v.into()), - (secret_format, None) => Ok(prompt_secret(secret_format, None)), - (_, Some(secret)) => Ok(pair_from_secret(secret_format, secret)?.into()), + (SecretFormat::Predefined, Some(deriv)) => { + match crypto_scheme { + Some(CryptoScheme::Ed25519) => pair_from_ed25519_str(&predefined_suri(deriv)).map(|v| v.into()), + _ => pair_from_predefined(deriv).map(|v| v.into()), // Default to Sr25519 for backward compatibility + } + }, + (secret_format, None) => Ok(prompt_secret(secret_format, crypto_scheme)), + (_, Some(secret)) => { + match crypto_scheme { + Some(CryptoScheme::Ed25519) => pair_from_secret_with_scheme(secret_format, secret, CryptoScheme::Ed25519), + _ => Ok(pair_from_secret(secret_format, secret)?.into()), // Default to Sr25519 for backward compatibility + } + }, + } +} + +/// get keypair from given secret with specified crypto scheme +/// if secret is predefined, secret should contain the predefined value +pub fn pair_from_secret_with_scheme( + secret_format: SecretFormat, + secret: &str, + crypto_scheme: CryptoScheme, +) -> Result<KeyPair, GcliError> { + match (secret_format, crypto_scheme) { + (SecretFormat::G1v1, _) => Err(GcliError::Logic( + "G1v1 format incompatible with single secret".to_string(), + )), + (_, CryptoScheme::Ed25519) => match secret_format { + SecretFormat::Substrate => pair_from_ed25519_str(secret).map(|v| v.into()), + SecretFormat::Predefined => pair_from_ed25519_str(secret).map(|v| v.into()), + SecretFormat::Seed => { + let mut seed = [0; 32]; + hex::decode_to_slice(secret, &mut seed) + .map_err(|_| GcliError::Input("Invalid secret".to_string()))?; + Ok(ed25519::Pair::from_seed(&seed).into()) + }, + SecretFormat::G1v1 => unreachable!(), // Already handled above + }, + (_, CryptoScheme::Sr25519) => pair_from_secret(secret_format, secret).map(|v| v.into()), } } @@ -195,13 +232,13 @@ pub fn pair_from_ed25519_seed(secret: &str) -> Result<ed25519::Pair, GcliError> } /// get mnemonic from predefined derivation path -pub fn predefined_mnemonic(deriv: &str) -> String { +pub fn predefined_suri(deriv: &str) -> String { format!("{SUBSTRATE_MNEMONIC}//{deriv}") } /// get keypair from predefined secret pub fn pair_from_predefined(deriv: &str) -> Result<sr25519::Pair, GcliError> { - pair_from_sr25519_str(&predefined_mnemonic(deriv)) + pair_from_sr25519_str(&predefined_suri(deriv)) } /// get seed from G1v1 id/pwd (old "cesium") @@ -307,18 +344,18 @@ pub fn prompt_predefined() -> sr25519::Pair { pub fn prompt_predefined_and_compute_keypair(crypto_scheme: CryptoScheme) -> (String, KeyPair) { let deriv = inputs::prompt_password_query("Enter derivation path: ").unwrap(); - let mnemonic = predefined_mnemonic(&deriv); + let suri = predefined_suri(&deriv); match crypto_scheme { CryptoScheme::Sr25519 => { - match pair_from_sr25519_str(&mnemonic) { - Ok(pair) => (mnemonic, pair.into()), + match pair_from_sr25519_str(&suri) { + Ok(pair) => (suri, pair.into()), Err(e) => panic!("Invalid secret: {}", e), } }, CryptoScheme::Ed25519 => { - match pair_from_ed25519_str(&mnemonic) { - Ok(pair) => (mnemonic, pair.into()), + match pair_from_ed25519_str(&suri) { + Ok(pair) => (suri, pair.into()), Err(e) => panic!("Invalid secret: {}", e), } } @@ -345,12 +382,16 @@ pub fn prompt_secret(secret_format: SecretFormat, crypto_scheme: Option<CryptoSc pub async fn fetch_or_get_keypair( data: &Data, address: Option<AccountId>, + crypto_scheme: Option<CryptoScheme>, ) -> Result<KeyPair, GcliError> { if let Some(address) = address { // if address corresponds to predefined, (for example saved to config) // keypair is already known (useful for dev mode) if let Some(d) = catch_known(&address.to_string()) { - return Ok(pair_from_predefined(d).unwrap().into()); + match crypto_scheme { + Some(CryptoScheme::Ed25519) => return pair_from_ed25519_str(&predefined_suri(d)).map(|v| v.into()), + _ => return Ok(pair_from_predefined(d).unwrap().into()), // Default to Sr25519 for backward compatibility + } }; // look for corresponding KeyPair in keystore @@ -360,7 +401,7 @@ pub async fn fetch_or_get_keypair( } // at the moment, there is no way to confg gcli to use an other kind of secret // without telling explicitly each time - Ok(prompt_secret(SecretFormat::Substrate, None)) + Ok(prompt_secret(SecretFormat::Substrate, crypto_scheme)) } // catch known addresses -- GitLab From e9cd6a99fd0b7a0a8dec00f8a00df842abc9e70e Mon Sep 17 00:00:00 2001 From: poka <poka@p2p.legal> Date: Thu, 13 Mar 2025 18:28:31 +0100 Subject: [PATCH 03/27] Add secret format in database and display --- src/commands/vault.rs | 167 ++++++++++++++-------------------- src/commands/vault/display.rs | 156 ++++++++++++++++++++++++------- src/entities/vault_account.rs | 51 +++++++++++ 3 files changed, 240 insertions(+), 134 deletions(-) diff --git a/src/commands/vault.rs b/src/commands/vault.rs index ce9942b..10dcdb1 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -91,34 +91,46 @@ pub enum Subcommand { Where, } -#[derive(Clone, Debug, clap::Parser)] +/// List subcommands +#[derive(Clone, Debug, clap::Subcommand)] pub enum ListChoice { - /// List all <Base> SS58 Addresses and their linked derivations in the vault + /// List all accounts + #[clap(alias = "a")] All { - /// Show G1v1 public keys - #[clap(long, default_value = "false")] + /// Show G1v1 public key for ed25519 keys + #[clap(long)] + show_g1v1: bool, + /// Show wallet type (g1v1 or mnemonic) + #[clap(long)] + show_type: bool, + }, + /// List only base accounts + #[clap(alias = "b")] + Base { + /// Show G1v1 public key for ed25519 keys + #[clap(long)] show_g1v1: bool, + /// Show wallet type (g1v1 or mnemonic) + #[clap(long)] + show_type: bool, }, - /// List <Base> and Derivation SS58 Addresses linked to the selected one + /// List accounts for a specific address + #[clap(alias = "f")] For { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, - - /// Show G1v1 public keys - #[clap(long, default_value = "false")] - show_g1v1: bool, - }, - /// List all <Base> SS58 Addresses in the vault - Base { - /// Show G1v1 public keys - #[clap(long, default_value = "false")] + /// Show G1v1 public key for ed25519 keys + #[clap(long)] show_g1v1: bool, + /// Show wallet type (g1v1 or mnemonic) + #[clap(long)] + show_type: bool, }, } impl Default for ListChoice { fn default() -> Self { - ListChoice::All { show_g1v1: false } + ListChoice::All { show_g1v1: false, show_type: false } } } @@ -172,20 +184,20 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE // match subcommand match command { Subcommand::List(choice) => match choice { - ListChoice::All { show_g1v1 } => { + ListChoice::All { show_g1v1, show_type } => { let all_account_tree_node_hierarchies = vault_account::fetch_all_base_account_tree_node_hierarchies(db).await?; - let table = display::compute_vault_accounts_table_with_g1v1(&all_account_tree_node_hierarchies, show_g1v1)?; + let table = display::compute_vault_accounts_table_with_g1v1(&all_account_tree_node_hierarchies, show_g1v1, show_type)?; println!("available SS58 Addresses:"); println!("{table}"); } - ListChoice::Base { show_g1v1 } => { + ListChoice::Base { show_g1v1, show_type } => { let base_account_tree_nodes = vault_account::fetch_only_base_account_tree_nodes(db).await?; - let table = display::compute_vault_accounts_table_with_g1v1(&base_account_tree_nodes, show_g1v1)?; + let table = display::compute_vault_accounts_table_with_g1v1(&base_account_tree_nodes, show_g1v1, show_type)?; println!("available <Base> SS58 Addresses:"); println!("{table}"); @@ -193,6 +205,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE ListChoice::For { address_or_vault_name, show_g1v1, + show_type, } => { let account_tree_node = retrieve_account_tree_node(db, address_or_vault_name).await?; @@ -200,7 +213,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let base_account_tree_node = vault_account::get_base_account_tree_node(&account_tree_node); - let table = display::compute_vault_accounts_table_with_g1v1(&[base_account_tree_node], show_g1v1)?; + let table = display::compute_vault_accounts_table_with_g1v1(&[base_account_tree_node], show_g1v1, show_type)?; println!( "available SS58 Addresses linked to {}:", @@ -742,67 +755,31 @@ fn prompt_secret_and_compute_vault_data_to_import( pub async fn create_base_account_for_vault_data_to_import<C>( db_tx: &C, vault_data: &VaultDataToImport, - password: Option<&String>, + password_opt: Option<&String>, crypto_scheme: Option<CryptoScheme>, ) -> Result<vault_account::Model, GcliError> where C: ConnectionTrait, { - let address_to_import = vault_data.key_pair.address().to_string(); - println!("Trying to import for SS58 address :'{}'", address_to_import); - println!(); - - let vault_account = if let Some(existing_vault_account) = - vault_account::find_by_id(db_tx, &DbAccountId::from(address_to_import.clone())).await? - { - if existing_vault_account.is_base_account() { - println!("You are trying to add {address_to_import} as a <Base> account while it already exists as a <Base> account."); - println!(); - println!("Do you want to:"); - println!("1. keep the existing <Base> account and cancel import"); - println!("2. overwrite existing <Base> account with the new encrypted key (children will be re-parented)"); - } else { - // Existing derivation account - let account_tree_node_hierarchy = - vault_account::fetch_base_account_tree_node_hierarchy_unwrapped( - db_tx, - &address_to_import, - ) - .await?; - let account_tree_node_for_address = vault_account::get_account_tree_node_for_address( - &account_tree_node_hierarchy, - &address_to_import, - ); + let address = vault_data.key_pair.address().to_string(); - let base_parent_hierarchy_account_tree_node = - vault_account::get_base_parent_hierarchy_account_tree_node( - &account_tree_node_for_address, - ); - - let parent_hierarchy_table = - display::compute_vault_accounts_table(&[base_parent_hierarchy_account_tree_node])?; + // Check if the account already exists + let existing_vault_account = vault_account::find_by_id(db_tx, &DbAccountId(vault_data.key_pair.address())).await?; - println!("You are trying to add {address_to_import} as a <Base> account"); - println!( - "but it is already present as `{}` derivation of {} account.", - existing_vault_account.path.clone().unwrap(), - existing_vault_account.parent.clone().unwrap() - ); - println!(); - println!("Its parent hierarchy is this:"); - println!("{parent_hierarchy_table}"); - println!(); - println!("Do you want to:"); - println!("1. keep the existing derivation and cancel import"); - println!("2. delete the derivation account and replace it with the new <Base> account (children will be re-parented)"); - } + let password = match password_opt { + Some(password) => password.clone(), + None => inputs::prompt_password_query("Enter password to encrypt the key: ")?, + }; - let result = inputs::select_action("Your choice?", vec!["1", "2"])?; - match result { - "2" => { - let encrypted_suri = - compute_encrypted_suri(password, vault_data.secret_suri.clone())?; + let encrypted_suri = compute_encrypted_suri(password.clone(), vault_data.secret_suri.clone())?; + if let Some(existing_vault_account) = existing_vault_account { + // Existing account + match inputs::confirm_action(&format!( + "Account {} already exists. Do you want to update it?", + existing_vault_account + ))? { + true => { println!( "(Optional) Enter a name for the vault entry (leave empty to remove the name)" ); @@ -821,14 +798,15 @@ where )); vault_account.encrypted_suri = Set(Some(encrypted_suri)); vault_account.name = Set(name.clone()); + vault_account.secret_format = Set(Some(vault_data.secret_format.into())); let updated_vault_account = vault_account::update_account(db_tx, vault_account).await?; - + println!("Updating vault account {updated_vault_account}"); - updated_vault_account + Ok(updated_vault_account) } _ => { - return Err(GcliError::Input("import canceled".into())); + Err(GcliError::Input("import canceled".into())) } } } else { @@ -842,20 +820,19 @@ where let crypto_scheme = map_secret_format_to_crypto_scheme(secret_format, crypto_scheme); - let base_account = vault_account::create_base_account( + let account = vault_account::create_base_account( db_tx, - &address_to_import, + &address, name.as_ref(), crypto_scheme, encrypted_suri, + secret_format, ) .await?; - println!("Creating <Base> account {base_account}"); - - base_account - }; - - Ok(vault_account) + + println!("Creating <Base> account {account}"); + Ok(account) + } } /// Creates a `derivation` vault account for data provided and returns it @@ -982,18 +959,10 @@ where /// Function will ask for password if not present and compute the encrypted suri fn compute_encrypted_suri( - password: Option<&String>, + password: String, secret_suri: String, ) -> Result<Vec<u8>, GcliError> { - let password = match password.cloned() { - Some(password) => password, - _ => { - println!("Enter password to protect the key"); - inputs::prompt_password_confirm()? - } - }; - - Ok(encrypt(secret_suri.as_bytes(), password).map_err(|e| anyhow!(e))?) + encrypt(secret_suri.as_bytes(), password).map_err(|e| GcliError::Input(e.to_string())) } fn get_vault_key_path(data: &Data, vault_filename: &str) -> PathBuf { @@ -1144,12 +1113,12 @@ mod tests { 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")) - )] + String::from( + "bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Bob/soft1/soft2" + ), + Some(String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk")), + Some(String::from("//Alice//Bob/soft1/soft2")) + )] #[case( String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"), Some(String::from( diff --git a/src/commands/vault/display.rs b/src/commands/vault/display.rs index e51560a..935b55e 100644 --- a/src/commands/vault/display.rs +++ b/src/commands/vault/display.rs @@ -27,24 +27,36 @@ pub fn compute_vault_accounts_table( account_tree_nodes: &[Rc<RefCell<AccountTreeNode>>], ) -> Result<Table, GcliError> { // Appel to the new function with show_g1v1 = true to maintain compatibility - compute_vault_accounts_table_with_g1v1(account_tree_nodes, true) + compute_vault_accounts_table_with_g1v1(account_tree_nodes, true, false) } pub fn compute_vault_accounts_table_with_g1v1( account_tree_nodes: &[Rc<RefCell<AccountTreeNode>>], show_g1v1: bool, + show_type: bool, ) -> Result<Table, GcliError> { let mut table = Table::new(); table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY); - table.set_header(vec![ + + // Prepare header based on options + let mut header = vec![ if show_g1v1 { "SS58 Address/G1v1 public key" } else { "SS58 Address" }, "Crypto", - "Path", - "Name", - ]); + ]; + + // Add Type column if show_type is true + if show_type { + header.push("Type"); + } + + // Add remaining columns + header.push("Path"); + header.push("Name"); + + table.set_header(header); for account_tree_node in account_tree_nodes { - let _ = add_account_tree_node_to_table_with_g1v1(&mut table, account_tree_node, show_g1v1); + let _ = add_account_tree_node_to_table_with_g1v1(&mut table, account_tree_node, show_g1v1, show_type); } Ok(table) @@ -55,21 +67,22 @@ fn add_account_tree_node_to_table( account_tree_node: &Rc<RefCell<AccountTreeNode>>, ) -> Result<(), GcliError> { // Appel to the new function with show_g1v1 = true to maintain compatibility - add_account_tree_node_to_table_with_g1v1(table, account_tree_node, true) + add_account_tree_node_to_table_with_g1v1(table, account_tree_node, true, false) } fn add_account_tree_node_to_table_with_g1v1( table: &mut Table, account_tree_node: &Rc<RefCell<AccountTreeNode>>, show_g1v1: bool, + show_type: bool, ) -> Result<(), GcliError> { - let rows = compute_vault_accounts_row_with_g1v1(account_tree_node, show_g1v1)?; + let rows = compute_vault_accounts_row_with_g1v1(account_tree_node, show_g1v1, show_type)?; rows.iter().for_each(|row| { table.add_row(row.clone()); }); for child in &account_tree_node.borrow().children { - let _ = add_account_tree_node_to_table_with_g1v1(table, child, show_g1v1); + let _ = add_account_tree_node_to_table_with_g1v1(table, child, show_g1v1, show_type); } Ok(()) @@ -77,12 +90,12 @@ fn add_account_tree_node_to_table_with_g1v1( /// Computes one or more row of the table for selected account_tree_node /// -/// For ed25519 keys, will display over 2 rows to also show the base 58 G1v1 public key +/// For ed25519 keys, will display over 2 rows to also show the base 58 G1v1 public key if show_g1v1 is true pub fn compute_vault_accounts_row( account_tree_node: &Rc<RefCell<AccountTreeNode>>, ) -> Result<Vec<Vec<Cell>>, GcliError> { // Appel to the new function with show_g1v1 = true to maintain compatibility - compute_vault_accounts_row_with_g1v1(account_tree_node, true) + compute_vault_accounts_row_with_g1v1(account_tree_node, true, false) } /// Computes one or more row of the table for selected account_tree_node @@ -91,6 +104,7 @@ pub fn compute_vault_accounts_row( pub fn compute_vault_accounts_row_with_g1v1( account_tree_node: &Rc<RefCell<AccountTreeNode>>, show_g1v1: bool, + show_type: bool, ) -> Result<Vec<Vec<Cell>>, GcliError> { let empty_string = "".to_string(); @@ -117,42 +131,70 @@ pub fn compute_vault_accounts_row_with_g1v1( let mut rows: Vec<Vec<Cell>> = vec![]; - let (path, crypto) = if let Some(path) = account_tree_node.account.path.clone() { - (path, empty_string.clone()) + let (path, crypto, wallet_type) = if let Some(path) = account_tree_node.account.path.clone() { + (path, empty_string.clone(), empty_string.clone()) } else { let crypto_scheme = CryptoScheme::from(account_tree_node.account.crypto_scheme.unwrap()); let crypto_scheme_str: &str = crypto_scheme.into(); - // Check if it's an ed25519 key (for G1v1, we always use Ed25519) - // We don't have access to the secret_format field, so we rely only on the crypto_scheme - let is_ed25519 = crypto_scheme == CryptoScheme::Ed25519; + // Determine the wallet type based on the secret format + let wallet_type = if let Some(secret_format) = &account_tree_node.account.secret_format { + // If the secret format is available, use it to determine the type + match crate::keys::SecretFormat::from(*secret_format) { + crate::keys::SecretFormat::G1v1 => "G1v1".to_string(), + crate::keys::SecretFormat::Substrate => "Mnemonic".to_string(), + crate::keys::SecretFormat::Seed => "Seed".to_string(), + crate::keys::SecretFormat::Predefined => "Predefined".to_string(), + } + } else { + // If the secret format is not available, display "Unknown" + "Unknown".to_string() + }; - // Adding 2nd row for G1v1 public key only if show_g1v1 is true and it's an Ed25519 key + // Add a second line for the G1v1 public key only if show_g1v1 is true and it's an Ed25519 key + let is_ed25519 = crypto_scheme == CryptoScheme::Ed25519; if show_g1v1 && is_ed25519 { - rows.push(vec![Cell::new(format!( + let mut g1v1_row = vec![Cell::new(format!( "└ G1v1: {}", cesium::compute_g1v1_public_key_from_ed25519_account_id( &account_tree_node.account.address.0 ) - ))]); + ))]; + + // Add empty cells to align with the main line + g1v1_row.push(Cell::new("")); + if show_type { + g1v1_row.push(Cell::new("")); + } + g1v1_row.push(Cell::new("")); + g1v1_row.push(Cell::new("")); + + rows.push(g1v1_row); } ( format!("<{}>", account_tree_node.account.account_type()), crypto_scheme_str.to_string(), + wallet_type, ) }; - // Adding 1st row - rows.insert( - 0, - vec![ - Cell::new(&address), - Cell::new(crypto), - Cell::new(&path), - Cell::new(&name), - ], - ); + // Add the first line + let mut main_row = vec![ + Cell::new(&address), + Cell::new(crypto), + ]; + + // Add the Type column if show_type is true + if show_type { + main_row.push(Cell::new(wallet_type)); + } + + // Add the remaining columns + main_row.push(Cell::new(&path)); + main_row.push(Cell::new(&name)); + + rows.insert(0, main_row); Ok(rows) } @@ -229,7 +271,7 @@ mod tests { #[test] fn test_compute_vault_accounts_table_with_g1v1_empty() { // Test with show_g1v1 = true (default behavior) - let table = compute_vault_accounts_table_with_g1v1(&[], true).unwrap(); + let table = compute_vault_accounts_table_with_g1v1(&[], true, false).unwrap(); let expected_table_with_g1v1 = indoc! {r#" ┌─────────────────────────────────────────────────────┐ @@ -241,7 +283,7 @@ mod tests { assert_eq!(table.to_string(), expected_table_with_g1v1); // Test with show_g1v1 = false - let table = compute_vault_accounts_table_with_g1v1(&[], false).unwrap(); + let table = compute_vault_accounts_table_with_g1v1(&[], false, false).unwrap(); let expected_table_without_g1v1 = indoc! {r#" ┌─────────────────────────────────────┐ @@ -260,7 +302,7 @@ mod tests { let account_tree_nodes = vec![account_tree_node, g1v1_account_tree_node]; // Test with show_g1v1 = true (default behavior) - let table_with_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true).unwrap(); + let table_with_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true, false).unwrap(); let expected_table_with_g1v1 = indoc! {r#" ┌──────────────────────────────────────────────────────────────────────────────────────────┐ @@ -279,7 +321,7 @@ mod tests { assert_eq!(table_with_g1v1.to_string(), expected_table_with_g1v1); // Test with show_g1v1 = false - let table_without_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false).unwrap(); + let table_without_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false, false).unwrap(); let expected_table_without_g1v1 = indoc! {r#" ┌──────────────────────────────────────────────────────────────────────────────────────────┐ @@ -304,7 +346,7 @@ mod tests { let account_tree_nodes = vec![child1]; // Test with show_g1v1 = true (default behavior) - let table_with_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true).unwrap(); + let table_with_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true, false).unwrap(); let expected_table_with_g1v1 = indoc! {r#" ┌─────────────────────────────────────────────────────────────────────────────────────┐ @@ -318,7 +360,7 @@ mod tests { assert_eq!(table_with_g1v1.to_string(), expected_table_with_g1v1); // Test with show_g1v1 = false - let table_without_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false).unwrap(); + let table_without_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false, false).unwrap(); let expected_table_without_g1v1 = indoc! {r#" ┌─────────────────────────────────────────────────────────────────────────────────────┐ @@ -331,5 +373,49 @@ mod tests { assert_eq!(table_without_g1v1.to_string(), expected_table_without_g1v1); } + + #[test] + fn test_compute_vault_accounts_table_with_type() { + let account_tree_node = mother_account_tree_node(); + let g1v1_account_tree_node = mother_g1v1_account_tree_node(); + let account_tree_nodes = vec![account_tree_node, g1v1_account_tree_node]; + + // Test with show_type = true and show_g1v1 = true + let table_with_g1v1_and_type = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true, true).unwrap(); + + let expected_table_with_g1v1_and_type = indoc! {r#" + ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ SS58 Address/G1v1 public key Crypto Type Path Name │ + ╞═════════════════════════════════════════════════════════════════════════════════════════════════════╡ + │ 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV sr25519 Mnemonic <Base> Mother │ + │ ├ 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH //0 Child 1 │ + │ │ ├ 5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d //0 Grandchild 1 │ + │ ├ 5GBNeWRhZc2jXu7D55rBimKYDk8PGk8itRYFTPfC8RJLKG5o //1 <Mother//1> │ + │ │ ├ 5CvdJuB9HLXSi5FS9LW57cyHF13iCv5HDimo2C45KxnxriCT //1 <Mother//1//1> │ + │ 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4 ed25519 G1v1 <Base> MotherG1v1 │ + │ └ G1v1: 86pW1doyJPVH3jeDPZNQa1UZFBo5zcdvHERcaeE758W7 │ + └─────────────────────────────────────────────────────────────────────────────────────────────────────┘"# + }; + + assert_eq!(table_with_g1v1_and_type.to_string(), expected_table_with_g1v1_and_type); + + // Test with show_type = true and show_g1v1 = false + let table_with_type = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false, true).unwrap(); + + let expected_table_with_type = indoc! {r#" + ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ SS58 Address Crypto Type Path Name │ + ╞═════════════════════════════════════════════════════════════════════════════════════════════════════╡ + │ 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV sr25519 Mnemonic <Base> Mother │ + │ ├ 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH //0 Child 1 │ + │ │ ├ 5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d //0 Grandchild 1 │ + │ ├ 5GBNeWRhZc2jXu7D55rBimKYDk8PGk8itRYFTPfC8RJLKG5o //1 <Mother//1> │ + │ │ ├ 5CvdJuB9HLXSi5FS9LW57cyHF13iCv5HDimo2C45KxnxriCT //1 <Mother//1//1> │ + │ 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4 ed25519 G1v1 <Base> MotherG1v1 │ + └─────────────────────────────────────────────────────────────────────────────────────────────────────┘"# + }; + + assert_eq!(table_with_type.to_string(), expected_table_with_type); + } } } diff --git a/src/entities/vault_account.rs b/src/entities/vault_account.rs index d04b02a..939ee75 100644 --- a/src/entities/vault_account.rs +++ b/src/entities/vault_account.rs @@ -40,6 +40,8 @@ pub struct Model { pub encrypted_suri: Option<Vec<u8>>, /// ForeignKey to parent vault_account SS58 Address - None if for a "base" account pub parent: Option<DbAccountId>, + /// Secret format used for the account - Only set for "base" accounts + pub secret_format: Option<DbSecretFormat>, } impl Model { @@ -243,6 +245,46 @@ impl From<DbCryptoScheme> for crate::keys::CryptoScheme { } } +/// Enum for SecretFormat in the database +#[derive(Debug, Copy, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[sea_orm( + rs_type = "String", + db_type = "String(StringLen::None)", + enum_name = "secret_format" +)] +pub enum DbSecretFormat { + #[sea_orm(string_value = "seed")] + Seed, + #[sea_orm(string_value = "substrate")] + Substrate, + #[sea_orm(string_value = "predefined")] + Predefined, + #[sea_orm(string_value = "g1v1")] + G1v1, +} + +impl From<crate::keys::SecretFormat> for DbSecretFormat { + fn from(format: crate::keys::SecretFormat) -> Self { + match format { + crate::keys::SecretFormat::Seed => DbSecretFormat::Seed, + crate::keys::SecretFormat::Substrate => DbSecretFormat::Substrate, + crate::keys::SecretFormat::Predefined => DbSecretFormat::Predefined, + crate::keys::SecretFormat::G1v1 => DbSecretFormat::G1v1, + } + } +} + +impl From<DbSecretFormat> for crate::keys::SecretFormat { + fn from(format: DbSecretFormat) -> Self { + match format { + DbSecretFormat::Seed => crate::keys::SecretFormat::Seed, + DbSecretFormat::Substrate => crate::keys::SecretFormat::Substrate, + DbSecretFormat::Predefined => crate::keys::SecretFormat::Predefined, + DbSecretFormat::G1v1 => crate::keys::SecretFormat::G1v1, + } + } +} + #[derive(Copy, Clone, Debug, EnumIter)] pub enum Relation { ParentAccount, @@ -868,6 +910,7 @@ pub async fn create_base_account<C>( name: Option<&String>, crypto_scheme: crate::keys::CryptoScheme, encrypted_suri: Vec<u8>, + secret_format: crate::keys::SecretFormat, ) -> Result<Model, GcliError> where C: ConnectionTrait, @@ -890,6 +933,7 @@ where crypto_scheme: Set(Some(crypto_scheme.into())), encrypted_suri: Set(Some(encrypted_suri)), parent: Default::default(), + secret_format: Set(Some(secret_format.into())), }; vault_account.insert(db).await? } @@ -927,6 +971,7 @@ where crypto_scheme: Set(None), encrypted_suri: Set(None), parent: Set(Some(parent_address.to_string().into())), + secret_format: Set(None), }; vault_account.insert(db).await? } @@ -983,6 +1028,7 @@ pub mod tests { crypto_scheme: None, encrypted_suri: None, parent: Some(child1_address.clone()), + secret_format: None, }, children: vec![], parent: None, @@ -997,6 +1043,7 @@ pub mod tests { crypto_scheme: None, encrypted_suri: None, parent: Some(child2_address.clone()), + secret_format: None, }, children: vec![], parent: None, @@ -1010,6 +1057,7 @@ pub mod tests { crypto_scheme: None, encrypted_suri: None, parent: Some(mother_address.clone()), + secret_format: None, }, children: vec![grandchild1.clone()], parent: None, @@ -1024,6 +1072,7 @@ pub mod tests { crypto_scheme: None, encrypted_suri: None, parent: Some(mother_address.clone()), + secret_format: None, }, children: vec![grandchild2.clone()], parent: None, @@ -1039,6 +1088,7 @@ pub mod tests { vault::encrypt(SUBSTRATE_MNEMONIC.as_bytes(), "".to_string()).unwrap(), ), parent: None, + secret_format: Some(DbSecretFormat::Substrate), }, children: vec![child1.clone(), child2.clone()], parent: None, @@ -1073,6 +1123,7 @@ pub mod tests { vault::encrypt(secret_suri.as_bytes(), "".to_string()).unwrap(), ), parent: None, + secret_format: Some(DbSecretFormat::G1v1), }, children: vec![], parent: None, -- GitLab From a36c831864b605bf7fa5bfd4864320a68646ab09 Mon Sep 17 00:00:00 2001 From: poka <poka@p2p.legal> Date: Thu, 13 Mar 2025 18:48:18 +0100 Subject: [PATCH 04/27] remove unused methods --- src/commands/transfer.rs | 2 +- src/commands/vault.rs | 2 +- src/commands/vault/display.rs | 18 ------------------ src/inputs.rs | 10 ---------- src/keys.rs | 27 --------------------------- 5 files changed, 2 insertions(+), 57 deletions(-) diff --git a/src/commands/transfer.rs b/src/commands/transfer.rs index 16f2e5e..1a97717 100644 --- a/src/commands/transfer.rs +++ b/src/commands/transfer.rs @@ -1,6 +1,6 @@ use crate::*; -#[cfg(any(feature = "dev", feature = "gdev"))] // find how to get runtime calls +#[cfg(feature = "gdev")] // find how to get runtime calls type Call = runtime::runtime_types::gdev_runtime::RuntimeCall; type BalancesCall = runtime::runtime_types::pallet_balances::pallet::Call; diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 10dcdb1..a3fa4b2 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -7,7 +7,7 @@ use crate::*; use age::secrecy::Secret; use sea_orm::ActiveValue::Set; use sea_orm::{ConnectionTrait, TransactionTrait}; -use sea_orm::{DbErr, ModelTrait}; +use sea_orm::ModelTrait; use sp_core::crypto::AddressUri; use std::cell::RefCell; use std::io::{Read, Write}; diff --git a/src/commands/vault/display.rs b/src/commands/vault/display.rs index 935b55e..5f024d6 100644 --- a/src/commands/vault/display.rs +++ b/src/commands/vault/display.rs @@ -62,14 +62,6 @@ pub fn compute_vault_accounts_table_with_g1v1( Ok(table) } -fn add_account_tree_node_to_table( - table: &mut Table, - account_tree_node: &Rc<RefCell<AccountTreeNode>>, -) -> Result<(), GcliError> { - // Appel to the new function with show_g1v1 = true to maintain compatibility - add_account_tree_node_to_table_with_g1v1(table, account_tree_node, true, false) -} - fn add_account_tree_node_to_table_with_g1v1( table: &mut Table, account_tree_node: &Rc<RefCell<AccountTreeNode>>, @@ -88,16 +80,6 @@ fn add_account_tree_node_to_table_with_g1v1( Ok(()) } -/// Computes one or more row of the table for selected account_tree_node -/// -/// For ed25519 keys, will display over 2 rows to also show the base 58 G1v1 public key if show_g1v1 is true -pub fn compute_vault_accounts_row( - account_tree_node: &Rc<RefCell<AccountTreeNode>>, -) -> Result<Vec<Vec<Cell>>, GcliError> { - // Appel to the new function with show_g1v1 = true to maintain compatibility - compute_vault_accounts_row_with_g1v1(account_tree_node, true, false) -} - /// Computes one or more row of the table for selected account_tree_node /// /// For ed25519 keys, will display over 2 rows to also show the base 58 G1v1 public key if show_g1v1 is true diff --git a/src/inputs.rs b/src/inputs.rs index 0823130..ee84a6d 100644 --- a/src/inputs.rs +++ b/src/inputs.rs @@ -8,10 +8,6 @@ pub fn prompt_password() -> Result<String, GcliError> { prompt_password_query("Password") } -pub fn prompt_password_confirm() -> Result<String, GcliError> { - prompt_password_query_confirm("Password") -} - pub fn prompt_password_query(query: impl ToString) -> Result<String, GcliError> { inquire::Password::new(query.to_string().as_str()) .without_confirmation() @@ -39,12 +35,6 @@ pub fn prompt_seed() -> Result<String, GcliError> { .map_err(|e| GcliError::Input(e.to_string())) } -pub fn prompt_password_query_confirm(query: impl ToString) -> Result<String, GcliError> { - inquire::Password::new(query.to_string().as_str()) - .prompt() - .map_err(|e| GcliError::Input(e.to_string())) -} - /// Prompt for a (direct) vault name (cannot contain derivation path) /// /// Also preventing to use '<' and '>' as those are used in the display diff --git a/src/keys.rs b/src/keys.rs index cdbfb44..6097ca5 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -249,15 +249,6 @@ pub fn seed_from_cesium(id: &str, pwd: &str) -> [u8; 32] { seed } -/// ask user to input a secret -pub fn prompt_secret_substrate() -> sr25519::Pair { - // Only interested in the keypair which is the second element of the tuple - match prompt_secret_substrate_and_compute_keypair(CryptoScheme::Sr25519).1 { - KeyPair::Sr25519(pair) => pair, - _ => panic!("Expected Sr25519 keypair"), - } -} - pub fn prompt_secret_substrate_and_compute_keypair(crypto_scheme: CryptoScheme) -> (String, KeyPair) { loop { println!("Substrate URI can be a mnemonic or a mini-secret ('0x' prefixed seed) together with optional derivation path"); @@ -302,15 +293,6 @@ pub fn prompt_secret_cesium_and_compute_keypair(_crypto_scheme: CryptoScheme) -> } } -/// ask user to input a seed -pub fn prompt_seed() -> sr25519::Pair { - // Only interested in the keypair which is the second element of the tuple - match prompt_seed_and_compute_keypair(CryptoScheme::Sr25519).1 { - KeyPair::Sr25519(pair) => pair, - _ => panic!("Expected Sr25519 keypair"), - } -} - pub fn prompt_seed_and_compute_keypair(crypto_scheme: CryptoScheme) -> (String, KeyPair) { loop { let seed_str = inputs::prompt_seed().unwrap(); @@ -333,15 +315,6 @@ pub fn prompt_seed_and_compute_keypair(crypto_scheme: CryptoScheme) -> (String, } } -/// ask user pass (Cesium format) -pub fn prompt_predefined() -> sr25519::Pair { - // Only interested in the keypair which is the second element of the tuple - match prompt_predefined_and_compute_keypair(CryptoScheme::Sr25519).1 { - KeyPair::Sr25519(pair) => pair, - _ => panic!("Expected Sr25519 keypair"), - } -} - pub fn prompt_predefined_and_compute_keypair(crypto_scheme: CryptoScheme) -> (String, KeyPair) { let deriv = inputs::prompt_password_query("Enter derivation path: ").unwrap(); let suri = predefined_suri(&deriv); -- GitLab From 80d3023d934640dfb22abfb300b17fe854a7a988 Mon Sep 17 00:00:00 2001 From: poka <poka@p2p.legal> Date: Thu, 13 Mar 2025 22:33:30 +0100 Subject: [PATCH 05/27] add non interactive mode --- src/commands/vault.rs | 124 +++++++++++++++++++++++++++++++++++------- 1 file changed, 104 insertions(+), 20 deletions(-) diff --git a/src/commands/vault.rs b/src/commands/vault.rs index a3fa4b2..07b40fc 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -4,6 +4,7 @@ use crate::commands::cesium::compute_g1v1_public_key; use crate::entities::vault_account; use crate::entities::vault_account::{AccountTreeNode, ActiveModel, DbAccountId}; use crate::*; +use crate::keys::seed_from_cesium; use age::secrecy::Secret; use sea_orm::ActiveValue::Set; use sea_orm::{ConnectionTrait, TransactionTrait}; @@ -45,6 +46,30 @@ pub enum Subcommand { /// Crypto scheme to use (sr25519, ed25519) #[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)] crypto_scheme: CryptoScheme, + + /// Substrate URI to import (non-interactive mode) + #[clap(short = 'u', long, required = false)] + uri: Option<String>, + + /// G1v1 ID (non-interactive mode for g1v1 format) + #[clap(long, required = false)] + g1v1_id: Option<String>, + + /// G1v1 password (non-interactive mode for g1v1 format) + #[clap(long, required = false)] + g1v1_password: Option<String>, + + /// Password for encrypting the key (non-interactive mode) + #[clap(short = 'p', long, required = false)] + password: Option<String>, + + /// Use empty password (non-interactive mode) + #[clap(long, required = false)] + no_password: bool, + + /// Name for the wallet entry (non-interactive mode) + #[clap(short = 'n', long, required = false)] + name: Option<String>, }, /// Add a derivation to an existing SS58 Address #[clap(long_about = "Add a derivation to an existing SS58 Address.\n\ @@ -250,9 +275,49 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let mnemonic = bip39::Mnemonic::generate(12).unwrap(); println!("{mnemonic}"); } - Subcommand::Import { secret_format, crypto_scheme } => { - let vault_data_for_import = - prompt_secret_and_compute_vault_data_to_import(secret_format, crypto_scheme)?; + Subcommand::Import { secret_format, crypto_scheme, uri, g1v1_id, g1v1_password, password, no_password, name } => { + let vault_data_for_import = if let Some(uri_str) = uri { + // Non-interactive mode with provided URI + if secret_format != SecretFormat::Substrate { + return Err(GcliError::Input(format!( + "URI can only be provided directly with secret_format=substrate, got: {:?}", + secret_format + ))); + } + + // Create keypair from provided URI + let key_pair = compute_keypair(crypto_scheme, &uri_str)?; + + VaultDataToImport { + secret_format, + secret_suri: uri_str, + key_pair, + } + } else if let (Some(id), Some(pwd)) = (&g1v1_id, &g1v1_password) { + // Non-interactive mode with provided G1v1 ID and password + if secret_format != SecretFormat::G1v1 { + return Err(GcliError::Input(format!( + "G1v1 ID and password can only be provided directly with secret_format=g1v1, got: {:?}", + secret_format + ))); + } + + // Create keypair from provided G1v1 ID and password + let seed = seed_from_cesium(id, pwd); + let secret_suri = format!("0x{}", hex::encode(seed)); + + // G1v1 always uses Ed25519 + let key_pair = compute_keypair(CryptoScheme::Ed25519, &secret_suri)?; + + VaultDataToImport { + secret_format, + secret_suri, + key_pair, + } + } else { + // Interactive mode + prompt_secret_and_compute_vault_data_to_import(secret_format, crypto_scheme)? + }; //Extra check for SecretFormat::G1v1 (old cesium) - showing the G1v1 cesium public key for confirmation if secret_format == SecretFormat::G1v1 { @@ -260,17 +325,28 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE "The G1v1 public key for the provided secret is: '{}'", compute_g1v1_public_key(&vault_data_for_import.key_pair)? ); - let confirmed = inputs::confirm_action("Is it the correct one (if not, you should try again to input G1v1 id/password) ?".to_string())?; - if !confirmed { - return Ok(()); + + // Skip confirmation in non-interactive mode + let is_non_interactive_g1v1 = g1v1_id.is_some() && g1v1_password.is_some(); + if !is_non_interactive_g1v1 { + let confirmed = inputs::confirm_action("Is it the correct one (if not, you should try again to input G1v1 id/password) ?".to_string())?; + if !confirmed { + return Ok(()); + } } } let txn = db.begin().await?; - - println!(); + + // Handle password in non-interactive mode + let provided_password = if no_password { + Some(String::new()) // Empty password + } else { + password + }; + let _account = - create_base_account_for_vault_data_to_import(&txn, &vault_data_for_import, None, Some(crypto_scheme)) + create_base_account_for_vault_data_to_import(&txn, &vault_data_for_import, provided_password.as_ref(), Some(crypto_scheme), name) .await?; txn.commit().await?; @@ -489,6 +565,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE &vault_data_to_import, Some(&vault_data_from_file.password), Some(CryptoScheme::Ed25519), + None, ) .await; @@ -757,6 +834,7 @@ pub async fn create_base_account_for_vault_data_to_import<C>( vault_data: &VaultDataToImport, password_opt: Option<&String>, crypto_scheme: Option<CryptoScheme>, + name_opt: Option<String>, ) -> Result<vault_account::Model, GcliError> where C: ConnectionTrait, @@ -780,14 +858,16 @@ where existing_vault_account ))? { true => { - println!( - "(Optional) Enter a name for the vault entry (leave empty to remove the name)" - ); - let name = inputs::prompt_vault_name_and_check_availability( - db_tx, - existing_vault_account.name.as_ref(), - ) - .await?; + let name = if let Some(name) = name_opt { + Some(name) + } else { + println!("(Optional) Enter a name for the vault entry (leave empty to remove the name)"); + inputs::prompt_vault_name_and_check_availability( + db_tx, + existing_vault_account.name.as_ref(), + ) + .await? + }; // Since links are made based on address / parent(address) we can just edit the existing entry and it should be fine let mut vault_account: ActiveModel = existing_vault_account.into(); @@ -797,7 +877,7 @@ where map_secret_format_to_crypto_scheme(vault_data.secret_format, crypto_scheme).into(), )); vault_account.encrypted_suri = Set(Some(encrypted_suri)); - vault_account.name = Set(name.clone()); + vault_account.name = Set(name); vault_account.secret_format = Set(Some(vault_data.secret_format.into())); let updated_vault_account = vault_account::update_account(db_tx, vault_account).await?; @@ -815,8 +895,12 @@ where let encrypted_suri = compute_encrypted_suri(password, vault_data.secret_suri.clone())?; - println!("(Optional) Enter a name for the vault entry"); - let name = inputs::prompt_vault_name_and_check_availability(db_tx, None).await?; + let name = if let Some(name) = name_opt { + Some(name) + } else { + println!("(Optional) Enter a name for the vault entry"); + inputs::prompt_vault_name_and_check_availability(db_tx, None).await? + }; let crypto_scheme = map_secret_format_to_crypto_scheme(secret_format, crypto_scheme); -- GitLab From cfda5c160fe4f01d13e845df1022583a9279e0bd Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sat, 22 Mar 2025 14:51:09 +0100 Subject: [PATCH 06/27] * Small fix in doc of function * Applied `cargo fmt` --- src/commands/vault.rs | 155 ++++++++++++++++++++++------------ src/commands/vault/display.rs | 101 +++++++++++++--------- src/keys.rs | 152 ++++++++++++++++----------------- 3 files changed, 233 insertions(+), 175 deletions(-) diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 07b40fc..8d7b814 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -3,12 +3,12 @@ mod display; use crate::commands::cesium::compute_g1v1_public_key; use crate::entities::vault_account; use crate::entities::vault_account::{AccountTreeNode, ActiveModel, DbAccountId}; -use crate::*; use crate::keys::seed_from_cesium; +use crate::*; use age::secrecy::Secret; use sea_orm::ActiveValue::Set; -use sea_orm::{ConnectionTrait, TransactionTrait}; use sea_orm::ModelTrait; +use sea_orm::{ConnectionTrait, TransactionTrait}; use sp_core::crypto::AddressUri; use std::cell::RefCell; use std::io::{Read, Write}; @@ -42,31 +42,31 @@ pub enum Subcommand { /// Secret key format (substrate, seed, g1v1) #[clap(short = 'S', long, required = false, default_value = SecretFormat::Substrate)] secret_format: SecretFormat, - + /// Crypto scheme to use (sr25519, ed25519) #[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)] crypto_scheme: CryptoScheme, - + /// Substrate URI to import (non-interactive mode) #[clap(short = 'u', long, required = false)] uri: Option<String>, - + /// G1v1 ID (non-interactive mode for g1v1 format) #[clap(long, required = false)] g1v1_id: Option<String>, - + /// G1v1 password (non-interactive mode for g1v1 format) #[clap(long, required = false)] g1v1_password: Option<String>, - + /// Password for encrypting the key (non-interactive mode) #[clap(short = 'p', long, required = false)] password: Option<String>, - + /// Use empty password (non-interactive mode) #[clap(long, required = false)] no_password: bool, - + /// Name for the wallet entry (non-interactive mode) #[clap(short = 'n', long, required = false)] name: Option<String>, @@ -155,7 +155,10 @@ pub enum ListChoice { impl Default for ListChoice { fn default() -> Self { - ListChoice::All { show_g1v1: false, show_type: false } + ListChoice::All { + show_g1v1: false, + show_type: false, + } } } @@ -209,20 +212,34 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE // match subcommand match command { Subcommand::List(choice) => match choice { - ListChoice::All { show_g1v1, show_type } => { + ListChoice::All { + show_g1v1, + show_type, + } => { let all_account_tree_node_hierarchies = vault_account::fetch_all_base_account_tree_node_hierarchies(db).await?; - let table = display::compute_vault_accounts_table_with_g1v1(&all_account_tree_node_hierarchies, show_g1v1, show_type)?; + let table = display::compute_vault_accounts_table_with_g1v1( + &all_account_tree_node_hierarchies, + show_g1v1, + show_type, + )?; println!("available SS58 Addresses:"); println!("{table}"); } - ListChoice::Base { show_g1v1, show_type } => { + ListChoice::Base { + show_g1v1, + show_type, + } => { let base_account_tree_nodes = vault_account::fetch_only_base_account_tree_nodes(db).await?; - let table = display::compute_vault_accounts_table_with_g1v1(&base_account_tree_nodes, show_g1v1, show_type)?; + let table = display::compute_vault_accounts_table_with_g1v1( + &base_account_tree_nodes, + show_g1v1, + show_type, + )?; println!("available <Base> SS58 Addresses:"); println!("{table}"); @@ -238,7 +255,11 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let base_account_tree_node = vault_account::get_base_account_tree_node(&account_tree_node); - let table = display::compute_vault_accounts_table_with_g1v1(&[base_account_tree_node], show_g1v1, show_type)?; + let table = display::compute_vault_accounts_table_with_g1v1( + &[base_account_tree_node], + show_g1v1, + show_type, + )?; println!( "available SS58 Addresses linked to {}:", @@ -275,19 +296,28 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let mnemonic = bip39::Mnemonic::generate(12).unwrap(); println!("{mnemonic}"); } - Subcommand::Import { secret_format, crypto_scheme, uri, g1v1_id, g1v1_password, password, no_password, name } => { + Subcommand::Import { + secret_format, + crypto_scheme, + uri, + g1v1_id, + g1v1_password, + password, + no_password, + name, + } => { let vault_data_for_import = if let Some(uri_str) = uri { // Non-interactive mode with provided URI if secret_format != SecretFormat::Substrate { return Err(GcliError::Input(format!( - "URI can only be provided directly with secret_format=substrate, got: {:?}", + "URI can only be provided directly with secret_format=substrate, got: {:?}", secret_format ))); } - + // Create keypair from provided URI let key_pair = compute_keypair(crypto_scheme, &uri_str)?; - + VaultDataToImport { secret_format, secret_suri: uri_str, @@ -301,14 +331,14 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE secret_format ))); } - + // Create keypair from provided G1v1 ID and password let seed = seed_from_cesium(id, pwd); let secret_suri = format!("0x{}", hex::encode(seed)); - + // G1v1 always uses Ed25519 let key_pair = compute_keypair(CryptoScheme::Ed25519, &secret_suri)?; - + VaultDataToImport { secret_format, secret_suri, @@ -325,7 +355,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE "The G1v1 public key for the provided secret is: '{}'", compute_g1v1_public_key(&vault_data_for_import.key_pair)? ); - + // Skip confirmation in non-interactive mode let is_non_interactive_g1v1 = g1v1_id.is_some() && g1v1_password.is_some(); if !is_non_interactive_g1v1 { @@ -337,17 +367,22 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE } let txn = db.begin().await?; - + // Handle password in non-interactive mode let provided_password = if no_password { Some(String::new()) // Empty password } else { password }; - - let _account = - create_base_account_for_vault_data_to_import(&txn, &vault_data_for_import, provided_password.as_ref(), Some(crypto_scheme), name) - .await?; + + let _account = create_base_account_for_vault_data_to_import( + &txn, + &vault_data_for_import, + provided_password.as_ref(), + Some(crypto_scheme), + name, + ) + .await?; txn.commit().await?; @@ -399,12 +434,12 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let derivation_secret_suri = format!("{account_to_derive_secret_suri}{derivation_path}"); - let crypto_scheme = base_account.crypto_scheme + let crypto_scheme = base_account + .crypto_scheme .map(CryptoScheme::from) .unwrap_or(CryptoScheme::Ed25519); // Fallback to Ed25519 if not defined (should never happen) - let derivation_keypair = - compute_keypair(crypto_scheme, &derivation_secret_suri)?; + let derivation_keypair = compute_keypair(crypto_scheme, &derivation_secret_suri)?; let derivation_address: String = derivation_keypair.address().to_string(); @@ -637,7 +672,10 @@ pub fn parse_prefix_and_derivation_path_from_suri( Ok((address_uri.phrase.map(|s| s.to_string()), full_path)) } -fn map_secret_format_to_crypto_scheme(secret_format: SecretFormat, override_crypto_scheme: Option<CryptoScheme>) -> CryptoScheme { +fn map_secret_format_to_crypto_scheme( + secret_format: SecretFormat, + override_crypto_scheme: Option<CryptoScheme>, +) -> CryptoScheme { // If a crypto_scheme is explicitly specified, use it except for G1v1 which must always use Ed25519 if let Some(scheme) = override_crypto_scheme { if secret_format == SecretFormat::G1v1 { @@ -806,19 +844,29 @@ fn prompt_secret_and_compute_vault_data_to_import( crypto_scheme: CryptoScheme, ) -> Result<VaultDataToImport, GcliError> { match secret_format { - SecretFormat::Substrate => { - create_vault_data_to_import(secret_format, crypto_scheme, prompt_secret_substrate_and_compute_keypair) - } - SecretFormat::Seed => { - create_vault_data_to_import(secret_format, crypto_scheme, prompt_seed_and_compute_keypair) - } + SecretFormat::Substrate => create_vault_data_to_import( + secret_format, + crypto_scheme, + prompt_secret_substrate_and_compute_keypair, + ), + SecretFormat::Seed => create_vault_data_to_import( + secret_format, + crypto_scheme, + prompt_seed_and_compute_keypair, + ), SecretFormat::G1v1 => { // G1v1 always uses Ed25519, ignore crypto_scheme - create_vault_data_to_import(secret_format, CryptoScheme::Ed25519, prompt_secret_cesium_and_compute_keypair) - } - SecretFormat::Predefined => { - create_vault_data_to_import(secret_format, crypto_scheme, prompt_predefined_and_compute_keypair) + create_vault_data_to_import( + secret_format, + CryptoScheme::Ed25519, + prompt_secret_cesium_and_compute_keypair, + ) } + SecretFormat::Predefined => create_vault_data_to_import( + secret_format, + crypto_scheme, + prompt_predefined_and_compute_keypair, + ), } } @@ -842,7 +890,8 @@ where let address = vault_data.key_pair.address().to_string(); // Check if the account already exists - let existing_vault_account = vault_account::find_by_id(db_tx, &DbAccountId(vault_data.key_pair.address())).await?; + let existing_vault_account = + vault_account::find_by_id(db_tx, &DbAccountId(vault_data.key_pair.address())).await?; let password = match password_opt { Some(password) => password.clone(), @@ -853,7 +902,7 @@ where if let Some(existing_vault_account) = existing_vault_account { // Existing account - match inputs::confirm_action(&format!( + match inputs::confirm_action(format!( "Account {} already exists. Do you want to update it?", existing_vault_account ))? { @@ -874,20 +923,19 @@ where vault_account.path = Set(None); vault_account.parent = Set(None); vault_account.crypto_scheme = Set(Some( - map_secret_format_to_crypto_scheme(vault_data.secret_format, crypto_scheme).into(), + map_secret_format_to_crypto_scheme(vault_data.secret_format, crypto_scheme) + .into(), )); vault_account.encrypted_suri = Set(Some(encrypted_suri)); vault_account.name = Set(name); vault_account.secret_format = Set(Some(vault_data.secret_format.into())); let updated_vault_account = vault_account::update_account(db_tx, vault_account).await?; - + println!("Updating vault account {updated_vault_account}"); Ok(updated_vault_account) } - _ => { - Err(GcliError::Input("import canceled".into())) - } + _ => Err(GcliError::Input("import canceled".into())), } } else { //New entry @@ -913,7 +961,7 @@ where secret_format, ) .await?; - + println!("Creating <Base> account {account}"); Ok(account) } @@ -1041,11 +1089,8 @@ where Ok(vault_account) } -/// Function will ask for password if not present and compute the encrypted suri -fn compute_encrypted_suri( - password: String, - secret_suri: String, -) -> Result<Vec<u8>, GcliError> { +/// Function will compute the encrypted suri +fn compute_encrypted_suri(password: String, secret_suri: String) -> Result<Vec<u8>, GcliError> { encrypt(secret_suri.as_bytes(), password).map_err(|e| GcliError::Input(e.to_string())) } diff --git a/src/commands/vault/display.rs b/src/commands/vault/display.rs index 5f024d6..fe29971 100644 --- a/src/commands/vault/display.rs +++ b/src/commands/vault/display.rs @@ -37,26 +37,35 @@ pub fn compute_vault_accounts_table_with_g1v1( ) -> Result<Table, GcliError> { let mut table = Table::new(); table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY); - + // Prepare header based on options let mut header = vec![ - if show_g1v1 { "SS58 Address/G1v1 public key" } else { "SS58 Address" }, + if show_g1v1 { + "SS58 Address/G1v1 public key" + } else { + "SS58 Address" + }, "Crypto", ]; - + // Add Type column if show_type is true if show_type { header.push("Type"); } - + // Add remaining columns header.push("Path"); header.push("Name"); - + table.set_header(header); for account_tree_node in account_tree_nodes { - let _ = add_account_tree_node_to_table_with_g1v1(&mut table, account_tree_node, show_g1v1, show_type); + let _ = add_account_tree_node_to_table_with_g1v1( + &mut table, + account_tree_node, + show_g1v1, + show_type, + ); } Ok(table) @@ -118,7 +127,7 @@ pub fn compute_vault_accounts_row_with_g1v1( } else { let crypto_scheme = CryptoScheme::from(account_tree_node.account.crypto_scheme.unwrap()); let crypto_scheme_str: &str = crypto_scheme.into(); - + // Determine the wallet type based on the secret format let wallet_type = if let Some(secret_format) = &account_tree_node.account.secret_format { // If the secret format is available, use it to determine the type @@ -132,7 +141,7 @@ pub fn compute_vault_accounts_row_with_g1v1( // If the secret format is not available, display "Unknown" "Unknown".to_string() }; - + // Add a second line for the G1v1 public key only if show_g1v1 is true and it's an Ed25519 key let is_ed25519 = crypto_scheme == CryptoScheme::Ed25519; if show_g1v1 && is_ed25519 { @@ -142,7 +151,7 @@ pub fn compute_vault_accounts_row_with_g1v1( &account_tree_node.account.address.0 ) ))]; - + // Add empty cells to align with the main line g1v1_row.push(Cell::new("")); if show_type { @@ -150,7 +159,7 @@ pub fn compute_vault_accounts_row_with_g1v1( } g1v1_row.push(Cell::new("")); g1v1_row.push(Cell::new("")); - + rows.push(g1v1_row); } @@ -162,20 +171,17 @@ pub fn compute_vault_accounts_row_with_g1v1( }; // Add the first line - let mut main_row = vec![ - Cell::new(&address), - Cell::new(crypto), - ]; - + let mut main_row = vec![Cell::new(&address), Cell::new(crypto)]; + // Add the Type column if show_type is true if show_type { main_row.push(Cell::new(wallet_type)); } - + // Add the remaining columns main_row.push(Cell::new(&path)); main_row.push(Cell::new(&name)); - + rows.insert(0, main_row); Ok(rows) @@ -184,7 +190,9 @@ pub fn compute_vault_accounts_row_with_g1v1( #[cfg(test)] mod tests { mod vault_accounts_table_tests { - use crate::commands::vault::display::{compute_vault_accounts_table, compute_vault_accounts_table_with_g1v1}; + use crate::commands::vault::display::{ + compute_vault_accounts_table, compute_vault_accounts_table_with_g1v1, + }; use crate::entities::vault_account::tests::account_tree_node_tests::{ mother_account_tree_node, mother_g1v1_account_tree_node, }; @@ -254,26 +262,26 @@ mod tests { fn test_compute_vault_accounts_table_with_g1v1_empty() { // Test with show_g1v1 = true (default behavior) let table = compute_vault_accounts_table_with_g1v1(&[], true, false).unwrap(); - + let expected_table_with_g1v1 = indoc! {r#" ┌─────────────────────────────────────────────────────┐ │ SS58 Address/G1v1 public key Crypto Path Name │ ╞═════════════════════════════════════════════════════╡ └─────────────────────────────────────────────────────┘"# }; - + assert_eq!(table.to_string(), expected_table_with_g1v1); // Test with show_g1v1 = false let table = compute_vault_accounts_table_with_g1v1(&[], false, false).unwrap(); - + let expected_table_without_g1v1 = indoc! {r#" ┌─────────────────────────────────────┐ │ SS58 Address Crypto Path Name │ ╞═════════════════════════════════════╡ └─────────────────────────────────────┘"# }; - + assert_eq!(table.to_string(), expected_table_without_g1v1); } @@ -284,8 +292,9 @@ mod tests { let account_tree_nodes = vec![account_tree_node, g1v1_account_tree_node]; // Test with show_g1v1 = true (default behavior) - let table_with_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true, false).unwrap(); - + let table_with_g1v1 = + compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true, false).unwrap(); + let expected_table_with_g1v1 = indoc! {r#" ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │ SS58 Address/G1v1 public key Crypto Path Name │ @@ -299,12 +308,13 @@ mod tests { │ └ G1v1: 86pW1doyJPVH3jeDPZNQa1UZFBo5zcdvHERcaeE758W7 │ └──────────────────────────────────────────────────────────────────────────────────────────┘"# }; - + assert_eq!(table_with_g1v1.to_string(), expected_table_with_g1v1); // Test with show_g1v1 = false - let table_without_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false, false).unwrap(); - + let table_without_g1v1 = + compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false, false).unwrap(); + let expected_table_without_g1v1 = indoc! {r#" ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │ SS58 Address Crypto Path Name │ @@ -317,7 +327,7 @@ mod tests { │ 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4 ed25519 <Base> MotherG1v1 │ └──────────────────────────────────────────────────────────────────────────────────────────┘"# }; - + assert_eq!(table_without_g1v1.to_string(), expected_table_without_g1v1); } @@ -328,8 +338,9 @@ mod tests { let account_tree_nodes = vec![child1]; // Test with show_g1v1 = true (default behavior) - let table_with_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true, false).unwrap(); - + let table_with_g1v1 = + compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true, false).unwrap(); + let expected_table_with_g1v1 = indoc! {r#" ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ SS58 Address/G1v1 public key Crypto Path Name │ @@ -338,12 +349,13 @@ mod tests { │ │ ├ 5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d //0 Grandchild 1 │ └─────────────────────────────────────────────────────────────────────────────────────┘"# }; - + assert_eq!(table_with_g1v1.to_string(), expected_table_with_g1v1); // Test with show_g1v1 = false - let table_without_g1v1 = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false, false).unwrap(); - + let table_without_g1v1 = + compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false, false).unwrap(); + let expected_table_without_g1v1 = indoc! {r#" ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ SS58 Address Crypto Path Name │ @@ -352,7 +364,7 @@ mod tests { │ │ ├ 5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d //0 Grandchild 1 │ └─────────────────────────────────────────────────────────────────────────────────────┘"# }; - + assert_eq!(table_without_g1v1.to_string(), expected_table_without_g1v1); } @@ -363,8 +375,9 @@ mod tests { let account_tree_nodes = vec![account_tree_node, g1v1_account_tree_node]; // Test with show_type = true and show_g1v1 = true - let table_with_g1v1_and_type = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true, true).unwrap(); - + let table_with_g1v1_and_type = + compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true, true).unwrap(); + let expected_table_with_g1v1_and_type = indoc! {r#" ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ SS58 Address/G1v1 public key Crypto Type Path Name │ @@ -378,12 +391,16 @@ mod tests { │ └ G1v1: 86pW1doyJPVH3jeDPZNQa1UZFBo5zcdvHERcaeE758W7 │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘"# }; - - assert_eq!(table_with_g1v1_and_type.to_string(), expected_table_with_g1v1_and_type); - + + assert_eq!( + table_with_g1v1_and_type.to_string(), + expected_table_with_g1v1_and_type + ); + // Test with show_type = true and show_g1v1 = false - let table_with_type = compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false, true).unwrap(); - + let table_with_type = + compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false, true).unwrap(); + let expected_table_with_type = indoc! {r#" ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ SS58 Address Crypto Type Path Name │ @@ -396,7 +413,7 @@ mod tests { │ 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4 ed25519 G1v1 <Base> MotherG1v1 │ └─────────────────────────────────────────────────────────────────────────────────────────────────────┘"# }; - + assert_eq!(table_with_type.to_string(), expected_table_with_type); } } diff --git a/src/keys.rs b/src/keys.rs index 6097ca5..5ca4191 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -141,17 +141,21 @@ pub fn get_keypair( match (secret_format, secret) { (SecretFormat::Predefined, Some(deriv)) => { match crypto_scheme { - Some(CryptoScheme::Ed25519) => pair_from_ed25519_str(&predefined_suri(deriv)).map(|v| v.into()), + Some(CryptoScheme::Ed25519) => { + pair_from_ed25519_str(&predefined_suri(deriv)).map(|v| v.into()) + } _ => pair_from_predefined(deriv).map(|v| v.into()), // Default to Sr25519 for backward compatibility } - }, + } (secret_format, None) => Ok(prompt_secret(secret_format, crypto_scheme)), (_, Some(secret)) => { match crypto_scheme { - Some(CryptoScheme::Ed25519) => pair_from_secret_with_scheme(secret_format, secret, CryptoScheme::Ed25519), + Some(CryptoScheme::Ed25519) => { + pair_from_secret_with_scheme(secret_format, secret, CryptoScheme::Ed25519) + } _ => Ok(pair_from_secret(secret_format, secret)?.into()), // Default to Sr25519 for backward compatibility } - }, + } } } @@ -174,7 +178,7 @@ pub fn pair_from_secret_with_scheme( hex::decode_to_slice(secret, &mut seed) .map_err(|_| GcliError::Input("Invalid secret".to_string()))?; Ok(ed25519::Pair::from_seed(&seed).into()) - }, + } SecretFormat::G1v1 => unreachable!(), // Already handled above }, (_, CryptoScheme::Sr25519) => pair_from_secret(secret_format, secret).map(|v| v.into()), @@ -249,100 +253,90 @@ pub fn seed_from_cesium(id: &str, pwd: &str) -> [u8; 32] { seed } -pub fn prompt_secret_substrate_and_compute_keypair(crypto_scheme: CryptoScheme) -> (String, KeyPair) { - loop { - 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 crypto_scheme { - CryptoScheme::Sr25519 => { - match pair_from_sr25519_str(&substrate_suri) { - Ok(pair) => return (substrate_suri, pair.into()), - Err(_) => println!("Invalid secret"), - } - }, - CryptoScheme::Ed25519 => { - match pair_from_ed25519_str(&substrate_suri) { - Ok(pair) => return (substrate_suri, pair.into()), - Err(_) => println!("Invalid secret"), - } - } - } - } +pub fn prompt_secret_substrate_and_compute_keypair( + crypto_scheme: CryptoScheme, +) -> (String, KeyPair) { + loop { + 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 crypto_scheme { + CryptoScheme::Sr25519 => match pair_from_sr25519_str(&substrate_suri) { + Ok(pair) => return (substrate_suri, pair.into()), + Err(_) => println!("Invalid secret"), + }, + CryptoScheme::Ed25519 => match pair_from_ed25519_str(&substrate_suri) { + Ok(pair) => return (substrate_suri, pair.into()), + Err(_) => println!("Invalid secret"), + }, + } + } } /// ask user pass (Cesium format) pub fn prompt_secret_cesium() -> ed25519::Pair { - // Only interested in the keypair which is the second element of the tuple - match prompt_secret_cesium_and_compute_keypair(CryptoScheme::Ed25519).1 { - KeyPair::Ed25519(pair) => pair, - _ => panic!("Expected Ed25519 keypair"), - } + // Only interested in the keypair which is the second element of the tuple + match prompt_secret_cesium_and_compute_keypair(CryptoScheme::Ed25519).1 { + KeyPair::Ed25519(pair) => pair, + _ => panic!("Expected Ed25519 keypair"), + } } pub fn prompt_secret_cesium_and_compute_keypair(_crypto_scheme: CryptoScheme) -> (String, KeyPair) { - let id = inputs::prompt_password_query("G1v1 id: ").unwrap(); - let pwd = inputs::prompt_password_query("G1v1 password: ").unwrap(); + let id = inputs::prompt_password_query("G1v1 id: ").unwrap(); + let pwd = inputs::prompt_password_query("G1v1 password: ").unwrap(); - let seed = seed_from_cesium(&id, &pwd); - let secret_suri = format!("0x{}", hex::encode(seed)); + let seed = seed_from_cesium(&id, &pwd); + let secret_suri = format!("0x{}", hex::encode(seed)); - // G1v1 always uses Ed25519, ignore crypto_scheme - match pair_from_ed25519_str(&secret_suri) { - Ok(pair) => (secret_suri, pair.into()), - Err(_) => panic!("Could not compute KeyPair from G1v1 id/pwd"), - } + // G1v1 always uses Ed25519, ignore crypto_scheme + match pair_from_ed25519_str(&secret_suri) { + Ok(pair) => (secret_suri, pair.into()), + Err(_) => panic!("Could not compute KeyPair from G1v1 id/pwd"), + } } pub fn prompt_seed_and_compute_keypair(crypto_scheme: CryptoScheme) -> (String, KeyPair) { - loop { - let seed_str = inputs::prompt_seed().unwrap(); - let secret_suri = format!("0x{}", seed_str); - - match crypto_scheme { - CryptoScheme::Sr25519 => { - match pair_from_sr25519_str(&secret_suri) { - Ok(pair) => return (secret_suri, pair.into()), - Err(_) => println!("Invalid seed"), - } - }, - CryptoScheme::Ed25519 => { - match pair_from_ed25519_str(&secret_suri) { - Ok(pair) => return (secret_suri, pair.into()), - Err(_) => println!("Invalid seed"), - } - } - } - } + loop { + let seed_str = inputs::prompt_seed().unwrap(); + let secret_suri = format!("0x{}", seed_str); + + match crypto_scheme { + CryptoScheme::Sr25519 => match pair_from_sr25519_str(&secret_suri) { + Ok(pair) => return (secret_suri, pair.into()), + Err(_) => println!("Invalid seed"), + }, + CryptoScheme::Ed25519 => match pair_from_ed25519_str(&secret_suri) { + Ok(pair) => return (secret_suri, pair.into()), + Err(_) => println!("Invalid seed"), + }, + } + } } pub fn prompt_predefined_and_compute_keypair(crypto_scheme: CryptoScheme) -> (String, KeyPair) { - let deriv = inputs::prompt_password_query("Enter derivation path: ").unwrap(); - let suri = predefined_suri(&deriv); - - match crypto_scheme { - CryptoScheme::Sr25519 => { - match pair_from_sr25519_str(&suri) { - Ok(pair) => (suri, pair.into()), - Err(e) => panic!("Invalid secret: {}", e), - } - }, - CryptoScheme::Ed25519 => { - match pair_from_ed25519_str(&suri) { - Ok(pair) => (suri, pair.into()), - Err(e) => panic!("Invalid secret: {}", e), - } - } - } + let deriv = inputs::prompt_password_query("Enter derivation path: ").unwrap(); + let suri = predefined_suri(&deriv); + + match crypto_scheme { + CryptoScheme::Sr25519 => match pair_from_sr25519_str(&suri) { + Ok(pair) => (suri, pair.into()), + Err(e) => panic!("Invalid secret: {}", e), + }, + CryptoScheme::Ed25519 => match pair_from_ed25519_str(&suri) { + Ok(pair) => (suri, pair.into()), + Err(e) => panic!("Invalid secret: {}", e), + }, + } } pub fn prompt_secret(secret_format: SecretFormat, crypto_scheme: Option<CryptoScheme>) -> KeyPair { let default_scheme = match secret_format { SecretFormat::G1v1 => CryptoScheme::Ed25519, // G1v1 always uses Ed25519 - _ => CryptoScheme::Ed25519, // All formats use Ed25519 by default + _ => CryptoScheme::Ed25519, // All formats use Ed25519 by default }; - + let scheme = crypto_scheme.unwrap_or(default_scheme); - + match secret_format { SecretFormat::Substrate => prompt_secret_substrate_and_compute_keypair(scheme).1, SecretFormat::G1v1 => prompt_secret_cesium_and_compute_keypair(CryptoScheme::Ed25519).1, // G1v1 always uses Ed25519 @@ -362,7 +356,9 @@ pub async fn fetch_or_get_keypair( // keypair is already known (useful for dev mode) if let Some(d) = catch_known(&address.to_string()) { match crypto_scheme { - Some(CryptoScheme::Ed25519) => return pair_from_ed25519_str(&predefined_suri(d)).map(|v| v.into()), + Some(CryptoScheme::Ed25519) => { + return pair_from_ed25519_str(&predefined_suri(d)).map(|v| v.into()) + } _ => return Ok(pair_from_predefined(d).unwrap().into()), // Default to Sr25519 for backward compatibility } }; -- GitLab From 3738e8b7ccab8bd943203f0f11322e3d6f71162a Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sat, 22 Mar 2025 14:52:36 +0100 Subject: [PATCH 07/27] * Fix for `vault migrate` that should use sr25519 scheme to be consistent --- src/commands/vault.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 8d7b814..3098833 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -595,11 +595,12 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let txn = db.begin().await?; + // Old key files were in Sr25519 format (and had the Address associated to that scheme) let account = create_base_account_for_vault_data_to_import( &txn, &vault_data_to_import, Some(&vault_data_from_file.password), - Some(CryptoScheme::Ed25519), + Some(CryptoScheme::Sr25519), None, ) .await; -- GitLab From 44073020267415cef2663c6637e4aae939ec56c7 Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sun, 23 Mar 2025 08:45:05 +0100 Subject: [PATCH 08/27] * Adding `-c` crypto scheme parameter (with default ed25519) in different places ** as global parameter (taken into account if `-S` parameter is given ** as extra parameter for identity.rs commands LinkAccount & ChangeOwnerKey *** Also added a simple display of "target" address and crypto-scheme when performing those commands --- src/commands/identity.rs | 26 ++++++++++++++++++++++---- src/data.rs | 6 +++++- src/keys.rs | 9 +++++++++ src/main.rs | 9 ++++++--- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/commands/identity.rs b/src/commands/identity.rs index 6a64121..5048f45 100644 --- a/src/commands/identity.rs +++ b/src/commands/identity.rs @@ -64,24 +64,30 @@ pub enum Subcommand { MemberCount, /// Link an account to the identity LinkAccount { - /// Secret key format (seed, substrate) + /// Secret key format of account to link (seed, substrate) #[clap(short = 'S', long, default_value = SecretFormat::Substrate)] secret_format: SecretFormat, /// Secret of account to link /// most likely different from the one owning the identity #[clap(short, long)] secret: Option<String>, + /// Crypto scheme of account to link (sr25519, ed25519) + #[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)] + crypto_scheme: CryptoScheme, }, /// Migrate identity to another account /// Change Owner Key ChangeOwnerKey { - /// Secret key format (seed, substrate) + /// Secret key format of account to link (seed, substrate) #[clap(short = 'S', long, default_value = SecretFormat::Substrate)] secret_format: SecretFormat, /// Secret of account to link /// most likely different from the one owning the identity #[clap(short, long)] secret: Option<String>, + /// Crypto scheme of account to link (sr25519, ed25519) + #[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)] + crypto_scheme: CryptoScheme, }, } @@ -153,8 +159,14 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE Subcommand::LinkAccount { secret_format, secret, + crypto_scheme, } => { - let keypair = get_keypair(secret_format, secret.as_deref(), None)?; + let keypair = get_keypair(secret_format, secret.as_deref(), Some(crypto_scheme))?; + println!( + "target address:'{}' (using crypto-scheme:{})", + keypair.address(), + <&'static str>::from(crypto_scheme) + ); let address = keypair.address(); data = data.fetch_idty_index().await?; // idty index required for payload link_account(&data, address, keypair).await?; @@ -162,8 +174,14 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE Subcommand::ChangeOwnerKey { secret_format, secret, + crypto_scheme, } => { - let keypair = get_keypair(secret_format, secret.as_deref(), None)?; + let keypair = get_keypair(secret_format, secret.as_deref(), Some(crypto_scheme))?; + println!( + "target address:'{}' (using crypto-scheme:{})", + keypair.address(), + <&'static str>::from(crypto_scheme) + ); let address = keypair.address(); data = data.fetch_idty_index().await?; // idty index required for payload change_owner_key(&data, address, keypair).await?; diff --git a/src/data.rs b/src/data.rs index 40f9918..c9aa7c9 100644 --- a/src/data.rs +++ b/src/data.rs @@ -207,7 +207,11 @@ impl Data { } // secret format and value if let Some(secret_format) = self.args.secret_format { - let keypair = get_keypair(secret_format, self.args.secret.as_deref(), None)?; + let keypair = get_keypair( + secret_format, + self.args.secret.as_deref(), + Some(self.args.crypto_scheme), + )?; self.cfg.address = Some(keypair.address()); self.keypair = Some(keypair); } diff --git a/src/keys.rs b/src/keys.rs index 5ca4191..415b50f 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -62,6 +62,15 @@ pub enum CryptoScheme { Sr25519, } +/// Setting a default to Ed25519 +/// +/// required when used in Args struct inside main.rs; even though we still have to give a clap "default_value" +impl Default for CryptoScheme { + fn default() -> Self { + CryptoScheme::Ed25519 + } +} + impl FromStr for CryptoScheme { type Err = std::io::Error; diff --git a/src/main.rs b/src/main.rs index 116552d..26a87da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,13 +46,16 @@ pub struct Args { /// Do not use indexer #[clap(long)] no_indexer: bool, + /// Secret key format (seed, substrate, g1v1) + #[clap(short = 'S', long)] + secret_format: Option<SecretFormat>, /// Secret key or BIP39 mnemonic (only used when secret format is compatible) /// (eventually followed by derivation path) #[clap(short, long)] secret: Option<String>, - /// Secret key format (seed, substrate, g1v1) - #[clap(short = 'S', long)] - secret_format: Option<SecretFormat>, + /// Crypto scheme to use (sr25519, ed25519) + #[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)] + crypto_scheme: CryptoScheme, /// SS58 Address #[clap(short, conflicts_with = "name")] address: Option<AccountId>, -- GitLab From da7c790d109059b77533ee0651ed90265ab816ac Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sun, 23 Mar 2025 15:46:16 +0100 Subject: [PATCH 09/27] * Added some logic between arguments of `vault import`; can't provide both `password` and `no-password` ** Added extra validation of non-interactive `name` argument value (same validation as when interactive: no '<', '>', '/' characters) * Added possibility to make a non-interactive derivation (given proper arguments are given and there is no issue found during the process) ** Added same validation for non-interactive `derivation_path` argument as when interactive ** If the same resulting address is already in the vault; interaction is still mandatory to make a choice * Had to change the name of argument AddressOrVaultNameGroup.name => vault_name to avoid conflict in `vault derive` ** Not changing the `-v` shortcut so no impact on existing commands * Allowing to pass "" empty string as non-interactive `name` argument and considering it as None (does a trim before checking empty; so only spaces will be considered as None as well) --- src/commands/vault.rs | 87 +++++++++++++++++++++++++++++++++---------- src/inputs.rs | 80 ++++++++++++++++++++++++--------------- 2 files changed, 118 insertions(+), 49 deletions(-) diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 3098833..0be934a 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -3,6 +3,7 @@ mod display; use crate::commands::cesium::compute_g1v1_public_key; use crate::entities::vault_account; use crate::entities::vault_account::{AccountTreeNode, ActiveModel, DbAccountId}; +use crate::inputs::{trim_and_reduce_empty_as_none, validate_derivation_path, validate_vault_name}; use crate::keys::seed_from_cesium; use crate::*; use age::secrecy::Secret; @@ -60,14 +61,14 @@ pub enum Subcommand { g1v1_password: Option<String>, /// Password for encrypting the key (non-interactive mode) - #[clap(short = 'p', long, required = false)] + #[clap(short = 'p', long, required = false, conflicts_with_all=["no_password"])] password: Option<String>, - /// Use empty password (non-interactive mode) + /// Use empty password for encrypting the key (non-interactive mode) #[clap(long, required = false)] no_password: bool, - /// Name for the wallet entry (non-interactive mode) + /// Name for the wallet entry (non-interactive mode) - "" empty string will be considered as None #[clap(short = 'n', long, required = false)] name: Option<String>, }, @@ -83,6 +84,22 @@ pub enum Subcommand { Derive { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, + + /// Derivation path (non-interactive mode) + #[clap(short = 'd', long, required = false)] + derivation_path: Option<String>, + + /// Password to decrypt the <Base> account key (non-interactive mode) + #[clap(short = 'p', long, required = false, requires = "derivation_path", conflicts_with_all=["no_password"])] + password: Option<String>, + + /// Use empty password to decrypt the <Base> account key (non-interactive mode) + #[clap(long, required = false, requires = "derivation_path")] + no_password: bool, + + /// Name for the wallet entry (non-interactive mode) - "" empty string will be considered as None + #[clap(short = 'n', long, required = false, requires = "derivation_path")] + name: Option<String>, }, /// Give a meaningful name to an SS58 Address in the vault Rename { @@ -170,7 +187,7 @@ pub struct AddressOrVaultNameGroup { address: Option<AccountId>, /// Name of an SS58 Address in the vault #[clap(short = 'v')] - name: Option<String>, + vault_name: Option<String>, } pub struct VaultDataToImport { @@ -390,6 +407,10 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE } Subcommand::Derive { address_or_vault_name, + derivation_path, + password, + no_password, + name, } => { let account_tree_node_to_derive = retrieve_account_tree_node(db, address_or_vault_name).await?; @@ -420,8 +441,15 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE println!("The linked <Base> account is {base_account}"); - println!("Enter password to decrypt the <Base> account key"); - let password = inputs::prompt_password()?; + // Handle password from non-interactive mode or ask for it + let password = if no_password { + String::new() + } else if let Some(password) = password { + password + } else { + println!("Enter password to decrypt the <Base> account key"); + inputs::prompt_password()? + }; let account_to_derive_secret_suri = vault_account::compute_suri_account_tree_node( &account_tree_node_to_derive, @@ -429,7 +457,14 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE )?; println!(); - let derivation_path = inputs::prompt_vault_derivation_path()?; + + // Handle derivation_path from non-interactive mode or ask for it + let derivation_path = if let Some(derivation_path) = derivation_path { + validate_derivation_path(derivation_path.clone())?; + derivation_path + } else { + inputs::prompt_vault_derivation_path()? + }; let derivation_secret_suri = format!("{account_to_derive_secret_suri}{derivation_path}"); @@ -451,6 +486,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE &derivation_address, &derivation_path, &account_to_derive.address.to_string(), + name, ) .await?; @@ -742,7 +778,7 @@ pub async fn retrieve_account_tree_node<C>( where C: ConnectionTrait, { - let account_tree_node = if let Some(name_input) = &address_or_vault_name.name { + let account_tree_node = if let Some(name_input) = &address_or_vault_name.vault_name { retrieve_account_tree_node_for_name(db, name_input).await? } else if let Some(address) = &address_or_vault_name.address { let base_account_tree_node = @@ -909,7 +945,8 @@ where ))? { true => { let name = if let Some(name) = name_opt { - Some(name) + validate_vault_name(&name)?; + trim_and_reduce_empty_as_none(name) } else { println!("(Optional) Enter a name for the vault entry (leave empty to remove the name)"); inputs::prompt_vault_name_and_check_availability( @@ -945,7 +982,8 @@ where let encrypted_suri = compute_encrypted_suri(password, vault_data.secret_suri.clone())?; let name = if let Some(name) = name_opt { - Some(name) + validate_vault_name(&name)?; + trim_and_reduce_empty_as_none(name) } else { println!("(Optional) Enter a name for the vault entry"); inputs::prompt_vault_name_and_check_availability(db_tx, None).await? @@ -980,6 +1018,7 @@ pub async fn create_derivation_account<C>( derivation_address: &String, derivation_path: &String, parent_address: &String, + name_opt: Option<String>, ) -> Result<vault_account::Model, GcliError> where C: ConnectionTrait, @@ -1044,14 +1083,17 @@ where let result = inputs::select_action("Your choice?", vec!["1", "2"])?; match result { "2" => { - println!( - "(Optional) Enter a name for the vault entry (leave empty to remove the name)" - ); - let name = inputs::prompt_vault_name_and_check_availability( - db_tx, - existing_vault_account.name.as_ref(), - ) - .await?; + let name = if let Some(name) = name_opt { + validate_vault_name(&name)?; + trim_and_reduce_empty_as_none(name) + } else { + println!("(Optional) Enter a name for the vault entry (leave empty to remove the name)"); + inputs::prompt_vault_name_and_check_availability( + db_tx, + existing_vault_account.name.as_ref(), + ) + .await? + }; // Since links are made based on address / parent(address) we can just edit the existing entry and it should be fine let mut vault_account: ActiveModel = existing_vault_account.into(); @@ -1071,8 +1113,13 @@ where } } } else { - println!("(Optional) Enter a name for the vault entry"); - let name = inputs::prompt_vault_name_and_check_availability(db_tx, None).await?; + let name = if let Some(name) = name_opt { + validate_vault_name(&name)?; + trim_and_reduce_empty_as_none(name) + } else { + println!("(Optional) Enter a name for the vault entry"); + inputs::prompt_vault_name_and_check_availability(db_tx, None).await? + }; let derivation = vault_account::create_derivation_account( db_tx, diff --git a/src/inputs.rs b/src/inputs.rs index ee84a6d..56ad96f 100644 --- a/src/inputs.rs +++ b/src/inputs.rs @@ -46,15 +46,16 @@ where C: ConnectionTrait, { loop { - let mut text_inquire = inquire::Text::new("Name:").with_validator({ - |input: &str| { - if input.contains('<') || input.contains('>') || input.contains('/') { - return Ok(Validation::Invalid( - "Name cannot contain characters '<', '>', '/'".into(), - )); + let mut text_inquire = inquire::Text::new("Name:").with_validator(|input: &str| { + match validate_vault_name(input) { + 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())) + } } - - Ok(Validation::Valid) } }); @@ -66,11 +67,7 @@ where .prompt() .map_err(|e| GcliError::Input(e.to_string()))?; - let name = if name.trim().is_empty() { - None - } else { - Some(name.trim().to_string()) - }; + let name = trim_and_reduce_empty_as_none(name); let available = vault_account::check_name_available(db, initial_name, name.as_ref()).await?; @@ -86,31 +83,56 @@ where } } +pub fn trim_and_reduce_empty_as_none(name: String) -> Option<String> { + if name.trim().is_empty() { + None + } else { + Some(name.trim().to_string()) + } +} + +pub fn validate_vault_name(vault_name: &str) -> Result<(), GcliError> { + if vault_name.contains('<') || vault_name.contains('>') || vault_name.contains('/') { + return Err(GcliError::Input( + "Name cannot contain characters '<', '>', '/'".into(), + )); + } + + Ok(()) +} + /// Prompt for a derivation path pub fn prompt_vault_derivation_path() -> Result<String, GcliError> { inquire::Text::new("Derivation path:") - .with_validator(|input: &str| { - if !input.starts_with("/") { - Ok(Validation::Invalid( - "derivation path needs to start with one or more '/'".into(), - )) - } else { - 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())) - } + .with_validator( + |input: &str| match validate_derivation_path(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() .map_err(|e| GcliError::Input(e.to_string())) } +pub fn validate_derivation_path(derivation_path: String) -> Result<(), GcliError> { + if !derivation_path.starts_with("/") { + Err(GcliError::Input( + "derivation path needs to start with one or more '/'".into(), + )) + } else { + match vault::parse_prefix_and_derivation_path_from_suri(derivation_path.to_string()) { + Ok(_) => Ok(()), + Err(error) => Err(error), + } + } +} + pub fn confirm_action(query: impl ToString) -> Result<bool, GcliError> { inquire::Confirm::new(query.to_string().as_str()) .prompt() -- GitLab From 8bf81cbcd143e4f878c4386b44a89fb3e48a4a69 Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sun, 23 Mar 2025 16:43:02 +0100 Subject: [PATCH 10/27] * Adapted catch_known of predefined derivations to properly handle both sr25519 and ed25519 versions of those keys. --- src/keys.rs | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/keys.rs b/src/keys.rs index 415b50f..759211f 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -362,14 +362,14 @@ pub async fn fetch_or_get_keypair( ) -> Result<KeyPair, GcliError> { if let Some(address) = address { // if address corresponds to predefined, (for example saved to config) - // keypair is already known (useful for dev mode) - if let Some(d) = catch_known(&address.to_string()) { - match crypto_scheme { - Some(CryptoScheme::Ed25519) => { - return pair_from_ed25519_str(&predefined_suri(d)).map(|v| v.into()) + // keypair is already known (useful for dev mode) - also overrides crypto_scheme if found + if let Some((deriv, crypto_scheme)) = catch_known(&address.to_string()) { + return match crypto_scheme { + CryptoScheme::Ed25519 => { + pair_from_ed25519_str(&predefined_suri(deriv)).map(|v| v.into()) } - _ => return Ok(pair_from_predefined(d).unwrap().into()), // Default to Sr25519 for backward compatibility - } + CryptoScheme::Sr25519 => pair_from_predefined(deriv).map(|v| v.into()), + }; }; // look for corresponding KeyPair in keystore @@ -383,13 +383,27 @@ pub async fn fetch_or_get_keypair( } // catch known addresses -fn catch_known(address: &str) -> Option<&str> { +fn catch_known(address: &str) -> Option<(&str, CryptoScheme)> { match address { - "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" => Some("Alice"), - "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" => Some("Bob"), - "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y" => Some("Charlie"), - "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" => Some("Dave"), - "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" => Some("Eve"), + "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" => { + Some(("Alice", CryptoScheme::Sr25519)) + } + "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" => Some(("Bob", CryptoScheme::Sr25519)), + "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y" => { + Some(("Charlie", CryptoScheme::Sr25519)) + } + "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" => Some(("Dave", CryptoScheme::Sr25519)), + "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" => Some(("Eve", CryptoScheme::Sr25519)), + + "5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu" => { + Some(("Alice", CryptoScheme::Ed25519)) + } + "5GoNkf6WdbxCFnPdAnYYQyCjAKPJgLNxXwPjwTh6DGg6gN3E" => Some(("Bob", CryptoScheme::Ed25519)), + "5DbKjhNLpqX3zqZdNBc9BGb4fHU1cRBaDhJUskrvkwfraDi6" => { + Some(("Charlie", CryptoScheme::Ed25519)) + } + "5ECTwv6cZ5nJQPk6tWfaTrEk8YH2L7X1VT4EL5Tx2ikfFwb7" => Some(("Dave", CryptoScheme::Ed25519)), + "5Ck2miBfCe1JQ4cY3NDsXyBaD6EcsgiVmEFTWwqNSs25XDEq" => Some(("Eve", CryptoScheme::Ed25519)), _ => None, } } -- GitLab From 0da710798faaca9b74759ba49252b195f254b1b0 Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sun, 23 Mar 2025 18:03:37 +0100 Subject: [PATCH 11/27] * reverted most of the commit e9cd6a99: "Add secret format in database and display" --- src/commands/vault.rs | 124 +++++++++++++++++------------ src/commands/vault/display.rs | 143 ++++++---------------------------- src/entities/vault_account.rs | 51 ------------ 3 files changed, 98 insertions(+), 220 deletions(-) diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 0be934a..5fe703f 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -142,9 +142,6 @@ pub enum ListChoice { /// Show G1v1 public key for ed25519 keys #[clap(long)] show_g1v1: bool, - /// Show wallet type (g1v1 or mnemonic) - #[clap(long)] - show_type: bool, }, /// List only base accounts #[clap(alias = "b")] @@ -152,9 +149,6 @@ pub enum ListChoice { /// Show G1v1 public key for ed25519 keys #[clap(long)] show_g1v1: bool, - /// Show wallet type (g1v1 or mnemonic) - #[clap(long)] - show_type: bool, }, /// List accounts for a specific address #[clap(alias = "f")] @@ -164,18 +158,12 @@ pub enum ListChoice { /// Show G1v1 public key for ed25519 keys #[clap(long)] show_g1v1: bool, - /// Show wallet type (g1v1 or mnemonic) - #[clap(long)] - show_type: bool, }, } impl Default for ListChoice { fn default() -> Self { - ListChoice::All { - show_g1v1: false, - show_type: false, - } + ListChoice::All { show_g1v1: false } } } @@ -229,33 +217,25 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE // match subcommand match command { Subcommand::List(choice) => match choice { - ListChoice::All { - show_g1v1, - show_type, - } => { + ListChoice::All { show_g1v1 } => { let all_account_tree_node_hierarchies = vault_account::fetch_all_base_account_tree_node_hierarchies(db).await?; let table = display::compute_vault_accounts_table_with_g1v1( &all_account_tree_node_hierarchies, show_g1v1, - show_type, )?; println!("available SS58 Addresses:"); println!("{table}"); } - ListChoice::Base { - show_g1v1, - show_type, - } => { + ListChoice::Base { show_g1v1 } => { let base_account_tree_nodes = vault_account::fetch_only_base_account_tree_nodes(db).await?; let table = display::compute_vault_accounts_table_with_g1v1( &base_account_tree_nodes, show_g1v1, - show_type, )?; println!("available <Base> SS58 Addresses:"); @@ -264,7 +244,6 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE ListChoice::For { address_or_vault_name, show_g1v1, - show_type, } => { let account_tree_node = retrieve_account_tree_node(db, address_or_vault_name).await?; @@ -275,7 +254,6 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let table = display::compute_vault_accounts_table_with_g1v1( &[base_account_tree_node], show_g1v1, - show_type, )?; println!( @@ -344,7 +322,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE // Non-interactive mode with provided G1v1 ID and password if secret_format != SecretFormat::G1v1 { return Err(GcliError::Input(format!( - "G1v1 ID and password can only be provided directly with secret_format=g1v1, got: {:?}", + "G1v1 ID and password can only be provided directly with secret_format=g1v1, got: {:?}", secret_format ))); } @@ -924,26 +902,67 @@ pub async fn create_base_account_for_vault_data_to_import<C>( where C: ConnectionTrait, { - let address = vault_data.key_pair.address().to_string(); + let address_to_import = vault_data.key_pair.address().to_string(); + println!("Trying to import for SS58 address :'{}'", address_to_import); + println!(); - // Check if the account already exists - let existing_vault_account = - vault_account::find_by_id(db_tx, &DbAccountId(vault_data.key_pair.address())).await?; + if let Some(existing_vault_account) = + vault_account::find_by_id(db_tx, &DbAccountId::from(address_to_import.clone())).await? + { + // Existing account + if existing_vault_account.is_base_account() { + println!("You are trying to add {address_to_import} as a <Base> account while it already exists as a <Base> account."); + println!(); + println!("Do you want to:"); + println!("1. keep the existing <Base> account and cancel import"); + println!("2. overwrite existing <Base> account with the new encrypted key (children will be re-parented)"); + } else { + // Existing derivation account + let account_tree_node_hierarchy = + vault_account::fetch_base_account_tree_node_hierarchy_unwrapped( + db_tx, + &address_to_import, + ) + .await?; + let account_tree_node_for_address = vault_account::get_account_tree_node_for_address( + &account_tree_node_hierarchy, + &address_to_import, + ); - let password = match password_opt { - Some(password) => password.clone(), - None => inputs::prompt_password_query("Enter password to encrypt the key: ")?, - }; + let base_parent_hierarchy_account_tree_node = + vault_account::get_base_parent_hierarchy_account_tree_node( + &account_tree_node_for_address, + ); - let encrypted_suri = compute_encrypted_suri(password.clone(), vault_data.secret_suri.clone())?; + let parent_hierarchy_table = + display::compute_vault_accounts_table(&[base_parent_hierarchy_account_tree_node])?; + + println!("You are trying to add {address_to_import} as a <Base> account"); + println!( + "but it is already present as `{}` derivation of {} account.", + existing_vault_account.path.clone().unwrap(), + existing_vault_account.parent.clone().unwrap() + ); + println!(); + println!("Its parent hierarchy is this:"); + println!("{parent_hierarchy_table}"); + println!(); + println!("Do you want to:"); + println!("1. keep the existing derivation and cancel import"); + println!("2. delete the derivation account and replace it with the new <Base> account (children will be re-parented)"); + } + + let result = inputs::select_action("Your choice?", vec!["1", "2"])?; + match result { + "2" => { + let password = match password_opt { + Some(password) => password.clone(), + None => inputs::prompt_password_query("Enter password to encrypt the key: ")?, + }; + + let encrypted_suri = + compute_encrypted_suri(password, vault_data.secret_suri.clone())?; - if let Some(existing_vault_account) = existing_vault_account { - // Existing account - match inputs::confirm_action(format!( - "Account {} already exists. Do you want to update it?", - existing_vault_account - ))? { - true => { let name = if let Some(name) = name_opt { validate_vault_name(&name)?; trim_and_reduce_empty_as_none(name) @@ -966,7 +985,6 @@ where )); vault_account.encrypted_suri = Set(Some(encrypted_suri)); vault_account.name = Set(name); - vault_account.secret_format = Set(Some(vault_data.secret_format.into())); let updated_vault_account = vault_account::update_account(db_tx, vault_account).await?; @@ -979,6 +997,11 @@ where //New entry let secret_format = vault_data.secret_format; + let password = match password_opt { + Some(password) => password.clone(), + None => inputs::prompt_password_query("Enter password to encrypt the key: ")?, + }; + let encrypted_suri = compute_encrypted_suri(password, vault_data.secret_suri.clone())?; let name = if let Some(name) = name_opt { @@ -993,11 +1016,10 @@ where let account = vault_account::create_base_account( db_tx, - &address, + &address_to_import, name.as_ref(), crypto_scheme, encrypted_suri, - secret_format, ) .await?; @@ -1290,12 +1312,12 @@ mod tests { 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")) - )] + String::from( + "bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Bob/soft1/soft2" + ), + Some(String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk")), + Some(String::from("//Alice//Bob/soft1/soft2")) + )] #[case( String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"), Some(String::from( diff --git a/src/commands/vault/display.rs b/src/commands/vault/display.rs index fe29971..8822282 100644 --- a/src/commands/vault/display.rs +++ b/src/commands/vault/display.rs @@ -27,45 +27,30 @@ pub fn compute_vault_accounts_table( account_tree_nodes: &[Rc<RefCell<AccountTreeNode>>], ) -> Result<Table, GcliError> { // Appel to the new function with show_g1v1 = true to maintain compatibility - compute_vault_accounts_table_with_g1v1(account_tree_nodes, true, false) + compute_vault_accounts_table_with_g1v1(account_tree_nodes, true) } pub fn compute_vault_accounts_table_with_g1v1( account_tree_nodes: &[Rc<RefCell<AccountTreeNode>>], show_g1v1: bool, - show_type: bool, ) -> Result<Table, GcliError> { let mut table = Table::new(); table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY); // Prepare header based on options - let mut header = vec![ + table.set_header(vec![ if show_g1v1 { "SS58 Address/G1v1 public key" } else { "SS58 Address" }, "Crypto", - ]; - - // Add Type column if show_type is true - if show_type { - header.push("Type"); - } - - // Add remaining columns - header.push("Path"); - header.push("Name"); - - table.set_header(header); + "Path", + "Name", + ]); for account_tree_node in account_tree_nodes { - let _ = add_account_tree_node_to_table_with_g1v1( - &mut table, - account_tree_node, - show_g1v1, - show_type, - ); + let _ = add_account_tree_node_to_table_with_g1v1(&mut table, account_tree_node, show_g1v1); } Ok(table) @@ -75,15 +60,14 @@ fn add_account_tree_node_to_table_with_g1v1( table: &mut Table, account_tree_node: &Rc<RefCell<AccountTreeNode>>, show_g1v1: bool, - show_type: bool, ) -> Result<(), GcliError> { - let rows = compute_vault_accounts_row_with_g1v1(account_tree_node, show_g1v1, show_type)?; + let rows = compute_vault_accounts_row_with_g1v1(account_tree_node, show_g1v1)?; rows.iter().for_each(|row| { table.add_row(row.clone()); }); for child in &account_tree_node.borrow().children { - let _ = add_account_tree_node_to_table_with_g1v1(table, child, show_g1v1, show_type); + let _ = add_account_tree_node_to_table_with_g1v1(table, child, show_g1v1); } Ok(()) @@ -95,7 +79,6 @@ fn add_account_tree_node_to_table_with_g1v1( pub fn compute_vault_accounts_row_with_g1v1( account_tree_node: &Rc<RefCell<AccountTreeNode>>, show_g1v1: bool, - show_type: bool, ) -> Result<Vec<Vec<Cell>>, GcliError> { let empty_string = "".to_string(); @@ -122,26 +105,12 @@ pub fn compute_vault_accounts_row_with_g1v1( let mut rows: Vec<Vec<Cell>> = vec![]; - let (path, crypto, wallet_type) = if let Some(path) = account_tree_node.account.path.clone() { - (path, empty_string.clone(), empty_string.clone()) + let (path, crypto) = if let Some(path) = account_tree_node.account.path.clone() { + (path, empty_string.clone()) } else { let crypto_scheme = CryptoScheme::from(account_tree_node.account.crypto_scheme.unwrap()); let crypto_scheme_str: &str = crypto_scheme.into(); - // Determine the wallet type based on the secret format - let wallet_type = if let Some(secret_format) = &account_tree_node.account.secret_format { - // If the secret format is available, use it to determine the type - match crate::keys::SecretFormat::from(*secret_format) { - crate::keys::SecretFormat::G1v1 => "G1v1".to_string(), - crate::keys::SecretFormat::Substrate => "Mnemonic".to_string(), - crate::keys::SecretFormat::Seed => "Seed".to_string(), - crate::keys::SecretFormat::Predefined => "Predefined".to_string(), - } - } else { - // If the secret format is not available, display "Unknown" - "Unknown".to_string() - }; - // Add a second line for the G1v1 public key only if show_g1v1 is true and it's an Ed25519 key let is_ed25519 = crypto_scheme == CryptoScheme::Ed25519; if show_g1v1 && is_ed25519 { @@ -154,9 +123,6 @@ pub fn compute_vault_accounts_row_with_g1v1( // Add empty cells to align with the main line g1v1_row.push(Cell::new("")); - if show_type { - g1v1_row.push(Cell::new("")); - } g1v1_row.push(Cell::new("")); g1v1_row.push(Cell::new("")); @@ -166,23 +132,19 @@ pub fn compute_vault_accounts_row_with_g1v1( ( format!("<{}>", account_tree_node.account.account_type()), crypto_scheme_str.to_string(), - wallet_type, ) }; // Add the first line - let mut main_row = vec![Cell::new(&address), Cell::new(crypto)]; - - // Add the Type column if show_type is true - if show_type { - main_row.push(Cell::new(wallet_type)); - } - - // Add the remaining columns - main_row.push(Cell::new(&path)); - main_row.push(Cell::new(&name)); - - rows.insert(0, main_row); + rows.insert( + 0, + vec![ + Cell::new(&address), + Cell::new(crypto), + Cell::new(&path), + Cell::new(&name), + ], + ); Ok(rows) } @@ -261,8 +223,7 @@ mod tests { #[test] fn test_compute_vault_accounts_table_with_g1v1_empty() { // Test with show_g1v1 = true (default behavior) - let table = compute_vault_accounts_table_with_g1v1(&[], true, false).unwrap(); - + let table = compute_vault_accounts_table_with_g1v1(&[], true).unwrap(); let expected_table_with_g1v1 = indoc! {r#" ┌─────────────────────────────────────────────────────┐ │ SS58 Address/G1v1 public key Crypto Path Name │ @@ -273,8 +234,7 @@ mod tests { assert_eq!(table.to_string(), expected_table_with_g1v1); // Test with show_g1v1 = false - let table = compute_vault_accounts_table_with_g1v1(&[], false, false).unwrap(); - + let table = compute_vault_accounts_table_with_g1v1(&[], false).unwrap(); let expected_table_without_g1v1 = indoc! {r#" ┌─────────────────────────────────────┐ │ SS58 Address Crypto Path Name │ @@ -293,8 +253,7 @@ mod tests { // Test with show_g1v1 = true (default behavior) let table_with_g1v1 = - compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true, false).unwrap(); - + compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true).unwrap(); let expected_table_with_g1v1 = indoc! {r#" ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │ SS58 Address/G1v1 public key Crypto Path Name │ @@ -313,8 +272,7 @@ mod tests { // Test with show_g1v1 = false let table_without_g1v1 = - compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false, false).unwrap(); - + compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false).unwrap(); let expected_table_without_g1v1 = indoc! {r#" ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │ SS58 Address Crypto Path Name │ @@ -339,8 +297,7 @@ mod tests { // Test with show_g1v1 = true (default behavior) let table_with_g1v1 = - compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true, false).unwrap(); - + compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true).unwrap(); let expected_table_with_g1v1 = indoc! {r#" ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ SS58 Address/G1v1 public key Crypto Path Name │ @@ -354,8 +311,7 @@ mod tests { // Test with show_g1v1 = false let table_without_g1v1 = - compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false, false).unwrap(); - + compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false).unwrap(); let expected_table_without_g1v1 = indoc! {r#" ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ SS58 Address Crypto Path Name │ @@ -367,54 +323,5 @@ mod tests { assert_eq!(table_without_g1v1.to_string(), expected_table_without_g1v1); } - - #[test] - fn test_compute_vault_accounts_table_with_type() { - let account_tree_node = mother_account_tree_node(); - let g1v1_account_tree_node = mother_g1v1_account_tree_node(); - let account_tree_nodes = vec![account_tree_node, g1v1_account_tree_node]; - - // Test with show_type = true and show_g1v1 = true - let table_with_g1v1_and_type = - compute_vault_accounts_table_with_g1v1(&account_tree_nodes, true, true).unwrap(); - - let expected_table_with_g1v1_and_type = indoc! {r#" - ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ SS58 Address/G1v1 public key Crypto Type Path Name │ - ╞═════════════════════════════════════════════════════════════════════════════════════════════════════╡ - │ 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV sr25519 Mnemonic <Base> Mother │ - │ ├ 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH //0 Child 1 │ - │ │ ├ 5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d //0 Grandchild 1 │ - │ ├ 5GBNeWRhZc2jXu7D55rBimKYDk8PGk8itRYFTPfC8RJLKG5o //1 <Mother//1> │ - │ │ ├ 5CvdJuB9HLXSi5FS9LW57cyHF13iCv5HDimo2C45KxnxriCT //1 <Mother//1//1> │ - │ 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4 ed25519 G1v1 <Base> MotherG1v1 │ - │ └ G1v1: 86pW1doyJPVH3jeDPZNQa1UZFBo5zcdvHERcaeE758W7 │ - └─────────────────────────────────────────────────────────────────────────────────────────────────────┘"# - }; - - assert_eq!( - table_with_g1v1_and_type.to_string(), - expected_table_with_g1v1_and_type - ); - - // Test with show_type = true and show_g1v1 = false - let table_with_type = - compute_vault_accounts_table_with_g1v1(&account_tree_nodes, false, true).unwrap(); - - let expected_table_with_type = indoc! {r#" - ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ SS58 Address Crypto Type Path Name │ - ╞═════════════════════════════════════════════════════════════════════════════════════════════════════╡ - │ 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV sr25519 Mnemonic <Base> Mother │ - │ ├ 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH //0 Child 1 │ - │ │ ├ 5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d //0 Grandchild 1 │ - │ ├ 5GBNeWRhZc2jXu7D55rBimKYDk8PGk8itRYFTPfC8RJLKG5o //1 <Mother//1> │ - │ │ ├ 5CvdJuB9HLXSi5FS9LW57cyHF13iCv5HDimo2C45KxnxriCT //1 <Mother//1//1> │ - │ 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4 ed25519 G1v1 <Base> MotherG1v1 │ - └─────────────────────────────────────────────────────────────────────────────────────────────────────┘"# - }; - - assert_eq!(table_with_type.to_string(), expected_table_with_type); - } } } diff --git a/src/entities/vault_account.rs b/src/entities/vault_account.rs index 939ee75..d04b02a 100644 --- a/src/entities/vault_account.rs +++ b/src/entities/vault_account.rs @@ -40,8 +40,6 @@ pub struct Model { pub encrypted_suri: Option<Vec<u8>>, /// ForeignKey to parent vault_account SS58 Address - None if for a "base" account pub parent: Option<DbAccountId>, - /// Secret format used for the account - Only set for "base" accounts - pub secret_format: Option<DbSecretFormat>, } impl Model { @@ -245,46 +243,6 @@ impl From<DbCryptoScheme> for crate::keys::CryptoScheme { } } -/// Enum for SecretFormat in the database -#[derive(Debug, Copy, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] -#[sea_orm( - rs_type = "String", - db_type = "String(StringLen::None)", - enum_name = "secret_format" -)] -pub enum DbSecretFormat { - #[sea_orm(string_value = "seed")] - Seed, - #[sea_orm(string_value = "substrate")] - Substrate, - #[sea_orm(string_value = "predefined")] - Predefined, - #[sea_orm(string_value = "g1v1")] - G1v1, -} - -impl From<crate::keys::SecretFormat> for DbSecretFormat { - fn from(format: crate::keys::SecretFormat) -> Self { - match format { - crate::keys::SecretFormat::Seed => DbSecretFormat::Seed, - crate::keys::SecretFormat::Substrate => DbSecretFormat::Substrate, - crate::keys::SecretFormat::Predefined => DbSecretFormat::Predefined, - crate::keys::SecretFormat::G1v1 => DbSecretFormat::G1v1, - } - } -} - -impl From<DbSecretFormat> for crate::keys::SecretFormat { - fn from(format: DbSecretFormat) -> Self { - match format { - DbSecretFormat::Seed => crate::keys::SecretFormat::Seed, - DbSecretFormat::Substrate => crate::keys::SecretFormat::Substrate, - DbSecretFormat::Predefined => crate::keys::SecretFormat::Predefined, - DbSecretFormat::G1v1 => crate::keys::SecretFormat::G1v1, - } - } -} - #[derive(Copy, Clone, Debug, EnumIter)] pub enum Relation { ParentAccount, @@ -910,7 +868,6 @@ pub async fn create_base_account<C>( name: Option<&String>, crypto_scheme: crate::keys::CryptoScheme, encrypted_suri: Vec<u8>, - secret_format: crate::keys::SecretFormat, ) -> Result<Model, GcliError> where C: ConnectionTrait, @@ -933,7 +890,6 @@ where crypto_scheme: Set(Some(crypto_scheme.into())), encrypted_suri: Set(Some(encrypted_suri)), parent: Default::default(), - secret_format: Set(Some(secret_format.into())), }; vault_account.insert(db).await? } @@ -971,7 +927,6 @@ where crypto_scheme: Set(None), encrypted_suri: Set(None), parent: Set(Some(parent_address.to_string().into())), - secret_format: Set(None), }; vault_account.insert(db).await? } @@ -1028,7 +983,6 @@ pub mod tests { crypto_scheme: None, encrypted_suri: None, parent: Some(child1_address.clone()), - secret_format: None, }, children: vec![], parent: None, @@ -1043,7 +997,6 @@ pub mod tests { crypto_scheme: None, encrypted_suri: None, parent: Some(child2_address.clone()), - secret_format: None, }, children: vec![], parent: None, @@ -1057,7 +1010,6 @@ pub mod tests { crypto_scheme: None, encrypted_suri: None, parent: Some(mother_address.clone()), - secret_format: None, }, children: vec![grandchild1.clone()], parent: None, @@ -1072,7 +1024,6 @@ pub mod tests { crypto_scheme: None, encrypted_suri: None, parent: Some(mother_address.clone()), - secret_format: None, }, children: vec![grandchild2.clone()], parent: None, @@ -1088,7 +1039,6 @@ pub mod tests { vault::encrypt(SUBSTRATE_MNEMONIC.as_bytes(), "".to_string()).unwrap(), ), parent: None, - secret_format: Some(DbSecretFormat::Substrate), }, children: vec![child1.clone(), child2.clone()], parent: None, @@ -1123,7 +1073,6 @@ pub mod tests { vault::encrypt(secret_suri.as_bytes(), "".to_string()).unwrap(), ), parent: None, - secret_format: Some(DbSecretFormat::G1v1), }, children: vec![], parent: None, -- GitLab From bbd95707072c0fc447e74336816aaa6b8331603c Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Tue, 11 Mar 2025 08:39:29 +0100 Subject: [PATCH 12/27] Added extra message when the DB parsing of DbAccountId fails, so we know for which string it failed. --- src/entities/vault_account.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/entities/vault_account.rs b/src/entities/vault_account.rs index d04b02a..f2db84c 100644 --- a/src/entities/vault_account.rs +++ b/src/entities/vault_account.rs @@ -134,7 +134,10 @@ impl sea_orm::TryGetable for DbAccountId { .try_get_by(idx) .map_err(|e| TryGetError::Null(e.to_string()))?; Ok(DbAccountId(AccountId::from_str(&value).map_err(|e| { - TryGetError::DbErr(DbErr::Custom(e.to_string())) + TryGetError::DbErr(DbErr::Custom(format!( + "Cannot parse DbAccountId for string '{}' - error: {}", + &value, e + ))) })?)) } } -- GitLab From f9beaf549a181da5016089d1e1eeac9cf81f55e2 Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sun, 23 Mar 2025 20:09:47 +0100 Subject: [PATCH 13/27] * Updated version to 0.4.1 * Added Changelog entry for the new version * Adapted some of the examples in config.md to take into account the new `-c` argument ** Did a simple addition of "-c sr25519" so that current examples still make sense and link to the same SS58 addresses --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- doc/config.md | 32 ++++++++++++++++++++++---------- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58f1d00..dced0c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,35 @@ List of changelogs ordered from latest to oldest +## [0.4.1] - 2025-03-23 +### Added / Changed +- We now use `ed25519` crypto scheme by default for all commands. It means that if you import your substrate mnemonic without giving a specific argument to change the crypto scheme, the resulting SS58 address will not be the same as before (it was using `sr25519` for substrate mnemonic previously) + - Extra `-c` / `--crypto-scheme` argument allows to specify which crypto scheme to use (`sr25519` or `ed25519`) and having a default value of `ed25519` in most places. + - this argument is present in all places where you could provide `-S` / `--secret-format` argument + - Due to that change, the display of the vault (`vault list ...` commands) will not display the g1v1 public key of all `ed25519` \<Base\> keys by default anymore; you will have to provide an extra `--show-g1v1` argument for that. + - Another impact is that we can now `vault derive` any key; including the ones with `ed25519` crypto-scheme. + - It is still highly recommended to **only derive** keys based on a **substrate mnemonic**; as old `g1v1` keys with their _manual_ `id` & `secret` are not as secure as using a generated substrate mnemonic. + - There is an exception to the impacted commands: `vault migrate` will still import old key files as `sr25519` crypto-scheme keys; as it would otherwise not correspond to the SS58 Address of those key files. +- It is now possible to perform `vault import` and `vault derive` commands without interactive prompts if all necessary optional arguments are provided. + - Please check for more details with commands: + - `vault import --help` + - `vault derive --help` + - If the command tries to override an existing vault entry, then it will still require manual input from the user! + +### Fixed +- None + +### Deprecated +- Two commands are still deprecated and will be removed in a future release: + - `gcli vault list-files` + - `gcli vault migrate` + +### Removed +- None + +### CI/CD +- None + ## [0.4.0] - 2025-02-xx ### Changed - Old key files cannot be used directly anymore, they have to be migrated to the SQLite file database. You can use the following commands for that: diff --git a/Cargo.lock b/Cargo.lock index a9cd348..0264e14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2338,7 +2338,7 @@ dependencies = [ [[package]] name = "gcli" -version = "0.4.0" +version = "0.4.1" dependencies = [ "age", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index ce66a64..8600c6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ license = "AGPL-3.0-only" name = "gcli" repository = "https://git.duniter.org/clients/rust/gcli-v2s" description = "A command-line interface for Duniter v2s uses" -version = "0.4.0" +version = "0.4.1" [dependencies] # subxt is main dependency diff --git a/doc/config.md b/doc/config.md index 3874272..111baea 100644 --- a/doc/config.md +++ b/doc/config.md @@ -5,7 +5,7 @@ Some Ğcli commands require to have an address configured (for example to get ac ```sh # save Alice address to config file -gcli -S predefined -s Alice config save +gcli -S predefined -s Alice -c sr25519 config save # show config gcli config show @@ -46,10 +46,9 @@ Here is an example that: * makes a transfer from selected Address ```sh -# add a new secret to the vault using substrate uri -gcli vault import +# add a new secret to the vault using substrate uri and crypto scheme sr25519 +gcli vault import -c sr25519 # [stdout] -# gcli vault import # Substrate URI can be a mnemonic or a mini-secret ('0x' prefixed seed) together with optional derivation path # > Substrate URI: ******** # @@ -159,8 +158,9 @@ gcli --help # Options: # -i, --indexer <INDEXER> Overwrite indexer endpoint # --no-indexer Do not use indexer -# -s, --secret <SECRET> Secret key or BIP39 mnemonic (only used when secret format is compatible) (eventually followed by derivation path) # -S, --secret-format <SECRET_FORMAT> Secret key format (seed, substrate, g1v1) +# -s, --secret <SECRET> Secret key or BIP39 mnemonic (only used when secret format is compatible) (eventually followed by derivation path) +# -c, --crypto-scheme <CRYPTO_SCHEME> Crypto scheme to use (sr25519, ed25519) [default: ed25519] # -a <ADDRESS> SS58 Address # -v <NAME> Name of an SS58 Address in the vault # -u, --url <URL> Overwrite duniter websocket RPC endpoint @@ -203,20 +203,32 @@ gcli vault derive --help # [stdout] # Add a derivation to an existing SS58 Address. # -# Only "sr25519" crypto scheme is supported for derivations. -# +# Both "sr25519" and "ed25519" crypto schemes are supported +# # Use command `vault list base` to see available <Base> account and their crypto scheme -# And then use command 'vault list for' to find all accounts linked to that <Base> account. +# And then use command 'vault list for' to find all accounts linked to that <Base> account # -# Usage: gcli vault derive <-a <ADDRESS>|-v <NAME>> +# Usage: gcli vault derive [OPTIONS] <-a <ADDRESS>|-v <VAULT_NAME>> # # Options: # -a <ADDRESS> # SS58 Address # -# -v <NAME> +# -v <VAULT_NAME> # Name of an SS58 Address in the vault # +# -d, --derivation-path <DERIVATION_PATH> +# Derivation path (non-interactive mode) +# +# -p, --password <PASSWORD> +# Password to decrypt the <Base> account key (non-interactive mode) +# +# --no-password +# Use empty password to decrypt the <Base> account key (non-interactive mode) +# +# -n, --name <NAME> +# Name for the wallet entry (non-interactive mode) - "" empty string will be considered as None +# # -h, --help # Print help (see a summary with '-h') ``` \ No newline at end of file -- GitLab From f9cf29e6a763ab0091c6f5dacd0248ae95d02875 Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Fri, 28 Mar 2025 14:43:05 +0100 Subject: [PATCH 14/27] Code review: * Small comment cleanup * Code cleanup removed unnecessary empty cells in "g1v1" row * Added doc on prompt_secret_substrate_and_compute_keypair method to explain the values returned in the tuple. --- src/commands/vault/display.rs | 13 +++---------- src/keys.rs | 12 ++++++++++++ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/commands/vault/display.rs b/src/commands/vault/display.rs index 8822282..251edfe 100644 --- a/src/commands/vault/display.rs +++ b/src/commands/vault/display.rs @@ -26,7 +26,7 @@ pub fn compute_vault_key_files_table(vault_key_addresses: &[String]) -> Result<T pub fn compute_vault_accounts_table( account_tree_nodes: &[Rc<RefCell<AccountTreeNode>>], ) -> Result<Table, GcliError> { - // Appel to the new function with show_g1v1 = true to maintain compatibility + // Calling the new function with show_g1v1 = true to maintain compatibility compute_vault_accounts_table_with_g1v1(account_tree_nodes, true) } @@ -114,19 +114,12 @@ pub fn compute_vault_accounts_row_with_g1v1( // Add a second line for the G1v1 public key only if show_g1v1 is true and it's an Ed25519 key let is_ed25519 = crypto_scheme == CryptoScheme::Ed25519; if show_g1v1 && is_ed25519 { - let mut g1v1_row = vec![Cell::new(format!( + rows.push(vec![Cell::new(format!( "└ G1v1: {}", cesium::compute_g1v1_public_key_from_ed25519_account_id( &account_tree_node.account.address.0 ) - ))]; - - // Add empty cells to align with the main line - g1v1_row.push(Cell::new("")); - g1v1_row.push(Cell::new("")); - g1v1_row.push(Cell::new("")); - - rows.push(g1v1_row); + ))]); } ( diff --git a/src/keys.rs b/src/keys.rs index 759211f..7d1552a 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -262,6 +262,18 @@ pub fn seed_from_cesium(id: &str, pwd: &str) -> [u8; 32] { seed } +/// This method will prompt for the (secret) substrate uri, compute the keypair, and return a tuple containing the (secret) substrate uri and the keypair. +/// +/// # Arguments +/// +/// * `crypto_scheme` - The cryptographic scheme to use (either Sr25519 or Ed25519). +/// +/// # Returns +/// +/// A tuple containing: +/// +/// * `String` - The (secret) substrate URI provided by the user. +/// * `KeyPair` - The computed keypair based on the provided substrate URI and cryptographic scheme. pub fn prompt_secret_substrate_and_compute_keypair( crypto_scheme: CryptoScheme, ) -> (String, KeyPair) { -- GitLab From 72462b985b7b45eadd23c02ed98d0c3525a07757 Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Fri, 28 Mar 2025 19:33:58 +0100 Subject: [PATCH 15/27] Code review: * Cleanup of keys.rs get_keypair method since it always gets a crypto_scheme. * Removed "default" values in the method * Relying on "default" value of `-c` / `--crypto-scheme` argument (present in several places) which is ed25519 --- src/commands/identity.rs | 4 ++-- src/data.rs | 2 +- src/keys.rs | 15 +++++---------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/commands/identity.rs b/src/commands/identity.rs index 5048f45..4c744cd 100644 --- a/src/commands/identity.rs +++ b/src/commands/identity.rs @@ -161,7 +161,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE secret, crypto_scheme, } => { - let keypair = get_keypair(secret_format, secret.as_deref(), Some(crypto_scheme))?; + let keypair = get_keypair(secret_format, secret.as_deref(), crypto_scheme)?; println!( "target address:'{}' (using crypto-scheme:{})", keypair.address(), @@ -176,7 +176,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE secret, crypto_scheme, } => { - let keypair = get_keypair(secret_format, secret.as_deref(), Some(crypto_scheme))?; + let keypair = get_keypair(secret_format, secret.as_deref(), crypto_scheme)?; println!( "target address:'{}' (using crypto-scheme:{})", keypair.address(), diff --git a/src/data.rs b/src/data.rs index c9aa7c9..0a41ea8 100644 --- a/src/data.rs +++ b/src/data.rs @@ -210,7 +210,7 @@ impl Data { let keypair = get_keypair( secret_format, self.args.secret.as_deref(), - Some(self.args.crypto_scheme), + self.args.crypto_scheme, )?; self.cfg.address = Some(keypair.address()); self.keypair = Some(keypair); diff --git a/src/keys.rs b/src/keys.rs index 7d1552a..3431e40 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -145,25 +145,20 @@ pub enum Signature { pub fn get_keypair( secret_format: SecretFormat, secret: Option<&str>, - crypto_scheme: Option<CryptoScheme>, + crypto_scheme: CryptoScheme, ) -> Result<KeyPair, GcliError> { match (secret_format, secret) { (SecretFormat::Predefined, Some(deriv)) => { match crypto_scheme { - Some(CryptoScheme::Ed25519) => { + CryptoScheme::Ed25519 => { pair_from_ed25519_str(&predefined_suri(deriv)).map(|v| v.into()) } - _ => pair_from_predefined(deriv).map(|v| v.into()), // Default to Sr25519 for backward compatibility + _ => pair_from_predefined(deriv).map(|v| v.into()), } } - (secret_format, None) => Ok(prompt_secret(secret_format, crypto_scheme)), + (secret_format, None) => Ok(prompt_secret(secret_format, Some(crypto_scheme))), (_, Some(secret)) => { - match crypto_scheme { - Some(CryptoScheme::Ed25519) => { - pair_from_secret_with_scheme(secret_format, secret, CryptoScheme::Ed25519) - } - _ => Ok(pair_from_secret(secret_format, secret)?.into()), // Default to Sr25519 for backward compatibility - } + pair_from_secret_with_scheme(secret_format, secret, crypto_scheme) } } } -- GitLab From 13307c1a5d8282328fa4f220bd8cc4aac53ea32d Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sat, 29 Mar 2025 20:05:22 +0100 Subject: [PATCH 16/27] * Changed sorting of children account so that they are sorted by Path value (makes more sense when viewing derivations of one account) * Added display of "Crypto scheme" when making `vault inspect` so that it is more coherent --- src/commands/vault.rs | 18 +++++++++++++++--- src/entities/vault_account.rs | 5 ++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 5fe703f..f407474 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -554,18 +554,30 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE Subcommand::Inspect { address_or_vault_name, } => { - let account_tree_node_to_derive = + let account_tree_node_to_inspect = retrieve_account_tree_node(db, address_or_vault_name).await?; println!("Enter password to decrypt the <Base> account key"); let password = inputs::prompt_password()?; let account_to_derive_secret_suri = vault_account::compute_suri_account_tree_node( - &account_tree_node_to_derive, + &account_tree_node_to_inspect, password, )?; + println!("Substrate URI: '{account_to_derive_secret_suri}'"); - println!("Substrate URI: '{account_to_derive_secret_suri}'") + let base_account_tree_node = + vault_account::get_base_account_tree_node(&account_tree_node_to_inspect); + let crypto_scheme: CryptoScheme = base_account_tree_node + .borrow() + .account + .crypto_scheme + .clone() + .ok_or(GcliError::Logic( + "Base account without crypto_scheme".to_string(), + ))? + .into(); + println!("Crypto scheme: {}", <&'static str>::from(crypto_scheme)); } Subcommand::Migrate => { println!("Migrating existing key files to db"); diff --git a/src/entities/vault_account.rs b/src/entities/vault_account.rs index f2db84c..60d663d 100644 --- a/src/entities/vault_account.rs +++ b/src/entities/vault_account.rs @@ -760,6 +760,9 @@ where Ok(Some(base_parent_account)) } +/// Finds direct children of given account. +/// +/// Sorts according to the Path as it makes the most sense when viewing derivations of one account. async fn find_direct_children_accounts<C>( db: &C, current_account: &Model, @@ -769,7 +772,7 @@ where { Entity::find() .filter(Column::Parent.eq(current_account.address.clone())) - .order_by_asc(Column::Address) + .order_by_asc(Column::Path) .all(db) .await .map_err(GcliError::from) -- GitLab From 317679682439193477d7572b88d5b96cc945e3f5 Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sun, 30 Mar 2025 18:09:29 +0200 Subject: [PATCH 17/27] * Small clippy cleanup --- src/commands/vault.rs | 1 - src/keys.rs | 16 ++++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/commands/vault.rs b/src/commands/vault.rs index f407474..1b66a60 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -572,7 +572,6 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE .borrow() .account .crypto_scheme - .clone() .ok_or(GcliError::Logic( "Base account without crypto_scheme".to_string(), ))? diff --git a/src/keys.rs b/src/keys.rs index 3431e40..1108a96 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -148,18 +148,14 @@ pub fn get_keypair( crypto_scheme: CryptoScheme, ) -> Result<KeyPair, GcliError> { match (secret_format, secret) { - (SecretFormat::Predefined, Some(deriv)) => { - match crypto_scheme { - CryptoScheme::Ed25519 => { - pair_from_ed25519_str(&predefined_suri(deriv)).map(|v| v.into()) - } - _ => pair_from_predefined(deriv).map(|v| v.into()), + (SecretFormat::Predefined, Some(deriv)) => match crypto_scheme { + CryptoScheme::Ed25519 => { + pair_from_ed25519_str(&predefined_suri(deriv)).map(|v| v.into()) } - } + _ => pair_from_predefined(deriv).map(|v| v.into()), + }, (secret_format, None) => Ok(prompt_secret(secret_format, Some(crypto_scheme))), - (_, Some(secret)) => { - pair_from_secret_with_scheme(secret_format, secret, crypto_scheme) - } + (_, Some(secret)) => pair_from_secret_with_scheme(secret_format, secret, crypto_scheme), } } -- GitLab From 49e609c7c7ff3ef9c3d9b6cb30b432f67a7c1bb0 Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sun, 30 Mar 2025 19:00:32 +0200 Subject: [PATCH 18/27] Code review: * Adapted commands identity `LinkAccount` & `ChangeOwnerKey` * Added possibility to provide * `-a` address of vault account to link * `-v` name of vault account to link * `-S` (secret_format) doesn't have a default anymore since it conflicts with using `-a` or `-v` * Added an error message if none of the params are provided --- src/commands/identity.rs | 160 ++++++++++++++++++++++++++------------- 1 file changed, 108 insertions(+), 52 deletions(-) diff --git a/src/commands/identity.rs b/src/commands/identity.rs index 4c744cd..25e8409 100644 --- a/src/commands/identity.rs +++ b/src/commands/identity.rs @@ -1,5 +1,6 @@ use crate::*; +use crate::commands::vault::retrieve_account_tree_node_for_name; use crate::{ commands::revocation::generate_revoc_doc, runtime::runtime_types::{ @@ -63,32 +64,30 @@ pub enum Subcommand { /// Display member count MemberCount, /// Link an account to the identity - LinkAccount { - /// Secret key format of account to link (seed, substrate) - #[clap(short = 'S', long, default_value = SecretFormat::Substrate)] - secret_format: SecretFormat, - /// Secret of account to link - /// most likely different from the one owning the identity - #[clap(short, long)] - secret: Option<String>, - /// Crypto scheme of account to link (sr25519, ed25519) - #[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)] - crypto_scheme: CryptoScheme, - }, + LinkAccount(AccountLinkParams), /// Migrate identity to another account /// Change Owner Key - ChangeOwnerKey { - /// Secret key format of account to link (seed, substrate) - #[clap(short = 'S', long, default_value = SecretFormat::Substrate)] - secret_format: SecretFormat, - /// Secret of account to link - /// most likely different from the one owning the identity - #[clap(short, long)] - secret: Option<String>, - /// Crypto scheme of account to link (sr25519, ed25519) - #[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)] - crypto_scheme: CryptoScheme, - }, + ChangeOwnerKey(AccountLinkParams), +} + +#[derive(clap::Args, Clone, Debug)] +pub struct AccountLinkParams { + /// SS58 Address of vault account to link + #[clap(short, conflicts_with_all=["vault_name","secret_format", "secret", "crypto_scheme"])] + address: Option<AccountId>, + /// Name of vault account to link + #[clap(short = 'v', conflicts_with_all=["secret_format", "secret", "crypto_scheme"])] + vault_name: Option<String>, + /// Secret key format of account to link (seed, substrate) + #[clap(short = 'S', long)] + secret_format: Option<SecretFormat>, + /// Secret of account to link + /// most likely different from the one owning the identity + #[clap(short, long)] + secret: Option<String>, + /// Crypto scheme of account to link (sr25519, ed25519) + #[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)] + crypto_scheme: CryptoScheme, } /// handle identity commands @@ -156,41 +155,98 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE .unwrap() ) } - Subcommand::LinkAccount { - secret_format, - secret, - crypto_scheme, - } => { - let keypair = get_keypair(secret_format, secret.as_deref(), crypto_scheme)?; - println!( - "target address:'{}' (using crypto-scheme:{})", - keypair.address(), - <&'static str>::from(crypto_scheme) - ); - let address = keypair.address(); - data = data.fetch_idty_index().await?; // idty index required for payload - link_account(&data, address, keypair).await?; + Subcommand::LinkAccount(params) => { + async fn perform_link_account(data: Data, keypair: KeyPair) -> Result<(), GcliError> { + println!("Trying to make the link"); + let address = keypair.address(); + let data = data.fetch_idty_index().await?; // idty index required for payload + link_account(&data, address, keypair).await?; + Ok(()) + } + + if let Some(address) = params.address { + let key_pair = fetch_vault_keypair_for_target_address(&data, address).await?; + perform_link_account(data, key_pair).await?; + } else if let Some(vault_name) = params.vault_name { + let account_tree_node = + retrieve_account_tree_node_for_name(data.connect_db(), &vault_name).await?; + let address = account_tree_node.borrow().account.address.0.clone(); + + let key_pair = fetch_vault_keypair_for_target_address(&data, address).await?; + perform_link_account(data, key_pair).await?; + } else if let Some(secret_format) = params.secret_format { + let keypair = get_keypair( + secret_format, + params.secret.as_deref(), + params.crypto_scheme, + )?; + println!( + "target address:'{}' (using crypto-scheme:{})", + keypair.address(), + <&'static str>::from(params.crypto_scheme) + ); + perform_link_account(data, keypair).await?; + } else { + return Err(GcliError::Input( + "One of `address`/`vault_name`/`secret_format`(and optional `secret` & `crypto_scheme`) must be provided".to_string(), + )); + } } - Subcommand::ChangeOwnerKey { - secret_format, - secret, - crypto_scheme, - } => { - let keypair = get_keypair(secret_format, secret.as_deref(), crypto_scheme)?; - println!( - "target address:'{}' (using crypto-scheme:{})", - keypair.address(), - <&'static str>::from(crypto_scheme) - ); - let address = keypair.address(); - data = data.fetch_idty_index().await?; // idty index required for payload - change_owner_key(&data, address, keypair).await?; + Subcommand::ChangeOwnerKey(params) => { + async fn perform_change_owner_key( + data: Data, + keypair: KeyPair, + ) -> Result<(), GcliError> { + println!("Trying to change owner key"); + let address = keypair.address(); + let data = data.fetch_idty_index().await?; // idty index required for payload + change_owner_key(&data, address, keypair).await?; + Ok(()) + } + + if let Some(address) = params.address { + let key_pair = fetch_vault_keypair_for_target_address(&data, address).await?; + perform_change_owner_key(data, key_pair).await?; + } else if let Some(vault_name) = params.vault_name { + let account_tree_node = + retrieve_account_tree_node_for_name(data.connect_db(), &vault_name).await?; + let address = account_tree_node.borrow().account.address.0.clone(); + + let key_pair = fetch_vault_keypair_for_target_address(&data, address).await?; + perform_change_owner_key(data, key_pair).await?; + } else if let Some(secret_format) = params.secret_format { + let keypair = get_keypair( + secret_format, + params.secret.as_deref(), + params.crypto_scheme, + )?; + println!( + "target address:'{}' (using crypto-scheme:{})", + keypair.address(), + <&'static str>::from(params.crypto_scheme) + ); + perform_change_owner_key(data, keypair).await?; + } else { + return Err(GcliError::Input( + "One of `address`/`vault_name`/`secret_format`(and optional `secret` & `crypto_scheme`) must be provided".to_string(), + )); + } } }; Ok(()) } +async fn fetch_vault_keypair_for_target_address( + data: &Data, + address: AccountId, +) -> Result<KeyPair, GcliError> { + println!("Trying to retrieve key pair for target address:'{address}'"); + commands::vault::try_fetch_key_pair(data, address) + .await? + .ok_or_else(|| GcliError::Input("target vault account not found".to_string())) +} + // ====================== // TODO derive this automatically -- GitLab From 4e670a75362f8af4cac1f50622ba2450fb66a6df Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sun, 30 Mar 2025 19:36:13 +0200 Subject: [PATCH 19/27] * Updated CHANGELOG.md --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dced0c3..83d20ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,15 @@ List of changelogs ordered from latest to oldest - `vault import --help` - `vault derive --help` - If the command tries to override an existing vault entry, then it will still require manual input from the user! +- Adapted commands `identity link-account` and `identity change-owner-key` + - Made it clear that the arguments are meant to "target" another account + - Added possibility to provide the "target" account from the vault + - `-a` address of vault account to link + - `-v` name of vault account to link + - `-S` (secret_format) doesn't have a default anymore since it conflicts with using `-a` or `-v` +- Small changes + - In the vault, changed sorting of children account so that they are sorted by derivation `Path` value (makes more sense when viewing derivations of one account) + - Added display of `crypto scheme` when calling `vault inspect` so that it is more coherent; since both the `substrate uri` and `crypto-scheme` are needed to create the key with the proper `address` ### Fixed - None -- GitLab From 4a82b65da8406eb7b92eafc790305028aaf0b36b Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sun, 30 Mar 2025 20:21:15 +0200 Subject: [PATCH 20/27] * Small change to show the linked `<Base>` account if different from the one we inspect. --- src/commands/vault.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 1b66a60..7a5ef9e 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -557,6 +557,14 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let account_tree_node_to_inspect = retrieve_account_tree_node(db, address_or_vault_name).await?; + let base_account_tree_node = + vault_account::get_base_account_tree_node(&account_tree_node_to_inspect); + + if !Rc::ptr_eq(&account_tree_node_to_inspect, &base_account_tree_node) { + let base_account = base_account_tree_node.borrow().account.clone(); + println!("The linked <Base> account is {base_account}"); + } + println!("Enter password to decrypt the <Base> account key"); let password = inputs::prompt_password()?; @@ -566,8 +574,6 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE )?; println!("Substrate URI: '{account_to_derive_secret_suri}'"); - let base_account_tree_node = - vault_account::get_base_account_tree_node(&account_tree_node_to_inspect); let crypto_scheme: CryptoScheme = base_account_tree_node .borrow() .account -- GitLab From 756418acf476c3d19f6b5b37ea35cc7805cd457f Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Fri, 4 Apr 2025 13:37:07 +0200 Subject: [PATCH 21/27] * Small refactoring to move method to vault.rs --- CHANGELOG.md | 2 +- src/commands/identity.rs | 18 ++++-------------- src/commands/vault.rs | 16 +++++++++++++++- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83d20ca..3e8b102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ List of changelogs ordered from latest to oldest -## [0.4.1] - 2025-03-23 +## [0.4.1] - 2025-04-04 ### Added / Changed - We now use `ed25519` crypto scheme by default for all commands. It means that if you import your substrate mnemonic without giving a specific argument to change the crypto scheme, the resulting SS58 address will not be the same as before (it was using `sr25519` for substrate mnemonic previously) - Extra `-c` / `--crypto-scheme` argument allows to specify which crypto scheme to use (`sr25519` or `ed25519`) and having a default value of `ed25519` in most places. diff --git a/src/commands/identity.rs b/src/commands/identity.rs index 25e8409..67e2628 100644 --- a/src/commands/identity.rs +++ b/src/commands/identity.rs @@ -165,14 +165,14 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE } if let Some(address) = params.address { - let key_pair = fetch_vault_keypair_for_target_address(&data, address).await?; + let key_pair = commands::vault::fetch_vault_keypair_for_address(&data, address).await?; perform_link_account(data, key_pair).await?; } else if let Some(vault_name) = params.vault_name { let account_tree_node = retrieve_account_tree_node_for_name(data.connect_db(), &vault_name).await?; let address = account_tree_node.borrow().account.address.0.clone(); - let key_pair = fetch_vault_keypair_for_target_address(&data, address).await?; + let key_pair = commands::vault::fetch_vault_keypair_for_address(&data, address).await?; perform_link_account(data, key_pair).await?; } else if let Some(secret_format) = params.secret_format { let keypair = get_keypair( @@ -205,14 +205,14 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE } if let Some(address) = params.address { - let key_pair = fetch_vault_keypair_for_target_address(&data, address).await?; + let key_pair = commands::vault::fetch_vault_keypair_for_address(&data, address).await?; perform_change_owner_key(data, key_pair).await?; } else if let Some(vault_name) = params.vault_name { let account_tree_node = retrieve_account_tree_node_for_name(data.connect_db(), &vault_name).await?; let address = account_tree_node.borrow().account.address.0.clone(); - let key_pair = fetch_vault_keypair_for_target_address(&data, address).await?; + let key_pair = commands::vault::fetch_vault_keypair_for_address(&data, address).await?; perform_change_owner_key(data, key_pair).await?; } else if let Some(secret_format) = params.secret_format { let keypair = get_keypair( @@ -237,16 +237,6 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE Ok(()) } -async fn fetch_vault_keypair_for_target_address( - data: &Data, - address: AccountId, -) -> Result<KeyPair, GcliError> { - println!("Trying to retrieve key pair for target address:'{address}'"); - commands::vault::try_fetch_key_pair(data, address) - .await? - .ok_or_else(|| GcliError::Input("target vault account not found".to_string())) -} - // ====================== // TODO derive this automatically diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 7a5ef9e..f6941cf 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -1195,7 +1195,21 @@ fn find_substrate_vault_key_file(data: &Data, address: &str) -> Result<Option<Pa Ok(None) } -/// try to get secret in keystore, prompt for the password and compute the keypair +/// Gets secret in keystore, prompt for the password and compute the keypair associated to `address` +/// +/// Returns an error if no entry was found in the keystore or if another error occurred during the process +pub async fn fetch_vault_keypair_for_address( + data: &Data, + address: AccountId, +) -> Result<KeyPair, GcliError> { + println!("Trying to retrieve key pair for address:'{address}'"); + try_fetch_key_pair(data, address) + .await? + .ok_or_else(|| GcliError::Input("vault account not found".to_string())) +} + + +/// try to get secret in keystore, prompt for the password and compute the keypair associated to `address` pub async fn try_fetch_key_pair( data: &Data, address: AccountId, -- GitLab From 1071f330b4a02f33392331a14b8002dcf371b9c8 Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Fri, 4 Apr 2025 14:30:25 +0200 Subject: [PATCH 22/27] * Renamed AccountLinkParams to SecretProvider and adapted descriptions to "target account" instead of "account to link" which makes it a bit more generic * Added generic method get_keypair_from_secret_provider that returns the keypair or an error * Adapted `LinkAccount` & `ChangeOwnerKey` to use those * Not sure the best place for the SecretProvider and new method - leaving in identity for now --- src/commands/identity.rs | 135 +++++++++++++++------------------------ 1 file changed, 53 insertions(+), 82 deletions(-) diff --git a/src/commands/identity.rs b/src/commands/identity.rs index 67e2628..708c9c5 100644 --- a/src/commands/identity.rs +++ b/src/commands/identity.rs @@ -64,28 +64,27 @@ pub enum Subcommand { /// Display member count MemberCount, /// Link an account to the identity - LinkAccount(AccountLinkParams), + LinkAccount(SecretProvider), /// Migrate identity to another account /// Change Owner Key - ChangeOwnerKey(AccountLinkParams), + ChangeOwnerKey(SecretProvider), } #[derive(clap::Args, Clone, Debug)] -pub struct AccountLinkParams { - /// SS58 Address of vault account to link +pub struct SecretProvider { + /// SS58 Address of target vault account #[clap(short, conflicts_with_all=["vault_name","secret_format", "secret", "crypto_scheme"])] address: Option<AccountId>, - /// Name of vault account to link + /// Name of target vault account #[clap(short = 'v', conflicts_with_all=["secret_format", "secret", "crypto_scheme"])] vault_name: Option<String>, - /// Secret key format of account to link (seed, substrate) + /// Secret key format of target account (seed, substrate) #[clap(short = 'S', long)] secret_format: Option<SecretFormat>, - /// Secret of account to link - /// most likely different from the one owning the identity + /// Secret of target account #[clap(short, long)] secret: Option<String>, - /// Crypto scheme of account to link (sr25519, ed25519) + /// Crypto scheme of target account (sr25519, ed25519) #[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)] crypto_scheme: CryptoScheme, } @@ -156,87 +155,59 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE ) } Subcommand::LinkAccount(params) => { - async fn perform_link_account(data: Data, keypair: KeyPair) -> Result<(), GcliError> { - println!("Trying to make the link"); - let address = keypair.address(); - let data = data.fetch_idty_index().await?; // idty index required for payload - link_account(&data, address, keypair).await?; - Ok(()) - } - - if let Some(address) = params.address { - let key_pair = commands::vault::fetch_vault_keypair_for_address(&data, address).await?; - perform_link_account(data, key_pair).await?; - } else if let Some(vault_name) = params.vault_name { - let account_tree_node = - retrieve_account_tree_node_for_name(data.connect_db(), &vault_name).await?; - let address = account_tree_node.borrow().account.address.0.clone(); - - let key_pair = commands::vault::fetch_vault_keypair_for_address(&data, address).await?; - perform_link_account(data, key_pair).await?; - } else if let Some(secret_format) = params.secret_format { - let keypair = get_keypair( - secret_format, - params.secret.as_deref(), - params.crypto_scheme, - )?; - println!( - "target address:'{}' (using crypto-scheme:{})", - keypair.address(), - <&'static str>::from(params.crypto_scheme) - ); - perform_link_account(data, keypair).await?; - } else { - return Err(GcliError::Input( - "One of `address`/`vault_name`/`secret_format`(and optional `secret` & `crypto_scheme`) must be provided".to_string(), - )); - } + let target_keypair = get_keypair_from_secret_provider(&data, params).await?; + + println!("Trying to make the link"); + let address = target_keypair.address(); + let data = data.fetch_idty_index().await?; // idty index required for payload + link_account(&data, address, target_keypair).await?; } Subcommand::ChangeOwnerKey(params) => { - async fn perform_change_owner_key( - data: Data, - keypair: KeyPair, - ) -> Result<(), GcliError> { - println!("Trying to change owner key"); - let address = keypair.address(); - let data = data.fetch_idty_index().await?; // idty index required for payload - change_owner_key(&data, address, keypair).await?; - Ok(()) - } - - if let Some(address) = params.address { - let key_pair = commands::vault::fetch_vault_keypair_for_address(&data, address).await?; - perform_change_owner_key(data, key_pair).await?; - } else if let Some(vault_name) = params.vault_name { - let account_tree_node = - retrieve_account_tree_node_for_name(data.connect_db(), &vault_name).await?; - let address = account_tree_node.borrow().account.address.0.clone(); - - let key_pair = commands::vault::fetch_vault_keypair_for_address(&data, address).await?; - perform_change_owner_key(data, key_pair).await?; - } else if let Some(secret_format) = params.secret_format { - let keypair = get_keypair( - secret_format, - params.secret.as_deref(), - params.crypto_scheme, - )?; - println!( - "target address:'{}' (using crypto-scheme:{})", - keypair.address(), - <&'static str>::from(params.crypto_scheme) - ); - perform_change_owner_key(data, keypair).await?; - } else { - return Err(GcliError::Input( - "One of `address`/`vault_name`/`secret_format`(and optional `secret` & `crypto_scheme`) must be provided".to_string(), - )); - } + let target_keypair = get_keypair_from_secret_provider(&data, params).await?; + + println!("Trying to change owner key"); + let address = target_keypair.address(); + let data = data.fetch_idty_index().await?; // idty index required for payload + change_owner_key(&data, address, target_keypair).await?; } }; Ok(()) } +async fn get_keypair_from_secret_provider( + data: &Data, + secret_provider: SecretProvider, +) -> Result<KeyPair, GcliError> { + let key_pair = if let Some(address) = secret_provider.address { + commands::vault::fetch_vault_keypair_for_address(data, address).await? + } else if let Some(vault_name) = secret_provider.vault_name { + let account_tree_node = + retrieve_account_tree_node_for_name(data.connect_db(), &vault_name).await?; + let address = account_tree_node.borrow().account.address.0.clone(); + + commands::vault::fetch_vault_keypair_for_address(data, address).await? + } else if let Some(secret_format) = secret_provider.secret_format { + let keypair = get_keypair( + secret_format, + secret_provider.secret.as_deref(), + secret_provider.crypto_scheme, + )?; + println!( + "target address:'{}' (using crypto-scheme:{})", + keypair.address(), + <&'static str>::from(secret_provider.crypto_scheme) + ); + keypair + } else { + return Err(GcliError::Input( + "One of `address`/`vault_name`/`secret_format`(and optional `secret` & `crypto_scheme`) must be provided".to_string(), + )); + }; + + Ok(key_pair) +} + // ====================== // TODO derive this automatically -- GitLab From d664f4a295224b6321f5cd514f6dca271c713f96 Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sat, 5 Apr 2025 17:54:51 +0200 Subject: [PATCH 23/27] * Refactored method get_keypair_from_secret_provider into SecretProvider internal method get_keypair * Added long_about for `LinkAccount` and `ChangeOwnerKey` to explain the target identity/account is provided with the options of those commands. * Applied cargo fmt --- src/commands/identity.rs | 87 ++++++++++++++++++++++------------------ src/commands/vault.rs | 3 +- 2 files changed, 48 insertions(+), 42 deletions(-) diff --git a/src/commands/identity.rs b/src/commands/identity.rs index 708c9c5..7ae600e 100644 --- a/src/commands/identity.rs +++ b/src/commands/identity.rs @@ -63,10 +63,15 @@ pub enum Subcommand { GenRevocDoc, /// Display member count MemberCount, - /// Link an account to the identity + /// Link an account to the [target] identity + #[clap(long_about = "Link an account to the [target] identity.\n\ + \n\ + The target identity can be passed as argument using any of the suggested options.")] LinkAccount(SecretProvider), - /// Migrate identity to another account - /// Change Owner Key + /// Migrate identity to another [target] account + #[clap(long_about = "Migrate identity to another [target] account.\n\ + \n\ + The target account can be passed as argument using any of the suggested options.")] ChangeOwnerKey(SecretProvider), } @@ -89,6 +94,41 @@ pub struct SecretProvider { crypto_scheme: CryptoScheme, } +impl SecretProvider { + /// Analyses the SecretProvider data and tries to retrieve a keypair. + /// + /// Will potentially request a password to decrypt the secret in keystore if it was asked to use a vault account. + /// + /// Or it could request the `secret` if using `secret-format` without also providing the `secret`. + /// + /// Will return an error if no data was provided or if it encountered another issue in the process. + async fn get_keypair(&self, data: &Data) -> Result<KeyPair, GcliError> { + let key_pair = if let Some(address) = self.address.clone() { + commands::vault::fetch_vault_keypair_for_address(data, address).await? + } else if let Some(vault_name) = self.vault_name.clone() { + let account_tree_node = + retrieve_account_tree_node_for_name(data.connect_db(), &vault_name).await?; + let address = account_tree_node.borrow().account.address.0.clone(); + + commands::vault::fetch_vault_keypair_for_address(data, address).await? + } else if let Some(secret_format) = self.secret_format { + let keypair = get_keypair(secret_format, self.secret.as_deref(), self.crypto_scheme)?; + println!( + "target address:'{}' (using crypto-scheme:{})", + keypair.address(), + <&'static str>::from(self.crypto_scheme) + ); + keypair + } else { + return Err(GcliError::Input( + "One of `address`/`vault_name`/`secret_format`(and optional `secret` & `crypto_scheme`) must be provided".to_string(), + )); + }; + + Ok(key_pair) + } +} + /// handle identity commands pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { let mut data = data.build_client().await?; @@ -154,16 +194,16 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE .unwrap() ) } - Subcommand::LinkAccount(params) => { - let target_keypair = get_keypair_from_secret_provider(&data, params).await?; + Subcommand::LinkAccount(secret_provider) => { + let target_keypair = secret_provider.get_keypair(&data).await?; println!("Trying to make the link"); let address = target_keypair.address(); let data = data.fetch_idty_index().await?; // idty index required for payload link_account(&data, address, target_keypair).await?; } - Subcommand::ChangeOwnerKey(params) => { - let target_keypair = get_keypair_from_secret_provider(&data, params).await?; + Subcommand::ChangeOwnerKey(secret_provider) => { + let target_keypair = secret_provider.get_keypair(&data).await?; println!("Trying to change owner key"); let address = target_keypair.address(); @@ -175,39 +215,6 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE Ok(()) } -async fn get_keypair_from_secret_provider( - data: &Data, - secret_provider: SecretProvider, -) -> Result<KeyPair, GcliError> { - let key_pair = if let Some(address) = secret_provider.address { - commands::vault::fetch_vault_keypair_for_address(data, address).await? - } else if let Some(vault_name) = secret_provider.vault_name { - let account_tree_node = - retrieve_account_tree_node_for_name(data.connect_db(), &vault_name).await?; - let address = account_tree_node.borrow().account.address.0.clone(); - - commands::vault::fetch_vault_keypair_for_address(data, address).await? - } else if let Some(secret_format) = secret_provider.secret_format { - let keypair = get_keypair( - secret_format, - secret_provider.secret.as_deref(), - secret_provider.crypto_scheme, - )?; - println!( - "target address:'{}' (using crypto-scheme:{})", - keypair.address(), - <&'static str>::from(secret_provider.crypto_scheme) - ); - keypair - } else { - return Err(GcliError::Input( - "One of `address`/`vault_name`/`secret_format`(and optional `secret` & `crypto_scheme`) must be provided".to_string(), - )); - }; - - Ok(key_pair) -} - // ====================== // TODO derive this automatically diff --git a/src/commands/vault.rs b/src/commands/vault.rs index f6941cf..e6cf88b 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -1196,7 +1196,7 @@ fn find_substrate_vault_key_file(data: &Data, address: &str) -> Result<Option<Pa } /// Gets secret in keystore, prompt for the password and compute the keypair associated to `address` -/// +/// /// Returns an error if no entry was found in the keystore or if another error occurred during the process pub async fn fetch_vault_keypair_for_address( data: &Data, @@ -1208,7 +1208,6 @@ pub async fn fetch_vault_keypair_for_address( .ok_or_else(|| GcliError::Input("vault account not found".to_string())) } - /// try to get secret in keystore, prompt for the password and compute the keypair associated to `address` pub async fn try_fetch_key_pair( data: &Data, -- GitLab From 3e96d537829c989698bfe187e30ebf22976f339d Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sat, 5 Apr 2025 18:04:59 +0200 Subject: [PATCH 24/27] * Small change to be in line with the adapted parameter descriptions --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e8b102..4008188 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,8 @@ List of changelogs ordered from latest to oldest - Adapted commands `identity link-account` and `identity change-owner-key` - Made it clear that the arguments are meant to "target" another account - Added possibility to provide the "target" account from the vault - - `-a` address of vault account to link - - `-v` name of vault account to link + - `-a` SS58 Address of target vault account + - `-v` Name of target vault account - `-S` (secret_format) doesn't have a default anymore since it conflicts with using `-a` or `-v` - Small changes - In the vault, changed sorting of children account so that they are sorted by derivation `Path` value (makes more sense when viewing derivations of one account) -- GitLab From 8d6df397c78b82afe4a492d68e213096b6484795 Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sun, 6 Apr 2025 16:08:12 +0200 Subject: [PATCH 25/27] * Added extra data in `vault inspect` command * Secret seed/mini-secret (if no soft derivation is used) * Public key (hex) * SS58 Address * (potential G1v1 public key if inspecting a <Base> account with ed25519 crypto-scheme) * Does most changes mentioned in Issue #28 (except the network id) --- src/commands/vault.rs | 40 ++++- src/keys.rs | 372 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 403 insertions(+), 9 deletions(-) diff --git a/src/commands/vault.rs b/src/commands/vault.rs index e6cf88b..b46606e 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -1,6 +1,6 @@ mod display; -use crate::commands::cesium::compute_g1v1_public_key; +use crate::commands::cesium; use crate::entities::vault_account; use crate::entities::vault_account::{AccountTreeNode, ActiveModel, DbAccountId}; use crate::inputs::{trim_and_reduce_empty_as_none, validate_derivation_path, validate_vault_name}; @@ -114,7 +114,7 @@ pub enum Subcommand { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, }, - /// Inspect a vault entry, retrieving its Substrate URI (will provide more data in a future version) + /// Inspect a vault entry, retrieving its Substrate URI, Crypto-Scheme, Secret seed/mini-secret(if possible), Public key (hex), SS58 Address and potential G1v1 public key if inspecting a <Base> account with ed25519 crypto-scheme Inspect { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, @@ -348,7 +348,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE if secret_format == SecretFormat::G1v1 { println!( "The G1v1 public key for the provided secret is: '{}'", - compute_g1v1_public_key(&vault_data_for_import.key_pair)? + cesium::compute_g1v1_public_key(&vault_data_for_import.key_pair)? ); // Skip confirmation in non-interactive mode @@ -560,7 +560,9 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let base_account_tree_node = vault_account::get_base_account_tree_node(&account_tree_node_to_inspect); - if !Rc::ptr_eq(&account_tree_node_to_inspect, &base_account_tree_node) { + let is_base_account = + Rc::ptr_eq(&account_tree_node_to_inspect, &base_account_tree_node); + if !is_base_account { let base_account = base_account_tree_node.borrow().account.clone(); println!("The linked <Base> account is {base_account}"); } @@ -583,6 +585,36 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE ))? .into(); println!("Crypto scheme: {}", <&'static str>::from(crypto_scheme)); + + match compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme( + &account_to_derive_secret_suri, + crypto_scheme, + ) { + Err(e) => { + println!("Secret seed/mini-secret: cannot be computed: {}", e) + } + Ok((_computed_pair, seed)) => { + println!("Secret seed/mini-secret: '0x{}'", hex::encode(seed)); + } + } + + let account_address: AccountId = account_tree_node_to_inspect + .borrow() + .account + .address + .clone() + .into(); + + println!("Public key (hex): '0x{}'", hex::encode(account_address.0)); + + println!("SS58 Address: '{account_address}'"); + + if CryptoScheme::Ed25519 == crypto_scheme && is_base_account { + println!( + "(potential G1v1 public key: '{}')", + cesium::compute_g1v1_public_key_from_ed25519_account_id(&account_address) + ); + } } Subcommand::Migrate => { println!("Migrating existing key files to db"); diff --git a/src/keys.rs b/src/keys.rs index 1108a96..4db3ca2 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -1,7 +1,9 @@ use crate::commands::vault; use crate::*; -use sp_core::ed25519; -use sp_core::sr25519; +use sp_core::crypto::AddressUri; +use sp_core::crypto::Pair as PairTrait; +use sp_core::DeriveJunction; +use sp_core::{ed25519, sr25519}; pub const SUBSTRATE_MNEMONIC: &str = "bottom drive obey lake curtain smoke basket hold race lonely fit walk"; @@ -235,6 +237,130 @@ pub fn pair_from_ed25519_seed(secret: &str) -> Result<ed25519::Pair, GcliError> Ok(pair) } +/// Check [compute_pair_and_mini_secret_seed_from_suri] method for details +pub fn compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme( + suri: &str, + crypto_scheme: CryptoScheme, +) -> Result<(KeyPair, [u8; 32]), GcliError> { + match crypto_scheme { + CryptoScheme::Ed25519 => { + let (pair, seed) = + compute_pair_and_mini_secret_seed_from_suri::<sp_core::ed25519::Pair>(suri)?; + Ok((pair.into(), seed)) + } + CryptoScheme::Sr25519 => { + let (pair, seed) = + compute_pair_and_mini_secret_seed_from_suri::<sp_core::sr25519::Pair>(suri)?; + Ok((pair.into(), seed)) + } + } +} + +/// Computes the pair and mini-secret/seed from a Substrate URI for either sr25519 or ed25519. +/// +/// # Arguments +/// * `suri` - The Substrate URI (e.g., mnemonic, hex seed, possibly with HARD derivation(s) like "//0") +/// +/// # Returns +/// A tuple `(P, [u8; 32])` where `P` is the computed `Pair` and `[u8; 32]` is the mini-secret. +/// +/// # Errors +/// Returns `GcliError` if: +/// - The Substrate URI lacks a phrase part +/// - Passwords are present in the derivation +/// - Soft derivation is used (not supported for seed retrieval) +/// - The computed pair's address is different from the one directly retrieved from the `suri` (should not happen) +pub fn compute_pair_and_mini_secret_seed_from_suri<P: PairTrait<Seed = [u8; 32]>>( + suri: &str, +) -> Result<(P, [u8; 32]), GcliError> +where + subxt::utils::AccountId32: std::convert::From<<P as sp_core::Pair>::Public>, +{ + let address_uri = AddressUri::parse(suri).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 base_uri = address_uri.phrase.ok_or(GcliError::Input( + "The `suri` need to contain the 'phrase' part".into(), + ))?; + + let pair_from_suri_for_validation = + P::from_string(suri, None).map_err(|_| GcliError::Input("Invalid secret".to_string()))?; + + // Get the <Base> pair and seed + // If using mini-secret / seed + let (base_pair, base_seed) = if let Some(base_uri_seed) = base_uri.strip_prefix("0x") { + let seed_bytes = hex::decode(base_uri_seed) + .map_err(|e| GcliError::Input(format!("Invalid hex seed: {e}")))?; + let seed: [u8; 32] = seed_bytes + .try_into() + .map_err(|_e| GcliError::Input("Incomplete seed".into()))?; + (P::from_seed(&seed), seed) + } else { + // If using mnemonic / passphrase + let (pair, seed_vec) = P::from_phrase(base_uri, None) + .map_err(|e| GcliError::Input(format!("Invalid mnemonic or passphrase: {e}")))?; + let seed: [u8; 32] = seed_vec + .as_slice() + .try_into() + .map_err(|_e| GcliError::Input("Seed should be 32 bytes".into()))?; + (pair, seed) + }; + + let derivation_paths = address_uri.paths; + + // Apply derivation if present + let (result_pair, result_seed) = if !derivation_paths.is_empty() { + // AddressUri paths have one less '/' everywhere, so matching for no '/' at the start to exclude SOFT derivations + if derivation_paths.iter().any(|path| !path.starts_with("/")) { + return Err(GcliError::Input( + "Soft derivation is not supported when trying to retrieve mini-secret/seed" + .to_string(), + )); + } + + let derive_junctions_from_path = derivation_paths.iter().map(|path| { + if let Some(hard_derivation_no_prefix) = path.strip_prefix("/") { + // AddressUri paths have one less '/' everywhere so this is for HARD derivation + // (and we excluded passwords before; so there won't be any '//') + if let Ok(num) = hard_derivation_no_prefix.parse::<u64>() { + DeriveJunction::hard(num) // Numeric hard derivation + } else { + DeriveJunction::hard(hard_derivation_no_prefix) // String hard derivation + } + } else { + unreachable!("Should not have SOFT derivation detected here"); + } + }); + + let (derived_pair, derived_seed_opt) = base_pair + .derive(derive_junctions_from_path.into_iter(), Some(base_seed)) + .map_err(|e| GcliError::Input(format!("Failed to derive key: {e}")))?; + let derived_seed = derived_seed_opt.ok_or(GcliError::Input(format!("Derived seed should be present when base seed is provided (and no soft derivation is used) - base seed was:'0x{}'",hex::encode(base_seed))))?; + let seed_array: [u8; 32] = derived_seed + .as_slice() + .try_into() + .map_err(|_e| GcliError::Input("Derived seed should be 32 bytes".into()))?; + + (derived_pair, seed_array) + } else { + (base_pair, base_seed) + }; + + let result_pair_account_id: AccountId = result_pair.public().into(); + let pair_from_suri_account_id: AccountId = pair_from_suri_for_validation.public().into(); + if result_pair_account_id != pair_from_suri_account_id { + return Err(GcliError::Input(format!("Computed pair has a different address: '{result_pair_account_id}' != '{pair_from_suri_account_id}'"))); + } + + Ok((result_pair, result_seed)) +} + /// get mnemonic from predefined derivation path pub fn predefined_suri(deriv: &str) -> String { format!("{SUBSTRATE_MNEMONIC}//{deriv}") @@ -534,6 +660,7 @@ mod tests { mod substrate { use super::*; + use crate::keys::SUBSTRATE_MNEMONIC; /// Testing sr25519 mnemonic derivations /// @@ -772,7 +899,7 @@ mod tests { } } - mod cesium { + mod g1v1 { use super::*; /// Test which verifies that it's possible to derive a key coming from a cesium v1 id & password @@ -806,7 +933,7 @@ mod tests { /// SS58 Address: 5Ca1HrNxQ4hiekd92Z99fzhfdSAqPy2rUkLBmwLsgLCjeSQf /// ``` #[test] - fn test_cesium_v1_key_derivation() { + fn test_g1v1_key_derivation() { let cesium_id = "test_cesium_id".to_string(); let cesium_pwd = "test_cesium_pwd".to_string(); @@ -919,7 +1046,7 @@ mod tests { /// SS58 Address: 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4 /// ``` #[test] - fn test_cesium_v1_seed_using_scrypt() { + fn test_g1v1_seed_using_scrypt() { let cesium_id = "test_cesium_id".to_string(); let cesium_pwd = "test_cesium_pwd".to_string(); @@ -963,4 +1090,239 @@ mod tests { ); } } + + mod parameterized_tests { + use super::*; + use rstest::rstest; + + #[rstest] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"), + String::from("5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV"), + String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e") + )] + #[case( + String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"), + String::from("5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV"), + String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e") + )] + #[case( + String::from( + "bottom drive obey lake curtain smoke basket hold race lonely fit walk//0" + ), + String::from("5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH"), + String::from("0x914dded06277afbe5b0e8a30bce539ec8a9552a784d08e530dc7c2915c478393") + )] + #[case( + String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0"), + String::from("5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH"), + String::from("0x914dded06277afbe5b0e8a30bce539ec8a9552a784d08e530dc7c2915c478393") + )] + #[case( + String::from( + "bottom drive obey lake curtain smoke basket hold race lonely fit walk//0//1" + ), + String::from("5Cr4pXhwGbmjQpWw86zYcym5QiixA9XEyMujtiFmHLMNE1wB"), + String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e") + )] + #[case( + String::from( + "0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0//1" + ), + String::from("5Cr4pXhwGbmjQpWw86zYcym5QiixA9XEyMujtiFmHLMNE1wB"), + String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e") + )] + #[case( + String::from("0x914dded06277afbe5b0e8a30bce539ec8a9552a784d08e530dc7c2915c478393//1"), + String::from("5Cr4pXhwGbmjQpWw86zYcym5QiixA9XEyMujtiFmHLMNE1wB"), + String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e") + )] + #[case( + String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e"), + String::from("5Cr4pXhwGbmjQpWw86zYcym5QiixA9XEyMujtiFmHLMNE1wB"), + String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e") + )] + #[case( + String::from( + "bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice" + ), + String::from("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"), + String::from("0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a") + )] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Alan" + ), + String::from("5HHCrQYbfr1n9me9LoB4REHZZ9BQZAcw2dnbUKdzdXkFLEr6"), + String::from("0x06ace9363c66d855542d55d8b233c697db3bd7fe2fbdc6c34cefb94ad43eccf0") + )] + /// Expected data was retrieved using `subkey inspect` command + fn sr25519_compute_pair_and_mini_secret_seed_from_suri( + #[case] suri_string: String, + #[case] expected_address: String, + #[case] expected_seed: String, + ) { + let (keypair, seed) = compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme( + &suri_string, + CryptoScheme::Sr25519, + ) + .unwrap(); + + assert_eq!(format!("0x{}", hex::encode(seed)), expected_seed); + + assert_eq!(keypair.address().to_string(), expected_address) + } + + #[rstest] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk/0"), + String::from("Input(\"Soft derivation is not supported when trying to retrieve mini-secret/seed\")") + )] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0/1"), + String::from("Input(\"Soft derivation is not supported when trying to retrieve mini-secret/seed\")") + )] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk///pass"), + String::from("Input(\"Having a password in the derivation path is not supported (password:'pass')\")") + )] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk///pass//0"), + String::from("Input(\"Having a password in the derivation path is not supported (password:'pass//0')\")") + )] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0///pass//1"), + String::from("Input(\"Having a password in the derivation path is not supported (password:'pass//1')\")") + )] + fn sr25519_compute_pair_and_mini_secret_seed_from_suri_expecting_errors( + #[case] suri_string: String, + #[case] expected_error: String, + ) { + let result = compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme( + &suri_string, + CryptoScheme::Sr25519, + ); + + match result { + Ok(_) => assert!(false, "expected error"), + Err(e) => assert_eq!(expected_error, e.to_string()), + } + } + + #[rstest] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"), + String::from("5DFJF7tY4bpbpcKPJcBTQaKuCDEPCpiz8TRjpmLeTtweqmXL"), + String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e") + )] + #[case( + String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"), + String::from("5DFJF7tY4bpbpcKPJcBTQaKuCDEPCpiz8TRjpmLeTtweqmXL"), + String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e") + )] + #[case( + String::from( + "bottom drive obey lake curtain smoke basket hold race lonely fit walk//0" + ), + String::from("5HrCphkqYygSXWt9rHebqaqbfEYekhzjyjQNjZiPxpb3XsKY"), + String::from("0xf8dfdb0f1103d9fb2905204ac32529d5f148761c4321b2865b0a40e15be75f57") + )] + #[case( + String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0"), + String::from("5HrCphkqYygSXWt9rHebqaqbfEYekhzjyjQNjZiPxpb3XsKY"), + String::from("0xf8dfdb0f1103d9fb2905204ac32529d5f148761c4321b2865b0a40e15be75f57") + )] + #[case( + String::from( + "bottom drive obey lake curtain smoke basket hold race lonely fit walk//0//1" + ), + String::from("5G4xo2TvB4Uv7MR1b35yWBW8g7WLaiuETWChtX1dHkyHrLEP"), + String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab") + )] + #[case( + String::from( + "0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0//1" + ), + String::from("5G4xo2TvB4Uv7MR1b35yWBW8g7WLaiuETWChtX1dHkyHrLEP"), + String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab") + )] + #[case( + String::from("0xf8dfdb0f1103d9fb2905204ac32529d5f148761c4321b2865b0a40e15be75f57//1"), + String::from("5G4xo2TvB4Uv7MR1b35yWBW8g7WLaiuETWChtX1dHkyHrLEP"), + String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab") + )] + #[case( + String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab"), + String::from("5G4xo2TvB4Uv7MR1b35yWBW8g7WLaiuETWChtX1dHkyHrLEP"), + String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab") + )] + #[case( + String::from( + "bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice" + ), + String::from("5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu"), + String::from("0xabf8e5bdbe30c65656c0a3cbd181ff8a56294a69dfedd27982aace4a76909115") + )] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Alan" + ), + String::from("5CvaztNtrHKyi4vPK4xHNRCyLdoWTFovSr9g2MekGUGEvBaS"), + String::from("0xb1f3996e8083bda16a43abfdbea6549bbfecdcc4b5c043a73645fff232765fca") + )] + /// Expected data was retrieved using `subkey inspect --scheme ed25519` command + fn ed25519_compute_pair_and_mini_secret_seed_from_suri( + #[case] suri_string: String, + #[case] expected_address: String, + #[case] expected_seed: String, + ) { + let (keypair, seed) = compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme( + &suri_string, + CryptoScheme::Ed25519, + ) + .unwrap(); + + assert_eq!(format!("0x{}", hex::encode(seed)), expected_seed); + + assert_eq!(keypair.address().to_string(), expected_address) + } + + #[rstest] + #[case( + String::from( + "bottom drive obey lake curtain smoke basket hold race lonely fit walk/0" + ), + String::from("Input(\"Invalid secret\")") + )] + #[case( + String::from( + "bottom drive obey lake curtain smoke basket hold race lonely fit walk//0/1" + ), + String::from("Input(\"Invalid secret\")") + )] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk///pass"), + String::from("Input(\"Having a password in the derivation path is not supported (password:'pass')\")") + )] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk///pass//0"), + String::from("Input(\"Having a password in the derivation path is not supported (password:'pass//0')\")") + )] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0///pass//1"), + String::from("Input(\"Having a password in the derivation path is not supported (password:'pass//1')\")") + )] + fn ed25519_compute_pair_and_mini_secret_seed_from_suri_expecting_errors( + #[case] suri_string: String, + #[case] expected_error: String, + ) { + let result = compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme( + &suri_string, + CryptoScheme::Ed25519, + ); + + match result { + Ok(_) => assert!(false, "expected error"), + Err(e) => assert_eq!(expected_error, e.to_string()), + } + } + } } -- GitLab From 4b410eeb1e09e97f316d875221ffa76da9b500eb Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sun, 6 Apr 2025 18:48:21 +0200 Subject: [PATCH 26/27] * Updated CHANGELOG.md with `vault inspect` changes --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4008188..0dd2859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,9 +22,15 @@ List of changelogs ordered from latest to oldest - `-a` SS58 Address of target vault account - `-v` Name of target vault account - `-S` (secret_format) doesn't have a default anymore since it conflicts with using `-a` or `-v` +- Adapted `vault inspect` command to display more information + - Substrate URI (was already present) + - Crypto scheme: to be more coherent since both the `substrate uri` and `crypto-scheme` are needed to create the key with the proper `address` + - Secret seed/mini-secret: only computed if no `soft` (single '/') derivation are used + - Public key (hex) + - SS58 Address + - (potential G1v1 public key if inspecting a \<Base\> account with ed25519 crypto-scheme) - Small changes - In the vault, changed sorting of children account so that they are sorted by derivation `Path` value (makes more sense when viewing derivations of one account) - - Added display of `crypto scheme` when calling `vault inspect` so that it is more coherent; since both the `substrate uri` and `crypto-scheme` are needed to create the key with the proper `address` ### Fixed - None -- GitLab From c0da476b9a7c7d70eaccb79d1824b5818fa3b004 Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Tue, 8 Apr 2025 20:39:14 +0200 Subject: [PATCH 27/27] * Updated CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dd2859..c772b67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ List of changelogs ordered from latest to oldest -## [0.4.1] - 2025-04-04 +## [0.4.1] - 2025-04-08 ### Added / Changed - We now use `ed25519` crypto scheme by default for all commands. It means that if you import your substrate mnemonic without giving a specific argument to change the crypto scheme, the resulting SS58 address will not be the same as before (it was using `sr25519` for substrate mnemonic previously) - Extra `-c` / `--crypto-scheme` argument allows to specify which crypto scheme to use (`sr25519` or `ed25519`) and having a default value of `ed25519` in most places. -- GitLab