Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 48-error-base-58-requirement-is-violated
  • hugo/dev
  • hugo/tx-comments
  • json-output
  • master
  • no-rename
  • nostr
  • poka/dev
  • tuxmain/mail
  • 0.1.0
  • 0.2.0
  • 0.2.1
  • 0.2.10
  • 0.2.12
  • 0.2.13
  • 0.2.14
  • 0.2.15
  • 0.2.16
  • 0.2.17
  • 0.2.3
  • 0.2.4
  • 0.2.5
  • 0.2.6
  • 0.2.7
  • 0.2.8
  • 0.2.9
  • 0.3.0
  • 0.4.0
  • 0.4.1
  • 0.4.2
  • 0.4.3-RC1
  • 0.4.3-RC2
32 results

Target

Select target project
  • clients/rust/gcli-v2s
  • d0p1/gcli-v2s
  • flebon/gcli-v2s
  • zicmama/gcli-v2s
  • Nicolas80/gcli-v2s
5 results
Select Git revision
  • hugo/dev
  • hugo/tx-comments
  • master
  • poka/dev
  • tuxmain/mail
  • vault-support-for-all-secret-format
  • 0.1.0
  • 0.2.0
  • 0.2.1
  • 0.2.10
  • 0.2.12
  • 0.2.13
  • 0.2.14
  • 0.2.15
  • 0.2.16
  • 0.2.17
  • 0.2.3
  • 0.2.4
  • 0.2.5
  • 0.2.6
  • 0.2.7
  • 0.2.8
  • 0.2.9
  • 0.3.0
