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 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 47c0fa7c94f96712d23f4d087c7d315da534a96f Mon Sep 17 00:00:00 2001 From: poka <poka@p2p.legal> Date: Tue, 11 Mar 2025 17:53:02 +0100 Subject: [PATCH 6/6] Add profile command for nostr data --- Cargo.lock | 55 +++ Cargo.toml | 8 +- src/commands.rs | 1 + src/commands/profile.rs | 947 ++++++++++++++++++++++++++++++++++++++++ src/commands/vault.rs | 2 +- src/conf.rs | 11 +- src/main.rs | 4 + 7 files changed, 1025 insertions(+), 3 deletions(-) create mode 100644 src/commands/profile.rs diff --git a/Cargo.lock b/Cargo.lock index a9cd348..b2ed55d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1587,6 +1587,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" + [[package]] name = "der" version = "0.7.9" @@ -2342,6 +2348,7 @@ version = "0.4.0" dependencies = [ "age", "anyhow", + "bech32", "bip39", "bs58", "clap", @@ -2361,12 +2368,16 @@ dependencies = [ "rstest", "scrypt", "sea-orm", + "secp256k1", "serde", "serde_json", + "sha2 0.10.8", "sp-core", "sp-runtime", "subxt", "tokio", + "tokio-tungstenite", + "url", ] [[package]] @@ -5172,6 +5183,7 @@ version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ + "rand", "secp256k1-sys", ] @@ -6742,6 +6754,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "rustls 0.22.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tungstenite", + "webpki-roots 0.26.6", +] + [[package]] name = "tokio-util" version = "0.7.12" @@ -6969,6 +6997,27 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "rustls 0.22.4", + "rustls-pki-types", + "sha1", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -7114,6 +7163,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index ce66a64..da38696 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ sp-runtime = { git = "https://github.com/duniter/duniter-polkadot-sdk.git", bran # crates.io dependencies anyhow = "^1.0" +bech32 = "^0.9.1" clap = { version = "^4.5.19", features = ["derive"] } codec = { package = "parity-scale-codec", version = "^3.6.12" } env_logger = "^0.10" @@ -35,16 +36,21 @@ hex = "^0.4.3" log = "^0.4.22" reqwest = { version = "^0.11.27", default-features = false, features = [ "rustls-tls", + "json", ] } inquire = "^0.7.5" serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0.128" -tokio = { version = "^1.40.0", features = ["macros"] } +tokio = { version = "^1.40.0", features = ["macros", "time"] } +tokio-tungstenite = { version = "0.21.0", features = ["rustls-tls-webpki-roots"] } +url = "2.5.0" confy = "^0.5.1" bs58 = "^0.5.1" directories = "^5.0.1" comfy-table = "^7.1.1" sea-orm = { version = "1.1.0", features = [ "sqlx-sqlite", "runtime-tokio-native-tls", "macros" ] } +sha2 = "0.10.8" +secp256k1 = { version = "0.28.2", features = ["rand", "recovery"] } # crypto scrypt = { version = "^0.11", default-features = false } # for old-style key generation diff --git a/src/commands.rs b/src/commands.rs index 15f96ba..a1e0dc8 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -8,6 +8,7 @@ pub mod expire; pub mod identity; pub mod net_test; pub mod oneshot; +pub mod profile; pub mod publish; pub mod revocation; pub mod runtime; diff --git a/src/commands/profile.rs b/src/commands/profile.rs new file mode 100644 index 0000000..954a408 --- /dev/null +++ b/src/commands/profile.rs @@ -0,0 +1,947 @@ +use crate::*; +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::collections::HashMap; +use std::time::{SystemTime, UNIX_EPOCH}; +use sp_core::crypto::Pair; +use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; +use futures::{SinkExt, StreamExt}; +use url::Url; +use sha2::{Sha256, Digest}; +use secp256k1::{Secp256k1, SecretKey, PublicKey}; +use bech32::{self, ToBase32, Variant}; + +/// Derive a Nostr key from a Substrate key +fn derive_nostr_keys_from_substrate(keypair: &KeyPair) -> Result<(SecretKey, PublicKey), GcliError> { + // Get the seed from the keypair - use a more direct approach + let seed = match keypair { + KeyPair::Sr25519(pair) => { + // For Sr25519, we'll use the raw bytes directly + let key_bytes = pair.to_raw_vec(); + // Take the first 32 bytes as the seed + let mut seed = [0u8; 32]; + for i in 0..std::cmp::min(32, key_bytes.len()) { + seed[i] = key_bytes[i]; + } + seed.to_vec() + } + KeyPair::Ed25519(pair) => { + // For Ed25519, we'll use the raw bytes directly + let key_bytes = pair.to_raw_vec(); + // Take the first 32 bytes as the seed + let mut seed = [0u8; 32]; + for i in 0..std::cmp::min(32, key_bytes.len()) { + seed[i] = key_bytes[i]; + } + seed.to_vec() + } + }; + + // Create a secp256k1 secret key from the seed + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&seed[0..32]) + .map_err(|e| anyhow!("Failed to create secp256k1 secret key: {}", e))?; + + // Derive the public key + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + + Ok((secret_key, public_key)) +} + +/// Convert a hex string to bech32 format with the given prefix (npub/nsec) +fn hex_to_bech32(hex_key: &str, prefix: &str) -> Result<String, GcliError> { + // Decode the hex string to bytes + let bytes = hex::decode(hex_key) + .map_err(|e| anyhow!("Failed to decode hex key: {}", e))?; + + // Convert bytes to base32 + let base32_data = bytes.to_base32(); + + // Encode as bech32 + let bech32_str = bech32::encode(prefix, base32_data, Variant::Bech32) + .map_err(|e| anyhow!("Failed to encode bech32: {}", e))?; + + Ok(bech32_str) +} + +/// Get Nostr public key in hex format +fn get_nostr_pubkey(keypair: &KeyPair) -> Result<String, GcliError> { + let (_, public_key) = derive_nostr_keys_from_substrate(keypair)?; + // Nostr uses the x-only public key (32 bytes) + let serialized = public_key.serialize(); + // Skip the first byte (format byte) and take only the x coordinate + let pubkey = hex::encode(&serialized[1..33]); + Ok(pubkey) +} + +/// Get Nostr public key in bech32 format (npub) +fn get_nostr_npub(keypair: &KeyPair) -> Result<String, GcliError> { + let hex_pubkey = get_nostr_pubkey(keypair)?; + let npub = hex_to_bech32(&hex_pubkey, "npub")?; + Ok(npub) +} + +/// Get Nostr private key in bech32 format (nsec) +fn get_nostr_nsec(keypair: &KeyPair) -> Result<String, GcliError> { + let (secret_key, _) = derive_nostr_keys_from_substrate(keypair)?; + let hex_seckey = hex::encode(secret_key.secret_bytes()); + let nsec = hex_to_bech32(&hex_seckey, "nsec")?; + Ok(nsec) +} + +/// Profile subcommands +#[derive(Clone, Debug, clap::Parser)] +pub enum Subcommand { + /// Get Nostr profile data from a relay + Get { + /// Relay URL to fetch profile from + #[clap(short, long)] + relay: Option<String>, + }, + /// Set Nostr profile data (NIP-01 kind 0 metadata) + Set { + /// Profile name + #[clap(short, long)] + name: Option<String>, + + /// Profile display name + #[clap(short = 'd', long)] + display_name: Option<String>, + + /// Profile picture URL + #[clap(short = 'p', long)] + picture: Option<String>, + + /// Profile about/description + #[clap(short, long)] + about: Option<String>, + + /// Profile website + #[clap(short = 'w', long)] + website: Option<String>, + + /// Profile NIP-05 identifier + #[clap(short = 'i', long)] + nip05: Option<String>, + + /// Relay URL to publish profile to + #[clap(short, long)] + relay: Option<String>, + }, +} + +/// Nostr profile metadata (NIP-01 kind 0) +#[derive(Debug, Serialize, Deserialize)] +pub struct NostrProfile { + pub name: Option<String>, + pub display_name: Option<String>, + pub picture: Option<String>, + pub about: Option<String>, + pub website: Option<String>, + pub nip05: Option<String>, + #[serde(flatten)] + pub additional_fields: HashMap<String, String>, +} + +impl Default for NostrProfile { + fn default() -> Self { + Self { + name: None, + display_name: None, + picture: None, + about: None, + website: None, + nip05: None, + additional_fields: HashMap::new(), + } + } +} + +/// Nostr event structure +#[derive(Debug, Serialize, Deserialize)] +pub struct NostrEvent { + pub id: String, + pub pubkey: String, + pub created_at: u64, + pub kind: u32, + pub tags: Vec<Vec<String>>, + pub content: String, + pub sig: String, +} + +impl NostrEvent { + /// Create a new unsigned Nostr event + pub fn new(pubkey: String, kind: u32, content: String) -> Self { + let created_at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + Self { + id: String::new(), // Will be set after serialization + pubkey, + created_at, + kind, + tags: Vec::new(), + content, + sig: String::new(), // Will be set after signing + } + } + + /// Calculate the event ID (SHA-256 hash of the serialized event) + pub fn calculate_id(&mut self) -> Result<(), GcliError> { + // Create a temporary event for serialization exactly as specified by NIP-01 + // The order is important: [0, pubkey, created_at, kind, tags, content] + let temp_event = json!([ + 0, + self.pubkey, + self.created_at, + self.kind, + self.tags, + self.content + ]); + + // Serialize the event with no whitespace and canonical ordering + // Using to_string() instead of to_string_pretty() to avoid whitespace + let serialized = serde_json::to_string(&temp_event) + .map_err(|e| anyhow!("Failed to serialize event: {}", e))?; + + // Calculate SHA-256 hash + let mut hasher = Sha256::new(); + hasher.update(serialized.as_bytes()); + let result = hasher.finalize(); + + // Set the ID + self.id = hex::encode(result); + Ok(()) + } + + /// Sign the event with the given keypair + pub fn sign(&mut self, keypair: &KeyPair) -> Result<(), GcliError> { + // Calculate ID if not already set + if self.id.is_empty() { + self.calculate_id()?; + } + + // Derive Nostr keys from Substrate keypair + let (secret_key, public_key) = derive_nostr_keys_from_substrate(keypair)?; + + // Verify that the pubkey in the event matches our derived pubkey + let derived_pubkey = hex::encode(&public_key.serialize()[1..33]); + if self.pubkey != derived_pubkey { + // Update the pubkey to match + self.pubkey = derived_pubkey; + // Recalculate ID with the correct pubkey + self.calculate_id()?; + } + + // Create a secp256k1 context for Schnorr signatures + let secp = Secp256k1::new(); + + // Create a message from the event ID + let id_bytes = hex::decode(&self.id) + .map_err(|e| anyhow!("Failed to decode hex ID: {}", e))?; + + let message = secp256k1::Message::from_digest_slice(&id_bytes) + .map_err(|e| anyhow!("Failed to create secp256k1 message: {}", e))?; + + // Sign the message with Schnorr + let aux_rand = [0u8; 32]; // Use zeros for deterministic signatures + let keypair_secp = secp256k1::Keypair::from_secret_key(&secp, &secret_key); + let signature = secp256k1::schnorr::Signature::from_slice( + secp.sign_schnorr_with_aux_rand(&message, &keypair_secp, &aux_rand).as_ref() + ).unwrap(); + + // Set the signature + self.sig = hex::encode(signature.as_ref()); + + // Verify the signature + let verify_result = verify_nostr_event(self); + + match verify_result { + Ok(true) => { + Ok(()) + }, + Ok(false) => { + self.try_alternative_signing(keypair) + }, + Err(_e) => { + self.try_alternative_signing(keypair) + } + } + } + + /// Try an alternative signing method if the first one fails + fn try_alternative_signing(&mut self, keypair: &KeyPair) -> Result<(), GcliError> { + // Use a different approach for key derivation + let seed = match keypair { + KeyPair::Sr25519(pair) => { + // Use a hash of the key bytes + let mut hasher = Sha256::new(); + hasher.update(&pair.to_raw_vec()); + hasher.finalize().to_vec() + } + KeyPair::Ed25519(pair) => { + // Use a hash of the key bytes + let mut hasher = Sha256::new(); + hasher.update(&pair.to_raw_vec()); + hasher.finalize().to_vec() + } + }; + + // Create a secp256k1 secret key from the seed + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&seed[0..32]) + .map_err(|e| anyhow!("Failed to create alternative secp256k1 secret key: {}", e))?; + + // Derive the public key + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + + // Update the pubkey in the event + let derived_pubkey = hex::encode(&public_key.serialize()[1..33]); + self.pubkey = derived_pubkey; + + // Recalculate ID with the new pubkey + self.calculate_id()?; + + // Create a message from the event ID + let id_bytes = hex::decode(&self.id) + .map_err(|e| anyhow!("Failed to decode hex ID: {}", e))?; + + let message = secp256k1::Message::from_digest_slice(&id_bytes) + .map_err(|e| anyhow!("Failed to create secp256k1 message: {}", e))?; + + // Sign the message with Schnorr + let aux_rand = [0u8; 32]; // Use zeros for deterministic signatures + let keypair_secp = secp256k1::Keypair::from_secret_key(&secp, &secret_key); + let signature = secp256k1::schnorr::Signature::from_slice( + secp.sign_schnorr_with_aux_rand(&message, &keypair_secp, &aux_rand).as_ref() + ).unwrap(); + + // Set the signature + self.sig = hex::encode(signature.as_ref()); + + // Verify the signature + let verify_result = verify_nostr_event(self); + + match verify_result { + Ok(true) => { + Ok(()) + }, + Ok(false) => { + self.try_third_signing_approach(keypair) + }, + Err(_e) => { + self.try_third_signing_approach(keypair) + } + } + } + + /// Try a third signing approach if the first two fail + fn try_third_signing_approach(&mut self, keypair: &KeyPair) -> Result<(), GcliError> { + // Use a more complex derivation approach + let seed = match keypair { + KeyPair::Sr25519(pair) => { + // Use multiple rounds of hashing + let mut hasher1 = Sha256::new(); + hasher1.update("nostr".as_bytes()); + hasher1.update(&pair.to_raw_vec()); + let first_hash = hasher1.finalize(); + + let mut hasher2 = Sha256::new(); + hasher2.update(first_hash); + hasher2.finalize().to_vec() + } + KeyPair::Ed25519(pair) => { + // Use multiple rounds of hashing + let mut hasher1 = Sha256::new(); + hasher1.update("nostr".as_bytes()); + hasher1.update(&pair.to_raw_vec()); + let first_hash = hasher1.finalize(); + + let mut hasher2 = Sha256::new(); + hasher2.update(first_hash); + hasher2.finalize().to_vec() + } + }; + + // Create a secp256k1 secret key from the seed + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&seed[0..32]) + .map_err(|e| anyhow!("Failed to create third secp256k1 secret key: {}", e))?; + + // Derive the public key + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + + // Update the pubkey in the event + let derived_pubkey = hex::encode(&public_key.serialize()[1..33]); + self.pubkey = derived_pubkey; + + // Recalculate ID with the new pubkey + self.calculate_id()?; + + // Create a message from the event ID + let id_bytes = hex::decode(&self.id) + .map_err(|e| anyhow!("Failed to decode hex ID: {}", e))?; + + let message = secp256k1::Message::from_digest_slice(&id_bytes) + .map_err(|e| anyhow!("Failed to create secp256k1 message: {}", e))?; + + // Sign the message with Schnorr + let aux_rand = [0u8; 32]; // Use zeros for deterministic signatures + let keypair_secp = secp256k1::Keypair::from_secret_key(&secp, &secret_key); + let signature = secp256k1::schnorr::Signature::from_slice( + secp.sign_schnorr_with_aux_rand(&message, &keypair_secp, &aux_rand).as_ref() + ).unwrap(); + + // Set the signature + self.sig = hex::encode(signature.as_ref()); + + // Verify the signature + let verify_result = verify_nostr_event(self); + + match verify_result { + Ok(true) => { + Ok(()) + }, + Ok(false) => { + Err(anyhow!("All signature approaches failed").into()) + }, + Err(e) => { + Err(anyhow!("Error verifying third signature: {}", e).into()) + } + } + } +} + +/// Verify a Nostr event signature +fn verify_nostr_event(event: &NostrEvent) -> Result<bool, GcliError> { + // Decode the pubkey + let pubkey_bytes = hex::decode(&event.pubkey) + .map_err(|e| anyhow!("Failed to decode pubkey: {}", e))?; + + // Create a secp256k1 context + let secp = Secp256k1::new(); + + // Create x-only public key from the 32-byte pubkey + let xonly_pubkey = secp256k1::XOnlyPublicKey::from_slice(&pubkey_bytes) + .map_err(|e| anyhow!("Failed to create x-only public key: {}", e))?; + + // Decode the ID + let id_bytes = hex::decode(&event.id) + .map_err(|e| anyhow!("Failed to decode ID: {}", e))?; + + // Create a message from the ID + let message = secp256k1::Message::from_digest_slice(&id_bytes) + .map_err(|e| anyhow!("Failed to create message: {}", e))?; + + // Decode the signature + let sig_bytes = hex::decode(&event.sig) + .map_err(|e| anyhow!("Failed to decode signature: {}", e))?; + + // Create a Schnorr signature from the signature bytes + let signature = secp256k1::schnorr::Signature::from_slice(&sig_bytes) + .map_err(|e| anyhow!("Failed to create Schnorr signature: {}", e))?; + + // Verify the Schnorr signature + match secp.verify_schnorr(&signature, &message, &xonly_pubkey) { + Ok(_) => { + Ok(true) + }, + Err(_e) => { + Ok(false) + } + } +} + +/// Handle profile commands +pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { + // Enable debug logging for this module if not already set + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "debug"); + log::debug!("RUST_LOG environment variable set to debug"); + } + + match command { + Subcommand::Get { relay } => get_profile(data, relay).await, + Subcommand::Set { + name, + display_name, + picture, + about, + website, + nip05, + relay, + } => { + set_profile( + data, + name, + display_name, + picture, + about, + website, + nip05, + relay, + ) + .await + } + } +} + +/// Get Nostr profile from a relay +async fn get_profile(data: Data, relay_url: Option<String>) -> Result<(), GcliError> { + // Get keypair from vault using configured address + let keypair = fetch_or_get_keypair(&data, data.cfg.address.clone()).await?; + + // Get Nostr pubkey in hex format (used for protocol) + let pubkey = get_nostr_pubkey(&keypair)?; + + // Get Nostr pubkey in bech32 format (for display) + let npub = get_nostr_npub(&keypair)?; + + println!("Searching for profile with pubkey: {}", pubkey); + + // Use default relay if none provided + let relay = relay_url.unwrap_or_else(|| { + data.cfg + .relay + .clone() + .unwrap_or_else(|| "wss://relay.copylaradio.com".to_string()) + }); + + // Ensure the relay URL starts with ws:// or wss:// + let relay = if !relay.starts_with("ws://") && !relay.starts_with("wss://") { + format!("wss://{}", relay) + } else { + relay + }; + + // Parse the URL + let url = Url::parse(&relay).map_err(|e| anyhow!("Invalid relay URL: {}", e))?; + + // Connect to the WebSocket + println!("Connecting to relay {}...", relay); + let (mut ws_stream, _) = connect_async(url).await + .map_err(|e| anyhow!("Failed to connect to relay: {}", e))?; + + println!("Connected to relay, requesting profile..."); + + // Create subscription request - CORRECTED FORMAT + // According to NIP-01, REQ should be an array: ["REQ", <subscription_id>, <filter>, ...] + let filter = json!({ + "kinds": [0], + "authors": [pubkey], + "limit": 1 + }); + + // Send the request in the correct format + let req_msg = json!(["REQ", "profile-request", filter]); + + println!("Sending request: {}", req_msg.to_string()); + ws_stream.send(Message::Text(req_msg.to_string())).await + .map_err(|e| anyhow!("Failed to send request: {}", e))?; + + // Wait for response with timeout + let mut profile_found = false; + let mut profile = NostrProfile::default(); + let mut all_messages: Vec<String> = Vec::new(); // Store all received messages for debugging + + // Set a timeout for receiving messages + let timeout = tokio::time::Duration::from_secs(10); // Increased timeout + let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(100)); + + let start_time = tokio::time::Instant::now(); + + while start_time.elapsed() < timeout { + interval.tick().await; + + // Check for messages with a timeout + match tokio::time::timeout( + tokio::time::Duration::from_millis(100), + ws_stream.next() + ).await { + Ok(Some(Ok(msg))) => { + if let Message::Text(text) = msg { + all_messages.push(text.clone()); + + // Parse the message + if let Ok(json) = serde_json::from_str::<Value>(&text) { + + // Check if it's an EVENT message + if let Some(event_type) = json.get(0).and_then(|v| v.as_str()) { + + if event_type == "EVENT" && json.get(1).is_some() && json.get(2).is_some() { + + if let Some(content) = json[2]["content"].as_str() { + + // Try to parse the profile + match serde_json::from_str::<NostrProfile>(content) { + Ok(parsed_profile) => { + profile = parsed_profile; + profile_found = true; + + // Close the subscription + let close_msg = json!(["CLOSE", "profile-request"]); + ws_stream.send(Message::Text(close_msg.to_string())).await + .map_err(|e| anyhow!("Failed to close subscription: {}", e))?; + + break; + }, + Err(_) => { + } + } + } + } else if event_type == "EOSE" { + // End of stored events + if !profile_found { + // No profile found, close the connection + let close_msg = json!(["CLOSE", "profile-request"]); + ws_stream.send(Message::Text(close_msg.to_string())).await + .map_err(|e| anyhow!("Failed to close subscription: {}", e))?; + + break; + } + } + } + } + } + }, + Ok(Some(Err(_e))) => { + break; + }, + Ok(None) => { + // Connection closed + break; + }, + Err(_) => { + // Timeout, continue + continue; + } + } + } + + // Close the WebSocket connection + ws_stream.close(None).await.ok(); + + // Display the profile or a message if not found + if profile_found { + match data.args.output_format { + OutputFormat::Human => { + println!("Profile for npub: {}", npub); + if let Some(name) = &profile.name { + println!("Name: {}", name); + } + if let Some(display_name) = &profile.display_name { + println!("Display Name: {}", display_name); + } + if let Some(picture) = &profile.picture { + println!("Picture: {}", picture); + } + if let Some(about) = &profile.about { + println!("About: {}", about); + } + if let Some(website) = &profile.website { + println!("Website: {}", website); + } + if let Some(nip05) = &profile.nip05 { + println!("NIP-05: {}", nip05); + } + for (key, value) in &profile.additional_fields { + println!("{}: {}", key, value); + } + } + OutputFormat::Json => { + let output = json!({ + "pubkey_hex": pubkey, + "pubkey_bech32": npub, + "profile": profile + }); + println!("{}", serde_json::to_string_pretty(&output).unwrap()); + } + } + } else { + match data.args.output_format { + OutputFormat::Human => { + println!("No profile found for npub: {}", npub); + } + OutputFormat::Json => { + let output = json!({ + "pubkey_hex": pubkey, + "pubkey_bech32": npub, + "profile": NostrProfile::default() + }); + println!("{}", serde_json::to_string_pretty(&output).unwrap()); + } + } + } + + Ok(()) +} + +/// Set Nostr profile and publish to a relay +async fn set_profile( + data: Data, + name: Option<String>, + display_name: Option<String>, + picture: Option<String>, + about: Option<String>, + website: Option<String>, + nip05: Option<String>, + relay_url: Option<String>, +) -> Result<(), GcliError> { + // Check if no options were provided, and if so, display help + if name.is_none() && display_name.is_none() && picture.is_none() && + about.is_none() && website.is_none() && nip05.is_none() { + println!("No profile options specified. Available options:"); + println!(" -n, --name <NAME> Set profile name"); + println!(" -d, --display-name <DISPLAY_NAME> Set profile display name"); + println!(" -p, --picture <PICTURE> Set profile picture URL"); + println!(" -a, --about <ABOUT> Set profile about/description"); + println!(" -w, --website <WEBSITE> Set profile website URL"); + println!(" -i, --nip05 <NIP05> Set profile NIP-05 identifier"); + println!(" -r, --relay <RELAY> Specify relay URL to publish to"); + println!("\nExample: gcli profile set --name \"Alice\" --about \"Nostr user\""); + return Ok(()); + } + + // Get keypair from vault using configured address + let keypair = fetch_or_get_keypair(&data, data.cfg.address.clone()).await?; + + // Get Nostr pubkey in hex format (used for protocol) + let pubkey = get_nostr_pubkey(&keypair)?; + + // Get Nostr pubkey in bech32 format (for display) + let npub = get_nostr_npub(&keypair)?; + + // Get Nostr private key in bech32 format (for display) + let nsec = get_nostr_nsec(&keypair)?; + + // Create profile data + let mut profile = NostrProfile::default(); + + if let Some(name) = name { + profile.name = Some(name); + } + if let Some(display_name) = display_name { + profile.display_name = Some(display_name); + } + if let Some(picture) = picture { + profile.picture = Some(picture); + } + if let Some(about) = about { + profile.about = Some(about); + } + if let Some(website) = website { + profile.website = Some(website); + } + if let Some(nip05) = nip05 { + profile.nip05 = Some(nip05); + } + + // Serialize profile to JSON + let profile_json = serde_json::to_string(&profile) + .map_err(|e| anyhow!("Failed to serialize profile: {}", e))?; + + // Create and sign Nostr event + let mut event = NostrEvent::new(pubkey.clone(), 0, profile_json); + + // Make sure tags is initialized as an empty array, not null + event.tags = Vec::new(); + + log::debug!("Created event with pubkey: {}", event.pubkey); + log::debug!("Event content: {}", event.content); + + // Calculate ID and sign + event.calculate_id()?; + log::debug!("Calculated event ID: {}", event.id); + + event.sign(&keypair)?; + log::debug!("Signed event with signature: {}", event.sig); + + // Log the complete event for debugging + log::debug!("Event to publish: {}", serde_json::to_string_pretty(&event).unwrap()); + + // Verify the event signature + match verify_nostr_event(&event) { + Ok(true) => log::debug!("Event signature verified successfully"), + Ok(false) => { + log::error!("Event signature verification failed - relay will likely reject this event"); + return Err(anyhow!("Event signature verification failed - cannot proceed").into()); + }, + Err(e) => log::warn!("Error verifying event signature: {}", e), + } + + // Use default relay if none provided + let relay = relay_url.unwrap_or_else(|| { + data.cfg + .relay + .clone() + .unwrap_or_else(|| "wss://relay.copylaradio.com".to_string()) + }); + + // Ensure the relay URL starts with ws:// or wss:// + let relay = if !relay.starts_with("ws://") && !relay.starts_with("wss://") { + format!("wss://{}", relay) + } else { + relay + }; + + log::debug!("Using relay URL: {}", relay); + + // Parse the URL + let url = Url::parse(&relay).map_err(|e| anyhow!("Invalid relay URL: {}", e))?; + + // Connect to the WebSocket + println!("Connecting to relay {}...", relay); + let (mut ws_stream, _) = connect_async(url).await + .map_err(|e| anyhow!("Failed to connect to relay: {}", e))?; + + println!("Connected to relay, publishing profile..."); + + // Create event message - CORRECTED FORMAT + // According to NIP-01, EVENT should be an array: ["EVENT", <event_object>] + let event_msg = json!([ + "EVENT", + event + ]); + + let event_msg_str = event_msg.to_string(); + log::debug!("Sending message to relay: {}", event_msg_str); + + // Send the event + ws_stream.send(Message::Text(event_msg_str)).await + .map_err(|e| anyhow!("Failed to send event: {}", e))?; + + // Wait for OK message with timeout + let mut success = false; + let mut response_message = String::new(); + + // Set a timeout for receiving messages + let timeout = tokio::time::Duration::from_secs(10); // Increased timeout + let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(100)); + + log::debug!("Waiting for relay response with timeout of {} seconds", timeout.as_secs()); + + let start_time = tokio::time::Instant::now(); + + while start_time.elapsed() < timeout { + interval.tick().await; + + // Check for messages with a timeout + match tokio::time::timeout( + tokio::time::Duration::from_millis(100), + ws_stream.next() + ).await { + Ok(Some(Ok(msg))) => { + if let Message::Text(text) = msg { + log::debug!("Received message from relay: {}", text); + response_message = text.clone(); + + // Parse the message + if let Ok(json) = serde_json::from_str::<Value>(&text) { + log::debug!("Parsed JSON response: {}", json); + + // Check if it's an OK message + if let Some(msg_type) = json.get(0).and_then(|v| v.as_str()) { + log::debug!("Message type: {}", msg_type); + + if msg_type == "OK" && json.get(1).is_some() && json.get(2).is_some() { + let success_status = json[2].as_bool().unwrap_or(false); + log::debug!("OK status: {}", success_status); + + if success_status { + success = true; + log::debug!("Event accepted by relay"); + break; + } else { + // Error message + if let Some(error_msg) = json.get(3).and_then(|v| v.as_str()) { + log::error!("Relay rejected event: {}", error_msg); + return Err(anyhow!("Relay rejected event: {}", error_msg).into()); + } else { + log::error!("Relay rejected event without specific error message"); + } + break; + } + } else if msg_type == "NOTICE" && json.get(1).and_then(|v| v.as_str()).is_some() { + log::debug!("Received NOTICE: {}", json[1].as_str().unwrap()); + } + } + } else { + log::warn!("Failed to parse relay response as JSON: {}", text); + } + } else { + log::debug!("Received non-text message from relay"); + } + }, + Ok(Some(Err(e))) => { + log::error!("WebSocket error: {}", e); + break; + }, + Ok(None) => { + log::debug!("Connection closed by relay"); + break; + }, + Err(_) => { + // Timeout, continue + continue; + } + } + } + + if start_time.elapsed() >= timeout { + log::warn!("Timeout waiting for relay response"); + } + + // Close the WebSocket connection + log::debug!("Closing WebSocket connection"); + ws_stream.close(None).await.ok(); + + match data.args.output_format { + OutputFormat::Human => { + println!("Profile data published to npub: {}", npub); + + if let Some(name) = &profile.name { + println!("Name: {}", name); + } + if let Some(display_name) = &profile.display_name { + println!("Display Name: {}", display_name); + } + if let Some(picture) = &profile.picture { + println!("Picture: {}", picture); + } + if let Some(about) = &profile.about { + println!("About: {}", about); + } + if let Some(website) = &profile.website { + println!("Website: {}", website); + } + if let Some(nip05) = &profile.nip05 { + println!("NIP-05: {}", nip05); + } + + println!("\nPublished to relay: {}", relay); + if success { + println!("Status: Success"); + } else { + println!("Status: No confirmation received from relay"); + println!("Last response: {}", response_message); + } + } + OutputFormat::Json => { + println!("{}", serde_json::to_string_pretty(&json!({ + "pubkey_hex": pubkey, + "pubkey_bech32": npub, + "seckey_bech32": nsec, + "profile": profile, + "event": event, + "relay": relay, + "success": success, + "response": response_message + })).unwrap()); + } + } + + Ok(()) +} \ No newline at end of file diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 07b40fc..dac5146 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -206,7 +206,7 @@ pub fn decrypt(input: &[u8], passphrase: String) -> Result<Vec<u8>, age::Decrypt pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { let db = data.connect_db(); - // match subcommand + #[allow(deprecated)] match command { Subcommand::List(choice) => match choice { ListChoice::All { show_g1v1, show_type } => { diff --git a/src/conf.rs b/src/conf.rs index 509e0c5..8809c39 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -15,6 +15,8 @@ pub struct Config { /// user address /// to perform actions, user must provide secret pub address: Option<AccountId>, + /// nostr relay endpoint + pub relay: Option<String>, } impl std::default::Default for Config { @@ -23,6 +25,7 @@ impl std::default::Default for Config { duniter_endpoint: String::from(data::LOCAL_DUNITER_ENDPOINT), indexer_endpoint: String::from(data::LOCAL_INDEXER_ENDPOINT), address: None, + relay: Some(String::from("wss://relay.copylaradio.com")), } } } @@ -34,10 +37,16 @@ impl std::fmt::Display for Config { } else { "(no address)".to_string() }; + let relay = if let Some(relay) = &self.relay { + relay.clone() + } else { + "(no relay)".to_string() + }; writeln!(f, "Äžcli config")?; writeln!(f, "duniter endpoint {}", self.duniter_endpoint)?; writeln!(f, "indexer endpoint {}", self.indexer_endpoint)?; - write!(f, "address {address}") + writeln!(f, "address {address}")?; + write!(f, "nostr relay {relay}") } } diff --git a/src/main.rs b/src/main.rs index 116552d..3f8b9fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -149,6 +149,9 @@ pub enum Subcommand { /// Key management (import, generate, list...) #[clap(subcommand)] Vault(commands::vault::Subcommand), + /// Nostr profile management (get, set...) + #[clap(subcommand)] + Profile(commands::profile::Subcommand), /// Cesium #[clap(subcommand, hide = true)] Cesium(commands::cesium::Subcommand), @@ -190,6 +193,7 @@ async fn main() -> Result<(), GcliError> { Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await, Subcommand::Config(subcommand) => conf::handle_command(data, subcommand).await, Subcommand::Vault(subcommand) => commands::vault::handle_command(data, subcommand).await, + Subcommand::Profile(subcommand) => commands::profile::handle_command(data, subcommand).await, Subcommand::Cesium(subcommand) => commands::cesium::handle_command(data, subcommand).await, Subcommand::Publish => commands::publish::handle_command().await, }; -- GitLab