diff --git a/doc/example.md b/doc/example.md index 5a1816a7e0d81756c8012f1b6fa7d4c8a4a0f342..01d1d40fe64927998f5ee5948163badd031c2711 100644 --- a/doc/example.md +++ b/doc/example.md @@ -34,6 +34,8 @@ gcli config gcli config where # save config to use gdev network for next commands gcli --network gdev config save +# save config to use Alice predefined secret +gcli -S predefined -s Alice config save ``` ## Commands diff --git a/src/commands/account.rs b/src/commands/account.rs index eab3ceb3d8d8cc0fece69cf4c2f88d2f01ad4d72..04d058ef4e43b40a277f0c9556db1db0e8bda28a 100644 --- a/src/commands/account.rs +++ b/src/commands/account.rs @@ -31,10 +31,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( let mut data = data.build_client().await?; match command { Subcommand::Balance => { - data = data - .build_address() - .fetch_system_properties() - .await?; + data = data.fetch_system_properties().await?; commands::account::get_balance(data).await? } Subcommand::Transfer { @@ -42,22 +39,11 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( dest, keep_alive, } => { - data = data.build_client().await?; - commands::transfer::transfer( - &data, - amount, - dest, - keep_alive, - ) - .await?; + data = data; + commands::transfer::transfer(&data, amount, dest, keep_alive).await?; } Subcommand::TransferMultiple { amount, dests } => { - data = data.build_client().await?; - commands::transfer::transfer_multiple(&data, - amount, - dests, - ) - .await?; + commands::transfer::transfer_multiple(&data, amount, dests).await?; } }; diff --git a/src/commands/blockchain.rs b/src/commands/blockchain.rs index 85d53d55b49a7a01538f1a07b3fc29286c31039f..ec869d2e7606b750147a658ea60bd785e4134fcf 100644 --- a/src/commands/blockchain.rs +++ b/src/commands/blockchain.rs @@ -27,9 +27,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( Subcommand::Repart { target, actual_repart, - } => { - commands::net_test::repart(&data, target, actual_repart).await? - } + } => commands::net_test::repart(&data, target, actual_repart).await?, Subcommand::SpamRoll { actual_repart } => { commands::net_test::spam_roll(&data, actual_repart).await? } diff --git a/src/commands/identity.rs b/src/commands/identity.rs index 81a08baf28f7242f8c869535173e53f537cc32f9..3d676d663c6169f48a71d325c70a372b1bd2df4c 100644 --- a/src/commands/identity.rs +++ b/src/commands/identity.rs @@ -55,22 +55,19 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( .await? } Subcommand::Create { target } => { - data = data.build_keypair(); + data = data; commands::identity::create_identity(data.keypair(), data.client(), target).await?; } Subcommand::Confirm { name } => { - data = data.build_keypair(); + data = data; commands::identity::confirm_identity(data.keypair(), data.client(), name).await?; } Subcommand::Revoke => { - data = data.build_keypair().fetch_idty_index().await?; + data = data.fetch_idty_index().await?; commands::identity::revoke_identity(data).await?; } Subcommand::GenRevocDoc => { - data = data - .build_keypair() - .fetch_idty_index() - .await?; + data = data.fetch_idty_index().await?; commands::revocation::print_revoc_sig(&data) } }; @@ -136,7 +133,10 @@ pub async fn get_identity( if let (Some(indexer), Some(account_id), None) = (&indexer, &account_id, &username) { username = indexer.username_by_pubkey(&account_id.to_string()).await?; } - println!("Username: {}", username.unwrap_or("<no indexer>".to_string())); + println!( + "Username: {}", + username.unwrap_or("<no indexer>".to_string()) + ); Ok(()) } diff --git a/src/commands/net_test.rs b/src/commands/net_test.rs index f216b6fb5fc513a67dce0c5b179758744155a3a4..ca8648021516a72b342639148c0d2964c685f507 100644 --- a/src/commands/net_test.rs +++ b/src/commands/net_test.rs @@ -3,14 +3,11 @@ use crate::*; use sp_core::DeriveJunction; use subxt::ext::sp_runtime::MultiAddress; -pub async fn repart( - data: &Data, - target: u32, - actual_repart: Option<u32>, -) -> anyhow::Result<()> { +pub async fn repart(data: &Data, target: u32, actual_repart: Option<u32>) -> anyhow::Result<()> { let mut pairs = Vec::new(); for i in actual_repart.unwrap_or_default()..target { - let pair_i = data.keypair() + let pair_i = data + .keypair() .derive(std::iter::once(DeriveJunction::hard::<u32>(i)), None) .map_err(|_| anyhow!("Fail to derive //{}", i))? .0; @@ -28,7 +25,8 @@ pub async fn repart( .await?; signer.increment_nonce();*/ - if let Some(pair_i_account) = data.client() + if let Some(pair_i_account) = data + .client() .storage() .fetch( &runtime::storage().system().account(&pair_i.public().into()), @@ -48,7 +46,8 @@ pub async fn spam_roll(data: &Data, actual_repart: usize) -> anyhow::Result<()> let mut nonce = 0; let mut pairs = Vec::<(PairSigner<Runtime, Pair>, AccountId)>::with_capacity(actual_repart); for i in 0..actual_repart { - let pair_i = data.keypair() + let pair_i = data + .keypair() .derive(std::iter::once(DeriveJunction::hard::<u32>(i as u32)), None) .map_err(|_| anyhow!("Fail to derive //{}", i))? .0; diff --git a/src/commands/smith.rs b/src/commands/smith.rs index 01525bbe3c176668c5b9d7bd536f16adf92db2a8..96092c4d60fdbffdaefc9eab6a7cd859cd07d11c 100644 --- a/src/commands/smith.rs +++ b/src/commands/smith.rs @@ -4,7 +4,8 @@ use std::ops::Deref; type SessionKeys = [u8; 128]; #[cfg(feature = "gdev")] -type SmithMembershipMetaData = runtime::runtime_types::common_runtime::entities::SmithMembershipMetaData::<SessionKeys>; +type SmithMembershipMetaData = + runtime::runtime_types::common_runtime::entities::SmithMembershipMetaData<SessionKeys>; /// define smith subcommands #[derive(Clone, Default, Debug, clap::Parser)] @@ -40,7 +41,6 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( let mut data = data.build_client().await?; match command { Subcommand::Request { endpoint } => { - data = data.build_keypair(); dbg!(request_smith_membership(&data, endpoint).await)?; } Subcommand::GoOnline => { @@ -50,14 +50,14 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( go_offline(&data).await?; } Subcommand::Cert { to } => { - data = data.build_keypair().fetch_idty_index().await?; + data = data.fetch_idty_index().await?; cert(&data, to).await? } Subcommand::UpdateKeys => { update_session_keys(&data).await?; } Subcommand::SudoSetKey { new_key } => { - data = data.build_keypair().build_client().await?; + data = data.build_client().await?; commands::sudo::set_key(data.keypair(), data.client(), new_key).await?; } Subcommand::ShowExpire { blocks, sessions } => { @@ -88,12 +88,11 @@ pub async fn rotate_keys(client: &Client) -> Result<SessionKeys, anyhow::Error> /// request smith membership pub async fn request_smith_membership(data: &Data, endpoint: String) -> Result<(), anyhow::Error> { let session_keys = rotate_keys(data.client()).await?; - let metadata = - SmithMembershipMetaData { - session_keys, - owner_key: data.address(), - p2p_endpoint: endpoint, - }; + let metadata = SmithMembershipMetaData { + session_keys, + owner_key: data.address(), + p2p_endpoint: endpoint, + }; let progress = data .client() .tx() diff --git a/src/commands/ud.rs b/src/commands/ud.rs index 6eccdd37d92d0da43953c5af4246d74e2208f589..7bc6d9faf5e1a60d00fb9512cfd676eecab44ef6 100644 --- a/src/commands/ud.rs +++ b/src/commands/ud.rs @@ -15,7 +15,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( // match subcommand match command { Subcommand::Claim => { - data = data.build_keypair(); + data = data; claim_ud(data).await?; } }; diff --git a/src/conf.rs b/src/conf.rs index 7b5afba8ca5ce5cf2085d2ffb21cb4fb0fb3fb47..da063b141c38e52b46b7a56246a174b3d5c92a07 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; const APP_NAME: &str = "gcli"; +/// defines structure of config file #[derive(Serialize, Deserialize, Debug)] pub struct Config { // duniter endpoint @@ -11,6 +12,8 @@ pub struct Config { pub indexer_endpoint: String, // user address pub address: Option<AccountId>, + // user secret (substrate format) + pub secret: Option<String>, } impl std::default::Default for Config { @@ -19,6 +22,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, + secret: None, } } } @@ -28,11 +32,16 @@ pub fn load_conf() -> Config { match confy::load(APP_NAME, None) { Ok(cfg) => cfg, Err(e) => { - log::warn!("met error while loading config file"); - log::error!("{}", e); - log::info!("removing the old conf file and creating a new one"); + log::warn!( + "met error while loading config file {}", + confy::get_configuration_file_path(APP_NAME, None) + .unwrap() + .display() + ); + log::error!("{:?}", e); + log::info!("using default config instead"); + log::info!("call `config save` to overwrite"); let cfg = Config::default(); - confy::store(APP_NAME, None, &cfg).expect("unable to write default config"); cfg } } diff --git a/src/data.rs b/src/data.rs index be67bec4881a9df1232adc10fa522c8f8df9d7cf..16901cdca612cfce799197a8cd64d09ff1e1da87 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,7 +1,11 @@ +use std::str::FromStr; + use crate::*; use indexer::Indexer; // consts +pub const SUBSTRATE_MNEMONIC: &str = + "bottom drive obey lake curtain smoke basket hold race lonely fit walk"; pub const LOCAL_DUNITER_ENDPOINT: &str = "ws://localhost:9944"; pub const LOCAL_INDEXER_ENDPOINT: &str = "http://localhost:8080/v1/graphql"; @@ -67,6 +71,7 @@ impl Data { ..Default::default() } .overwrite_from_args() + .build_from_config() } // --- getters --- // the "unwrap" should not fail if data is well prepared @@ -80,7 +85,10 @@ impl Data { self.cfg.address.clone().expect("an address is needed") } pub fn keypair(&self) -> Pair { - self.keypair.clone().expect("a keypair is needed") + match self.keypair.clone() { + Some(keypair) => keypair, + None => prompt_secret(self.args.secret_format), + } } pub fn idty_index(&self) -> u32 { self.idty_index.expect("must fetch idty index first") @@ -138,45 +146,39 @@ impl Data { if let Some(indexer_endpoint) = self.args.indexer.clone() { self.cfg.indexer_endpoint = indexer_endpoint } - // secret - if self.args.secret.is_some() { - self = self.build_keypair(); + // predefined secret + if self.args.secret_format == SecretFormat::Predefined { + match self.args.secret.clone() { + None => {} + Some(derivation) => { + self.cfg.secret = Some(format!("{SUBSTRATE_MNEMONIC}//{derivation}")); + } + }; + } else if let Some(secret) = self.args.secret.clone() { + // other secret type + self.cfg.secret = Some(secret); } // address - if self.args.address.is_some() { - self = self.build_address(); - } - self - } - /// ask user to input an address if needed - pub fn build_address(mut self) -> Self { - if self.cfg.address.is_none() { - self.cfg.address = Some( - get_keys( - self.args.secret_format, - &self.args.address, - &self.args.secret, - NeededKeys::Public, - ) - .expect("needed") - .0 - .expect("needed"), - ); + if let Some(address) = self.args.address.clone() { + self.cfg.address = Some(AccountId::from_str(&address).expect("invalid address")); } self } - /// ask user to input a keypair if needed - pub fn build_keypair(mut self) -> Self { - if self.keypair.is_none() { - let (address, keypair) = get_keys( - self.args.secret_format, - &self.args.address, - &self.args.secret, - NeededKeys::Secret, - ) - .expect("needed"); - self.cfg.address = address; - self.keypair = keypair; + /// build from config + pub fn build_from_config(mut self) -> Self { + // if a secret is defined, build keypair + if let Some(secret) = self.cfg.secret.clone() { + let (address, keypair) = + addr_and_pair_from_secret(SecretFormat::Predefined, &secret).unwrap(); + // if an address is already defined and differs from secret, warns user + if let Some(address_) = self.cfg.address { + if address_ != address { + println!("overwriting address ({address_}) from secret ({address})"); + } + } + self.cfg.address = Some(address); + self.cfg.secret = Some(secret); + self.keypair = Some(keypair); } self } @@ -187,7 +189,7 @@ impl Data { GcliError::Duniter(format!( "could not establish connection with the server {}, due to error {}", duniter_endpoint, - dbg!(e) + dbg!(e) // needed to get more details TODO fixme )) })?); self.genesis_hash = commands::blockchain::fetch_genesis_hash(&self).await?; diff --git a/src/keys.rs b/src/keys.rs index 8df0ebae6656bd7ac0d58b29c7eb91cd739509df..5ac176d21c621522c78ed385b0651c2b71b945d0 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -1,16 +1,14 @@ use crate::*; use clap::builder::OsStr; -use sp_core::crypto::Ss58Codec; use std::str::FromStr; -#[allow(dead_code)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum NeededKeys { - None, - Public, - Secret, -} +// #[derive(Clone, Copy, Debug, Eq, PartialEq)] +// pub enum NeededKeys { +// None, +// Public, +// Secret, +// } #[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] pub enum SecretFormat { @@ -19,6 +17,8 @@ pub enum SecretFormat { /// Substrate secret key or BIP39 mnemonic (optionally followed by derivation path) #[default] Substrate, + /// Predefined (Alice, Bob, ...) + Predefined, } impl FromStr for SecretFormat { @@ -28,6 +28,7 @@ impl FromStr for SecretFormat { match s { "seed" => Ok(SecretFormat::Seed), "substrate" => Ok(SecretFormat::Substrate), + "predefined" => Ok(SecretFormat::Predefined), _ => Err(std::io::Error::from(std::io::ErrorKind::InvalidInput)), } } @@ -38,6 +39,7 @@ impl From<SecretFormat> for &'static str { match val { SecretFormat::Seed => "seed", SecretFormat::Substrate => "substrate", + SecretFormat::Predefined => "predefined", } } } @@ -48,6 +50,7 @@ impl From<SecretFormat> for OsStr { } } +/// get keypair from given string secret pub fn pair_from_str(secret_format: SecretFormat, secret: &str) -> anyhow::Result<Pair> { match secret_format { SecretFormat::Seed => { @@ -56,12 +59,24 @@ pub fn pair_from_str(secret_format: SecretFormat, secret: &str) -> anyhow::Resul let pair = Pair::from_seed(&seed); Ok(pair) } - SecretFormat::Substrate => { + // "predefined" replaces secret before + SecretFormat::Substrate | SecretFormat::Predefined => { Pair::from_string(secret, None).map_err(|_| anyhow!("Invalid secret")) } } } +/// get keypair and address from given secret string +pub fn addr_and_pair_from_secret( + secret_format: SecretFormat, + secret: &str, +) -> anyhow::Result<(AccountId, Pair)> { + let pair = pair_from_str(secret_format, secret)?; + let address = pair.public().into(); + Ok((address, pair)) +} + +/// ask user to input a secret pub fn prompt_secret(secret_format: SecretFormat) -> Pair { loop { match pair_from_str( @@ -74,52 +89,53 @@ pub fn prompt_secret(secret_format: SecretFormat) -> Pair { } } -pub fn get_keys( - secret_format: SecretFormat, - address: &Option<String>, - secret: &Option<String>, - needed_keys: NeededKeys, -) -> anyhow::Result<(Option<AccountId>, Option<Pair>)> { - // Get from args - let mut account_id = match (address, secret) { - (Some(address), Some(secret)) => { - let pair = pair_from_str(secret_format, secret)?; - let address = AccountId::from_string(address) - .map_err(|_| anyhow!("Invalid address {}", address))?; - assert_eq!( - address, - pair.public().into(), - "Secret and address do not match." - ); - return Ok((Some(pair.public().into()), Some(pair))); - } - (None, Some(secret)) => { - let pair = pair_from_str(secret_format, secret)?; - return Ok((Some(pair.public().into()), Some(pair))); - } - (Some(address), None) => { - Some(AccountId::from_str(address).map_err(|_| anyhow!("Invalid address {}", address))?) - } - (None, None) => None, - }; +// /// get keys with interactive prompt if needed +// pub fn get_keys( +// secret_format: SecretFormat, +// accout_id: Option<AccountId>, +// secret: &Option<String>, +// needed_keys: NeededKeys, +// ) -> anyhow::Result<(Option<AccountId>, Option<Pair>)> { +// // Get from args +// let mut account_id = match (accout_id, secret) { +// // both are defined, check that they match. +// // if they do not match, use secret +// (Some(accout_id), Some(secret)) => { +// let pair = pair_from_str(secret_format, secret)?; +// let secret_address = pair.public().into(); +// if accout_id != secret_address { +// println!("Secret ({secret_address}) and address ({accout_id}) do not match, using secret"); +// } +// return Ok((Some(secret_address), Some(pair))); +// } +// // only secret, build both +// (None, Some(secret)) => { +// let pair = pair_from_str(secret_format, secret)?; +// return Ok((Some(pair.public().into()), Some(pair))); +// } +// // only address +// (Some(accout_id), None) => Some(accout_id), +// // none of them +// (None, None) => None, +// }; - // Prompt - if needed_keys == NeededKeys::Secret - || (account_id.is_none() && needed_keys == NeededKeys::Public) - { - loop { - let pair = prompt_secret(secret_format); +// // Prompt +// if needed_keys == NeededKeys::Secret +// || (account_id.is_none() && needed_keys == NeededKeys::Public) +// { +// loop { +// let pair = prompt_secret(secret_format); - if let Some(account_id) = &account_id { - if account_id != &pair.public().into() { - println!("Secret and address do not match."); - } - } else { - account_id = Some(pair.public().into()); - return Ok((account_id, Some(pair))); - } - } - } +// if let Some(account_id) = &account_id { +// if account_id != &pair.public().into() { +// println!("Secret and address do not match."); +// } +// } else { +// account_id = Some(pair.public().into()); +// return Ok((account_id, Some(pair))); +// } +// } +// } - Ok((account_id, None)) -} +// Ok((account_id, None)) +// } diff --git a/src/main.rs b/src/main.rs index 071115e16afcd884946a77aaa5189f00ecf469d5..3633553b7598cf71d4754d930f9e1d4c482e26fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -112,7 +112,9 @@ async fn main() -> Result<(), GcliError> { Subcommand::Oneshot(subcommand) => { commands::oneshot::handle_command(data, subcommand).await? } - Subcommand::Blockchain(subcommand) => commands::blockchain::handle_command(data, subcommand).await?, + Subcommand::Blockchain(subcommand) => { + commands::blockchain::handle_command(data, subcommand).await? + } Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await?, Subcommand::Config(subcommand) => conf::handle_command(data, subcommand)?, }