24 results
Show changes
use crate::*; 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 Call = runtime::runtime_types::gdev_runtime::RuntimeCall;
type BalancesCall = runtime::runtime_types::pallet_balances::pallet::Call; type BalancesCall = runtime::runtime_types::pallet_balances::pallet::Call;
...@@ -10,35 +10,58 @@ pub async fn transfer( ...@@ -10,35 +10,58 @@ pub async fn transfer(
balance: u64, balance: u64,
dest: AccountId, dest: AccountId,
keep_alive: bool, keep_alive: bool,
is_ud: bool,
) -> Result<(), subxt::Error> { ) -> Result<(), subxt::Error> {
let progress = if keep_alive { match (keep_alive, is_ud) {
data.client() (true, false) => {
.tx() submit_call_and_look_event::<
.sign_and_submit_then_watch( runtime::balances::events::Transfer,
&runtime::tx().balances().transfer(dest.into(), balance), StaticPayload<runtime::balances::calls::types::TransferKeepAlive>,
&PairSigner::new(data.keypair()), >(
BaseExtrinsicParamsBuilder::new(), data,
)
.await?
} else {
data.client()
.tx()
.sign_and_submit_then_watch(
&runtime::tx() &runtime::tx()
.balances() .balances()
.transfer_keep_alive(dest.into(), balance), .transfer_keep_alive(dest.into(), balance),
&PairSigner::new(data.keypair()),
BaseExtrinsicParamsBuilder::new(),
) )
.await? .await
}; }
(false, false) => {
let events = track_progress(progress).await?; submit_call_and_look_event::<
runtime::balances::events::Transfer,
if let Some(e) = events.find_first::<runtime::balances::events::Transfer>()? { StaticPayload<runtime::balances::calls::types::TransferAllowDeath>,
println!("{e:?}"); >(
data,
&runtime::tx()
.balances()
.transfer_allow_death(dest.into(), balance),
)
.await
}
(true, true) => {
submit_call_and_look_event::<
runtime::balances::events::Transfer,
StaticPayload<runtime::universal_dividend::calls::types::TransferUdKeepAlive>,
>(
data,
&runtime::tx()
.universal_dividend()
.transfer_ud_keep_alive(dest.into(), balance),
)
.await
}
(false, true) => {
submit_call_and_look_event::<
runtime::balances::events::Transfer,
StaticPayload<runtime::universal_dividend::calls::types::TransferUd>,
>(
data,
&runtime::tx()
.universal_dividend()
.transfer_ud(dest.into(), balance),
)
.await
}
} }
Ok(())
} }
/// transfer balance to multiple target /// transfer balance to multiple target
...@@ -57,22 +80,10 @@ pub async fn transfer_multiple( ...@@ -57,22 +80,10 @@ pub async fn transfer_multiple(
}) })
}) })
.collect(); .collect();
// wrap these calls in a batch call // wrap these calls in a batch call
let progress = data submit_call_and_look_event::<
.client() runtime::utility::events::BatchCompleted,
.tx() StaticPayload<runtime::utility::calls::types::Batch>,
.sign_and_submit_then_watch( >(data, &runtime::tx().utility().batch(transactions))
&runtime::tx().utility().batch(transactions), .await
&PairSigner::new(data.keypair()),
BaseExtrinsicParamsBuilder::new(),
)
.await?;
let events = track_progress(progress).await?;
// TODO all transfer
if let Some(e) = events.find_first::<runtime::balances::events::Transfer>()? {
println!("{e:?}");
}
Ok(())
} }
...@@ -9,13 +9,13 @@ pub enum Subcommand { ...@@ -9,13 +9,13 @@ pub enum Subcommand {
} }
/// handle ud commands /// handle ud commands
pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<()> { pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
// build indexer because it is needed for all subcommands // build indexer because it is needed for all subcommands
let data = data.build_client().await?; let data = data.build_client().await?.fetch_system_properties().await?;
// match subcommand // match subcommand
match command { match command {
Subcommand::Claim => { Subcommand::Claim => {
claim_ud(data).await?; claim_ud(&data).await?;
} }
}; };
...@@ -23,21 +23,10 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<( ...@@ -23,21 +23,10 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<(
} }
/// claim universal dividend /// claim universal dividend
pub async fn claim_ud(data: Data) -> Result<(), anyhow::Error> { pub async fn claim_ud(data: &Data) -> Result<(), subxt::Error> {
let progress = data submit_call_and_look_event::<
.client() runtime::universal_dividend::events::UdsClaimed,
.tx() StaticPayload<runtime::universal_dividend::calls::types::ClaimUds>,
.sign_and_submit_then_watch( >(data, &runtime::tx().universal_dividend().claim_uds())
&runtime::tx().universal_dividend().claim_uds(), .await
&PairSigner::new(data.keypair()),
BaseExtrinsicParamsBuilder::new(),
)
.await?;
let events = track_progress(progress).await?;
if let Some(e) = events.find_first::<runtime::universal_dividend::events::UdsClaimed>()? {
println!("{e:?}");
}
Ok(())
} }
mod display;
use crate::commands::cesium;
use crate::entities::vault_account;
use crate::entities::vault_account::{AccountTreeNode, ActiveModel, DbAccountId};
use crate::inputs::{trim_and_reduce_empty_as_none, validate_derivation_path, validate_vault_name};
use crate::keys::seed_from_cesium;
use crate::*;
use age::secrecy::Secret;
use sea_orm::ActiveValue::Set;
use sea_orm::ModelTrait;
use sea_orm::{ConnectionTrait, TransactionTrait};
use sp_core::crypto::AddressUri;
use std::cell::RefCell;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::rc::Rc;
/// vault subcommands
#[derive(Clone, Debug, clap::Parser)]
pub enum Subcommand {
/// List available SS58 Addresses in the vault
#[clap(subcommand)]
List(ListChoice),
/// Use specific SS58 Address (changes the config Address)
Use {
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
},
/// Generate a mnemonic
Generate,
/// Import key from (substrate uri) or other format with interactive prompt
#[clap(
long_about = "Import key from (substrate uri) or other format with interactive prompt.\n\
\n\
This will create a <Base> account in the vault for the provided/computed Substrate URI \n\
and associated SS58 Address.\n\
\n\
If using default format (or specifically \"substrate\") a derivation path is supported\n\
in the substrate uri value."
)]
Import {
/// Secret key format (substrate, seed, g1v1)
#[clap(short = 'S', long, required = false, default_value = SecretFormat::Substrate)]
secret_format: SecretFormat,
/// Crypto scheme to use (sr25519, ed25519)
#[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)]
crypto_scheme: CryptoScheme,
/// Substrate URI to import (non-interactive mode)
#[clap(short = 'u', long, required = false)]
uri: Option<String>,
/// G1v1 ID (non-interactive mode for g1v1 format)
#[clap(long, required = false)]
g1v1_id: Option<String>,
/// G1v1 password (non-interactive mode for g1v1 format)
#[clap(long, required = false)]
g1v1_password: Option<String>,
/// Password for encrypting the key (non-interactive mode)
#[clap(short = 'p', long, required = false, conflicts_with_all=["no_password"])]
password: Option<String>,
/// Use empty password for encrypting the key (non-interactive mode)
#[clap(long, required = false)]
no_password: bool,
/// Name for the wallet entry (non-interactive mode) - "" empty string will be considered as None
#[clap(short = 'n', long, required = false)]
name: Option<String>,
},
/// Add a derivation to an existing SS58 Address
#[clap(long_about = "Add a derivation to an existing SS58 Address.\n\
\n\
Both \"sr25519\" and \"ed25519\" crypto schemes are supported
\n\
Use command `vault list base` to see available <Base> account and their crypto scheme\n\
And then use command 'vault list for' to find all accounts linked to that <Base> account")]
#[clap(alias = "deriv")]
#[clap(alias = "derivation")]
Derive {
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
/// Derivation path (non-interactive mode)
#[clap(short = 'd', long, required = false)]
derivation_path: Option<String>,
/// Password to decrypt the <Base> account key (non-interactive mode)
#[clap(short = 'p', long, required = false, requires = "derivation_path", conflicts_with_all=["no_password"])]
password: Option<String>,
/// Use empty password to decrypt the <Base> account key (non-interactive mode)
#[clap(long, required = false, requires = "derivation_path")]
no_password: bool,
/// Name for the wallet entry (non-interactive mode) - "" empty string will be considered as None
#[clap(short = 'n', long, required = false, requires = "derivation_path")]
name: Option<String>,
},
/// Give a meaningful name to an SS58 Address in the vault
Rename {
/// SS58 Address
address: AccountId,
},
/// Remove an SS58 Address from the vault together with its linked derivations
#[clap(long_about = "Remove an SS58 Address from the vault\n\
\n\
If a <Base> Address is given it will also remove the saved key")]
Remove {
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
},
/// Inspect a vault entry, retrieving its Substrate URI, Crypto-Scheme, Secret seed/mini-secret(if possible), Public key (hex), SS58 Address and potential G1v1 public key if inspecting a <Base> account with ed25519 crypto-scheme
Inspect {
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
},
/// (deprecated) List available key files (needs to be migrated with command `vault migrate` in order to use them)
#[deprecated(
note = "Should be removed in a future version when db persistence of vault is present for a while"
)]
ListFiles,
/// (deprecated) Migrate old key files into db (will have to provide password for each key)
#[deprecated(
note = "Should be removed in a future version when db persistence of vault is present for a while"
)]
Migrate,
/// Show where vault db (or old keys) is stored
Where,
}
/// List subcommands
#[derive(Clone, Debug, clap::Subcommand)]
pub enum ListChoice {
/// List all accounts
#[clap(alias = "a")]
All {
/// Show G1v1 public key for ed25519 keys
#[clap(long)]
show_g1v1: bool,
},
/// List only base accounts
#[clap(alias = "b")]
Base {
/// Show G1v1 public key for ed25519 keys
#[clap(long)]
show_g1v1: bool,
},
/// List accounts for a specific address
#[clap(alias = "f")]
For {
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
/// Show G1v1 public key for ed25519 keys
#[clap(long)]
show_g1v1: bool,
},
}
impl Default for ListChoice {
fn default() -> Self {
ListChoice::All { show_g1v1: false }
}
}
#[derive(Debug, clap::Args, Clone)]
#[group(required = true, multiple = false)]
pub struct AddressOrVaultNameGroup {
/// SS58 Address
#[clap(short)]
address: Option<AccountId>,
/// Name of an SS58 Address in the vault
#[clap(short = 'v')]
vault_name: Option<String>,
}
pub struct VaultDataToImport {
secret_format: SecretFormat,
secret_suri: String,
key_pair: KeyPair,
}
// encrypt input with passphrase
pub fn encrypt(input: &[u8], passphrase: String) -> Result<Vec<u8>, age::EncryptError> {
let encryptor = age::Encryptor::with_user_passphrase(Secret::new(passphrase));
let mut encrypted = vec![];
let mut writer = encryptor.wrap_output(age::armor::ArmoredWriter::wrap_output(
&mut encrypted,
age::armor::Format::AsciiArmor,
)?)?;
writer.write_all(input)?;
writer.finish().and_then(|armor| armor.finish())?;
Ok(encrypted)
}
// decrypt cypher with passphrase
pub fn decrypt(input: &[u8], passphrase: String) -> Result<Vec<u8>, age::DecryptError> {
let age::Decryptor::Passphrase(decryptor) =
age::Decryptor::new(age::armor::ArmoredReader::new(input))?
else {
unimplemented!()
};
let mut decrypted = vec![];
let mut reader = decryptor.decrypt(&Secret::new(passphrase.to_owned()), None)?;
reader.read_to_end(&mut decrypted)?;
Ok(decrypted)
}
/// handle vault commands
pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
let db = data.connect_db();
// match subcommand
match command {
Subcommand::List(choice) => match choice {
ListChoice::All { show_g1v1 } => {
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,
)?;
println!("available SS58 Addresses:");
println!("{table}");
}
ListChoice::Base { show_g1v1 } => {
let base_account_tree_nodes =
vault_account::fetch_only_base_account_tree_nodes(db).await?;
let table = display::compute_vault_accounts_table_with_g1v1(
&base_account_tree_nodes,
show_g1v1,
)?;
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?;
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,
)?;
println!(
"available SS58 Addresses linked to {}:",
account_tree_node.borrow().account
);
println!("{table}");
}
},
Subcommand::ListFiles => {
let vault_key_addresses = fetch_vault_key_addresses(&data).await?;
let table = display::compute_vault_key_files_table(&vault_key_addresses)?;
println!("available key files (needs to be migrated with command `vault migrate` in order to use them):");
println!("{table}");
}
Subcommand::Use {
address_or_vault_name,
} => {
let account = retrieve_vault_account(db, address_or_vault_name).await?;
println!("Using: {}", account);
let updated_cfg = conf::Config {
address: Some(account.address.0),
..data.cfg
};
//This updated configuration will be picked up with next GCli execution
conf::save(&updated_cfg);
}
Subcommand::Generate => {
// TODO allow custom word count
let mnemonic = bip39::Mnemonic::generate(12).unwrap();
println!("{mnemonic}");
}
Subcommand::Import {
secret_format,
crypto_scheme,
uri,
g1v1_id,
g1v1_password,
password,
no_password,
name,
} => {
let vault_data_for_import = if let Some(uri_str) = uri {
// Non-interactive mode with provided URI
if secret_format != SecretFormat::Substrate {
return Err(GcliError::Input(format!(
"URI can only be provided directly with secret_format=substrate, got: {:?}",
secret_format
)));
}
// Create keypair from provided URI
let key_pair = compute_keypair(crypto_scheme, &uri_str)?;
VaultDataToImport {
secret_format,
secret_suri: uri_str,
key_pair,
}
} else if let (Some(id), Some(pwd)) = (&g1v1_id, &g1v1_password) {
// Non-interactive mode with provided G1v1 ID and password
if secret_format != SecretFormat::G1v1 {
return Err(GcliError::Input(format!(
"G1v1 ID and password can only be provided directly with secret_format=g1v1, got: {:?}",
secret_format
)));
}
// Create keypair from provided G1v1 ID and password
let seed = seed_from_cesium(id, pwd);
let secret_suri = format!("0x{}", hex::encode(seed));
// G1v1 always uses Ed25519
let key_pair = compute_keypair(CryptoScheme::Ed25519, &secret_suri)?;
VaultDataToImport {
secret_format,
secret_suri,
key_pair,
}
} else {
// Interactive mode
prompt_secret_and_compute_vault_data_to_import(secret_format, crypto_scheme)?
};
//Extra check for SecretFormat::G1v1 (old cesium) - showing the G1v1 cesium public key for confirmation
if secret_format == SecretFormat::G1v1 {
println!(
"The G1v1 public key for the provided secret is: '{}'",
cesium::compute_g1v1_public_key(&vault_data_for_import.key_pair)?
);
// Skip confirmation in non-interactive mode
let is_non_interactive_g1v1 = g1v1_id.is_some() && g1v1_password.is_some();
if !is_non_interactive_g1v1 {
let confirmed = inputs::confirm_action("Is it the correct one (if not, you should try again to input G1v1 id/password) ?".to_string())?;
if !confirmed {
return Ok(());
}
}
}
let txn = db.begin().await?;
// Handle password in non-interactive mode
let provided_password = if no_password {
Some(String::new()) // Empty password
} else {
password
};
let _account = create_base_account_for_vault_data_to_import(
&txn,
&vault_data_for_import,
provided_password.as_ref(),
Some(crypto_scheme),
name,
)
.await?;
txn.commit().await?;
println!("Change done");
}
Subcommand::Derive {
address_or_vault_name,
derivation_path,
password,
no_password,
name,
} => {
let account_tree_node_to_derive =
retrieve_account_tree_node(db, address_or_vault_name).await?;
let account_to_derive = account_tree_node_to_derive.borrow().account.clone();
let base_account_tree_node =
vault_account::get_base_account_tree_node(&account_tree_node_to_derive);
let base_account = &base_account_tree_node.borrow().account.clone();
println!("Adding derivation to: {account_to_derive}");
let base_parent_hierarchy_account_tree_node_to_derive =
vault_account::get_base_parent_hierarchy_account_tree_node(
&account_tree_node_to_derive,
);
let parent_hierarchy_table_account_to_derive =
display::compute_vault_accounts_table(&[
base_parent_hierarchy_account_tree_node_to_derive,
])?;
println!();
println!("Its parent hierarchy is this:");
println!("{parent_hierarchy_table_account_to_derive}");
println!();
println!("The linked <Base> account is {base_account}");
// Handle password from non-interactive mode or ask for it
let password = if no_password {
String::new()
} else if let Some(password) = password {
password
} else {
println!("Enter password to decrypt the <Base> account key");
inputs::prompt_password()?
};
let account_to_derive_secret_suri = vault_account::compute_suri_account_tree_node(
&account_tree_node_to_derive,
password,
)?;
println!();
// Handle derivation_path from non-interactive mode or ask for it
let derivation_path = if let Some(derivation_path) = derivation_path {
validate_derivation_path(derivation_path.clone())?;
derivation_path
} else {
inputs::prompt_vault_derivation_path()?
};
let derivation_secret_suri =
format!("{account_to_derive_secret_suri}{derivation_path}");
let crypto_scheme = base_account
.crypto_scheme
.map(CryptoScheme::from)
.unwrap_or(CryptoScheme::Ed25519); // Fallback to Ed25519 if not defined (should never happen)
let derivation_keypair = compute_keypair(crypto_scheme, &derivation_secret_suri)?;
let derivation_address: String = derivation_keypair.address().to_string();
let txn = db.begin().await?;
println!();
let _derivation = create_derivation_account(
&txn,
&derivation_address,
&derivation_path,
&account_to_derive.address.to_string(),
name,
)
.await?;
txn.commit().await?;
println!("Change done");
}
Subcommand::Rename { address } => {
let account =
vault_account::find_by_id(db, &DbAccountId::from(address.clone())).await?;
if account.is_none() {
println!("No vault entry found for address:'{address}'");
println!("You might want to import it first with 'vault import'");
return Ok(());
}
let account = account.unwrap();
println!(
"Current name for address:'{address}' is {:?}",
&account.name
);
println!("Enter new name for address (leave empty to remove the name)");
let name =
inputs::prompt_vault_name_and_check_availability(db, account.name.as_ref()).await?;
let _account = vault_account::update_account_name(db, account, name.as_ref()).await?;
println!("Rename done");
}
Subcommand::Remove {
address_or_vault_name,
} => {
let account_tree_node_to_delete =
retrieve_account_tree_node(db, address_or_vault_name).await?;
let txn = db.begin().await?;
let account_to_delete = account_tree_node_to_delete.borrow().account.clone();
let address_to_delete = account_tree_node_to_delete.borrow().account.address.clone();
//If account to delete has children; also delete all linked derivations
if !account_tree_node_to_delete.borrow().children.is_empty() {
let table =
display::compute_vault_accounts_table(&[account_tree_node_to_delete.clone()])?;
println!("All addresses linked to: {account_to_delete}");
println!("{table}");
println!(
"This {} account has {} addresses in total",
account_to_delete.account_type(),
vault_account::count_accounts_in_account_tree_node_and_children(
&account_tree_node_to_delete
)
);
let confirmation_message = if account_to_delete.is_base_account() {
"Are you sure you want to delete it along with the saved key?"
} else {
"Are you sure you want to delete it?"
};
let confirmed = inputs::confirm_action(confirmation_message.to_string())?;
if !confirmed {
return Ok(());
}
for account_to_delete in
vault_account::extract_accounts_depth_first_from_account_tree_node(
&account_tree_node_to_delete,
)? {
let delete_result = account_to_delete.delete(&txn).await?;
println!("Deleting {} address", delete_result.rows_affected);
}
} else {
let delete_result = account_to_delete.delete(&txn).await?;
println!("Deleting {} address", delete_result.rows_affected);
}
txn.commit().await?;
println!("Done removing address:'{address_to_delete}'");
}
Subcommand::Inspect {
address_or_vault_name,
} => {
let account_tree_node_to_inspect =
retrieve_account_tree_node(db, address_or_vault_name).await?;
let base_account_tree_node =
vault_account::get_base_account_tree_node(&account_tree_node_to_inspect);
let is_base_account =
Rc::ptr_eq(&account_tree_node_to_inspect, &base_account_tree_node);
if !is_base_account {
let base_account = base_account_tree_node.borrow().account.clone();
println!("The linked <Base> account is {base_account}");
}
println!("Enter password to decrypt the <Base> account key");
let password = inputs::prompt_password()?;
let account_to_derive_secret_suri = vault_account::compute_suri_account_tree_node(
&account_tree_node_to_inspect,
password,
)?;
println!("Substrate URI: '{account_to_derive_secret_suri}'");
let crypto_scheme: CryptoScheme = base_account_tree_node
.borrow()
.account
.crypto_scheme
.ok_or(GcliError::Logic(
"Base account without crypto_scheme".to_string(),
))?
.into();
println!("Crypto scheme: {}", <&'static str>::from(crypto_scheme));
match compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme(
&account_to_derive_secret_suri,
crypto_scheme,
) {
Err(e) => {
println!("Secret seed/mini-secret: cannot be computed: {}", e)
}
Ok((_computed_pair, seed)) => {
println!("Secret seed/mini-secret: '0x{}'", hex::encode(seed));
}
}
let account_address: AccountId = account_tree_node_to_inspect
.borrow()
.account
.address
.clone()
.into();
println!("Public key (hex): '0x{}'", hex::encode(account_address.0));
println!("SS58 Address: '{account_address}'");
if CryptoScheme::Ed25519 == crypto_scheme && is_base_account {
println!(
"(potential G1v1 public key: '{}')",
cesium::compute_g1v1_public_key_from_ed25519_account_id(&account_address)
);
}
}
Subcommand::Migrate => {
println!("Migrating existing key files to db");
let vault_key_addresses = fetch_vault_key_addresses(&data).await?;
let table = display::compute_vault_key_files_table(&vault_key_addresses)?;
println!("available key files to possibly migrate:");
println!("{table}");
for address in vault_key_addresses {
//Check if we already have a vault_derivation for that address
let existing_account =
vault_account::find_by_id(db, &DbAccountId::from_str(&address)?).await?;
if existing_account.is_some() {
//Already migrated
continue;
}
println!();
println!("Trying to migrate key {address}");
let vault_data_from_file = match try_fetch_vault_data_from_file(&data, &address) {
Ok(Some(vault_data)) => vault_data,
Ok(None) => {
println!("No vault entry file found for address {address}");
continue;
}
Err(e) => {
println!("Error while fetching vault data for address {address}: {e}");
println!("Continuing to next one");
continue;
}
};
let vault_data_to_import = VaultDataToImport {
secret_format: vault_data_from_file.secret_format,
secret_suri: vault_data_from_file.secret,
key_pair: vault_data_from_file.key_pair,
};
let txn = db.begin().await?;
// Old key files were in Sr25519 format (and had the Address associated to that scheme)
let account = create_base_account_for_vault_data_to_import(
&txn,
&vault_data_to_import,
Some(&vault_data_from_file.password),
Some(CryptoScheme::Sr25519),
None,
)
.await;
match account {
Ok(_account) => {
txn.commit().await?;
println!("Change done");
}
Err(error) => {
println!("Error occurred: {error}");
println!("Continuing to next key");
}
}
}
println!("Migration done");
}
Subcommand::Where => {
println!("{}", data.project_dir.data_dir().to_str().unwrap());
}
};
Ok(())
}
/// Method used to separate vault `name` part from optional `derivation` part in computed names that can be provided by users in the different `vault` commands using `AddressOrVaultNameGroup`
fn parse_vault_name_and_derivation_path_from_user_input(
user_input_name: String,
) -> Result<(String, Option<String>), GcliError> {
if user_input_name.contains("/") {
user_input_name.find("/").map_or(
Err(GcliError::Input("Invalid format".to_string())),
|idx| {
let (prefix, derivation_path) = user_input_name.split_at(idx);
Ok((prefix.to_string(), Some(derivation_path.to_string())))
},
)
} else {
Ok((user_input_name, None))
}
}
/// Method that can be used to parse a Substrate URI (which can also be only a derivation path)
///
/// Does some internal verification (done by sp_core::address_uri::AddressUri)
///
/// It extracts the (optional) `phrase` and the (optional) recomposed full `derivation path`
///
/// It also checks if a derivation `password` was provided and returns an error if one was found
pub fn parse_prefix_and_derivation_path_from_suri(
raw_string: String,
) -> Result<(Option<String>, Option<String>), GcliError> {
let address_uri =
AddressUri::parse(&raw_string).map_err(|e| GcliError::Input(e.to_string()))?;
if let Some(pass) = address_uri.pass {
return Err(GcliError::Input(format!(
"Having a password in the derivation path is not supported (password:'{}')",
pass
)));
}
let full_path = if address_uri.paths.is_empty() {
None
} else {
Some("/".to_owned() + &address_uri.paths.into_iter().collect::<Vec<_>>().join("/"))
};
Ok((address_uri.phrase.map(|s| s.to_string()), full_path))
}
fn map_secret_format_to_crypto_scheme(
secret_format: SecretFormat,
override_crypto_scheme: Option<CryptoScheme>,
) -> CryptoScheme {
// If a crypto_scheme is explicitly specified, use it except for G1v1 which must always use Ed25519
if let Some(scheme) = override_crypto_scheme {
if secret_format == SecretFormat::G1v1 {
// G1v1 must always use Ed25519
CryptoScheme::Ed25519
} else {
scheme
}
} else {
// Default behavior if no crypto_scheme is specified
match secret_format {
// All formats use Ed25519 by default
SecretFormat::Seed => CryptoScheme::Ed25519,
SecretFormat::Substrate => CryptoScheme::Ed25519,
SecretFormat::Predefined => CryptoScheme::Ed25519,
SecretFormat::G1v1 => CryptoScheme::Ed25519,
}
}
}
/// This method will scan files in the data directory and return the addresses of the vault keys found
#[deprecated(
note = "Should be removed in a future version when db persistence of vault is present for a while"
)]
async fn fetch_vault_key_addresses(data: &Data) -> Result<Vec<String>, GcliError> {
let mut entries = std::fs::read_dir(data.project_dir.data_dir())?
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, std::io::Error>>()?;
// To have consistent ordering
entries.sort();
let mut vault_key_addresses: Vec<String> = vec![];
entries.iter().for_each(|dir_path| {
let filename = dir_path.file_name().unwrap().to_str().unwrap();
// If potential_address is a valid AccountId
if AccountId::from_str(filename).is_ok() {
vault_key_addresses.push(filename.to_string());
}
});
Ok(vault_key_addresses)
}
pub async fn retrieve_vault_account_for_name<C>(
db: &C,
name_input: &String,
) -> Result<vault_account::Model, GcliError>
where
C: ConnectionTrait,
{
let account_tree_node = retrieve_account_tree_node_for_name(db, name_input).await?;
//Need this extra step to avoid borrowing issues
let account = account_tree_node.borrow().account.clone();
Ok(account)
}
pub async fn retrieve_account_tree_node<C>(
db: &C,
address_or_vault_name: AddressOrVaultNameGroup,
) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError>
where
C: ConnectionTrait,
{
let account_tree_node = if let Some(name_input) = &address_or_vault_name.vault_name {
retrieve_account_tree_node_for_name(db, name_input).await?
} else if let Some(address) = &address_or_vault_name.address {
let base_account_tree_node =
vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
db,
&address.to_string(),
)
.await?;
let account_tree_node_for_address = vault_account::get_account_tree_node_for_address(
&base_account_tree_node,
&address.to_string(),
);
Rc::clone(&account_tree_node_for_address)
} else {
//Should never happen since clap enforces exactly one of the 2 options
return Err(GcliError::Input("No address or name provided".to_string()));
};
Ok(account_tree_node)
}
pub async fn retrieve_account_tree_node_for_name<C>(
db: &C,
name_input: &String,
) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError>
where
C: ConnectionTrait,
{
let (name, derivation_path_opt) =
parse_vault_name_and_derivation_path_from_user_input(name_input.to_string())?;
let account_for_name = vault_account::find_by_name(db, &name).await?;
let account_for_name = account_for_name.ok_or(GcliError::Input(format!(
"No account found with name:'{name}'"
)))?;
let base_account_tree_node = vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
db,
&account_for_name.address.to_string(),
)
.await?;
let account_tree_node_for_name = vault_account::get_account_tree_node_for_address(
&base_account_tree_node,
&account_for_name.address.to_string(),
);
Ok(match derivation_path_opt {
None => Rc::clone(&account_tree_node_for_name),
Some(path) => {
let account_tree_node_for_name_input =
vault_account::compute_name_map_for_account_tree_node(&account_tree_node_for_name)?
.get(name_input)
.cloned()
.ok_or(GcliError::Input(format!(
"No account found with name:'{name}' and path:'{path}'"
)))?;
Rc::clone(&account_tree_node_for_name_input)
}
})
}
pub async fn retrieve_vault_account<C>(
db: &C,
address_or_vault_name: AddressOrVaultNameGroup,
) -> Result<vault_account::Model, GcliError>
where
C: ConnectionTrait,
{
let account_tree_node = retrieve_account_tree_node(db, address_or_vault_name).await?;
//Need this extra step to avoid borrowing issues
let account = account_tree_node.borrow().account.clone();
Ok(account)
}
fn create_vault_data_to_import<F, P>(
secret_format: SecretFormat,
crypto_scheme: CryptoScheme,
prompt_fn: F,
) -> Result<VaultDataToImport, GcliError>
where
F: Fn(CryptoScheme) -> (String, P),
P: Into<KeyPair>,
{
let (secret, pair) = prompt_fn(crypto_scheme);
let key_pair = pair.into();
Ok(VaultDataToImport {
secret_format,
secret_suri: secret,
key_pair,
})
}
fn prompt_secret_and_compute_vault_data_to_import(
secret_format: SecretFormat,
crypto_scheme: CryptoScheme,
) -> Result<VaultDataToImport, GcliError> {
match secret_format {
SecretFormat::Substrate => create_vault_data_to_import(
secret_format,
crypto_scheme,
prompt_secret_substrate_and_compute_keypair,
),
SecretFormat::Seed => create_vault_data_to_import(
secret_format,
crypto_scheme,
prompt_seed_and_compute_keypair,
),
SecretFormat::G1v1 => {
// G1v1 always uses Ed25519, ignore crypto_scheme
create_vault_data_to_import(
secret_format,
CryptoScheme::Ed25519,
prompt_secret_cesium_and_compute_keypair,
)
}
SecretFormat::Predefined => create_vault_data_to_import(
secret_format,
crypto_scheme,
prompt_predefined_and_compute_keypair,
),
}
}
/// Creates a `base` vault account for vault_data provided and returns it
///
/// Does extra checks and asks for user input in case the address is already present in the vault.
///
/// Can request password and (optional) name to the user at the proper time
///
/// Typically used for `vault import|migrate` commands
pub async fn create_base_account_for_vault_data_to_import<C>(
db_tx: &C,
vault_data: &VaultDataToImport,
password_opt: Option<&String>,
crypto_scheme: Option<CryptoScheme>,
name_opt: Option<String>,
) -> Result<vault_account::Model, GcliError>
where
C: ConnectionTrait,
{
let address_to_import = vault_data.key_pair.address().to_string();
println!("Trying to import for SS58 address :'{}'", address_to_import);
println!();
if let Some(existing_vault_account) =
vault_account::find_by_id(db_tx, &DbAccountId::from(address_to_import.clone())).await?
{
// Existing account
if existing_vault_account.is_base_account() {
println!("You are trying to add {address_to_import} as a <Base> account while it already exists as a <Base> account.");
println!();
println!("Do you want to:");
println!("1. keep the existing <Base> account and cancel import");
println!("2. overwrite existing <Base> account with the new encrypted key (children will be re-parented)");
} else {
// Existing derivation account
let account_tree_node_hierarchy =
vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
db_tx,
&address_to_import,
)
.await?;
let account_tree_node_for_address = vault_account::get_account_tree_node_for_address(
&account_tree_node_hierarchy,
&address_to_import,
);
let 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])?;
println!("You are trying to add {address_to_import} as a <Base> account");
println!(
"but it is already present as `{}` derivation of {} account.",
existing_vault_account.path.clone().unwrap(),
existing_vault_account.parent.clone().unwrap()
);
println!();
println!("Its parent hierarchy is this:");
println!("{parent_hierarchy_table}");
println!();
println!("Do you want to:");
println!("1. keep the existing derivation and cancel import");
println!("2. delete the derivation account and replace it with the new <Base> account (children will be re-parented)");
}
let result = inputs::select_action("Your choice?", vec!["1", "2"])?;
match result {
"2" => {
let password = match password_opt {
Some(password) => password.clone(),
None => inputs::prompt_password_query("Enter password to encrypt the key: ")?,
};
let encrypted_suri =
compute_encrypted_suri(password, vault_data.secret_suri.clone())?;
let name = if let Some(name) = name_opt {
validate_vault_name(&name)?;
trim_and_reduce_empty_as_none(name)
} else {
println!("(Optional) Enter a name for the vault entry (leave empty to remove the name)");
inputs::prompt_vault_name_and_check_availability(
db_tx,
existing_vault_account.name.as_ref(),
)
.await?
};
// Since links are made based on address / parent(address) we can just edit the existing entry and it should be fine
let mut vault_account: ActiveModel = existing_vault_account.into();
vault_account.path = Set(None);
vault_account.parent = Set(None);
vault_account.crypto_scheme = Set(Some(
map_secret_format_to_crypto_scheme(vault_data.secret_format, crypto_scheme)
.into(),
));
vault_account.encrypted_suri = Set(Some(encrypted_suri));
vault_account.name = Set(name);
let updated_vault_account =
vault_account::update_account(db_tx, vault_account).await?;
println!("Updating vault account {updated_vault_account}");
Ok(updated_vault_account)
}
_ => Err(GcliError::Input("import canceled".into())),
}
} else {
//New entry
let secret_format = vault_data.secret_format;
let password = match password_opt {
Some(password) => password.clone(),
None => inputs::prompt_password_query("Enter password to encrypt the key: ")?,
};
let encrypted_suri = compute_encrypted_suri(password, vault_data.secret_suri.clone())?;
let name = if let Some(name) = name_opt {
validate_vault_name(&name)?;
trim_and_reduce_empty_as_none(name)
} else {
println!("(Optional) Enter a name for the vault entry");
inputs::prompt_vault_name_and_check_availability(db_tx, None).await?
};
let crypto_scheme = map_secret_format_to_crypto_scheme(secret_format, crypto_scheme);
let account = vault_account::create_base_account(
db_tx,
&address_to_import,
name.as_ref(),
crypto_scheme,
encrypted_suri,
)
.await?;
println!("Creating <Base> account {account}");
Ok(account)
}
}
/// Creates a `derivation` vault account for data provided and returns it
///
/// Does extra checks and asks for user input in case the address is already present in the vault.
///
/// Can request (optional) name to the user at the proper time
///
/// Typically used for `vault derive` command
pub async fn create_derivation_account<C>(
db_tx: &C,
derivation_address: &String,
derivation_path: &String,
parent_address: &String,
name_opt: Option<String>,
) -> Result<vault_account::Model, GcliError>
where
C: ConnectionTrait,
{
println!("Trying to create derivation with address '{derivation_address}'");
println!();
let vault_account = if let Some(existing_vault_account) =
vault_account::find_by_id(db_tx, &DbAccountId::from(derivation_address.clone())).await?
{
// Existing account
println!("You are trying to derive '{derivation_path}' from parent '{parent_address}'");
if existing_vault_account.is_base_account() {
println!(
"but it is already present as a direct <Base> account '{}'",
existing_vault_account.address
);
println!("Do you want to:");
println!("1. keep the existing <Base> account and cancel import");
println!("2. delete the existing <Base> account and associated key and replace it with the new derivation account (children will be re-parented)");
} else {
//Existing derivation
let existing_account_tree_node_hierarchy =
vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
db_tx,
derivation_address,
)
.await?;
let existing_account_tree_node_for_address =
vault_account::get_account_tree_node_for_address(
&existing_account_tree_node_hierarchy,
derivation_address,
);
let base_parent_hierarchy_existing_account_tree_node =
vault_account::get_base_parent_hierarchy_account_tree_node(
&existing_account_tree_node_for_address,
);
let parent_hierarchy_table_existing_account =
display::compute_vault_accounts_table(&[
base_parent_hierarchy_existing_account_tree_node,
])?;
println!(
"but it is already present as `{}` derivation of '{}' account.",
existing_vault_account.path.clone().unwrap(),
existing_vault_account.parent.clone().unwrap()
);
println!();
println!("Its parent hierarchy is this:");
println!("{parent_hierarchy_table_existing_account}");
println!();
println!("Do you want to:");
println!("1. keep the existing derivation and cancel import");
println!("2. delete the derivation account and replace it with this new derivation (children will be re-parented)");
}
let result = inputs::select_action("Your choice?", vec!["1", "2"])?;
match result {
"2" => {
let name = if let Some(name) = name_opt {
validate_vault_name(&name)?;
trim_and_reduce_empty_as_none(name)
} else {
println!("(Optional) Enter a name for the vault entry (leave empty to remove the name)");
inputs::prompt_vault_name_and_check_availability(
db_tx,
existing_vault_account.name.as_ref(),
)
.await?
};
// Since links are made based on address / parent(address) we can just edit the existing entry and it should be fine
let mut vault_account: ActiveModel = existing_vault_account.into();
vault_account.path = Set(Some(derivation_path.clone()));
vault_account.parent = Set(Some(DbAccountId::from(parent_address.clone())));
vault_account.crypto_scheme = Set(None);
vault_account.encrypted_suri = Set(None);
vault_account.name = Set(name.clone());
let updated_vault_account =
vault_account::update_account(db_tx, vault_account).await?;
println!("Updating vault account {updated_vault_account}");
updated_vault_account
}
_ => {
return Err(GcliError::Input("derive canceled".into()));
}
}
} else {
let name = if let Some(name) = name_opt {
validate_vault_name(&name)?;
trim_and_reduce_empty_as_none(name)
} else {
println!("(Optional) Enter a name for the vault entry");
inputs::prompt_vault_name_and_check_availability(db_tx, None).await?
};
let derivation = vault_account::create_derivation_account(
db_tx,
derivation_address,
name.as_ref(),
derivation_path,
parent_address,
)
.await?;
println!("Creating derivation account {derivation}");
derivation
};
Ok(vault_account)
}
/// Function will compute the encrypted suri
fn compute_encrypted_suri(password: String, secret_suri: String) -> Result<Vec<u8>, GcliError> {
encrypt(secret_suri.as_bytes(), password).map_err(|e| GcliError::Input(e.to_string()))
}
fn get_vault_key_path(data: &Data, vault_filename: &str) -> PathBuf {
data.project_dir.data_dir().join(vault_filename)
}
/// look for different possible paths for vault keys and return both format and path
fn find_substrate_vault_key_file(data: &Data, address: &str) -> Result<Option<PathBuf>, GcliError> {
let path = get_vault_key_path(data, address);
if path.exists() {
return Ok(Some(path));
}
Ok(None)
}
/// Gets secret in keystore, prompt for the password and compute the keypair associated to `address`
///
/// Returns an error if no entry was found in the keystore or if another error occurred during the process
pub async fn fetch_vault_keypair_for_address(
data: &Data,
address: AccountId,
) -> Result<KeyPair, GcliError> {
println!("Trying to retrieve key pair for address:'{address}'");
try_fetch_key_pair(data, address)
.await?
.ok_or_else(|| GcliError::Input("vault account not found".to_string()))
}
/// try to get secret in keystore, prompt for the password and compute the keypair associated to `address`
pub async fn try_fetch_key_pair(
data: &Data,
address: AccountId,
) -> Result<Option<KeyPair>, GcliError> {
if let Some(account_tree_node_hierarchy) =
vault_account::fetch_base_account_tree_node_hierarchy(
data.connect_db(),
&address.to_string(),
)
.await?
{
let account_tree_node = vault_account::get_account_tree_node_for_address(
&account_tree_node_hierarchy,
&address.to_string(),
);
println!("(Vault: {})", account_tree_node.borrow().account);
let password = inputs::prompt_password()?;
let secret_suri =
vault_account::compute_suri_account_tree_node(&account_tree_node, password)?;
let base_account_tree_node = vault_account::get_base_account_tree_node(&account_tree_node);
let base_account = &base_account_tree_node.borrow().account.clone();
let key_pair = compute_keypair(base_account.crypto_scheme.unwrap().into(), &secret_suri)?;
//To be safe
if address != key_pair.address() {
return Err(GcliError::Input(format!(
"Computed address {} does not match the expected address {}",
key_pair.address(),
address
)));
}
Ok(Some(key_pair))
} else {
Ok(None)
}
}
pub fn compute_keypair(
crypto_scheme: CryptoScheme,
secret_suri: &str,
) -> Result<KeyPair, GcliError> {
let key_pair = match crypto_scheme {
CryptoScheme::Sr25519 => pair_from_sr25519_str(secret_suri)?.into(),
CryptoScheme::Ed25519 => pair_from_ed25519_str(secret_suri)?.into(),
};
Ok(key_pair)
}
pub struct VaultDataFromFile {
secret_format: SecretFormat,
secret: String,
#[allow(dead_code)]
path: PathBuf,
password: String,
key_pair: KeyPair,
}
/// try to get secret in keystore, prompt for the password and compute the keypair
#[deprecated(
note = "Should be removed in a future version when db persistence of vault is present for a while"
)]
pub fn try_fetch_vault_data_from_file(
data: &Data,
address: &str,
) -> Result<Option<VaultDataFromFile>, GcliError> {
if let Some(path) = find_substrate_vault_key_file(data, address)? {
println!("Enter password to unlock account {address}");
let password = inputs::prompt_password()?;
let mut file = std::fs::OpenOptions::new().read(true).open(path.clone())?;
let mut cypher = vec![];
file.read_to_end(&mut cypher)?;
let secret_vec =
decrypt(&cypher, password.clone()).map_err(|e| GcliError::Input(e.to_string()))?;
let secret = String::from_utf8(secret_vec).map_err(|e| anyhow!(e))?;
let key_pair = pair_from_sr25519_str(&secret)?.into();
Ok(Some(VaultDataFromFile {
secret_format: SecretFormat::Substrate,
secret,
path,
password,
key_pair,
}))
} else {
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
/// test that armored encryption/decryption work as intended
#[test]
fn test_encrypt_decrypt() {
let plaintext = b"Hello world!";
let passphrase = "this is not a good passphrase".to_string();
let encrypted = encrypt(plaintext, passphrase.clone()).unwrap();
let decrypted = decrypt(&encrypted, passphrase).unwrap();
assert_eq!(decrypted, plaintext);
}
#[rstest]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0"),
Some(String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk"
)),
Some(String::from("//0"))
)]
#[case(
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0"),
Some(String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")),
Some(String::from("//0"))
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice"
),
Some(String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk"
)),
Some(String::from("//Alice"))
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Bob/soft1/soft2"
),
Some(String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk")),
Some(String::from("//Alice//Bob/soft1/soft2"))
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"),
Some(String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk"
)),
None
)]
#[case(
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"),
Some(String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")),
None
)]
#[case(
String::from("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"),
Some(String::from("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")),
None
)]
#[case(
String::from("someVaultName//Alice"),
Some(String::from("someVaultName")),
Some(String::from("//Alice"))
)]
#[case(
String::from("someVaultName"),
Some(String::from("someVaultName")),
None
)]
fn test_parse_prefix_and_derivation_path_from_suri(
#[case] raw_string: String,
#[case] expected_prefix: Option<String>,
#[case] expected_derivation_path: Option<String>,
) {
let (root_secret, derivation_path) =
parse_prefix_and_derivation_path_from_suri(raw_string).unwrap();
assert_eq!(expected_prefix, root_secret);
assert_eq!(expected_derivation_path, derivation_path);
}
#[rstest]
#[case(
String::from("//Alice//Bob/soft1/soft2"),
None,
Some(String::from("//Alice//Bob/soft1/soft2"))
)]
#[case(String::from(""), None, None)]
#[case(String::from("//0"), None, Some(String::from("//0")))]
fn test_parse_prefix_and_derivation_path_from_suri_works_with_empty_prefix_phrase(
#[case] raw_string: String,
#[case] expected_prefix: Option<String>,
#[case] expected_derivation_path: Option<String>,
) {
let (root_secret, derivation_path) =
parse_prefix_and_derivation_path_from_suri(raw_string).unwrap();
assert_eq!(expected_prefix, root_secret);
assert_eq!(expected_derivation_path, derivation_path);
}
#[rstest]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Bob/soft1/soft2///password"
),
)]
#[case(String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk///password"
))]
#[case(String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk///"
))]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk///password//NotDerivations//Still/password/part"
),
)]
fn test_parse_prefix_and_derivation_path_from_suri_does_not_allow_password(
#[case] raw_string: String,
) {
let result = parse_prefix_and_derivation_path_from_suri(raw_string);
match result.unwrap_err() {
GcliError::Input(err) => {
println!("Error message: {}", err);
assert!(
err.starts_with("Having a password in the derivation path is not supported")
);
}
other => panic!("Should have been an Input error; got: {:?}", other),
}
}
}
use crate::commands::cesium;
use crate::entities::vault_account;
use crate::entities::vault_account::AccountTreeNode;
use crate::keys::CryptoScheme;
use crate::utils::GcliError;
use comfy_table::{Cell, Table};
use std::cell::RefCell;
use std::rc::Rc;
use std::str;
#[deprecated(
note = "Should be removed in a future version when db persistence of vault is present for a while"
)]
pub fn compute_vault_key_files_table(vault_key_addresses: &[String]) -> Result<Table, GcliError> {
let mut table = Table::new();
table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY);
table.set_header(vec!["Key file"]);
vault_key_addresses.iter().for_each(|address| {
table.add_row(vec![Cell::new(address)]);
});
Ok(table)
}
pub fn compute_vault_accounts_table(
account_tree_nodes: &[Rc<RefCell<AccountTreeNode>>],
) -> Result<Table, GcliError> {
// Calling 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);
// Prepare header based on options
table.set_header(vec![
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_with_g1v1(&mut table, account_tree_node, show_g1v1);
}
Ok(table)
}
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_with_g1v1(table, child, show_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_with_g1v1(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
show_g1v1: bool,
) -> Result<Vec<Vec<Cell>>, GcliError> {
let empty_string = "".to_string();
let depth_account_tree_node = vault_account::count_depth_account_tree_node(account_tree_node);
let name = if let Some(name) = account_tree_node.borrow().account.name.clone() {
name
} else if let Some(computed_name) =
vault_account::compute_name_account_tree_node(account_tree_node)
{
format!("<{}>", computed_name)
} else {
empty_string.clone()
};
let account_tree_node = account_tree_node.borrow();
let address = if depth_account_tree_node > 0 {
let ancestors = "│ ".repeat(depth_account_tree_node - 1);
format!("{}├ {}", ancestors, account_tree_node.account.address)
} else {
account_tree_node.account.address.to_string()
};
let mut rows: Vec<Vec<Cell>> = vec![];
let (path, crypto) = if let Some(path) = account_tree_node.account.path.clone() {
(path, empty_string.clone())
} else {
let crypto_scheme = CryptoScheme::from(account_tree_node.account.crypto_scheme.unwrap());
let crypto_scheme_str: &str = crypto_scheme.into();
// 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!(
"└ G1v1: {}",
cesium::compute_g1v1_public_key_from_ed25519_account_id(
&account_tree_node.account.address.0
)
))]);
}
(
format!("<{}>", account_tree_node.account.account_type()),
crypto_scheme_str.to_string(),
)
};
// Add the first line
rows.insert(
0,
vec![
Cell::new(&address),
Cell::new(crypto),
Cell::new(&path),
Cell::new(&name),
],
);
Ok(rows)
}
#[cfg(test)]
mod tests {
mod vault_accounts_table_tests {
use crate::commands::vault::display::{
compute_vault_accounts_table, compute_vault_accounts_table_with_g1v1,
};
use crate::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();
let expected_table = indoc! {r#"
┌─────────────────────────────────────────────────────┐
│ SS58 Address/G1v1 public key Crypto Path Name │
╞═════════════════════════════════════════════════════╡
└─────────────────────────────────────────────────────┘"#
};
assert_eq!(table.to_string(), expected_table);
}
#[test]
fn test_compute_vault_accounts_table() {
let account_tree_node = mother_account_tree_node();
let g1v1_account_tree_node = mother_g1v1_account_tree_node();
let table =
compute_vault_accounts_table(&[account_tree_node, g1v1_account_tree_node]).unwrap();
let expected_table = 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.to_string(), expected_table);
}
#[test]
fn test_compute_vault_accounts_table_partial() {
let mother = mother_account_tree_node();
let child1 = mother.borrow().children[0].clone();
let table = compute_vault_accounts_table(&[child1]).unwrap();
let expected_table = indoc! {r#"
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ SS58 Address/G1v1 public key Crypto Path Name │
╞═════════════════════════════════════════════════════════════════════════════════════╡
│ ├ 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH //0 Child 1 │
│ │ ├ 5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d //0 Grandchild 1 │
└─────────────────────────────────────────────────────────────────────────────────────┘"#
};
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();
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();
let expected_table_without_g1v1 = indoc! {r#"
┌─────────────────────────────────────┐
│ SS58 Address Crypto Path Name │
╞═════════════════════════════════════╡
└─────────────────────────────────────┘"#
};
assert_eq!(table.to_string(), expected_table_without_g1v1);
}
#[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_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_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]
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_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_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);
}
}
}
use crate::entities::vault_account;
use crate::entities::vault_account::DbAccountId;
use crate::*; use crate::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
...@@ -6,14 +8,13 @@ const APP_NAME: &str = "gcli"; ...@@ -6,14 +8,13 @@ const APP_NAME: &str = "gcli";
/// defines structure of config file /// defines structure of config file
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Config { pub struct Config {
// duniter endpoint /// duniter endpoint
pub duniter_endpoint: String, pub duniter_endpoint: String,
// indexer endpoint /// indexer endpoint
pub indexer_endpoint: String, pub indexer_endpoint: String,
// user address /// user address
/// to perform actions, user must provide secret
pub address: Option<AccountId>, pub address: Option<AccountId>,
// user secret (substrate format)
pub secret: Option<String>,
} }
impl std::default::Default for Config { impl std::default::Default for Config {
...@@ -22,11 +23,24 @@ impl std::default::Default for Config { ...@@ -22,11 +23,24 @@ impl std::default::Default for Config {
duniter_endpoint: String::from(data::LOCAL_DUNITER_ENDPOINT), duniter_endpoint: String::from(data::LOCAL_DUNITER_ENDPOINT),
indexer_endpoint: String::from(data::LOCAL_INDEXER_ENDPOINT), indexer_endpoint: String::from(data::LOCAL_INDEXER_ENDPOINT),
address: None, address: None,
secret: None,
} }
} }
} }
impl std::fmt::Display for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let address = if let Some(address) = &self.address {
format!("{}", address)
} else {
"(no address)".to_string()
};
writeln!(f, "Ğcli config")?;
writeln!(f, "duniter endpoint {}", self.duniter_endpoint)?;
writeln!(f, "indexer endpoint {}", self.indexer_endpoint)?;
write!(f, "address {address}")
}
}
/// load config file and manage error if could not /// load config file and manage error if could not
pub fn load_conf() -> Config { pub fn load_conf() -> Config {
match confy::load(APP_NAME, None) { match confy::load(APP_NAME, None) {
...@@ -55,10 +69,12 @@ pub enum Subcommand { ...@@ -55,10 +69,12 @@ pub enum Subcommand {
Show, Show,
/// Save config as modified by command line arguments /// Save config as modified by command line arguments
Save, Save,
/// Rest config to default
Default,
} }
/// handle conf command /// handle conf command
pub fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<()> { pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
// match subcommand // match subcommand
match command { match command {
Subcommand::Where => { Subcommand::Where => {
...@@ -68,12 +84,30 @@ pub fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<()> { ...@@ -68,12 +84,30 @@ pub fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<()> {
); );
} }
Subcommand::Show => { Subcommand::Show => {
println!("{:?}", data.cfg); println!("{}", data.cfg);
if let Some(ref account_id) = data.cfg.address {
if let Some(account) = vault_account::find_by_id(
data.connect_db(),
&DbAccountId::from(account_id.clone()),
)
.await?
{
println!("(Vault: {})", account);
}
}
} }
Subcommand::Save => { Subcommand::Save => {
confy::store(APP_NAME, None, &data.cfg).expect("unable to write config"); save(&data.cfg);
}
Subcommand::Default => {
confy::store(APP_NAME, None, Config::default()).expect("unable to write config");
} }
}; };
Ok(()) Ok(())
} }
pub fn save(cfg: &Config) {
confy::store(APP_NAME, None, cfg).expect("unable to write config");
println!("Configuration updated!");
}
use std::str::FromStr; use crate::commands::vault;
use crate::*; use crate::*;
use indexer::Indexer; use indexer::Indexer;
use sea_orm::DatabaseConnection;
// consts // consts
pub const SUBSTRATE_MNEMONIC: &str =
"bottom drive obey lake curtain smoke basket hold race lonely fit walk";
pub const TEST_MNEMONIC: &str =
"pipe paddle ketchup filter life ice feel embody glide quantum ride usage";
pub const LOCAL_DUNITER_ENDPOINT: &str = "ws://localhost:9944"; pub const LOCAL_DUNITER_ENDPOINT: &str = "ws://localhost:9944";
pub const LOCAL_INDEXER_ENDPOINT: &str = "http://localhost:8080/v1/graphql"; pub const LOCAL_INDEXER_ENDPOINT: &str = "http://localhost:4350/graphql";
pub const SQLITE_DB_FILENAME: &str = "gcli.sqlite";
const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(feature = "gdev")] #[cfg(feature = "gdev")]
pub const GDEV_DUNITER_ENDPOINTS: [&str; 5] = [ pub const GDEV_DUNITER_ENDPOINTS: [&str; 5] = [
...@@ -21,36 +19,40 @@ pub const GDEV_DUNITER_ENDPOINTS: [&str; 5] = [ ...@@ -21,36 +19,40 @@ pub const GDEV_DUNITER_ENDPOINTS: [&str; 5] = [
]; ];
#[cfg(feature = "gdev")] #[cfg(feature = "gdev")]
pub const GDEV_INDEXER_ENDPOINTS: [&str; 2] = [ pub const GDEV_INDEXER_ENDPOINTS: [&str; 2] = [
"https://gdev-indexer.p2p.legal/v1/graphql", // "https://squid.gdev.coinduf.eu/v1/graphql",
"https://hasura.gdev.coinduf.eu/v1/graphql", "https://squid.gdev.gyroi.de/v1/graphql",
"https://gdev-squid.axiom-team.fr/v1/graphql",
]; ];
// data derived from command arguments // data derived from command arguments
/// Data of current command /// Data of current command
/// can also include fetched information /// can also include fetched information
#[derive(Default)]
pub struct Data { pub struct Data {
// command line arguments /// command line arguments
pub args: Args, pub args: Args,
// config /// config
pub cfg: conf::Config, pub cfg: conf::Config,
// rpc to substrate client /// database connection
connection: Option<DatabaseConnection>,
/// rpc to substrate client
pub client: Option<Client>, pub client: Option<Client>,
// graphql to duniter-indexer /// graphql to duniter-indexer
pub indexer: Option<Indexer>, pub indexer: Option<Indexer>,
// user keypair /// user keypair
pub keypair: Option<Pair>, pub keypair: Option<KeyPair>,
// user identity index /// user identity index
pub idty_index: Option<u32>, pub idty_index: Option<IdtyId>,
// token decimals /// token decimals
pub token_decimals: u32, pub token_decimals: u32,
// token symbol /// token symbol
pub token_symbol: String, pub token_symbol: String,
// genesis hash /// genesis hash
pub genesis_hash: Hash, pub genesis_hash: Hash,
// indexer genesis hash /// indexer genesis hash
pub indexer_genesis_hash: Hash, pub indexer_genesis_hash: Hash,
/// gcli base path
pub project_dir: directories::ProjectDirs,
} }
/// system properties defined in client specs /// system properties defined in client specs
...@@ -61,22 +63,53 @@ struct SystemProperties { ...@@ -61,22 +63,53 @@ struct SystemProperties {
token_symbol: String, token_symbol: String,
} }
impl Default for Data {
fn default() -> Self {
let project_dir = directories::ProjectDirs::from("org", "duniter", "gcli").unwrap();
if !project_dir.data_dir().exists() {
std::fs::create_dir_all(project_dir.data_dir()).expect("could not create data dir");
};
Self {
project_dir,
args: Default::default(),
cfg: Default::default(),
connection: Default::default(),
client: Default::default(),
indexer: Default::default(),
keypair: Default::default(),
idty_index: Default::default(),
token_decimals: Default::default(),
token_symbol: Default::default(),
genesis_hash: Default::default(),
indexer_genesis_hash: Default::default(),
}
}
}
// implement helper functions for Data // implement helper functions for Data
impl Data { impl Data {
/// --- constructor --- /// --- constructor ---
pub fn new(args: Args) -> Self { pub async fn new(args: Args) -> Result<Self, GcliError> {
Self { let mut data = Self {
args, args,
cfg: conf::load_conf(), cfg: conf::load_conf(),
token_decimals: 0, token_decimals: 0,
token_symbol: "tokens".into(), token_symbol: "tokens".into(),
..Default::default() ..Default::default()
} };
.overwrite_from_args() //Necessary to support checking "vault names" in the base arguments
.build_from_config() data = data.build_connection().await?;
data = data.overwrite_from_args().await?;
Ok(data)
} }
// --- getters --- // --- getters ---
// the "unwrap" should not fail if data is well prepared // the "unwrap" should not fail if data is well prepared
/// Returns the DatabaseConnection reference
pub fn connect_db(&self) -> &DatabaseConnection {
self.connection
.as_ref()
.expect("Database connection is not available")
}
pub fn client(&self) -> &Client { pub fn client(&self) -> &Client {
self.client.as_ref().expect("must build client first") self.client.as_ref().expect("must build client first")
} }
...@@ -86,27 +119,51 @@ impl Data { ...@@ -86,27 +119,51 @@ impl Data {
pub fn address(&self) -> AccountId { pub fn address(&self) -> AccountId {
self.cfg.address.clone().expect("an address is needed") self.cfg.address.clone().expect("an address is needed")
} }
pub fn keypair(&self) -> Pair { pub async fn keypair(&self) -> KeyPair {
match self.keypair.clone() { match self.keypair.clone() {
Some(keypair) => keypair, Some(keypair) => keypair,
None => prompt_secret(self.args.secret_format), None => loop {
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
//otherwise only way was to kill the process !
if let GcliError::Input(message) = &e {
match message.as_str() {
"Operation was interrupted by the user"
| "Operation was canceled by the user" => {
panic!("{}", e.to_string());
} }
_ => {}
} }
pub fn idty_index(&self) -> u32 { }
println!("{e:?} → retry")
}
}
},
}
}
pub fn idty_index(&self) -> IdtyId {
self.idty_index.expect("must fetch idty index first") self.idty_index.expect("must fetch idty index first")
} }
// --- methods --- // --- methods ---
pub fn format_balance(&self, amount: Balance) -> String { pub fn format_balance(&self, amount: Balance) -> String {
let base: u32 = 10; let base: u64 = 10;
let integer_part = amount / base.pow(self.token_decimals);
let fractional_part = amount % base.pow(self.token_decimals);
format!( format!(
"{} {}", "{}.{:0left_padding$} {}",
(amount as f32) / (base.pow(self.token_decimals) as f32), integer_part,
self.token_symbol fractional_part,
self.token_symbol,
left_padding = self.token_decimals as usize
) )
} }
// --- mutators --- // --- mutators ---
/// use arguments to overwrite config /// use arguments to overwrite config
pub fn overwrite_from_args(mut self) -> Self { pub async fn overwrite_from_args(mut self) -> Result<Self, GcliError> {
// network // network
if let Some(network) = self.args.network.clone() { if let Some(network) = self.args.network.clone() {
// a network was provided as arugment // a network was provided as arugment
...@@ -148,75 +205,55 @@ impl Data { ...@@ -148,75 +205,55 @@ impl Data {
if let Some(indexer_endpoint) = self.args.indexer.clone() { if let Some(indexer_endpoint) = self.args.indexer.clone() {
self.cfg.indexer_endpoint = indexer_endpoint self.cfg.indexer_endpoint = indexer_endpoint
} }
// predefined secret // secret format and value
if self.args.secret_format == SecretFormat::Predefined { if let Some(secret_format) = self.args.secret_format {
match self.args.secret.clone() { let keypair = get_keypair(
None => {} secret_format,
Some(derivation) => { self.args.secret.as_deref(),
if derivation.starts_with("test") { self.args.crypto_scheme,
let derivation = match &derivation[..] { )?;
"test1" => "2", self.cfg.address = Some(keypair.address());
"test2" => "4", self.keypair = Some(keypair);
"test3" => "3",
_ => ""
};
self.cfg.secret = Some(format!("{TEST_MNEMONIC}//{derivation}"));
} else {
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 // address
if let Some(address) = self.args.address.clone() { if let Some(address) = self.args.address.clone() {
self.cfg.address = Some(AccountId::from_str(&address).expect("invalid address")); self.cfg.address = Some(address.clone());
} // if giving address, cancel secret
self self.keypair = None
}
/// 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); // (vault)name
self.cfg.secret = Some(secret); if let Some(name) = self.args.name.clone() {
self.keypair = Some(keypair); let account = vault::retrieve_vault_account_for_name(self.connect_db(), &name).await?;
self.cfg.address = Some(account.address.0.clone());
// if giving (vault)name, cancel secret
self.keypair = None
} }
self Ok(self)
} }
/// build a client from url /// build a client from url
pub async fn build_client(mut self) -> Result<Self, GcliError> { pub async fn build_client(mut self) -> Result<Self, GcliError> {
let duniter_endpoint = self.cfg.duniter_endpoint.clone(); let duniter_endpoint = &self.cfg.duniter_endpoint;
self.client = Some(Client::from_url(&duniter_endpoint).await.map_err(|e| { let client = Client::from_url(duniter_endpoint).await.map_err(|e| {
GcliError::Duniter(format!( // to get more details TODO fixme, see issue #18
"could not establish connection with the server {}, due to error {}", dbg!(e);
duniter_endpoint, GcliError::Duniter(format!("can not connect to duniter {duniter_endpoint}",))
dbg!(e) // needed to get more details TODO fixme })?;
)) self.client = Some(client);
})?);
self.genesis_hash = commands::blockchain::fetch_genesis_hash(&self).await?; self.genesis_hash = commands::blockchain::fetch_genesis_hash(&self).await?;
Ok(self) Ok(self)
} }
/// build an indexer if not disabled /// build an indexer if not disabled
pub async fn build_indexer(mut self) -> Result<Self, anyhow::Error> { pub async fn build_indexer(mut self) -> Result<Self, GcliError> {
if self.args.no_indexer { if self.args.no_indexer {
log::info!("called build_indexer while providing no_indexer"); log::info!("called build_indexer while providing no_indexer");
self.indexer = None; self.indexer = None;
} else { } else {
self.indexer = Some(Indexer { self.indexer = Some(Indexer {
gql_client: reqwest::Client::builder() gql_client: reqwest::Client::builder()
.user_agent("gcli/0.1.0") .user_agent(format!("gcli/{PKG_VERSION}"))
.build()?, .build()
.unwrap(),
gql_url: self.cfg.indexer_endpoint.clone(), gql_url: self.cfg.indexer_endpoint.clone(),
}); });
self.indexer_genesis_hash = self.indexer().fetch_genesis_hash().await?; self.indexer_genesis_hash = self.indexer().fetch_genesis_hash().await?;
...@@ -226,22 +263,37 @@ impl Data { ...@@ -226,22 +263,37 @@ impl Data {
}; };
Ok(self) Ok(self)
} }
/// get issuer index
/// build a database connection
async fn build_connection(mut self) -> Result<Self, GcliError> {
let data_dir = self.project_dir.data_dir();
let connection = database::build_sqlite_connection(data_dir, SQLITE_DB_FILENAME).await?;
self.connection = Some(connection);
Ok(self)
}
/// get issuer index<br>
/// needs address and client first /// needs address and client first
pub async fn fetch_idty_index(mut self) -> Result<Self, anyhow::Error> { pub async fn fetch_idty_index(mut self) -> Result<Self, GcliError> {
self.idty_index = Some( self.idty_index = Some(
commands::identity::get_idty_index_by_account_id(self.client(), &self.address()) commands::identity::get_idty_index_by_account_id(self.client(), &self.address())
.await? .await?
.ok_or(anyhow::anyhow!("needs to be member to use this command"))?, .ok_or(GcliError::Logic(
"you need to be member to use this command".to_string(),
))?,
); );
Ok(self) Ok(self)
} }
/// get properties /// get properties
pub async fn fetch_system_properties(mut self) -> Result<Self, anyhow::Error> { pub async fn fetch_system_properties(mut self) -> Result<Self, GcliError> {
let system_properties = self.client().rpc().system_properties().await?; let system_properties = self.legacy_rpc_methods().await.system_properties().await?;
let system_properties = serde_json::from_value::<SystemProperties>( let system_properties = serde_json::from_value::<SystemProperties>(
serde_json::Value::Object(system_properties), serde_json::Value::Object(system_properties),
)?; )
.map_err(|e| {
dbg!(e);
GcliError::Duniter("could not read duniter system properties".to_string())
})?;
self.token_decimals = system_properties.token_decimals; self.token_decimals = system_properties.token_decimals;
self.token_symbol = system_properties.token_symbol; self.token_symbol = system_properties.token_symbol;
Ok(self) Ok(self)
...@@ -259,3 +311,18 @@ impl Data { ...@@ -259,3 +311,18 @@ impl Data {
// ); // );
// ); // );
} }
// legacy methods (see subxt changelog)
use subxt::{
backend::{legacy::LegacyRpcMethods, rpc::RpcClient},
config::SubstrateConfig,
};
impl Data {
pub async fn legacy_rpc_methods(&self) -> LegacyRpcMethods<SubstrateConfig> {
let rpc_client = RpcClient::from_url(self.cfg.duniter_endpoint.clone())
.await
.expect("error");
LegacyRpcMethods::<SubstrateConfig>::new(rpc_client)
}
}
use crate::entities::vault_account;
use crate::utils::GcliError;
use sea_orm::sea_query::IndexCreateStatement;
use sea_orm::{ConnectionTrait, Database, DatabaseConnection, Schema};
use std::fs;
use std::path::Path;
pub async fn build_sqlite_connection(
data_dir: &Path,
filename: &str,
) -> Result<DatabaseConnection, GcliError> {
let sqlite_path = data_dir.join(filename);
// Check if the file exists, and create it if it doesn't (otherwise the connection will fail)
if !Path::new(&sqlite_path).exists() {
fs::File::create(sqlite_path.clone())?;
}
let sqlite_path_str = sqlite_path
.into_os_string()
.into_string()
.map_err(|_| GcliError::Input("Invalid SQLite path".to_string()))?;
let sqlite_db_url = format!("sqlite://{}", sqlite_path_str);
let connection = initialize_db(&sqlite_db_url).await?;
Ok(connection)
}
pub async fn initialize_db(db_url: &str) -> Result<DatabaseConnection, GcliError> {
let db = Database::connect(db_url).await?;
let schema = Schema::new(db.get_database_backend());
create_table_if_not_exists(&db, &schema, vault_account::Entity).await?;
Ok(db)
}
async fn create_table_if_not_exists<E: sea_orm::EntityTrait>(
db: &DatabaseConnection,
schema: &Schema,
entity: E,
) -> Result<(), GcliError> {
db.execute(
db.get_database_backend()
.build(schema.create_table_from_entity(entity).if_not_exists()),
)
.await?;
Ok(())
}
/// The only way to add composed unique index...
#[allow(dead_code)]
async fn create_table_if_not_exists_with_index<E: sea_orm::EntityTrait>(
db: &DatabaseConnection,
schema: &Schema,
entity: E,
index: &mut IndexCreateStatement,
) -> Result<(), GcliError> {
db.execute(
db.get_database_backend().build(
schema
.create_table_from_entity(entity)
.index(index)
.if_not_exists(),
),
)
.await?;
Ok(())
}
use crate::*;
use std::str;
// display events in a friendly manner
pub trait DisplayEvent {
fn display(&self, data: &Data) -> String;
}
impl DisplayEvent for runtime::universal_dividend::events::UdsClaimed {
fn display(&self, data: &Data) -> String {
format!(
"claimed {} UD, for a total of {}",
self.count,
data.format_balance(self.total)
)
}
}
impl DisplayEvent for runtime::certification::events::CertAdded {
fn display(&self, _data: &Data) -> String {
format!("new certification {} → {}", self.issuer, self.receiver)
}
}
impl DisplayEvent for runtime::certification::events::CertRenewed {
fn display(&self, _data: &Data) -> String {
format!("renewed cert {:?}", self)
}
}
impl DisplayEvent for runtime::account::events::AccountUnlinked {
fn display(&self, _data: &Data) -> String {
format!("account unlinked: {}", self.0)
}
}
impl DisplayEvent for runtime::technical_committee::events::Voted {
fn display(&self, _data: &Data) -> String {
format!("voted {:?}", self)
}
}
impl DisplayEvent for runtime::identity::events::IdtyCreated {
fn display(&self, _data: &Data) -> String {
format!(
"identity created for {} with index {}",
self.owner_key, self.idty_index
)
}
}
impl DisplayEvent for runtime::identity::events::IdtyConfirmed {
fn display(&self, _data: &Data) -> String {
format!(
"identity confirmed with name \"{}\" (index {}, owner key {})",
str::from_utf8(&self.name.0).unwrap(),
self.idty_index,
self.owner_key
)
}
}
impl DisplayEvent for runtime::identity::events::IdtyChangedOwnerKey {
fn display(&self, _data: &Data) -> String {
format!("identity changed owner key {:?}", self)
}
}
impl DisplayEvent for runtime::distance::events::EvaluationRequested {
fn display(&self, _data: &Data) -> String {
format!("evaluation requested {:?}", self)
}
}
impl DisplayEvent for runtime::smith_members::events::InvitationSent {
fn display(&self, _data: &Data) -> String {
format!("sent smith invitation {:?}", self)
}
}
impl DisplayEvent for runtime::smith_members::events::InvitationAccepted {
fn display(&self, _data: &Data) -> String {
format!("accepted smith invitation {:?}", self)
}
}
impl DisplayEvent for runtime::smith_members::events::SmithCertAdded {
fn display(&self, _data: &Data) -> String {
format!("new smith certification {:?}", self)
}
}
impl DisplayEvent for runtime::smith_members::events::SmithMembershipAdded {
fn display(&self, _data: &Data) -> String {
format!("new smith promoted {:?}", self)
}
}
impl DisplayEvent for runtime::identity::events::IdtyRemoved {
fn display(&self, _data: &Data) -> String {
format!("identity removed {:?}", self)
}
}
impl DisplayEvent for runtime::account::events::AccountLinked {
fn display(&self, _data: &Data) -> String {
format!("account {} linked to identity {}", self.who, self.identity)
}
}
impl DisplayEvent for runtime::oneshot_account::events::OneshotAccountCreated {
fn display(&self, _data: &Data) -> String {
format!("oneshot {:?}", self)
}
}
impl DisplayEvent for runtime::oneshot_account::events::OneshotAccountConsumed {
fn display(&self, _data: &Data) -> String {
format!("oneshot {:?}", self)
}
}
impl DisplayEvent for runtime::authority_members::events::MemberGoOnline {
fn display(&self, _data: &Data) -> String {
format!("smith went online {:?}", self)
}
}
impl DisplayEvent for runtime::authority_members::events::MemberGoOffline {
fn display(&self, _data: &Data) -> String {
format!("smith went offline {:?}", self)
}
}
impl DisplayEvent for runtime::sudo::events::KeyChanged {
fn display(&self, _data: &Data) -> String {
format!("sudo key changed {:?}", self)
}
}
impl DisplayEvent for runtime::balances::events::Transfer {
fn display(&self, data: &Data) -> String {
format!(
"transfered {} ({} → {})",
data.format_balance(self.amount),
self.from,
self.to
)
}
}
impl DisplayEvent for runtime::utility::events::BatchCompleted {
fn display(&self, _data: &Data) -> String {
format!("batch completed {:?}", self)
}
}
impl DisplayEvent for runtime::sudo::events::Sudid {
fn display(&self, _data: &Data) -> String {
format!("SUDO call succeeded {:?}", self)
}
}
impl DisplayEvent for runtime::technical_committee::events::Proposed {
fn display(&self, _data: &Data) -> String {
format!("proposed {:?}", self)
}
}
pub mod vault_account;
use crate::commands::{cesium, vault};
use crate::runtime_config::AccountId;
use crate::utils::GcliError;
use anyhow::anyhow;
use sea_orm::entity::prelude::*;
use sea_orm::prelude::async_trait::async_trait;
use sea_orm::prelude::StringLen;
use sea_orm::ActiveValue::Set;
use sea_orm::PaginatorTrait;
use sea_orm::QueryFilter;
use sea_orm::TryGetError;
use sea_orm::{
ActiveModelBehavior, ColumnTrait, DbErr, DeriveEntityModel, DerivePrimaryKey, EnumIter, Linked,
ModelTrait, QueryOrder, RelationDef, RelationTrait, TryFromU64,
};
use sea_orm::{ActiveModelTrait, ConnectionTrait, PrimaryKeyTrait};
use sea_orm::{DeriveActiveEnum, EntityTrait};
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::Display;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::str::FromStr;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "vault_account")]
pub struct Model {
/// SS58 Address of account
#[sea_orm(primary_key, auto_increment = false)]
pub address: DbAccountId,
/// Optional name for the account
#[sea_orm(unique)]
pub name: Option<String>,
/// derivation path - None if for a "base" account that has `crypto_scheme` and `encrypted_suri` set and no `parent`
pub path: Option<String>,
/// Crypto scheme used for the account - Only set for "base" accounts
pub crypto_scheme: Option<DbCryptoScheme>,
/// Encrypted SURI for the account - Only set for "base" accounts
pub encrypted_suri: Option<Vec<u8>>,
/// ForeignKey to parent vault_account SS58 Address - None if for a "base" account
pub parent: Option<DbAccountId>,
}
impl Model {
pub fn is_base_account(&self) -> bool {
self.parent.is_none()
}
#[allow(unused)]
pub fn is_derivation_account(&self) -> bool {
self.parent.is_some()
}
pub fn account_type(&self) -> String {
if self.is_base_account() {
"Base".to_string()
} else {
"Derivation".to_string()
}
}
}
impl Display for Model {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_base_account() {
match self.crypto_scheme {
None => {
unreachable!()
}
Some(DbCryptoScheme::Ed25519) => {
// Also showing G1v1 public key for Ed25519 (base) accounts
write!(
f,
"{}[address:{}, g1v1_pub_key:{}, name:{:?}, crypto_scheme:{:?}]",
self.account_type(),
self.address,
cesium::compute_g1v1_public_key_from_ed25519_account_id(&self.address.0),
self.name,
self.crypto_scheme
)
}
Some(DbCryptoScheme::Sr25519) => {
write!(
f,
"{}[address:{}, name:{:?}, crypto_scheme:{:?}]",
self.account_type(),
self.address,
self.name,
self.crypto_scheme
)
}
}
} else {
fn get_parent_name(parent: &Option<DbAccountId>) -> String {
if let Some(parent) = parent {
format!("Some(\"{parent}\")")
} else {
"None".to_string()
}
}
write!(
f,
"{}[address:{}, name:{:?}, path:{:?}, parent:{}]",
self.account_type(),
self.address,
self.name,
self.path,
get_parent_name(&self.parent)
)
}
}
}
/// Necessary to create a wrapper over AccountId to implement sea-orm traits
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DbAccountId(pub AccountId);
/// All the next methods are necessary to support the proper mapping of the DbAccountId from/to String in DB and
/// to allow using it as primaryKey
impl sea_orm::sea_query::Nullable for DbAccountId {
fn null() -> Value {
Value::String(None)
}
}
impl sea_orm::TryGetable for DbAccountId {
/// Had to really pay attention to return proper TryGetError type when value not present => TryGetError::Null
///
/// as otherwise, when using `Option<DbAccountId>` with a None value it was crashing (when no "parent")
fn try_get_by<I: sea_orm::ColIdx>(res: &QueryResult, idx: I) -> Result<Self, TryGetError> {
let value: String = res
.try_get_by(idx)
.map_err(|e| TryGetError::Null(e.to_string()))?;
Ok(DbAccountId(AccountId::from_str(&value).map_err(|e| {
TryGetError::DbErr(DbErr::Custom(format!(
"Cannot parse DbAccountId for string '{}' - error: {}",
&value, e
)))
})?))
}
}
impl sea_orm::sea_query::ValueType for DbAccountId {
fn try_from(v: Value) -> Result<Self, sea_orm::sea_query::ValueTypeErr> {
match v {
Value::String(Some(value)) => Ok(DbAccountId(
AccountId::from_str(&value).map_err(|_| sea_orm::sea_query::ValueTypeErr)?,
)),
_ => Err(sea_orm::sea_query::ValueTypeErr),
}
}
fn type_name() -> String {
stringify!(DbAccountId).to_owned()
}
fn array_type() -> sea_orm::sea_query::ArrayType {
sea_orm::sea_query::ArrayType::String
}
fn column_type() -> ColumnType {
ColumnType::String(StringLen::None)
}
}
impl From<DbAccountId> for Value {
fn from(account_id: DbAccountId) -> Self {
Value::String(Some(Box::new(account_id.0.to_string())))
}
}
/// sea-orm forces us to implement this one; but since we map from/to string, we can't convert from a u64
impl TryFromU64 for DbAccountId {
fn try_from_u64(_v: u64) -> Result<Self, DbErr> {
Err(DbErr::Custom(
"AccountIdWrapper cannot be created from U64".to_owned(),
))
}
}
impl Display for DbAccountId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for DbAccountId {
type Err = GcliError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(DbAccountId(
AccountId::from_str(s).map_err(|e| GcliError::Input(e.to_string()))?,
))
}
}
impl From<String> for DbAccountId {
fn from(s: String) -> Self {
DbAccountId(AccountId::from_str(&s).expect("Invalid AccountId format"))
}
}
impl From<AccountId> for DbAccountId {
fn from(account_id: AccountId) -> Self {
DbAccountId(account_id)
}
}
impl From<DbAccountId> for AccountId {
fn from(db_account_id: DbAccountId) -> Self {
db_account_id.0
}
}
/// Didn't want to pollute the keys::CryptoScheme enum with sea-orm specific derivations
///
/// created a separate enum for the database with conversions between the two
#[derive(Debug, Copy, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
#[sea_orm(
rs_type = "String",
db_type = "String(StringLen::None)",
rename_all = "PascalCase"
)]
pub enum DbCryptoScheme {
Ed25519,
Sr25519,
}
impl From<crate::keys::CryptoScheme> for DbCryptoScheme {
fn from(scheme: crate::keys::CryptoScheme) -> Self {
match scheme {
crate::keys::CryptoScheme::Ed25519 => DbCryptoScheme::Ed25519,
crate::keys::CryptoScheme::Sr25519 => DbCryptoScheme::Sr25519,
}
}
}
impl From<DbCryptoScheme> for crate::keys::CryptoScheme {
fn from(scheme: DbCryptoScheme) -> Self {
match scheme {
DbCryptoScheme::Ed25519 => crate::keys::CryptoScheme::Ed25519,
DbCryptoScheme::Sr25519 => crate::keys::CryptoScheme::Sr25519,
}
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
ParentAccount,
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::ParentAccount => Entity::belongs_to(Entity)
.from(Column::Parent)
.to(Column::Address)
.into(),
}
}
}
pub struct ParentAccountLink;
impl Linked for ParentAccountLink {
type FromEntity = Entity;
type ToEntity = Entity;
fn link(&self) -> Vec<RelationDef> {
vec![Relation::ParentAccount.def()]
}
}
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
/// This method is called before saving or updating the model to the database.
/// It ensures that the model is valid according to the following constraints:
///
/// - A "base" vault account must have path:None, parent:None, crypto_scheme:Some(_), encrypted_suri:Some(_)
/// - A "derivation" vault account must have path:Some(_), parent:Some(_), crypto_scheme:None, encrypted_suri:None
async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
if insert {
// If one of the elements of a "base" account is seen, all must be correctly filled
if (self.path.is_not_set() || self.path.try_as_ref().unwrap().is_none())
|| (self.parent.is_not_set() || self.parent.try_as_ref().unwrap().is_none())
|| (self.crypto_scheme.is_set()
&& self.crypto_scheme.try_as_ref().unwrap().is_some())
|| (self.encrypted_suri.is_set()
&& self.encrypted_suri.try_as_ref().unwrap().is_some())
{
if !((self.path.is_not_set() || self.path.try_as_ref().unwrap().is_none())
&& (self.parent.is_not_set() || self.parent.try_as_ref().unwrap().is_none())
&& (self.crypto_scheme.is_set()
&& self.crypto_scheme.try_as_ref().unwrap().is_some())
&& (self.encrypted_suri.is_set()
&& self.encrypted_suri.try_as_ref().unwrap().is_some()))
{
return Err(DbErr::Custom(
"A \"base\" vault account must have path:None, parent:None, crypto_scheme:Some(_), encrypted_suri:Some(_)".into(),
));
}
} else if !((self.path.is_set() && self.path.try_as_ref().unwrap().is_some())
&& (self.parent.is_set() && self.parent.try_as_ref().unwrap().is_some())
&& (self.crypto_scheme.is_not_set()
|| self.crypto_scheme.try_as_ref().unwrap().is_none())
&& (self.encrypted_suri.is_not_set()
|| self.encrypted_suri.try_as_ref().unwrap().is_none()))
{
return Err(DbErr::Custom(
"A \"derivation\" vault account must have path:Some(_), parent:Some(_), crypto_scheme:None, encrypted_suri:None".into(),
));
}
} else {
//Updates to accept:
// * Name Only
// * Overriding Base with Base account => only changing encrypted_suri
// * Should also support changing name at the same time
// * Overriding Derivation with Base account => upd Path=>None Parent=>None crypto=>Some enc_suri=>Some
// * Should also support changing name at the same time
// * Overriding Derivation with Derivation account => upd (Path=>Some) (Parent=>Some)
// * Should also support changing name at the same time
// * Overriding Base with Derivation account => upd Path=>Some Parent=>Some crypto=>None enc_suri=>None
// * Should also support changing name at the same time
// If updating all path, parent, crypto_scheme, encrypted_suri
if self.path.is_set()
&& self.parent.is_set()
&& self.crypto_scheme.is_set()
&& self.encrypted_suri.is_set()
{
if self.parent.try_as_ref().unwrap().is_some() {
if !(self.path.try_as_ref().unwrap().is_some()
&& self.crypto_scheme.try_as_ref().unwrap().is_none()
&& self.encrypted_suri.try_as_ref().unwrap().is_none())
{
return Err(DbErr::Custom(
"An update to \"derivation\" vault account must have path:Some(_), parent:Some(_), crypto_scheme:None, encrypted_suri:None".into(),
));
}
} else if !(self.path.try_as_ref().unwrap().is_none()
&& self.crypto_scheme.try_as_ref().unwrap().is_some()
&& self.encrypted_suri.try_as_ref().unwrap().is_some())
{
return Err(DbErr::Custom(
"An update to \"base\" vault account must have path:None, parent:None, crypto_scheme:Some(_), encrypted_suri:Some(_)".into(),
));
}
}
// Else if updating path || parent both needs to have Some(_) value (update of Derivation)
else if (self.path.is_set() || self.parent.is_set())
&& !(self.parent.try_as_ref().unwrap().is_some()
&& self.path.try_as_ref().unwrap().is_some())
{
return Err(DbErr::Custom(
"An update of \"derivation\" parent/path must have both path:Some(_), parent:Some(_)".into(),
));
}
}
Ok(self)
}
}
pub async fn find_by_id<C>(db: &C, address: &DbAccountId) -> Result<Option<Model>, GcliError>
where
C: ConnectionTrait,
{
Entity::find_by_id(address.clone())
.one(db)
.await
.map_err(GcliError::from)
}
pub async fn find_by_name<C>(db: &C, name: &str) -> Result<Option<Model>, GcliError>
where
C: ConnectionTrait,
{
Entity::find()
.filter(Column::Name.eq(Some(name.to_string())))
.one(db)
.await
.map_err(GcliError::from)
}
pub async fn find_base_accounts<C>(db: &C) -> Result<Vec<Model>, GcliError>
where
C: ConnectionTrait,
{
Entity::find()
.filter(Column::Path.is_null())
.order_by_asc(Column::Address)
.all(db)
.await
.map_err(GcliError::from)
}
/// Represents a node in the hierarchy of accounts
pub struct AccountTreeNode {
pub account: Model,
pub children: Vec<Rc<RefCell<AccountTreeNode>>>,
pub parent: Option<Rc<RefCell<AccountTreeNode>>>,
}
/// Counts the depth of an `AccountTreeNode` in the hierarchy.
pub fn count_depth_account_tree_node(account_tree_node: &Rc<RefCell<AccountTreeNode>>) -> usize {
let mut depth = 0;
let mut current_node = Rc::clone(account_tree_node);
while let Some(parent_node) = {
let borrowed_node = current_node.borrow();
borrowed_node.parent.as_ref().map(Rc::clone)
} {
depth += 1;
current_node = parent_node;
}
depth
}
/// Counts number of accounts in an `AccountTreeNode` hierarchy, starting from account_tree_node and only visiting children.
pub fn count_accounts_in_account_tree_node_and_children(
node: &Rc<RefCell<AccountTreeNode>>,
) -> usize {
let borrowed_node = node.borrow();
let mut count = 1; // Count the current node
for child in &borrowed_node.children {
count += count_accounts_in_account_tree_node_and_children(child);
}
count
}
/// Gets the base account tree node of the `AccountTreeNode` hierarchy.
pub fn get_base_account_tree_node(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
) -> Rc<RefCell<AccountTreeNode>> {
//Move up to the base node
let mut base_node = Rc::clone(account_tree_node);
while let Some(parent_node) = {
let borrowed_node = base_node.borrow();
borrowed_node.parent.as_ref().map(Rc::clone)
} {
base_node = parent_node;
}
Rc::clone(&base_node)
}
/// Gets the account tree node for given address from the `AccountTreeNode` hierarchy.
pub fn get_account_tree_node_for_address(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
address: &str,
) -> Rc<RefCell<AccountTreeNode>> {
fn find_address_recursive(
node: &Rc<RefCell<AccountTreeNode>>,
address: &str,
) -> Option<Rc<RefCell<AccountTreeNode>>> {
let borrowed_node = node.borrow();
if borrowed_node.account.address.to_string() == address {
return Some(Rc::clone(node));
}
for child in &borrowed_node.children {
if let Some(found) = find_address_recursive(child, address) {
return Some(found);
}
}
None
}
//Move up to the base node
let base_account_tree_node = get_base_account_tree_node(account_tree_node);
let account_tree_node_for_address = find_address_recursive(&base_account_tree_node, address)
.unwrap_or_else(|| {
panic!(
"Could not find account with address:{} in the hierarchy",
address
)
});
Rc::clone(&account_tree_node_for_address)
}
/// Returns a new (limited) `AccountTreeNode` hierarchy including the selected account_tree_node and all its parents.
///
/// The base of the new hierarchy will be returned
pub fn get_base_parent_hierarchy_account_tree_node(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
) -> Rc<RefCell<AccountTreeNode>> {
// Clone the current node to start the new hierarchy
let new_node = Rc::new(RefCell::new(AccountTreeNode {
account: account_tree_node.borrow().account.clone(),
children: Vec::new(),
parent: None,
}));
// Traverse up to the base node, creating new nodes for each parent
let mut current_new_node = Rc::clone(&new_node);
let mut current_node = Rc::clone(account_tree_node);
while let Some(parent_node) = {
let borrowed_node = current_node.borrow();
borrowed_node.parent.as_ref().map(Rc::clone)
} {
let new_parent_node = Rc::new(RefCell::new(AccountTreeNode {
account: parent_node.borrow().account.clone(),
children: vec![Rc::clone(&current_new_node)],
parent: None,
}));
current_new_node.borrow_mut().parent = Some(Rc::clone(&new_parent_node));
current_new_node = new_parent_node;
current_node = parent_node;
}
// Return the base of the new hierarchy
current_new_node
}
/// Returns a vec of all the accounts starting from `account_tree_node` and all its children; depth first
///
/// Can be used to delete all the accounts in the hierarchy in the proper order
pub fn extract_accounts_depth_first_from_account_tree_node(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
) -> Result<Vec<Model>, GcliError> {
fn retrieve_recursive_depth_first(
node: &Rc<RefCell<AccountTreeNode>>,
accounts: &mut Vec<Model>,
) -> Result<(), GcliError> {
let borrowed_node = node.borrow();
for child in &borrowed_node.children {
retrieve_recursive_depth_first(child, accounts)?;
}
accounts.push(borrowed_node.account.clone());
Ok(())
}
let mut accounts = Vec::new();
retrieve_recursive_depth_first(account_tree_node, &mut accounts)?;
Ok(accounts)
}
/// Computes the name to reference the `AccountTreeNode` in the hierarchy if we can find/compute one.
///
/// Returns `None` otherwise.
pub fn compute_name_account_tree_node(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
) -> Option<String> {
let mut name = String::new();
let mut current_node = Rc::clone(account_tree_node);
while let Some(parent_node) = {
let borrowed_node = current_node.borrow();
if let Some(account_name) = &borrowed_node.account.name {
name.insert_str(0, account_name);
return Some(name);
} else if let Some(account_path) = &borrowed_node.account.path {
name.insert_str(0, account_path);
} else {
return None;
}
borrowed_node.parent.as_ref().map(Rc::clone)
} {
current_node = parent_node;
}
Some(name)
}
/// Computes a map of names to reference of `AccountTreeNodes` in the hierarchy of its children.
pub fn compute_name_map_for_account_tree_node(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
) -> Result<HashMap<String, Rc<RefCell<AccountTreeNode>>>, GcliError> {
let mut names_to_ref_map = HashMap::<String, Rc<RefCell<AccountTreeNode>>>::new();
fn compute_recursive_name_map(
node: &Rc<RefCell<AccountTreeNode>>,
current_name: Option<String>,
names_to_ref_map: &mut HashMap<String, Rc<RefCell<AccountTreeNode>>>,
) -> Result<(), GcliError> {
let borrowed_node = node.borrow();
let current_name = match &borrowed_node.account.name {
Some(name) => Some(name.clone()),
None => match &borrowed_node.account.path {
Some(path) => current_name
.as_ref()
.map(|name| format!("{}{}", name, path)),
None => None,
},
};
if let Some(name) = &current_name {
names_to_ref_map.insert(name.clone(), Rc::clone(node));
}
for child in &borrowed_node.children {
compute_recursive_name_map(child, current_name.clone(), names_to_ref_map)?;
}
Ok(())
}
compute_recursive_name_map(account_tree_node, None, &mut names_to_ref_map)?;
Ok(names_to_ref_map)
}
/// Computes the SURI of the `AccountTreeNode` in the hierarchy; using the password to decrypt the encrypted SURI of Base account.
pub fn compute_suri_account_tree_node(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
password: String,
) -> Result<String, GcliError> {
let mut suri = String::new();
let mut current_node = Rc::clone(account_tree_node);
while let Some(parent_node) = {
let borrowed_node = current_node.borrow();
if let Some(account_path) = &borrowed_node.account.path {
suri.insert_str(0, account_path);
} else if let Some(encrypted_suri) = &borrowed_node.account.encrypted_suri {
let decrypted_suri = vault::decrypt(encrypted_suri, password.clone())
.map_err(|e| GcliError::Input(e.to_string()))?;
let secret_suri = String::from_utf8(decrypted_suri).map_err(|e| anyhow!(e))?;
suri.insert_str(0, &secret_suri);
} else {
return Err(GcliError::Input("No encrypted SURI found".to_string()));
}
borrowed_node.parent.as_ref().map(Rc::clone)
} {
current_node = parent_node;
}
Ok(suri)
}
/// Fetches all the `base` account tree nodes with their hierarchies
pub async fn fetch_all_base_account_tree_node_hierarchies<C>(
db: &C,
) -> Result<Vec<Rc<RefCell<AccountTreeNode>>>, GcliError>
where
C: ConnectionTrait,
{
let base_accounts = find_base_accounts(db).await?;
let mut account_tree_nodes = Vec::new();
for base_account in base_accounts {
let base_account_tree_node =
fetch_children_account_tree_nodes_boxed(db, base_account, None).await?;
account_tree_nodes.push(base_account_tree_node);
}
Ok(account_tree_nodes)
}
/// Only returns the `base` accounts without their children
///
/// To be used in compute_vault_accounts_table to display only the base accounts
pub async fn fetch_only_base_account_tree_nodes<C>(
db: &C,
) -> Result<Vec<Rc<RefCell<AccountTreeNode>>>, GcliError>
where
C: ConnectionTrait,
{
let base_accounts = find_base_accounts(db).await?;
let mut account_tree_nodes = Vec::new();
for base_account in base_accounts {
let current_node = Rc::new(RefCell::new(AccountTreeNode {
account: base_account.clone(),
children: Vec::new(),
parent: None,
}));
account_tree_nodes.push(current_node);
}
Ok(account_tree_nodes)
}
/// Fetches the `base` account tree node hierarchy for the given address
///
/// This one unwraps the Option and gives a proper error message in case of None
pub async fn fetch_base_account_tree_node_hierarchy_unwrapped<C>(
db: &C,
address: &str,
) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError>
where
C: ConnectionTrait,
{
fetch_base_account_tree_node_hierarchy(db, address)
.await?
.ok_or(GcliError::Input(format!(
"Could not compute tree of accounts for address:'{}'",
address
)))
}
/// Fetches the `base` account tree node hierarchy for the given address using db
pub async fn fetch_base_account_tree_node_hierarchy<C>(
db: &C,
address: &str,
) -> Result<Option<Rc<RefCell<AccountTreeNode>>>, GcliError>
where
C: ConnectionTrait,
{
if let Some(base_parent_account) = find_base_parent_account(db, address).await? {
let base_account_tree_node =
fetch_children_account_tree_nodes_boxed(db, base_parent_account, None).await?;
Ok(Some(base_account_tree_node))
} else {
Ok(None)
}
}
/// Finds the `base` account in db for the given address
async fn find_base_parent_account<C>(db: &C, address: &str) -> Result<Option<Model>, GcliError>
where
C: ConnectionTrait,
{
let account = find_by_id(
db,
&DbAccountId::from_str(address).expect("invalid address"),
)
.await?;
if account.is_none() {
return Ok(None);
}
let mut base_parent_account = account.unwrap();
while let Some(parent_account) = base_parent_account
.find_linked(ParentAccountLink)
.one(db)
.await
.map_err(GcliError::from)?
{
base_parent_account = parent_account;
}
Ok(Some(base_parent_account))
}
/// Finds direct children of given account.
///
/// Sorts according to the Path as it makes the most sense when viewing derivations of one account.
async fn find_direct_children_accounts<C>(
db: &C,
current_account: &Model,
) -> Result<Vec<Model>, GcliError>
where
C: ConnectionTrait,
{
Entity::find()
.filter(Column::Parent.eq(current_account.address.clone()))
.order_by_asc(Column::Path)
.all(db)
.await
.map_err(GcliError::from)
}
/// To make clippy happy... "warning: very complex type used. Consider factoring parts into `type` definitions"
type AccountTreeNodeResult<'c> =
Pin<Box<dyn Future<Output = Result<Rc<RefCell<AccountTreeNode>>, GcliError>> + 'c>>;
/// This one seems necessary in order to handle async + recursion issue
///
/// Was suggested by AI and seems to work (might be improved)
fn fetch_children_account_tree_nodes_boxed<'c, C>(
db: &'c C,
current_account: Model,
parent_node: Option<Rc<RefCell<AccountTreeNode>>>,
) -> AccountTreeNodeResult<'c>
where
C: ConnectionTrait + 'c,
{
Box::pin(fetch_children_account_tree_nodes(
db,
current_account,
parent_node,
))
}
/// Fetches the children account tree nodes for the given account and parent node
async fn fetch_children_account_tree_nodes<C>(
db: &C,
current_account: Model,
parent_node: Option<Rc<RefCell<AccountTreeNode>>>,
) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError>
where
C: ConnectionTrait,
{
let children_accounts = find_direct_children_accounts(db, &current_account).await?;
let current_node = Rc::new(RefCell::new(AccountTreeNode {
account: current_account.clone(),
children: Vec::new(),
parent: parent_node,
}));
let mut children = Vec::new();
for child_account in children_accounts {
let child_node = fetch_children_account_tree_nodes_boxed(
db,
child_account,
Some(Rc::clone(&current_node)),
)
.await?;
children.push(child_node);
}
current_node.borrow_mut().children = children;
Ok(current_node)
}
pub async fn check_name_available<C>(
db: &C,
old_name: Option<&String>,
new_name: Option<&String>,
) -> Result<bool, GcliError>
where
C: ConnectionTrait,
{
if old_name == new_name {
return Ok(true);
}
if let Some(new_name) = new_name {
let name_usage_count = Entity::find()
.filter(Column::Name.eq(Some(new_name.clone())))
.count(db)
.await?;
Ok(name_usage_count == 0)
} else {
Ok(true)
}
}
pub async fn update_account<C>(db: &C, vault_account: ActiveModel) -> Result<Model, GcliError>
where
C: ConnectionTrait,
{
Ok(vault_account.update(db).await?)
}
/// Creates a `base` vault account and returns it
///
/// Typically used for `vault import|migrate` commands
pub async fn create_base_account<C>(
db: &C,
address: &str,
name: Option<&String>,
crypto_scheme: crate::keys::CryptoScheme,
encrypted_suri: Vec<u8>,
) -> Result<Model, GcliError>
where
C: ConnectionTrait,
{
let account_id = DbAccountId::from_str(address)?;
let existing_vault_account = Entity::find_by_id(account_id.clone()).one(db).await?;
Ok(match existing_vault_account {
Some(existing_vault_account) => {
// To be safe
return Err(GcliError::Input(format!(
"Already existing vault account {existing_vault_account}"
)));
}
None => {
let vault_account = ActiveModel {
address: Set(account_id),
name: Set(name.cloned()),
path: Set(None),
crypto_scheme: Set(Some(crypto_scheme.into())),
encrypted_suri: Set(Some(encrypted_suri)),
parent: Default::default(),
};
vault_account.insert(db).await?
}
})
}
/// Creates a `derivation` vault account and returns it
///
/// Typically used for `vault derive` command
pub async fn create_derivation_account<C>(
db: &C,
address: &str,
name: Option<&String>,
derivation_path: &str,
parent_address: &str,
) -> Result<Model, GcliError>
where
C: ConnectionTrait,
{
let account_id = DbAccountId::from_str(address)?;
let vault_account = Entity::find_by_id(account_id.clone()).one(db).await?;
Ok(match vault_account {
Some(vault_account) => {
// To be safe
return Err(GcliError::Input(format!(
"Already existing vault account {vault_account}"
)));
}
None => {
let vault_account = ActiveModel {
address: Set(address.to_string().into()),
name: Set(name.cloned()),
path: Set(Some(derivation_path.to_string())),
crypto_scheme: Set(None),
encrypted_suri: Set(None),
parent: Set(Some(parent_address.to_string().into())),
};
vault_account.insert(db).await?
}
})
}
pub async fn update_account_name<C>(
db: &C,
account: Model,
new_name: Option<&String>,
) -> Result<Model, GcliError>
where
C: ConnectionTrait,
{
let old_name = account.name.clone();
let mut account: ActiveModel = account.into();
account.name = Set(new_name.cloned());
let account = account.update(db).await?;
println!(
"Renamed address:'{}' from {:?} to {:?}",
&account.address, old_name, new_name
);
Ok(account)
}
// Unit tests
#[cfg(test)]
pub mod tests {
use super::*;
pub mod account_tree_node_tests {
use super::*;
use crate::commands::vault;
use crate::keys;
use crate::keys::SUBSTRATE_MNEMONIC;
pub fn mother_account_tree_node() -> Rc<RefCell<AccountTreeNode>> {
let mother_address =
DbAccountId::from_str("5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV").unwrap();
let child1_address =
DbAccountId::from_str("5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH").unwrap();
let grandchild1_address =
DbAccountId::from_str("5Fh5PLQNt1xuEXm71dfDtQdnwceSew4oHewWBLsWAkKspV7d").unwrap();
let child2_address =
DbAccountId::from_str("5GBNeWRhZc2jXu7D55rBimKYDk8PGk8itRYFTPfC8RJLKG5o").unwrap();
let grandchild2_address =
DbAccountId::from_str("5CvdJuB9HLXSi5FS9LW57cyHF13iCv5HDimo2C45KxnxriCT").unwrap();
let grandchild1 = Rc::new(RefCell::new(AccountTreeNode {
account: Model {
address: grandchild1_address.clone(),
name: Some("Grandchild 1".to_string()),
path: Some("//0".to_string()),
crypto_scheme: None,
encrypted_suri: None,
parent: Some(child1_address.clone()),
},
children: vec![],
parent: None,
}));
let grandchild2 = Rc::new(RefCell::new(AccountTreeNode {
account: Model {
address: grandchild2_address.clone(),
// name: Some("Grandchild 2".to_string()),
name: None,
path: Some("//1".to_string()),
crypto_scheme: None,
encrypted_suri: None,
parent: Some(child2_address.clone()),
},
children: vec![],
parent: None,
}));
let child1 = Rc::new(RefCell::new(AccountTreeNode {
account: Model {
address: child1_address.clone(),
name: Some("Child 1".to_string()),
path: Some("//0".to_string()),
crypto_scheme: None,
encrypted_suri: None,
parent: Some(mother_address.clone()),
},
children: vec![grandchild1.clone()],
parent: None,
}));
let child2 = Rc::new(RefCell::new(AccountTreeNode {
account: Model {
address: child2_address.clone(),
// name: Some("Child 2".to_string()),
name: None,
path: Some("//1".to_string()),
crypto_scheme: None,
encrypted_suri: None,
parent: Some(mother_address.clone()),
},
children: vec![grandchild2.clone()],
parent: None,
}));
let mother = Rc::new(RefCell::new(AccountTreeNode {
account: Model {
address: mother_address.clone(),
name: Some("Mother".to_string()),
path: None,
crypto_scheme: Some(DbCryptoScheme::Sr25519),
encrypted_suri: Some(
vault::encrypt(SUBSTRATE_MNEMONIC.as_bytes(), "".to_string()).unwrap(),
),
parent: None,
},
children: vec![child1.clone(), child2.clone()],
parent: None,
}));
// Set parent references
grandchild1.borrow_mut().parent = Some(child1.clone());
grandchild2.borrow_mut().parent = Some(child2.clone());
child1.borrow_mut().parent = Some(mother.clone());
child2.borrow_mut().parent = Some(mother.clone());
mother
}
pub fn mother_g1v1_account_tree_node() -> Rc<RefCell<AccountTreeNode>> {
let mother_address =
DbAccountId::from_str("5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4").unwrap();
let cesium_id = "test_cesium_id".to_string();
let cesium_pwd = "test_cesium_pwd".to_string();
let seed = keys::seed_from_cesium(&cesium_id, &cesium_pwd);
let secret_suri = format!("0x{}", hex::encode(seed));
let mother_g1v1 = Rc::new(RefCell::new(AccountTreeNode {
account: Model {
address: mother_address.clone(),
name: Some("MotherG1v1".to_string()),
path: None,
crypto_scheme: Some(DbCryptoScheme::Ed25519),
encrypted_suri: Some(
vault::encrypt(secret_suri.as_bytes(), "".to_string()).unwrap(),
),
parent: None,
},
children: vec![],
parent: None,
}));
mother_g1v1
}
#[test]
fn test_count_depth_account_tree_node() {
let mother = mother_account_tree_node();
assert_eq!(count_depth_account_tree_node(&mother), 0);
let child1 = mother.borrow().children[0].clone();
assert_eq!(count_depth_account_tree_node(&child1), 1);
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(count_depth_account_tree_node(&grandchild1), 2);
}
#[test]
fn test_count_accounts_in_account_tree_node_and_children() {
let mother = mother_account_tree_node();
assert_eq!(count_accounts_in_account_tree_node_and_children(&mother), 5);
let child1 = mother.borrow().children[0].clone();
assert_eq!(count_accounts_in_account_tree_node_and_children(&child1), 2);
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(
count_accounts_in_account_tree_node_and_children(&grandchild1),
1
);
}
#[test]
fn test_get_base_parent_hierarchy_account_tree_node() {
let mother = mother_account_tree_node();
let child1 = mother.borrow().children[0].clone();
let new_mother = get_base_parent_hierarchy_account_tree_node(&child1);
// Check if the base of the new hierarchy is the mother node
assert_eq!(
new_mother.borrow().account.address.to_string(),
mother.borrow().account.address.to_string()
);
assert_eq!(new_mother.borrow().children.len(), 1);
// Check if the child1 node is correctly linked in the new hierarchy
let new_child1 = new_mother.borrow().children[0].clone();
assert_eq!(
new_child1.borrow().account.address.to_string(),
child1.borrow().account.address.to_string()
);
assert_eq!(new_child1.borrow().children.len(), 0);
// Check if the parent references are correctly set
assert!(new_mother.borrow().parent.is_none());
assert_eq!(
new_child1
.borrow()
.parent
.as_ref()
.unwrap()
.borrow()
.account
.address
.to_string(),
new_mother.borrow().account.address.to_string()
);
}
#[test]
fn test_retrieve_accounts_depth_first_from_account_tree_node() {
let mother = mother_account_tree_node();
let accounts = extract_accounts_depth_first_from_account_tree_node(&mother).unwrap();
assert_eq!(accounts.len(), 5);
assert_eq!(
accounts[0].address.to_string(),
mother.borrow().children[0].borrow().children[0]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
accounts[1].address.to_string(),
mother.borrow().children[0]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
accounts[2].address.to_string(),
mother.borrow().children[1].borrow().children[0]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
accounts[3].address.to_string(),
mother.borrow().children[1]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
accounts[4].address.to_string(),
mother.borrow().account.address.to_string()
);
let child1 = mother.borrow().children[0].clone();
let accounts = extract_accounts_depth_first_from_account_tree_node(&child1).unwrap();
assert_eq!(accounts.len(), 2);
assert_eq!(
accounts[0].address.to_string(),
mother.borrow().children[0].borrow().children[0]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
accounts[1].address.to_string(),
mother.borrow().children[0]
.borrow()
.account
.address
.to_string()
);
let grandchild1 = child1.borrow().children[0].clone();
let accounts =
extract_accounts_depth_first_from_account_tree_node(&grandchild1).unwrap();
assert_eq!(accounts.len(), 1);
assert_eq!(
accounts[0].address.to_string(),
mother.borrow().children[0].borrow().children[0]
.borrow()
.account
.address
.to_string()
);
}
#[test]
fn test_compute_name_account_tree_node() {
let mother = mother_account_tree_node();
assert_eq!(
compute_name_account_tree_node(&mother),
Some("Mother".to_string())
);
let child1 = mother.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&child1),
Some("Child 1".to_string())
);
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&grandchild1),
Some("Grandchild 1".to_string())
);
let child2 = mother.borrow().children[1].clone();
assert_eq!(
compute_name_account_tree_node(&child2),
Some("Mother//1".to_string())
);
let grandchild2 = child2.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&grandchild2),
Some("Mother//1//1".to_string())
);
}
#[test]
fn test_compute_name_account_tree_node_mother_without_name() {
let mother = mother_account_tree_node();
mother.borrow_mut().account.name = None;
assert_eq!(compute_name_account_tree_node(&mother), None);
let child1 = mother.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&child1),
Some("Child 1".to_string())
);
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&grandchild1),
Some("Grandchild 1".to_string())
);
let child2 = mother.borrow().children[1].clone();
assert_eq!(compute_name_account_tree_node(&child2), None);
let grandchild2 = child2.borrow().children[0].clone();
assert_eq!(compute_name_account_tree_node(&grandchild2), None);
}
#[test]
fn test_compute_name_account_tree_node_grandchild1_without_name() {
let mother = mother_account_tree_node();
mother.borrow().children[0].borrow().children[0]
.borrow_mut()
.account
.name = None;
assert_eq!(
compute_name_account_tree_node(&mother),
Some("Mother".to_string())
);
let child1 = mother.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&child1),
Some("Child 1".to_string())
);
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(
compute_name_account_tree_node(&grandchild1),
Some("Child 1//0".to_string())
);
}
#[test]
fn test_compute_name_map_for_account_tree_node() {
let mother = mother_account_tree_node();
let name_map = compute_name_map_for_account_tree_node(&mother).unwrap();
assert_eq!(name_map.len(), 5);
assert_eq!(
name_map
.get("Mother")
.unwrap()
.borrow()
.account
.address
.to_string(),
mother.borrow().account.address.to_string()
);
assert_eq!(
name_map
.get("Child 1")
.unwrap()
.borrow()
.account
.address
.to_string(),
mother.borrow().children[0]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
name_map
.get("Grandchild 1")
.unwrap()
.borrow()
.account
.address
.to_string(),
mother.borrow().children[0].borrow().children[0]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
name_map
.get("Mother//1")
.unwrap()
.borrow()
.account
.address
.to_string(),
mother.borrow().children[1]
.borrow()
.account
.address
.to_string()
);
assert_eq!(
name_map
.get("Mother//1//1")
.unwrap()
.borrow()
.account
.address
.to_string(),
mother.borrow().children[1].borrow().children[0]
.borrow()
.account
.address
.to_string()
);
}
#[test]
fn test_get_base_account_tree_node() {
let mother = mother_account_tree_node();
let child1 = mother.borrow().children[0].clone();
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(
get_base_account_tree_node(&grandchild1)
.borrow()
.account
.address
.to_string(),
mother.borrow().account.address.to_string()
);
}
#[test]
fn test_get_account_tree_node_for_address() {
let mother = mother_account_tree_node();
let grandchild1 = mother.borrow().children[0].borrow().children[0].clone();
let grandchild1_address = &grandchild1.borrow().account.address.to_string();
assert_eq!(
get_account_tree_node_for_address(&mother, grandchild1_address)
.borrow()
.account
.address
.to_string(),
grandchild1_address.to_string()
);
}
#[test]
fn test_compute_suri_account_tree_node() {
let mother = mother_account_tree_node();
let password = "".to_string();
assert_eq!(
compute_suri_account_tree_node(&mother, password.clone()).unwrap(),
SUBSTRATE_MNEMONIC
);
let child1 = mother.borrow().children[0].clone();
assert_eq!(
compute_suri_account_tree_node(&child1, password.clone()).unwrap(),
SUBSTRATE_MNEMONIC.to_string() + "//0"
);
let grandchild1 = child1.borrow().children[0].clone();
assert_eq!(
compute_suri_account_tree_node(&grandchild1, password.clone()).unwrap(),
SUBSTRATE_MNEMONIC.to_string() + "//0//0"
);
let child2 = mother.borrow().children[1].clone();
assert_eq!(
compute_suri_account_tree_node(&child2, password.clone()).unwrap(),
SUBSTRATE_MNEMONIC.to_string() + "//1"
);
let grandchild2 = child2.borrow().children[0].clone();
assert_eq!(
compute_suri_account_tree_node(&grandchild2, password.clone()).unwrap(),
SUBSTRATE_MNEMONIC.to_string() + "//1//1"
);
}
}
}
use graphql_client::reqwest::post_graphql; pub mod queries;
use graphql_client::GraphQLQuery;
use crate::*; use crate::*;
use comfy_table::*;
// type used in parameters query use comfy_table::{ContentArrangement, Table};
#[allow(non_camel_case_types)] use graphql_client::reqwest::post_graphql;
type jsonb = serde_json::Value; use graphql_client::GraphQLQuery;
use queries::*;
#[derive(GraphQLQuery)] // use sp_core::Bytes;
#[graphql(
schema_path = "res/indexer-schema.json",
query_path = "res/indexer-queries.graphql"
)]
pub struct IdentityNameByPubkey;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "res/indexer-schema.json",
query_path = "res/indexer-queries.graphql"
)]
pub struct IdentityPubkeyByName;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "res/indexer-schema.json",
query_path = "res/indexer-queries.graphql"
)]
pub struct LatestBlock;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "res/indexer-schema.json",
query_path = "res/indexer-queries.graphql"
)]
pub struct GenesisHash;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Indexer { pub struct Indexer {
...@@ -42,113 +15,235 @@ pub struct Indexer { ...@@ -42,113 +15,235 @@ pub struct Indexer {
} }
impl Indexer { impl Indexer {
pub async fn username_by_pubkey(&self, pubkey: &str) -> anyhow::Result<Option<String>> { /// graphql query without error management
Ok(post_graphql::<IdentityNameByPubkey, _>( async fn query<T: GraphQLQuery>(
&self.gql_client, &self,
&self.gql_url, var: <T as GraphQLQuery>::Variables,
identity_name_by_pubkey::Variables { ) -> <T as GraphQLQuery>::ResponseData {
let response = post_graphql::<T, _>(&self.gql_client, &self.gql_url, var)
.await
.expect("indexer connexion error");
if let Some(errs) = response.errors {
log::debug!("{:?}", errs)
}
response.data.expect("indexer error")
}
/// index → name
pub async fn username_by_index(&self, index: u32) -> Option<String> {
self.query::<IdentityNameByIndex>(identity_name_by_index::Variables {
index: index.into(),
})
.await
.identity
.pop()
.map(|idty| idty.name)
}
/// index → name (multiple)
pub async fn names_by_indexes(&self, indexes: &[IdtyId]) -> Vec<(IdtyId, String)> {
self.query::<NamesByIndexes>(names_by_indexes::Variables {
indexes: indexes.iter().map(|i| *i as i64).collect(),
})
.await
.identity
.into_iter()
.map(|idty| (idty.index as IdtyId, idty.name))
.collect()
}
/// pubkey → name
pub async fn username_by_pubkey(&self, pubkey: &str) -> Option<String> {
self.query::<IdentityNameByPubkey>(identity_name_by_pubkey::Variables {
pubkey: pubkey.to_string(), pubkey: pubkey.to_string(),
}, })
) .await
.await? .identity
.data .pop()
.and_then(|data| data.identity.into_iter().next().map(|idty| idty.name))) .map(|idty| idty.name)
} }
pub async fn pubkey_by_username(&self, username: &str) -> anyhow::Result<Option<String>> { /// pubkey → was name
Ok(post_graphql::<IdentityPubkeyByName, _>( pub async fn wasname_by_pubkey(&self, pubkey: &str) -> Option<String> {
&self.gql_client, self.query::<WasIdentityNameByPubkey>(was_identity_name_by_pubkey::Variables {
self.gql_url.clone(), pubkey: pubkey.to_string(),
identity_pubkey_by_name::Variables { })
name: username.to_string(), .await
}, .account_by_pk
) .and_then(|mut acc| acc.was_identity.pop())
.await? .map(|idty| idty.identity.unwrap().name)
.data
.and_then(|data| data.identity_by_pk.map(|idty| idty.pubkey)))
} }
/// fetch latest block number /// index → info
pub async fn fetch_latest_block(&self) -> Result<u64, anyhow::Error> { pub async fn identity_info(&self, index: u32) -> Option<identity_info::IdentityInfoIdentity> {
Ok(post_graphql::<LatestBlock, _>( self.query::<IdentityInfo>(identity_info::Variables {
&self.gql_client, index: index.into(),
self.gql_url.clone(), })
latest_block::Variables {}, .await
) .identity
.await? .pop()
.data }
.unwrap() // must have a data field
.parameters /// fetch latest block
.first() pub async fn fetch_latest_block(&self) -> Option<latest_block::LatestBlockBlock> {
.unwrap() // must have one and only one parameter matching request self.query::<LatestBlock>(latest_block::Variables {})
.value .await
.clone() .block
.unwrap() // must have a value field .pop()
.as_u64() }
.unwrap()) // must be a Number of blocks
/// fetch block by number
pub async fn fetch_block_by_number(
&self,
number: BlockNumber,
) -> Option<block_by_number::BlockByNumberBlock> {
self.query::<BlockByNumber>(block_by_number::Variables {
number: number.into(),
})
.await
.block
.pop()
} }
/// fetch genesis hash /// fetch genesis hash
pub async fn fetch_genesis_hash(&self) -> Result<Hash, anyhow::Error> { // since this is always called before any other indexer request, check errors in a more detailed way
Ok(post_graphql::<GenesisHash, _>( pub async fn fetch_genesis_hash(&self) -> Result<Hash, GcliError> {
// try to connect to indexer
let response = post_graphql::<GenesisHash, _>(
&self.gql_client, &self.gql_client,
self.gql_url.clone(), self.gql_url.clone(),
genesis_hash::Variables {}, genesis_hash::Variables {},
) )
.await? .await
.map_err(|_e| {
dbg!(_e); // for more info
GcliError::Indexer(format!("can not connect to indexer {}", &self.gql_url))
})?;
// debug errors if any
response.errors.map_or_else(Vec::new, |e| dbg!(e));
// extract hash
let hash = response
.data .data
.ok_or(GcliError::Indexer("could not reach indexer".to_string()))? .ok_or(GcliError::Indexer(
"no field 'data' when getting genesis hash".to_string(),
))?
.block .block
.first() .first()
.unwrap() // must have one and only one block matching request .ok_or_else(|| GcliError::Indexer("genesis block not yet indexed".to_string()))?
.hash .hash
.clone() .clone();
.parse::<Hash>()
.unwrap()) // convert it
Ok(convert_hash_bytea(hash))
}
} }
/// convert indexer bytes into hash
// pub fn convert_hash(hash: Bytes) -> Hash {
// let hash = TryInto::<[u8; 32]>::try_into(hash.as_ref()).unwrap();
// hash.into()
// }
/// convert indexer bytes into hash
pub fn convert_hash_bytea(hash: queries::Bytea) -> Hash {
let hash = TryInto::<[u8; 32]>::try_into(hash.bytes.as_ref()).unwrap();
hash.into()
} }
/// define indexer subcommands /// define indexer subcommands
#[derive(Clone, Default, Debug, clap::Parser)] #[derive(Clone, Default, Debug, clap::Parser)]
pub enum Subcommand { pub enum Subcommand {
#[default] #[default]
/// Check that indexer and node are on the same network (same genesis hash) /// Check that indexer and node are on the same network
/// (genesis hash, latest indexed block...)
Check, Check,
/// Fetch latest indexed block
LatestBlock,
} }
/// handle indexer commands /// handle indexer commands
pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<()> { pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
// build indexer because it is needed for all subcommands // build indexer because it is needed for all subcommands
let mut data = data.build_indexer().await?; let data = data.build_client().await?.build_indexer().await?;
let indexer = data
.indexer
.clone()
.ok_or_else(|| GcliError::Logic("indexer needed for this command".to_string()))?;
// match subcommand // match subcommand
match command { match command {
Subcommand::Check => { Subcommand::Check => {
data = data.build_client().await?; let d_url = &data.cfg.duniter_endpoint;
if data.genesis_hash == data.indexer_genesis_hash { let i_url = &indexer.gql_url;
println!( let d_gen_hash = &data.genesis_hash.to_string();
"{} and {} have the same genesis hash: {}", let i_gen_hash = &data.indexer_genesis_hash.to_string();
data.cfg.duniter_endpoint, let (d_finalized_n, d_finalized_h) =
data.indexer().gql_url, commands::blockchain::fetch_finalized_number_and_hash(&data).await?;
data.genesis_hash let i_finalized_block = indexer.fetch_block_by_number(d_finalized_n).await;
); let (i_finalized_h, i_finalized_n) = if let Some(block) = i_finalized_block {
(Some(convert_hash_bytea(block.hash)), Some(block.height))
} else { } else {
println!( (None, None)
"⚠️ {} ({}) and {} ({}) do not share same genesis", };
data.cfg.duniter_endpoint,
data.genesis_hash, let (d_latest_n, d_latest_h) =
data.indexer().gql_url, commands::blockchain::fetch_latest_number_and_hash(&data).await?;
data.indexer_genesis_hash let i_latest_block = indexer.fetch_latest_block().await.expect("no latest block");
); let i_latest_h = convert_hash_bytea(i_latest_block.hash);
} let i_latest_n = i_latest_block.height;
}
Subcommand::LatestBlock => { fn color(x: bool) -> Color {
println!( match x {
"latest block indexed by {} is: {}", true => Color::Green,
data.cfg.indexer_endpoint, false => Color::Red,
data.indexer().fetch_latest_block().await? }
); }
let mut table = Table::new();
table
.load_preset(presets::UTF8_FULL)
.apply_modifier(modifiers::UTF8_ROUND_CORNERS)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_width(120)
.set_header(vec!["Variable", "Duniter", "Indexer"])
.add_row(vec!["URL", d_url, i_url])
.add_row(vec![
Cell::new("genesis hash"),
Cell::new(d_gen_hash),
Cell::new(i_gen_hash).fg(color(d_gen_hash == i_gen_hash)),
])
.add_row(vec![
Cell::new("finalized block number"),
Cell::new(d_finalized_n),
match i_finalized_n {
None => Cell::new("not indexed").fg(Color::Yellow),
Some(n) => {
// if it exists, it must be the same
assert_eq!(n, d_finalized_n as i64);
Cell::new("")
}
},
])
.add_row(vec![
Cell::new("finalized block hash"),
Cell::new(d_finalized_h),
match i_finalized_h {
// number already tells it is not indexed
None => Cell::new(""),
Some(h) => Cell::new(h).fg(color(h == d_finalized_h)),
},
])
.add_row(vec![
Cell::new("latest block number"),
Cell::new(d_latest_n),
Cell::new(i_latest_n).fg(color(i_latest_n == d_latest_n as i64)),
])
.add_row(vec![
Cell::new("latest block hash"),
Cell::new(d_latest_h),
Cell::new(i_latest_h).fg(color(i_latest_h == d_latest_h)),
]);
println!("{table}");
} }
}; };
......
use graphql_client::GraphQLQuery;
use serde::{Deserialize, Deserializer};
// implementation of byte array
#[derive(Debug, Clone)]
pub struct Bytea {
pub bytes: Vec<u8>,
}
// Hasura uses lowercase type name
#[allow(non_camel_case_types)]
type bytea = Bytea;
// implement deserializing \\x prefixed hexadecimal
impl<'de> Deserialize<'de> for Bytea {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// Deserialize as a string
let hex_string = String::deserialize(deserializer)?;
// Parse the hexadecimal string into a byte vector
let bytes = hex::decode(&hex_string[2..]).map_err(serde::de::Error::custom)?;
Ok(Bytea { bytes })
}
}
// generate code for given graphql query
macro_rules! graphql_query {
($name:ident) => {
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "res/indexer-schema.json",
query_path = "res/indexer-queries.graphql"
)]
pub struct $name;
};
}
// repeat generation for multiple queries
macro_rules! graphql_query_for {
( $($Name:ident),+ ) => {
$( graphql_query!($Name); )+
};
}
// generate code for all queries in indexer-queries.graphql
graphql_query_for!(
IdentityNameByIndex,
IdentityInfo,
IdentityNameByPubkey,
WasIdentityNameByPubkey,
LatestBlock,
BlockByNumber,
GenesisHash,
NamesByIndexes
);
use crate::commands::vault;
use crate::entities::vault_account;
use crate::utils::GcliError;
use inquire::validator::{ErrorMessage, Validation};
use sea_orm::ConnectionTrait;
pub fn prompt_password() -> Result<String, GcliError> {
prompt_password_query("Password")
}
pub fn prompt_password_query(query: impl ToString) -> Result<String, GcliError> {
inquire::Password::new(query.to_string().as_str())
.without_confirmation()
.prompt()
.map_err(|e| GcliError::Input(e.to_string()))
}
pub fn prompt_seed() -> Result<String, GcliError> {
inquire::Password::new("Seed:")
.without_confirmation()
.with_validator(|input: &str| {
if input.chars().any(|c| !c.is_ascii_hexdigit()) {
Ok(Validation::Invalid(
"Seed value must only contain valid hexadecimal characters [0-9a-fA-F]".into(),
))
} else if input.len() < 64 || input.len() > 64 {
Ok(Validation::Invalid(
"Seed value must be 32 bytes in hexadecimal format (64 characters long)".into(),
))
} else {
Ok(Validation::Valid)
}
})
.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
pub async fn prompt_vault_name_and_check_availability<C>(
db: &C,
initial_name: Option<&String>,
) -> Result<Option<String>, GcliError>
where
C: ConnectionTrait,
{
loop {
let mut text_inquire = inquire::Text::new("Name:").with_validator(|input: &str| {
match validate_vault_name(input) {
Ok(_) => Ok(Validation::Valid),
Err(error) => {
if let GcliError::Input(message) = error {
Ok(Validation::Invalid(ErrorMessage::from(message)))
} else {
Ok(Validation::Invalid("Unknown error".into()))
}
}
}
});
if let Some(initial_name) = initial_name {
text_inquire = text_inquire.with_initial_value(initial_name);
}
let name = text_inquire
.prompt()
.map_err(|e| GcliError::Input(e.to_string()))?;
let name = trim_and_reduce_empty_as_none(name);
let available =
vault_account::check_name_available(db, initial_name, name.as_ref()).await?;
if available {
return Ok(name);
}
println!(
"Name '{}' is already in use in the vault. Please choose another name.",
name.unwrap()
);
}
}
pub fn trim_and_reduce_empty_as_none(name: String) -> Option<String> {
if name.trim().is_empty() {
None
} else {
Some(name.trim().to_string())
}
}
pub fn validate_vault_name(vault_name: &str) -> Result<(), GcliError> {
if vault_name.contains('<') || vault_name.contains('>') || vault_name.contains('/') {
return Err(GcliError::Input(
"Name cannot contain characters '<', '>', '/'".into(),
));
}
Ok(())
}
/// Prompt for a derivation path
pub fn prompt_vault_derivation_path() -> Result<String, GcliError> {
inquire::Text::new("Derivation path:")
.with_validator(
|input: &str| match validate_derivation_path(input.to_string()) {
Ok(_) => Ok(Validation::Valid),
Err(error) => {
if let GcliError::Input(message) = error {
Ok(Validation::Invalid(ErrorMessage::from(message)))
} else {
Ok(Validation::Invalid("Unknown error".into()))
}
}
},
)
.prompt()
.map_err(|e| GcliError::Input(e.to_string()))
}
pub fn validate_derivation_path(derivation_path: String) -> Result<(), GcliError> {
if !derivation_path.starts_with("/") {
Err(GcliError::Input(
"derivation path needs to start with one or more '/'".into(),
))
} else {
match vault::parse_prefix_and_derivation_path_from_suri(derivation_path.to_string()) {
Ok(_) => Ok(()),
Err(error) => Err(error),
}
}
}
pub fn confirm_action(query: impl ToString) -> Result<bool, GcliError> {
inquire::Confirm::new(query.to_string().as_str())
.prompt()
.map_err(|e| GcliError::Input(e.to_string()))
}
pub fn select_action(query: impl ToString, choices: Vec<&str>) -> Result<&str, GcliError> {
inquire::Select::new(query.to_string().as_str(), choices)
.prompt()
.map_err(|e| GcliError::Input(e.to_string()))
}
use crate::commands::vault;
use crate::*; use crate::*;
use sp_core::crypto::AddressUri;
use sp_core::crypto::Pair as PairTrait;
use sp_core::DeriveJunction;
use sp_core::{ed25519, sr25519};
use clap::builder::OsStr; pub const SUBSTRATE_MNEMONIC: &str =
use std::str::FromStr; "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
// #[derive(Clone, Copy, Debug, Eq, PartialEq)]
// pub enum NeededKeys {
// None,
// Public,
// Secret,
// }
/// secret format
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
pub enum SecretFormat { pub enum SecretFormat {
/// Raw 32B seed /// Raw 32B seed
...@@ -19,6 +18,8 @@ pub enum SecretFormat { ...@@ -19,6 +18,8 @@ pub enum SecretFormat {
Substrate, Substrate,
/// Predefined (Alice, Bob, ...) /// Predefined (Alice, Bob, ...)
Predefined, Predefined,
/// G1v1 id+secret using (scrypt + ed25519)
G1v1,
} }
impl FromStr for SecretFormat { impl FromStr for SecretFormat {
...@@ -29,113 +30,1299 @@ impl FromStr for SecretFormat { ...@@ -29,113 +30,1299 @@ impl FromStr for SecretFormat {
"seed" => Ok(SecretFormat::Seed), "seed" => Ok(SecretFormat::Seed),
"substrate" => Ok(SecretFormat::Substrate), "substrate" => Ok(SecretFormat::Substrate),
"predefined" => Ok(SecretFormat::Predefined), "predefined" => Ok(SecretFormat::Predefined),
"g1v1" => Ok(SecretFormat::G1v1),
//Still support "cesium" input as well for backward compatibility
"cesium" => Ok(SecretFormat::G1v1),
_ => Err(std::io::Error::from(std::io::ErrorKind::InvalidInput)), _ => Err(std::io::Error::from(std::io::ErrorKind::InvalidInput)),
} }
} }
} }
impl From<SecretFormat> for &'static str { impl From<SecretFormat> for &'static str {
fn from(val: SecretFormat) -> &'static str { fn from(val: SecretFormat) -> &'static str {
match val { match val {
SecretFormat::Seed => "seed", SecretFormat::Seed => "seed",
SecretFormat::Substrate => "substrate", SecretFormat::Substrate => "substrate",
SecretFormat::Predefined => "predefined", SecretFormat::Predefined => "predefined",
SecretFormat::G1v1 => "g1v1",
} }
} }
} }
impl From<SecretFormat> for OsStr { impl From<SecretFormat> for OsStr {
fn from(val: SecretFormat) -> OsStr { fn from(val: SecretFormat) -> OsStr {
OsStr::from(Into::<&str>::into(val)) OsStr::from(Into::<&str>::into(val))
} }
} }
/// get keypair from given string secret /// The crypto scheme to use - partial copy from sc_cli::arg_enums::CryptoScheme
pub fn pair_from_str(secret_format: SecretFormat, secret: &str) -> anyhow::Result<Pair> { ///
match secret_format { /// Preferred making a copy since adding the dependency to sc-cli brings more than 300 dependencies
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum CryptoScheme {
/// Use ed25519 - used for SecretFormat::Cesium
Ed25519,
/// Use sr25519.
Sr25519,
}
/// Setting a default to Ed25519
///
/// required when used in Args struct inside main.rs; even though we still have to give a clap "default_value"
impl Default for CryptoScheme {
fn default() -> Self {
CryptoScheme::Ed25519
}
}
impl FromStr for CryptoScheme {
type Err = std::io::Error;
fn from_str(s: &str) -> std::io::Result<Self> {
match s {
"ed25519" => Ok(CryptoScheme::Ed25519),
"sr25519" => Ok(CryptoScheme::Sr25519),
_ => Err(std::io::Error::from(std::io::ErrorKind::InvalidInput)),
}
}
}
impl From<CryptoScheme> for &'static str {
fn from(val: CryptoScheme) -> &'static str {
match val {
CryptoScheme::Ed25519 => "ed25519",
CryptoScheme::Sr25519 => "sr25519",
}
}
}
impl From<CryptoScheme> for OsStr {
fn from(val: CryptoScheme) -> OsStr {
OsStr::from(Into::<&str>::into(val))
}
}
/// wrapper type for keys + signature
//FIXME check if it's ok to keep large enum variant
// Sr25519 second-largest variant contains at least 256 bytes
// Ed25519 largest variant contains at least 480 bytes
//FIXME
// Replace by CryptoType from sp-core ?
// sp_core::crypto::CryptoType (trait)
#[allow(clippy::large_enum_variant)]
pub enum KeyPair {
Sr25519(sr25519::Pair),
Ed25519(ed25519::Pair),
}
impl KeyPair {
pub fn address(&self) -> AccountId {
match self {
KeyPair::Sr25519(keypair) => keypair.public().into(),
KeyPair::Ed25519(keypair) => keypair.public().into(),
}
}
}
// can not derive clone because nacl does not implement it
impl Clone for KeyPair {
fn clone(&self) -> Self {
match self {
KeyPair::Sr25519(keypair) => KeyPair::Sr25519(keypair.clone()),
KeyPair::Ed25519(keypair) => KeyPair::Ed25519(*keypair),
}
}
}
impl From<sr25519::Pair> for KeyPair {
fn from(pair: sr25519::Pair) -> KeyPair {
KeyPair::Sr25519(pair)
}
}
impl From<ed25519::Pair> for KeyPair {
fn from(pair: ed25519::Pair) -> KeyPair {
KeyPair::Ed25519(pair)
}
}
pub enum Signature {
Sr25519(sr25519::Signature),
Ed25519(ed25519::Signature),
}
/// get keypair in any possible way
/// at this point, if secret is predefined, it's not replaced yet
pub fn get_keypair(
secret_format: SecretFormat,
secret: Option<&str>,
crypto_scheme: CryptoScheme,
) -> Result<KeyPair, GcliError> {
match (secret_format, secret) {
(SecretFormat::Predefined, Some(deriv)) => match crypto_scheme {
CryptoScheme::Ed25519 => {
pair_from_ed25519_str(&predefined_suri(deriv)).map(|v| v.into())
}
_ => pair_from_predefined(deriv).map(|v| v.into()),
},
(secret_format, None) => Ok(prompt_secret(secret_format, Some(crypto_scheme))),
(_, Some(secret)) => pair_from_secret_with_scheme(secret_format, secret, crypto_scheme),
}
}
/// 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 => { SecretFormat::Seed => {
let mut seed = [0; 32]; let mut seed = [0; 32];
hex::decode_to_slice(secret, &mut seed).map_err(|_| anyhow!("Invalid secret"))?; hex::decode_to_slice(secret, &mut seed)
let pair = Pair::from_seed(&seed); .map_err(|_| GcliError::Input("Invalid secret".to_string()))?;
Ok(pair) Ok(ed25519::Pair::from_seed(&seed).into())
}
// "predefined" replaces secret before
SecretFormat::Substrate | SecretFormat::Predefined => {
Pair::from_string(secret, None).map_err(|_| anyhow!("Invalid secret"))
} }
SecretFormat::G1v1 => unreachable!(), // Already handled above
},
(_, CryptoScheme::Sr25519) => pair_from_secret(secret_format, secret).map(|v| v.into()),
} }
} }
/// get keypair and address from given secret string /// get keypair from given secret
pub fn addr_and_pair_from_secret( /// if secret is predefined, secret should contain the predefined value
pub fn pair_from_secret(
secret_format: SecretFormat, secret_format: SecretFormat,
secret: &str, secret: &str,
) -> anyhow::Result<(AccountId, Pair)> { ) -> Result<sr25519::Pair, GcliError> {
let pair = pair_from_str(secret_format, secret)?; match secret_format {
let address = pair.public().into(); SecretFormat::Substrate => pair_from_sr25519_str(secret),
Ok((address, pair)) SecretFormat::Predefined => pair_from_sr25519_str(secret), /* if predefined, secret arg is replaced in config */
SecretFormat::Seed => pair_from_sr25519_seed(secret),
SecretFormat::G1v1 => Err(GcliError::Logic(
"G1v1 format incompatible with single secret".to_string(),
)),
}
}
/// get keypair from given string secret
pub fn pair_from_sr25519_str(secret: &str) -> Result<sr25519::Pair, GcliError> {
let _validation_only = vault::parse_prefix_and_derivation_path_from_suri(secret.to_string())?;
sr25519::Pair::from_string(secret, None)
.map_err(|_| GcliError::Input("Invalid secret".to_string()))
}
/// get keypair from given seed
// note: sr25519::Pair::from_string does exactly that when seed is 0x prefixed
// (see from_string_with_seed method in crypto core)
pub fn pair_from_sr25519_seed(secret: &str) -> Result<sr25519::Pair, GcliError> {
let mut seed = [0; 32];
hex::decode_to_slice(secret, &mut seed)
.map_err(|_| GcliError::Input("Invalid secret".to_string()))?;
let pair = sr25519::Pair::from_seed(&seed);
Ok(pair)
} }
/// ask user to input a secret /// get keypair from given ed25519 string secret (used for cesium)
pub fn prompt_secret(secret_format: SecretFormat) -> Pair { pub fn pair_from_ed25519_str(secret: &str) -> Result<ed25519::Pair, GcliError> {
ed25519::Pair::from_string(secret, None)
.map_err(|_| GcliError::Input("Invalid secret".to_string()))
}
/// get keypair from given ed25519 seed (used for cesium)
#[allow(unused)]
pub fn pair_from_ed25519_seed(secret: &str) -> Result<ed25519::Pair, GcliError> {
let mut seed = [0; 32];
hex::decode_to_slice(secret, &mut seed)
.map_err(|_| GcliError::Input("Invalid secret".to_string()))?;
let pair = ed25519::Pair::from_seed(&seed);
Ok(pair)
}
/// Check [compute_pair_and_mini_secret_seed_from_suri] method for details
pub fn compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme(
suri: &str,
crypto_scheme: CryptoScheme,
) -> Result<(KeyPair, [u8; 32]), GcliError> {
match crypto_scheme {
CryptoScheme::Ed25519 => {
let (pair, seed) =
compute_pair_and_mini_secret_seed_from_suri::<sp_core::ed25519::Pair>(suri)?;
Ok((pair.into(), seed))
}
CryptoScheme::Sr25519 => {
let (pair, seed) =
compute_pair_and_mini_secret_seed_from_suri::<sp_core::sr25519::Pair>(suri)?;
Ok((pair.into(), seed))
}
}
}
/// Computes the pair and mini-secret/seed from a Substrate URI for either sr25519 or ed25519.
///
/// # Arguments
/// * `suri` - The Substrate URI (e.g., mnemonic, hex seed, possibly with HARD derivation(s) like "//0")
///
/// # Returns
/// A tuple `(P, [u8; 32])` where `P` is the computed `Pair` and `[u8; 32]` is the mini-secret.
///
/// # Errors
/// Returns `GcliError` if:
/// - The Substrate URI lacks a phrase part
/// - Passwords are present in the derivation
/// - Soft derivation is used (not supported for seed retrieval)
/// - The computed pair's address is different from the one directly retrieved from the `suri` (should not happen)
pub fn compute_pair_and_mini_secret_seed_from_suri<P: PairTrait<Seed = [u8; 32]>>(
suri: &str,
) -> Result<(P, [u8; 32]), GcliError>
where
subxt::utils::AccountId32: std::convert::From<<P as sp_core::Pair>::Public>,
{
let address_uri = AddressUri::parse(suri).map_err(|e| GcliError::Input(e.to_string()))?;
if let Some(pass) = address_uri.pass {
return Err(GcliError::Input(format!(
"Having a password in the derivation path is not supported (password:'{}')",
pass
)));
}
let base_uri = address_uri.phrase.ok_or(GcliError::Input(
"The `suri` need to contain the 'phrase' part".into(),
))?;
let pair_from_suri_for_validation =
P::from_string(suri, None).map_err(|_| GcliError::Input("Invalid secret".to_string()))?;
// Get the <Base> pair and seed
// If using mini-secret / seed
let (base_pair, base_seed) = if let Some(base_uri_seed) = base_uri.strip_prefix("0x") {
let seed_bytes = hex::decode(base_uri_seed)
.map_err(|e| GcliError::Input(format!("Invalid hex seed: {e}")))?;
let seed: [u8; 32] = seed_bytes
.try_into()
.map_err(|_e| GcliError::Input("Incomplete seed".into()))?;
(P::from_seed(&seed), seed)
} else {
// If using mnemonic / passphrase
let (pair, seed_vec) = P::from_phrase(base_uri, None)
.map_err(|e| GcliError::Input(format!("Invalid mnemonic or passphrase: {e}")))?;
let seed: [u8; 32] = seed_vec
.as_slice()
.try_into()
.map_err(|_e| GcliError::Input("Seed should be 32 bytes".into()))?;
(pair, seed)
};
let derivation_paths = address_uri.paths;
// Apply derivation if present
let (result_pair, result_seed) = if !derivation_paths.is_empty() {
// AddressUri paths have one less '/' everywhere, so matching for no '/' at the start to exclude SOFT derivations
if derivation_paths.iter().any(|path| !path.starts_with("/")) {
return Err(GcliError::Input(
"Soft derivation is not supported when trying to retrieve mini-secret/seed"
.to_string(),
));
}
let derive_junctions_from_path = derivation_paths.iter().map(|path| {
if let Some(hard_derivation_no_prefix) = path.strip_prefix("/") {
// AddressUri paths have one less '/' everywhere so this is for HARD derivation
// (and we excluded passwords before; so there won't be any '//')
if let Ok(num) = hard_derivation_no_prefix.parse::<u64>() {
DeriveJunction::hard(num) // Numeric hard derivation
} else {
DeriveJunction::hard(hard_derivation_no_prefix) // String hard derivation
}
} else {
unreachable!("Should not have SOFT derivation detected here");
}
});
let (derived_pair, derived_seed_opt) = base_pair
.derive(derive_junctions_from_path.into_iter(), Some(base_seed))
.map_err(|e| GcliError::Input(format!("Failed to derive key: {e}")))?;
let derived_seed = derived_seed_opt.ok_or(GcliError::Input(format!("Derived seed should be present when base seed is provided (and no soft derivation is used) - base seed was:'0x{}'",hex::encode(base_seed))))?;
let seed_array: [u8; 32] = derived_seed
.as_slice()
.try_into()
.map_err(|_e| GcliError::Input("Derived seed should be 32 bytes".into()))?;
(derived_pair, seed_array)
} else {
(base_pair, base_seed)
};
let result_pair_account_id: AccountId = result_pair.public().into();
let pair_from_suri_account_id: AccountId = pair_from_suri_for_validation.public().into();
if result_pair_account_id != pair_from_suri_account_id {
return Err(GcliError::Input(format!("Computed pair has a different address: '{result_pair_account_id}' != '{pair_from_suri_account_id}'")));
}
Ok((result_pair, result_seed))
}
/// get mnemonic from predefined derivation path
pub fn predefined_suri(deriv: &str) -> String {
format!("{SUBSTRATE_MNEMONIC}//{deriv}")
}
/// get keypair from predefined secret
pub fn pair_from_predefined(deriv: &str) -> Result<sr25519::Pair, GcliError> {
pair_from_sr25519_str(&predefined_suri(deriv))
}
/// get seed from G1v1 id/pwd (old "cesium")
pub fn seed_from_cesium(id: &str, pwd: &str) -> [u8; 32] {
let params = scrypt::Params::new(12u8, 16u32, 1u32, 32).unwrap();
let mut seed = [0u8; 32];
scrypt::scrypt(pwd.as_bytes(), id.as_bytes(), &params, &mut seed).unwrap();
seed
}
/// This method will prompt for the (secret) substrate uri, compute the keypair, and return a tuple containing the (secret) substrate uri and the keypair.
///
/// # Arguments
///
/// * `crypto_scheme` - The cryptographic scheme to use (either Sr25519 or Ed25519).
///
/// # Returns
///
/// A tuple containing:
///
/// * `String` - The (secret) substrate URI provided by the user.
/// * `KeyPair` - The computed keypair based on the provided substrate URI and cryptographic scheme.
pub fn prompt_secret_substrate_and_compute_keypair(
crypto_scheme: CryptoScheme,
) -> (String, KeyPair) {
loop { loop {
match pair_from_str( println!("Substrate URI can be a mnemonic or a mini-secret ('0x' prefixed seed) together with optional derivation path");
secret_format, let substrate_suri = inputs::prompt_password_query("Substrate URI: ").unwrap();
&rpassword::prompt_password(format!("Secret key ({secret_format:?}): ")).unwrap(), match crypto_scheme {
) { CryptoScheme::Sr25519 => match pair_from_sr25519_str(&substrate_suri) {
Ok(pair) => return pair, Ok(pair) => return (substrate_suri, pair.into()),
Err(_) => println!("Invalid secret"), Err(_) => println!("Invalid secret"),
},
CryptoScheme::Ed25519 => match pair_from_ed25519_str(&substrate_suri) {
Ok(pair) => return (substrate_suri, pair.into()),
Err(_) => println!("Invalid secret"),
},
}
}
}
/// ask user pass (Cesium format)
pub fn prompt_secret_cesium() -> ed25519::Pair {
// Only interested in the keypair which is the second element of the tuple
match prompt_secret_cesium_and_compute_keypair(CryptoScheme::Ed25519).1 {
KeyPair::Ed25519(pair) => pair,
_ => panic!("Expected Ed25519 keypair"),
}
}
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));
// G1v1 always uses Ed25519, ignore crypto_scheme
match pair_from_ed25519_str(&secret_suri) {
Ok(pair) => (secret_suri, pair.into()),
Err(_) => panic!("Could not compute KeyPair from G1v1 id/pwd"),
}
}
pub fn prompt_seed_and_compute_keypair(crypto_scheme: CryptoScheme) -> (String, KeyPair) {
loop {
let seed_str = inputs::prompt_seed().unwrap();
let secret_suri = format!("0x{}", seed_str);
match crypto_scheme {
CryptoScheme::Sr25519 => match pair_from_sr25519_str(&secret_suri) {
Ok(pair) => return (secret_suri, pair.into()),
Err(_) => println!("Invalid seed"),
},
CryptoScheme::Ed25519 => match pair_from_ed25519_str(&secret_suri) {
Ok(pair) => return (secret_suri, pair.into()),
Err(_) => println!("Invalid seed"),
},
}
} }
} }
pub fn prompt_predefined_and_compute_keypair(crypto_scheme: CryptoScheme) -> (String, KeyPair) {
let deriv = inputs::prompt_password_query("Enter derivation path: ").unwrap();
let suri = predefined_suri(&deriv);
match crypto_scheme {
CryptoScheme::Sr25519 => match pair_from_sr25519_str(&suri) {
Ok(pair) => (suri, pair.into()),
Err(e) => panic!("Invalid secret: {}", e),
},
CryptoScheme::Ed25519 => match pair_from_ed25519_str(&suri) {
Ok(pair) => (suri, pair.into()),
Err(e) => panic!("Invalid secret: {}", e),
},
} }
}
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);
// /// get keys with interactive prompt if needed match secret_format {
// pub fn get_keys( SecretFormat::Substrate => prompt_secret_substrate_and_compute_keypair(scheme).1,
// secret_format: SecretFormat, SecretFormat::G1v1 => prompt_secret_cesium_and_compute_keypair(CryptoScheme::Ed25519).1, // G1v1 always uses Ed25519
// accout_id: Option<AccountId>, SecretFormat::Seed => prompt_seed_and_compute_keypair(scheme).1,
// secret: &Option<String>, SecretFormat::Predefined => prompt_predefined_and_compute_keypair(scheme).1,
// needed_keys: NeededKeys, }
// ) -> anyhow::Result<(Option<AccountId>, Option<Pair>)> { }
// // Get from args
// let mut account_id = match (accout_id, secret) { /// get the secret from user, trying first keystore then input
// // both are defined, check that they match. pub async fn fetch_or_get_keypair(
// // if they do not match, use secret data: &Data,
// (Some(accout_id), Some(secret)) => { address: Option<AccountId>,
// let pair = pair_from_str(secret_format, secret)?; crypto_scheme: Option<CryptoScheme>,
// let secret_address = pair.public().into(); ) -> Result<KeyPair, GcliError> {
// if accout_id != secret_address { if let Some(address) = address {
// println!("Secret ({secret_address}) and address ({accout_id}) do not match, using secret"); // if address corresponds to predefined, (for example saved to config)
// } // keypair is already known (useful for dev mode) - also overrides crypto_scheme if found
// return Ok((Some(secret_address), Some(pair))); if let Some((deriv, crypto_scheme)) = catch_known(&address.to_string()) {
// } return match crypto_scheme {
// // only secret, build both CryptoScheme::Ed25519 => {
// (None, Some(secret)) => { pair_from_ed25519_str(&predefined_suri(deriv)).map(|v| v.into())
// let pair = pair_from_str(secret_format, secret)?; }
// return Ok((Some(pair.public().into()), Some(pair))); CryptoScheme::Sr25519 => pair_from_predefined(deriv).map(|v| v.into()),
// } };
// // only address };
// (Some(accout_id), None) => Some(accout_id),
// // none of them // look for corresponding KeyPair in keystore
// (None, None) => None, if let Some(key_pair) = commands::vault::try_fetch_key_pair(data, address).await? {
// }; return Ok(key_pair);
};
// // Prompt }
// if needed_keys == NeededKeys::Secret // at the moment, there is no way to confg gcli to use an other kind of secret
// || (account_id.is_none() && needed_keys == NeededKeys::Public) // without telling explicitly each time
// { Ok(prompt_secret(SecretFormat::Substrate, crypto_scheme))
// loop { }
// let pair = prompt_secret(secret_format);
// catch known addresses
// if let Some(account_id) = &account_id { fn catch_known(address: &str) -> Option<(&str, CryptoScheme)> {
// if account_id != &pair.public().into() { match address {
// println!("Secret and address do not match."); "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" => {
// } Some(("Alice", CryptoScheme::Sr25519))
// } else { }
// account_id = Some(pair.public().into()); "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" => Some(("Bob", CryptoScheme::Sr25519)),
// return Ok((account_id, Some(pair))); "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y" => {
// } Some(("Charlie", CryptoScheme::Sr25519))
// } }
// } "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" => Some(("Dave", CryptoScheme::Sr25519)),
"5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" => Some(("Eve", CryptoScheme::Sr25519)),
// Ok((account_id, None))
// } "5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu" => {
Some(("Alice", CryptoScheme::Ed25519))
}
"5GoNkf6WdbxCFnPdAnYYQyCjAKPJgLNxXwPjwTh6DGg6gN3E" => Some(("Bob", CryptoScheme::Ed25519)),
"5DbKjhNLpqX3zqZdNBc9BGb4fHU1cRBaDhJUskrvkwfraDi6" => {
Some(("Charlie", CryptoScheme::Ed25519))
}
"5ECTwv6cZ5nJQPk6tWfaTrEk8YH2L7X1VT4EL5Tx2ikfFwb7" => Some(("Dave", CryptoScheme::Ed25519)),
"5Ck2miBfCe1JQ4cY3NDsXyBaD6EcsgiVmEFTWwqNSs25XDEq" => Some(("Eve", CryptoScheme::Ed25519)),
_ => None,
}
}
// Unit tests
#[cfg(test)]
mod tests {
use super::*;
mod subkey_like_tests {
use super::keys::SUBSTRATE_MNEMONIC;
use sp_core::crypto::Ss58Codec;
use sp_core::crypto::{Ss58AddressFormat, Ss58AddressFormatRegistry};
use sp_core::ByteArray;
use sp_runtime::traits::IdentifyAccount;
use sp_runtime::MultiSigner;
#[test]
fn test_print_from_suri() {
// sc_cli::CryptoSchemeFlag::augment_args()
let suri_str = SUBSTRATE_MNEMONIC.to_string();
print_from_suri::<sp_core::sr25519::Pair>(&suri_str, None, None);
print_from_suri::<sp_core::sr25519::Pair>(
&suri_str,
None,
Some(Ss58AddressFormatRegistry::G1Account.into()),
);
// bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice
let suri_str = SUBSTRATE_MNEMONIC.to_string() + "//Alice";
print_from_suri::<sp_core::sr25519::Pair>(&suri_str, None, None);
print_from_suri::<sp_core::sr25519::Pair>(
&suri_str,
None,
Some(Ss58AddressFormatRegistry::G1Account.into()),
);
}
/// print account information from suri - simplification of code from
/// sc_cli::commands::utils::print_from_uri
pub fn print_from_suri<Pair>(
uri: &str,
password: Option<&str>,
network_override: Option<Ss58AddressFormat>,
) where
Pair: sp_core::Pair,
Pair::Public: Into<MultiSigner>,
{
let network_id = String::from(unwrap_or_default_ss58_version(network_override));
if let Ok((pair, seed)) = Pair::from_string_with_seed(uri, password) {
let public_key = pair.public();
let network_override = unwrap_or_default_ss58_version(network_override);
println!(
"Secret Key URI `{}` is account:\n \
Network ID: {}\n \
Secret seed: {}\n \
Public key (hex): {}\n \
Account ID: {}\n \
Public key (SS58): {}\n \
SS58 Address: {}",
uri,
network_id,
if let Some(seed) = seed {
// sc_cli::utils::format_seed::<Pair>(seed)
format!("0x{}", hex::encode(seed.as_ref()))
} else {
"n/a".into()
},
format_public_key::<Pair>(public_key.clone()),
format_account_id::<Pair>(public_key.clone()),
public_key.to_ss58check_with_version(network_override),
pair.public()
.into()
.into_account()
.to_ss58check_with_version(network_override),
);
} else {
println!("Invalid phrase/URI given");
}
}
/// Public key type for Runtime
pub type PublicFor<P> = <P as sp_core::Pair>::Public;
/// formats public key as hex
fn format_public_key<P: sp_core::Pair>(public_key: PublicFor<P>) -> String {
format!("0x{}", hex::encode(&public_key.as_ref()))
}
/// formats public key as accountId as hex
fn format_account_id<P: sp_core::Pair>(public_key: PublicFor<P>) -> String
where
PublicFor<P>: Into<MultiSigner>,
{
format!(
"0x{}",
hex::encode(&public_key.into().into_account().as_slice())
)
}
pub fn unwrap_or_default_ss58_version(
network: Option<Ss58AddressFormat>,
) -> Ss58AddressFormat {
network.unwrap_or_else(default_ss58_version)
}
pub fn default_ss58_version() -> Ss58AddressFormat {
DEFAULT_VERSION
.load(core::sync::atomic::Ordering::Relaxed)
.into()
}
static DEFAULT_VERSION: core::sync::atomic::AtomicU16 = core::sync::atomic::AtomicU16::new(
from_known_address_format(Ss58AddressFormatRegistry::SubstrateAccount),
);
pub const fn from_known_address_format(x: Ss58AddressFormatRegistry) -> u16 {
x as u16
}
}
mod substrate {
use super::*;
use crate::keys::SUBSTRATE_MNEMONIC;
/// Testing sr25519 mnemonic derivations
///
/// Using `subkey` command to have expected values from mnemonic derivations (using `SUBSTRATE_MNEMONIC` for tests)
///
/// ##### The root mnemonic
/// ```
/// subkey inspect
/// URI:
/// Secret phrase: bottom drive obey lake curtain smoke basket hold race lonely fit walk
/// Network ID: substrate
/// Secret seed: 0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e
/// Public key (hex): 0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a
/// Account ID: 0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a
/// Public key (SS58): 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
/// SS58 Address: 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
/// ```
///
/// ##### The '//0' derivation
/// ```
/// subkey inspect
/// URI:
/// Secret Key URI `bottom drive obey lake curtain smoke basket hold race lonely fit walk//0` is account:
/// Network ID: substrate
/// Secret seed: 0x914dded06277afbe5b0e8a30bce539ec8a9552a784d08e530dc7c2915c478393
/// Public key (hex): 0x2afba9278e30ccf6a6ceb3a8b6e336b70068f045c666f2e7f4f9cc5f47db8972
/// Account ID: 0x2afba9278e30ccf6a6ceb3a8b6e336b70068f045c666f2e7f4f9cc5f47db8972
/// Public key (SS58): 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH
/// SS58 Address: 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH
/// ```
#[test]
fn test_sr25519_mnemonic_derivations() {
let root_sr25519_pair = pair_from_sr25519_str(SUBSTRATE_MNEMONIC).unwrap();
let expected_root_ss58_address_string =
"5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV".to_string();
let root_ss58_address: AccountId = root_sr25519_pair.public().into();
println!("root SS58 Address: '{}'", root_ss58_address);
assert_eq!(
expected_root_ss58_address_string,
root_ss58_address.to_string()
);
// Using derive on root keypair to get '//0'
let (deriv_0_sr25519_pair, _seed) = root_sr25519_pair
.derive(Some(sp_core::DeriveJunction::hard(0)).into_iter(), None)
.unwrap();
let expected_deriv_0_ss58_address_string =
"5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH".to_string();
let deriv_0_ss58_address: AccountId = deriv_0_sr25519_pair.public().into();
println!("derived '//0' SS58 Address: '{}'", deriv_0_ss58_address);
assert_eq!(
expected_deriv_0_ss58_address_string,
deriv_0_ss58_address.to_string()
);
// Using sp_core::sr25519::Pair::from_string(suri, None) to derive keypair from suri
let deriv_0_suri = SUBSTRATE_MNEMONIC.to_string() + "//0";
let deriv_0_suri_sr25519_pair =
sp_core::sr25519::Pair::from_string(&deriv_0_suri, None).unwrap();
let deriv_0_suri_ss58_address: AccountId = deriv_0_suri_sr25519_pair.public().into();
println!(
"derived '//0' from suri SS58 Address: '{}'",
deriv_0_suri_ss58_address
);
assert_eq!(
expected_deriv_0_ss58_address_string,
deriv_0_suri_ss58_address.to_string()
);
}
}
mod seed {
use super::*;
/// Testing sr25519 seed derivations
///
/// Using `subkey` command to have expected values from seed derivations (using a newly generated mnemonic `festival insane keep vivid surface photo razor unaware twice sudden involve false` for this test)
///
/// ##### The root seed
/// (from the mnemonic first)
/// ```
/// subkey inspect
/// URI:
/// Secret phrase: festival insane keep vivid surface photo razor unaware twice sudden involve false
/// Network ID: substrate
/// Secret seed: 0xf813535799c3a15b8e419a06964e87fabd3f265caebcbb38c935a1acdbe05253
/// Public key (hex): 0xce47000c942392afacc938b0db0b79d3377b1d1f5fbad6374c2943af05dfe379
/// Account ID: 0xce47000c942392afacc938b0db0b79d3377b1d1f5fbad6374c2943af05dfe379
/// Public key (SS58): 5GjAp6kkbsDjxABGouMvu1Mxbr7PaFqbMrprEoZ466vPF2Vt
/// SS58 Address: 5GjAp6kkbsDjxABGouMvu1Mxbr7PaFqbMrprEoZ466vPF2Vt
/// ```
///
/// When using the seed directly
/// ```
/// subkey inspect
/// URI:
/// Secret Key URI `0xf813535799c3a15b8e419a06964e87fabd3f265caebcbb38c935a1acdbe05253` is account:
/// Network ID: substrate
/// Secret seed: 0xf813535799c3a15b8e419a06964e87fabd3f265caebcbb38c935a1acdbe05253
/// Public key (hex): 0xce47000c942392afacc938b0db0b79d3377b1d1f5fbad6374c2943af05dfe379
/// Account ID: 0xce47000c942392afacc938b0db0b79d3377b1d1f5fbad6374c2943af05dfe379
/// Public key (SS58): 5GjAp6kkbsDjxABGouMvu1Mxbr7PaFqbMrprEoZ466vPF2Vt
/// SS58 Address: 5GjAp6kkbsDjxABGouMvu1Mxbr7PaFqbMrprEoZ466vPF2Vt
/// ```
///
/// ##### The '//0' derivation
/// ```
/// subkey inspect
/// URI:
/// Secret Key URI `0xf813535799c3a15b8e419a06964e87fabd3f265caebcbb38c935a1acdbe05253//0` is account:
/// Network ID: substrate
/// Secret seed: 0xb1f84cbfd8db9de2b198f6658cf8ad5aacc157589e891a653ca1eed3979f8220
/// Public key (hex): 0xe2b36186911ac4bc7480516b8711be124d97c625f255cbf494bd5f997b8e6023
/// Account ID: 0xe2b36186911ac4bc7480516b8711be124d97c625f255cbf494bd5f997b8e6023
/// Public key (SS58): 5HBwy19piXNWg7bfShuNgWsBCWRzkG99M8eRGB6PHkxeAKAV
/// SS58 Address: 5HBwy19piXNWg7bfShuNgWsBCWRzkG99M8eRGB6PHkxeAKAV
/// ```
#[test]
fn test_sr25519_seed_derivations() {
let root_seed = "f813535799c3a15b8e419a06964e87fabd3f265caebcbb38c935a1acdbe05253";
let root_sr25519_pair = pair_from_sr25519_seed(root_seed).unwrap();
let expected_root_ss58_address_string =
"5GjAp6kkbsDjxABGouMvu1Mxbr7PaFqbMrprEoZ466vPF2Vt".to_string();
let root_ss58_address: AccountId = root_sr25519_pair.public().into();
println!("root SS58 Address: '{}'", root_ss58_address);
assert_eq!(
expected_root_ss58_address_string,
root_ss58_address.to_string()
);
// Using derive on root keypair to get "//0"
let (deriv_0_sr25519_pair, _seed) = root_sr25519_pair
.derive(Some(sp_core::DeriveJunction::hard(0)).into_iter(), None)
.unwrap();
let expected_deriv_0_ss58_address_string =
"5HBwy19piXNWg7bfShuNgWsBCWRzkG99M8eRGB6PHkxeAKAV".to_string();
let deriv_0_ss58_address: AccountId = deriv_0_sr25519_pair.public().into();
println!("derived '//0' SS58 Address: '{}'", deriv_0_ss58_address);
assert_eq!(
expected_deriv_0_ss58_address_string,
deriv_0_ss58_address.to_string()
);
// Using sp_core::sr25519::Pair::from_string(suri, None) to derive keypair from suri
let deriv_0_suri = "0x".to_string() + root_seed + "//0";
let deriv_0_suri_sr25519_pair =
sp_core::sr25519::Pair::from_string(&deriv_0_suri, None).unwrap();
let deriv_0_suri_ss58_address: AccountId = deriv_0_suri_sr25519_pair.public().into();
println!(
"derived '//0' from suri SS58 Address: '{}'",
deriv_0_suri_ss58_address
);
assert_eq!(
expected_deriv_0_ss58_address_string,
deriv_0_suri_ss58_address.to_string()
);
}
}
mod predefined {
use super::*;
/// Testing predefined mnemonic derivations (using names instead of indexes)
///
/// Using `subkey` command to have expected values from predefined mnemonic derivations
///
/// ##### The root mnemonic
/// ```
/// subkey inspect
/// URI:
/// Secret phrase: bottom drive obey lake curtain smoke basket hold race lonely fit walk
/// Network ID: substrate
/// Secret seed: 0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e
/// Public key (hex): 0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a
/// Account ID: 0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a
/// Public key (SS58): 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
/// SS58 Address: 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
/// ```
///
/// ##### The '//Alice' derivation
/// ```
/// subkey inspect
/// URI:
/// Secret Key URI `bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice` is account:
/// Network ID: substrate
/// Secret seed: 0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a
/// Public key (hex): 0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d
/// Account ID: 0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d
/// Public key (SS58): 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
/// SS58 Address: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
/// ```
#[test]
fn test_predefined_mnemonic_derivations() {
let root_sr25519_pair = pair_from_sr25519_str(SUBSTRATE_MNEMONIC).unwrap();
let expected_root_ss58_address_string =
"5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV".to_string();
let root_ss58_address: AccountId = root_sr25519_pair.public().into();
println!("root SS58 Address: '{}'", root_ss58_address);
assert_eq!(
expected_root_ss58_address_string,
root_ss58_address.to_string()
);
// Using derive on root keypair to get Alice
let (alice_sr25519_pair, _seed) = root_sr25519_pair
.derive(
Some(sp_core::DeriveJunction::hard("Alice")).into_iter(),
None,
)
.unwrap();
let expected_alice_ss58_address_string =
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY".to_string();
let alice_ss58_address: AccountId = alice_sr25519_pair.public().into();
println!("Alice SS58 Address: '{}'", alice_ss58_address);
assert_eq!(
expected_alice_ss58_address_string,
alice_ss58_address.to_string()
);
// Using sp_core::sr25519::Pair::from_string(suri, None) to derive keypair from suri
let alice_suri = SUBSTRATE_MNEMONIC.to_owned() + "//Alice";
let alice_suri_sr25519_pair =
sp_core::sr25519::Pair::from_string(&alice_suri, None).unwrap();
let alice_suri_ss58_address: AccountId = alice_suri_sr25519_pair.public().into();
println!("Alice suri SS58 Address: '{}'", alice_suri_ss58_address);
assert_eq!(
expected_alice_ss58_address_string,
alice_suri_ss58_address.to_string()
);
}
}
mod g1v1 {
use super::*;
/// Test which verifies that it's possible to derive a key coming from a cesium v1 id & password
///
/// Using subkey command with **ed25519** scheme to show we can derive a key from a seed
/// and to retrieve expected values.
///
/// ##### Without derivation (using seed from the test)
/// ```
/// subkey inspect --scheme ed25519
/// URI:
/// Secret Key URI `0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84` is account:
/// Network ID: substrate
/// Secret seed: 0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84
/// Public key (hex): 0x697f6bd16ddebf142384e503fd3f3efc39fe5c7be7c693bd98d982403bb6eb74
/// Account ID: 0x697f6bd16ddebf142384e503fd3f3efc39fe5c7be7c693bd98d982403bb6eb74
/// Public key (SS58): 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4
/// SS58 Address: 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4
/// ```
///
/// ##### With derivation '//0' (using seed from the test)
/// ```
/// subkey inspect --scheme ed25519
/// URI:
/// Secret Key URI `0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84//0` is account:
/// Network ID: substrate
/// Secret seed: 0x916e95359a49c82e3d84269b90551433352c433eb9bb270fb8cb86e8a6c9ec85
/// Public key (hex): 0x1658ce32f039dff26d83b5282f611cfed8c71296a311417ef737db4e016194de
/// Account ID: 0x1658ce32f039dff26d83b5282f611cfed8c71296a311417ef737db4e016194de
/// Public key (SS58): 5Ca1HrNxQ4hiekd92Z99fzhfdSAqPy2rUkLBmwLsgLCjeSQf
/// SS58 Address: 5Ca1HrNxQ4hiekd92Z99fzhfdSAqPy2rUkLBmwLsgLCjeSQf
/// ```
#[test]
fn test_g1v1_key_derivation() {
let cesium_id = "test_cesium_id".to_string();
let cesium_pwd = "test_cesium_pwd".to_string();
let expected_cesium_v1_ss58_address: String =
"5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4".to_string();
let seed = seed_from_cesium(&cesium_id, &cesium_pwd);
println!();
println!("seed: '0x{}'", hex::encode(seed));
let ed25519_pair_from_seed = sp_core::ed25519::Pair::from_seed(&seed);
println!();
println!(
"ed25519 keypair from seed: public:'0x{}' raw_vec:'0x{}'",
hex::encode(ed25519_pair_from_seed.public().0),
hex::encode(ed25519_pair_from_seed.to_raw_vec().as_slice())
);
println!(
"ed25519 keypair from seed: Cesium v1 Pubkey: '{}'",
bs58::encode(ed25519_pair_from_seed.public()).into_string()
);
let ed25519_address_from_seed: AccountId = ed25519_pair_from_seed.public().into();
println!(
"ed25519 keypair from seed: public SS58 Address:'{}'",
ed25519_address_from_seed
);
assert_eq!(
expected_cesium_v1_ss58_address,
ed25519_address_from_seed.to_string()
);
let root_suri = "0x".to_string() + &hex::encode(seed);
let ed25519_pair_from_suri =
sp_core::ed25519::Pair::from_string(&root_suri, None).unwrap();
let ed25519_address_from_suri: AccountId = ed25519_pair_from_suri.public().into();
println!(
"ed25519 keypair from suri: public SS58 Address:'{}'",
ed25519_address_from_suri
);
assert_eq!(
expected_cesium_v1_ss58_address,
ed25519_address_from_suri.to_string()
);
// Tested derivation manually with `subkey` command using adapted suri: `0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84//0`
let expected_ss58_address_derivation_0 =
"5Ca1HrNxQ4hiekd92Z99fzhfdSAqPy2rUkLBmwLsgLCjeSQf".to_string();
// Using derive on the ed25519 keypair
let (derived_ed25519_pair, _seed) = ed25519_pair_from_seed
.derive(Some(sp_core::DeriveJunction::hard(0)).into_iter(), None)
.unwrap();
println!();
println!(
"derived ed25519 keypair: public:'0x{}' raw_vec:'0x{}'",
hex::encode(derived_ed25519_pair.public().0),
hex::encode(derived_ed25519_pair.to_raw_vec().as_slice())
);
println!(
"derived ed25519 keypair: Cesium v1 Pubkey: '{}'",
bs58::encode(derived_ed25519_pair.public()).into_string()
);
let derived_ed25519_address =
subxt::utils::AccountId32::from(derived_ed25519_pair.public());
println!(
"derived ed25519 keypair: public SS58 Address:'{}'",
derived_ed25519_address
);
assert_eq!(
expected_ss58_address_derivation_0,
derived_ed25519_address.to_string()
);
// Using sp_core::ed25519::Pair::from_string(suri, None) to derive keypair from suri
let deriv_0_suri = root_suri.clone() + "//0";
let derived_ed25519_pair_from_suri =
sp_core::ed25519::Pair::from_string(&deriv_0_suri, None).unwrap();
let derived_ed25519_address_from_suri: AccountId =
derived_ed25519_pair_from_suri.public().into();
println!(
"derived ed25519 keypair from suri: public SS58 Address:'{}'",
derived_ed25519_address_from_suri
);
assert_eq!(
expected_ss58_address_derivation_0,
derived_ed25519_address_from_suri.to_string()
);
}
/// Test which verifies that it's possible to directly use a cesium v1 seed retrieved using scryt
/// as seed or suri to instantiate an ed25519 keypair - keeping the same resulting SS58 Address as when using nacl::sign::Keypair
///
/// Using subkey command with **ed25519** scheme to show the seed/suri and associated SS58 Address
///
/// ```
/// subkey inspect --scheme ed25519
/// URI:
/// Secret Key URI `0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84` is account:
/// Network ID: substrate
/// Secret seed: 0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84
/// Public key (hex): 0x697f6bd16ddebf142384e503fd3f3efc39fe5c7be7c693bd98d982403bb6eb74
/// Account ID: 0x697f6bd16ddebf142384e503fd3f3efc39fe5c7be7c693bd98d982403bb6eb74
/// Public key (SS58): 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4
/// SS58 Address: 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4
/// ```
#[test]
fn test_g1v1_seed_using_scrypt() {
let cesium_id = "test_cesium_id".to_string();
let cesium_pwd = "test_cesium_pwd".to_string();
let expected_cesium_v1_ss58_address: String =
"5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4".to_string();
// retrieving seed using scrypt
let seed = seed_from_cesium(&cesium_id, &cesium_pwd);
println!("seed value from scrypt: '0x{}'", hex::encode(&seed));
let ed25519_pair_from_seed = sp_core::ed25519::Pair::from_seed(&seed);
println!();
println!(
"ed25519 keypair from seed : public:'0x{}' raw_vec:'0x{}'",
hex::encode(ed25519_pair_from_seed.public().0),
hex::encode(ed25519_pair_from_seed.to_raw_vec().as_slice())
);
let ed25519_address_from_seed: AccountId = ed25519_pair_from_seed.public().into();
println!(
"ed25519 keypair from seed : public SS58 Address:'{}'",
ed25519_address_from_seed
);
assert_eq!(
expected_cesium_v1_ss58_address,
ed25519_address_from_seed.to_string()
);
let root_suri = "0x".to_string() + &hex::encode(seed);
let ed25519_pair_from_suri =
sp_core::ed25519::Pair::from_string(&root_suri, None).unwrap();
let ed25519_address_from_suri: AccountId = ed25519_pair_from_suri.public().into();
println!(
"ed25519 keypair from suri : public SS58 Address:'{}'",
ed25519_address_from_suri
);
assert_eq!(
expected_cesium_v1_ss58_address,
ed25519_address_from_suri.to_string()
);
}
}
mod parameterized_tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"),
String::from("5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV"),
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")
)]
#[case(
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"),
String::from("5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV"),
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//0"
),
String::from("5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH"),
String::from("0x914dded06277afbe5b0e8a30bce539ec8a9552a784d08e530dc7c2915c478393")
)]
#[case(
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0"),
String::from("5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH"),
String::from("0x914dded06277afbe5b0e8a30bce539ec8a9552a784d08e530dc7c2915c478393")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//0//1"
),
String::from("5Cr4pXhwGbmjQpWw86zYcym5QiixA9XEyMujtiFmHLMNE1wB"),
String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e")
)]
#[case(
String::from(
"0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0//1"
),
String::from("5Cr4pXhwGbmjQpWw86zYcym5QiixA9XEyMujtiFmHLMNE1wB"),
String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e")
)]
#[case(
String::from("0x914dded06277afbe5b0e8a30bce539ec8a9552a784d08e530dc7c2915c478393//1"),
String::from("5Cr4pXhwGbmjQpWw86zYcym5QiixA9XEyMujtiFmHLMNE1wB"),
String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e")
)]
#[case(
String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e"),
String::from("5Cr4pXhwGbmjQpWw86zYcym5QiixA9XEyMujtiFmHLMNE1wB"),
String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice"
),
String::from("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"),
String::from("0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Alan"
),
String::from("5HHCrQYbfr1n9me9LoB4REHZZ9BQZAcw2dnbUKdzdXkFLEr6"),
String::from("0x06ace9363c66d855542d55d8b233c697db3bd7fe2fbdc6c34cefb94ad43eccf0")
)]
/// Expected data was retrieved using `subkey inspect` command
fn sr25519_compute_pair_and_mini_secret_seed_from_suri(
#[case] suri_string: String,
#[case] expected_address: String,
#[case] expected_seed: String,
) {
let (keypair, seed) = compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme(
&suri_string,
CryptoScheme::Sr25519,
)
.unwrap();
assert_eq!(format!("0x{}", hex::encode(seed)), expected_seed);
assert_eq!(keypair.address().to_string(), expected_address)
}
#[rstest]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk/0"),
String::from("Input(\"Soft derivation is not supported when trying to retrieve mini-secret/seed\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0/1"),
String::from("Input(\"Soft derivation is not supported when trying to retrieve mini-secret/seed\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk///pass"),
String::from("Input(\"Having a password in the derivation path is not supported (password:'pass')\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk///pass//0"),
String::from("Input(\"Having a password in the derivation path is not supported (password:'pass//0')\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0///pass//1"),
String::from("Input(\"Having a password in the derivation path is not supported (password:'pass//1')\")")
)]
fn sr25519_compute_pair_and_mini_secret_seed_from_suri_expecting_errors(
#[case] suri_string: String,
#[case] expected_error: String,
) {
let result = compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme(
&suri_string,
CryptoScheme::Sr25519,
);
match result {
Ok(_) => assert!(false, "expected error"),
Err(e) => assert_eq!(expected_error, e.to_string()),
}
}
#[rstest]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"),
String::from("5DFJF7tY4bpbpcKPJcBTQaKuCDEPCpiz8TRjpmLeTtweqmXL"),
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")
)]
#[case(
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"),
String::from("5DFJF7tY4bpbpcKPJcBTQaKuCDEPCpiz8TRjpmLeTtweqmXL"),
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//0"
),
String::from("5HrCphkqYygSXWt9rHebqaqbfEYekhzjyjQNjZiPxpb3XsKY"),
String::from("0xf8dfdb0f1103d9fb2905204ac32529d5f148761c4321b2865b0a40e15be75f57")
)]
#[case(
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0"),
String::from("5HrCphkqYygSXWt9rHebqaqbfEYekhzjyjQNjZiPxpb3XsKY"),
String::from("0xf8dfdb0f1103d9fb2905204ac32529d5f148761c4321b2865b0a40e15be75f57")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//0//1"
),
String::from("5G4xo2TvB4Uv7MR1b35yWBW8g7WLaiuETWChtX1dHkyHrLEP"),
String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab")
)]
#[case(
String::from(
"0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0//1"
),
String::from("5G4xo2TvB4Uv7MR1b35yWBW8g7WLaiuETWChtX1dHkyHrLEP"),
String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab")
)]
#[case(
String::from("0xf8dfdb0f1103d9fb2905204ac32529d5f148761c4321b2865b0a40e15be75f57//1"),
String::from("5G4xo2TvB4Uv7MR1b35yWBW8g7WLaiuETWChtX1dHkyHrLEP"),
String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab")
)]
#[case(
String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab"),
String::from("5G4xo2TvB4Uv7MR1b35yWBW8g7WLaiuETWChtX1dHkyHrLEP"),
String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice"
),
String::from("5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu"),
String::from("0xabf8e5bdbe30c65656c0a3cbd181ff8a56294a69dfedd27982aace4a76909115")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Alan"
),
String::from("5CvaztNtrHKyi4vPK4xHNRCyLdoWTFovSr9g2MekGUGEvBaS"),
String::from("0xb1f3996e8083bda16a43abfdbea6549bbfecdcc4b5c043a73645fff232765fca")
)]
/// Expected data was retrieved using `subkey inspect --scheme ed25519` command
fn ed25519_compute_pair_and_mini_secret_seed_from_suri(
#[case] suri_string: String,
#[case] expected_address: String,
#[case] expected_seed: String,
) {
let (keypair, seed) = compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme(
&suri_string,
CryptoScheme::Ed25519,
)
.unwrap();
assert_eq!(format!("0x{}", hex::encode(seed)), expected_seed);
assert_eq!(keypair.address().to_string(), expected_address)
}
#[rstest]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk/0"
),
String::from("Input(\"Invalid secret\")")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//0/1"
),
String::from("Input(\"Invalid secret\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk///pass"),
String::from("Input(\"Having a password in the derivation path is not supported (password:'pass')\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk///pass//0"),
String::from("Input(\"Having a password in the derivation path is not supported (password:'pass//0')\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0///pass//1"),
String::from("Input(\"Having a password in the derivation path is not supported (password:'pass//1')\")")
)]
fn ed25519_compute_pair_and_mini_secret_seed_from_suri_expecting_errors(
#[case] suri_string: String,
#[case] expected_error: String,
) {
let result = compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme(
&suri_string,
CryptoScheme::Ed25519,
);
match result {
Ok(_) => assert!(false, "expected error"),
Err(e) => assert_eq!(expected_error, e.to_string()),
}
}
}
}
mod cache;
mod commands; mod commands;
mod conf; mod conf;
mod data; mod data;
mod database;
mod display;
mod entities;
mod indexer; mod indexer;
mod inputs;
mod keys; mod keys;
mod runtime_config; mod runtime_config;
mod utils; mod utils;
use anyhow::anyhow; use anyhow::anyhow;
use clap::Parser; use clap::builder::OsStr;
use clap::{CommandFactory, Parser};
use clap_complete::{generate, Shell};
use codec::Encode; use codec::Encode;
use colored::Colorize;
use data::*; use data::*;
use display::DisplayEvent;
use indoc::indoc;
use keys::*; use keys::*;
use runtime_config::*; use runtime_config::*;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use sp_core::{sr25519::Pair, Pair as _}; use std::io;
use subxt::blocks::ExtrinsicEvents; use std::str::FromStr;
use subxt::tx::{BaseExtrinsicParamsBuilder, PairSigner, TxStatus}; use subxt::{
blocks::ExtrinsicEvents,
config::DefaultExtrinsicParamsBuilder,
events::StaticEvent,
ext::sp_core::{sr25519, Pair as _},
tx::{DefaultPayload, PairSigner, Payload, TxStatus},
};
use utils::*; use utils::*;
// alias
pub type StaticPayload<Calldata> = DefaultPayload<Calldata>;
/// define command line arguments /// define command line arguments
#[derive(Clone, clap::Parser, Debug, Default)] #[derive(Clone, clap::Parser, Debug, Default)]
#[clap(author, version, about, long_about = None)] #[clap(author, version, about, long_about = None)]
...@@ -27,27 +44,74 @@ pub struct Args { ...@@ -27,27 +44,74 @@ pub struct Args {
#[clap(subcommand)] #[clap(subcommand)]
pub subcommand: Subcommand, pub subcommand: Subcommand,
/// Overwrite indexer endpoint /// Overwrite indexer endpoint
#[clap(short, long)] #[clap(short, long, conflicts_with_all=["no_indexer","network"])]
indexer: Option<String>, indexer: Option<String>,
/// Do not use indexer /// Do not use indexer
#[clap(long)] #[clap(long)]
no_indexer: bool, no_indexer: bool,
/// Secret key or BIP39 mnemonic /// Secret key format (seed, substrate, g1v1)
#[clap(short = 'S', long)]
secret_format: Option<SecretFormat>,
/// Secret key or BIP39 mnemonic (only used when secret format is compatible)
/// (eventually followed by derivation path) /// (eventually followed by derivation path)
#[clap(short, long)] #[clap(short, long)]
secret: Option<String>, secret: Option<String>,
/// Secret key format (seed, substrate) /// Crypto scheme to use (sr25519, ed25519)
#[clap(short = 'S', long, default_value = SecretFormat::Substrate)] #[clap(short = 'c', long, required = false, default_value = CryptoScheme::Ed25519)]
secret_format: SecretFormat, crypto_scheme: CryptoScheme,
/// Address /// SS58 Address
#[clap(short, long)] #[clap(short, conflicts_with = "name")]
address: Option<String>, address: Option<AccountId>,
/// Name of an SS58 Address in the vault
#[clap(short = 'v')]
name: Option<String>,
/// Overwrite duniter websocket RPC endpoint /// Overwrite duniter websocket RPC endpoint
#[clap(short, long)] #[clap(short, long, conflicts_with = "network")]
url: Option<String>, url: Option<String>,
/// Target network (local, gdev, gtest...) /// Target network (local, gdev, gtest...)
#[clap(short, long)] #[clap(short, long)]
network: Option<String>, network: Option<String>,
/// prevent waiting for extrinsic completion
#[clap(long)]
no_wait: bool,
/// Output format (human, json, ...)
#[clap(short = 'o', long, default_value = OutputFormat::Human)]
output_format: OutputFormat,
}
// TODO derive the fromstr implementation
/// secret format
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
enum OutputFormat {
/// Human
#[default]
Human,
/// JSON
Json,
}
impl FromStr for OutputFormat {
type Err = std::io::Error;
fn from_str(s: &str) -> std::io::Result<Self> {
match s {
"human" => Ok(OutputFormat::Human),
"json" => Ok(OutputFormat::Json),
_ => Err(std::io::Error::from(std::io::ErrorKind::InvalidInput)),
}
}
}
impl From<OutputFormat> for &'static str {
fn from(val: OutputFormat) -> &'static str {
match val {
OutputFormat::Human => "human",
OutputFormat::Json => "json",
}
}
}
impl From<OutputFormat> for OsStr {
fn from(val: OutputFormat) -> OsStr {
OsStr::from(Into::<&str>::into(val))
}
} }
/// define subcommands /// define subcommands
...@@ -56,7 +120,7 @@ pub enum Subcommand { ...@@ -56,7 +120,7 @@ pub enum Subcommand {
/// Nothing /// Nothing
#[default] #[default]
#[clap(hide = true)] #[clap(hide = true)]
DoNothing, Nothing,
/// Account (balance, transfer...) /// Account (balance, transfer...)
#[clap(subcommand)] #[clap(subcommand)]
Account(commands::account::Subcommand), Account(commands::account::Subcommand),
...@@ -66,6 +130,10 @@ pub enum Subcommand { ...@@ -66,6 +130,10 @@ pub enum Subcommand {
/// Smith (certify, go-online, go-offline...) /// Smith (certify, go-online, go-offline...)
#[clap(subcommand)] #[clap(subcommand)]
Smith(commands::smith::Subcommand), Smith(commands::smith::Subcommand),
/// Sudo (set key, sudo calls...)
#[clap(hide = true)]
#[clap(subcommand)]
Sudo(commands::sudo::Subcommand),
/// Tech (list members, proposals, vote...) /// Tech (list members, proposals, vote...)
#[clap(subcommand)] #[clap(subcommand)]
Tech(commands::collective::Subcommand), Tech(commands::collective::Subcommand),
...@@ -84,20 +152,66 @@ pub enum Subcommand { ...@@ -84,20 +152,66 @@ pub enum Subcommand {
/// Config (show, save...) /// Config (show, save...)
#[clap(subcommand)] #[clap(subcommand)]
Config(conf::Subcommand), Config(conf::Subcommand),
/// Key management (import, generate, list...)
#[clap(subcommand)]
Vault(commands::vault::Subcommand),
/// Cesium
#[clap(subcommand, hide = true)]
Cesium(commands::cesium::Subcommand),
/// Publish a new git tag with actual version
#[clap(hide = true)]
Publish,
/// Generate a completions script for a specified shell (use `completion --help` for more info)
#[clap(long_about = indoc! {r#"Generate a completions script for a specified shell.
The base completion scripts for `bash`, `zsh` or `fish` are already registered when installing the `.deb` package.
If you did not/could not install the `.deb` package we can generate the script and then configure the shell to use it.
Example for `bash`:
mkdir -p ~/.local/share/gcli
gcli completion --shell bash > ~/.local/share/gcli/completion.bash
# Direct test - gcli should have the completion after the source command
source ~/.local/share/gcli/completion.bash
# Persisting the configuration in the shell configuration file, add this at the end of your `~/.bashrc` file
[[ -f $HOME/.local/share/gcli/completion.bash ]] && source $HOME/.local/share/gcli/completion.bash
# Might need to reopen the shell for the configuration to be applied
Example for `zsh`:
mkdir -p ~/.local/share/gcli
gcli completion --shell zsh > ~/.local/share/gcli/completion.zsh
# Direct test - gcli should have the completion after the source command
source ~/.local/share/gcli/completion.zsh
# Persisting the configuration in the shell configuration file, add this at the end of your `~/.zshrc` file
[[ -f $HOME/.local/share/gcli/completion.zsh ]] && source $HOME/.local/share/gcli/completion.zsh
# Might need to reopen the shell for the configuration to be applied
"#})]
Completion {
/// target shell
#[clap(long)]
shell: Shell,
},
} }
/// maint function /// main function
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), GcliError> { async fn main() -> Result<(), GcliError> {
// init logger // init logger
env_logger::init(); env_logger::init();
// parse argument and initialize data // parse argument and initialize data
let data = Data::new(Args::parse()); let data = Data::new(Args::parse()).await?;
// match subcommands // match subcommands
match data.args.subcommand.clone() { let result = match data.args.subcommand.clone() {
Subcommand::DoNothing => {Ok(())} // handle shell completions
Subcommand::Completion { shell } => {
let mut app = Args::command();
generate(shell, &mut app, "gcli", &mut io::stdout());
return Ok(());
}
Subcommand::Nothing => Ok(()),
Subcommand::Account(subcommand) => { Subcommand::Account(subcommand) => {
commands::account::handle_command(data, subcommand).await commands::account::handle_command(data, subcommand).await
} }
...@@ -105,6 +219,7 @@ async fn main() -> Result<(), GcliError> { ...@@ -105,6 +219,7 @@ async fn main() -> Result<(), GcliError> {
commands::identity::handle_command(data, subcommand).await commands::identity::handle_command(data, subcommand).await
} }
Subcommand::Smith(subcommand) => commands::smith::handle_command(data, subcommand).await, Subcommand::Smith(subcommand) => commands::smith::handle_command(data, subcommand).await,
Subcommand::Sudo(subcommand) => commands::sudo::handle_command(data, subcommand).await,
Subcommand::Tech(subcommand) => { Subcommand::Tech(subcommand) => {
commands::collective::handle_command(data, subcommand).await commands::collective::handle_command(data, subcommand).await
} }
...@@ -116,6 +231,16 @@ async fn main() -> Result<(), GcliError> { ...@@ -116,6 +231,16 @@ async fn main() -> Result<(), GcliError> {
commands::blockchain::handle_command(data, subcommand).await commands::blockchain::handle_command(data, subcommand).await
} }
Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await, Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await,
Subcommand::Config(subcommand) => conf::handle_command(data, subcommand), Subcommand::Config(subcommand) => conf::handle_command(data, subcommand).await,
}.map_err(|e| dbg!(e).into()) Subcommand::Vault(subcommand) => commands::vault::handle_command(data, subcommand).await,
Subcommand::Cesium(subcommand) => commands::cesium::handle_command(data, subcommand).await,
Subcommand::Publish => commands::publish::handle_command().await,
};
if let Err(ref e) = result {
println!("{}", e.to_string().red())
}
// still return result for detailed error message
// println!();
// result
Ok(())
} }
// #[allow(clippy::enum_variant_names)]
#[cfg(feature = "gdev")] #[cfg(feature = "gdev")]
#[subxt::subxt( #[subxt::subxt(
runtime_metadata_path = "res/metadata.scale", runtime_metadata_path = "res/metadata.scale",
derive_for_all_types = "Debug" derive_for_all_types = "Debug"
)] )]
pub mod runtime { pub mod runtime {}
// IF NEEDED
// #[subxt(substitute_type = "spcore::sr25519::Signature")]
// use crate::gdev::runtime_types::sp_core::sr25519::Signature;
}
// declare custom types // declare custom types
pub type Client = subxt::OnlineClient<Runtime>; pub type Client = subxt::OnlineClient<Runtime>;
pub type AccountId = subxt::ext::sp_runtime::AccountId32; pub type AccountId = subxt::utils::AccountId32;
pub type IdtyId = u32;
pub type BlockNumber = u32;
pub type TxProgress = subxt::tx::TxProgress<Runtime, Client>; pub type TxProgress = subxt::tx::TxProgress<Runtime, Client>;
pub type Balance = u64; pub type Balance = u64;
pub type AccountData = runtime::runtime_types::pallet_duniter_account::types::AccountData<Balance>; pub type AccountData =
runtime::runtime_types::pallet_duniter_account::types::AccountData<Balance, IdtyId>;
pub type AccountInfo = runtime::runtime_types::frame_system::AccountInfo<u32, AccountData>; pub type AccountInfo = runtime::runtime_types::frame_system::AccountInfo<u32, AccountData>;
pub type Hash = sp_core::H256; pub type Hash = sp_core::H256;
// declare runtime types // declare runtime types
pub enum Runtime {} pub enum Runtime {}
impl subxt::config::Config for Runtime { impl subxt::config::Config for Runtime {
type Index = u32; type AssetId = ();
type BlockNumber = u32;
type Hash = Hash; type Hash = Hash;
type Hashing = subxt::ext::sp_runtime::traits::BlakeTwo256;
type AccountId = AccountId; type AccountId = AccountId;
type Address = subxt::ext::sp_runtime::MultiAddress<Self::AccountId, u32>; type Address = sp_runtime::MultiAddress<Self::AccountId, u32>;
type Header = subxt::ext::sp_runtime::generic::Header< type Signature = sp_runtime::MultiSignature;
Self::BlockNumber, type Hasher = subxt::config::substrate::BlakeTwo256;
subxt::ext::sp_runtime::traits::BlakeTwo256, type Header = subxt::config::substrate::SubstrateHeader<BlockNumber, Self::Hasher>;
>; type ExtrinsicParams = subxt::config::DefaultExtrinsicParams<Self>;
type Signature = subxt::ext::sp_runtime::MultiSignature;
type ExtrinsicParams = subxt::tx::BaseExtrinsicParams<Self, Tip>;
} }
// Tip for transaction fee // Tip for transaction fee
......
use crate::*; use crate::*;
use sea_orm::DbErr;
/// track progress of transaction on the network /// track progress of transaction on the network
/// until it is in block with success or failure /// until it is in block with success or failure
...@@ -6,14 +7,14 @@ pub async fn track_progress( ...@@ -6,14 +7,14 @@ pub async fn track_progress(
mut progress: TxProgress, mut progress: TxProgress,
) -> Result<ExtrinsicEvents<Runtime>, subxt::Error> { ) -> Result<ExtrinsicEvents<Runtime>, subxt::Error> {
loop { loop {
if let Some(status) = progress.next_item().await { if let Some(status) = progress.next().await {
match status? { match status? {
TxStatus::Ready => { TxStatus::Validated => {
println!("transaction submitted to the network, waiting 6 seconds..."); println!("transaction submitted to the network, waiting 6 seconds...");
} }
TxStatus::InBlock(in_block) => break in_block, TxStatus::InBestBlock(in_block) => break in_block,
TxStatus::Invalid => { TxStatus::Invalid { message } => {
println!("Invalid"); println!("Invalid {message}");
} }
_ => continue, _ => continue,
} }
...@@ -23,23 +24,109 @@ pub async fn track_progress( ...@@ -23,23 +24,109 @@ pub async fn track_progress(
.await .await
} }
/// generic extrinsic submitter
pub async fn submit_call_and_look_event<
E: std::fmt::Debug + StaticEvent + DisplayEvent,
TxPayload: Payload,
>(
data: &Data,
payload: &TxPayload,
) -> Result<(), subxt::Error> {
// submit call
let progress = submit_call(data, payload).await?;
// if no wait, return immediately
if data.args.no_wait {
return Ok(());
}
// collect events
let events = track_progress(progress).await?;
// print given event if there
look_event::<E>(data, &events)
}
/// submit call
pub async fn submit_call<TxPayload: Payload>(
data: &Data,
payload: &TxPayload,
) -> Result<TxProgress, subxt::Error> {
// get account nonce manually to be based on last block and not last finalized
let nonce = data
.legacy_rpc_methods() // see issue #32
.await
.system_account_next_index(&data.address())
.await?;
// sign and submit
match data.keypair().await {
// sr25519 key pair
KeyPair::Sr25519(keypair) => data.client().tx().create_signed_offline(
payload,
&PairSigner::<Runtime, sp_core::sr25519::Pair>::new(keypair),
DefaultExtrinsicParamsBuilder::new().nonce(nonce).build(),
),
KeyPair::Ed25519(keypair) => data.client().tx().create_signed_offline(
payload,
&PairSigner::<Runtime, sp_core::ed25519::Pair>::new(keypair),
DefaultExtrinsicParamsBuilder::new().nonce(nonce).build(),
),
}?
.submit_and_watch()
.await
}
/// look event
pub fn look_event<E: std::fmt::Debug + StaticEvent + DisplayEvent>(
data: &Data,
events: &ExtrinsicEvents<Runtime>,
) -> Result<(), subxt::Error> {
if let Some(e) = events.find_first::<E>()? {
println!("{}", e.display(data));
} else {
// print nothing, this could happen for
// - new cert vs renew cert
// - new smith cert and smith "promotion"
// println!("(no event of type {})", std::any::type_name::<E>())
}
Ok(())
}
/// custom error type intended to provide more convenient error message to user /// custom error type intended to provide more convenient error message to user
#[derive(Debug)] #[derive(Debug)]
pub enum GcliError { pub enum GcliError {
/// error coming from subxt /// error coming from subxt
Subxt(subxt::Error), Subxt(subxt::Error),
/// error coming from duniter /// error coming from duniter
#[allow(dead_code)]
Duniter(String), Duniter(String),
/// error coming from indexer /// error coming from indexer
#[allow(dead_code)]
Indexer(String), Indexer(String),
/// error coming from database
#[allow(dead_code)]
DatabaseError(DbErr),
/// logic error (illegal operation or security) /// logic error (illegal operation or security)
#[allow(dead_code)]
Logic(String), Logic(String),
/// error coming from anyhow /// input error
#[allow(dead_code)]
Input(String),
/// error coming from anyhow (to be removed)
#[allow(dead_code)]
Anyhow(anyhow::Error), Anyhow(anyhow::Error),
/// error coming from io
#[allow(dead_code)]
IoError(std::io::Error),
} }
impl std::fmt::Display for GcliError { impl std::fmt::Display for GcliError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self) match self {
// prettier runtime error
GcliError::Subxt(subxt::Error::Runtime(e)) => {
write!(f, "{e}")
}
// debug log for detailed error
_ => write!(f, "{:?}", self),
}
} }
} }
impl std::error::Error for GcliError {} impl std::error::Error for GcliError {}
...@@ -53,3 +140,18 @@ impl From<anyhow::Error> for GcliError { ...@@ -53,3 +140,18 @@ impl From<anyhow::Error> for GcliError {
GcliError::Anyhow(e) GcliError::Anyhow(e)
} }
} }
impl From<confy::ConfyError> for GcliError {
fn from(e: confy::ConfyError) -> GcliError {
GcliError::Anyhow(e.into())
}
}
impl From<std::io::Error> for GcliError {
fn from(error: std::io::Error) -> Self {
GcliError::IoError(error)
}
}
impl From<sea_orm::DbErr> for GcliError {
fn from(error: sea_orm::DbErr) -> Self {
GcliError::DatabaseError(error)
}
}