Newer
Older
mod display;
Nicolas80
committed
use crate::commands::cesium::compute_g1v1_public_key;
use crate::entities::vault_account;
use crate::entities::vault_account::{AccountTreeNode, ActiveModel, DbAccountId};
use crate::*;
use age::secrecy::Secret;
use sea_orm::ActiveValue::Set;
Nicolas80
committed
use sea_orm::{ConnectionTrait, TransactionTrait};
use sp_core::crypto::AddressUri;
use std::cell::RefCell;
Nicolas80
committed
use std::path::PathBuf;
use std::rc::Rc;
Nicolas80
committed
#[derive(Clone, Debug, clap::Parser)]
Nicolas80
committed
/// List available SS58 Addresses in the vault
Nicolas80
committed
#[clap(subcommand)]
List(ListChoice),
Nicolas80
committed
/// Use specific SS58 Address (changes the config Address)
Nicolas80
committed
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\
)]
Nicolas80
committed
Import {
/// Secret key format (substrate, seed, g1v1)
Nicolas80
committed
#[clap(short = 'S', long, required = false, default_value = SecretFormat::Substrate)]
secret_format: SecretFormat,
},
/// Add a derivation to an existing SS58 Address
#[clap(long_about = "Add a derivation to an existing SS58 Address.\n\
\n\
Nicolas80
committed
Only \"sr25519\" crypto scheme is supported for derivations.\n\
Nicolas80
committed
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.")]
Nicolas80
committed
#[clap(alias = "deriv")]
#[clap(alias = "derivation")]
Derive {
Nicolas80
committed
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
},
Nicolas80
committed
/// Give a meaningful name to an SS58 Address in the vault
Nicolas80
committed
Rename {
/// SS58 Address
Nicolas80
committed
address: AccountId,
},
/// Remove an SS58 Address from the vault together with its linked derivations
Nicolas80
committed
#[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")]
Nicolas80
committed
Remove {
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
},
/// Inspect a vault entry, retrieving its Substrate URI (will provide more data in a future version)
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"
)]
Nicolas80
committed
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"
)]
Nicolas80
committed
Migrate,
/// Show where vault db (or old keys) is stored
Where,
}
#[derive(Clone, Default, Debug, clap::Parser)]
pub enum ListChoice {
/// List all <Base> SS58 Addresses and their linked derivations in the vault
Nicolas80
committed
#[default]
All,
/// List <Base> and Derivation SS58 Addresses linked to the selected one
For {
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
},
/// List all <Base> SS58 Addresses in the vault
Base,
Nicolas80
committed
}
#[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')]
name: Option<String>,
}
Nicolas80
committed
pub struct VaultDataToImport {
secret_format: SecretFormat,
Nicolas80
committed
secret_suri: String,
Nicolas80
committed
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)
}
Nicolas80
committed
pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
Nicolas80
committed
let db = data.connect_db();
// match subcommand
match command {
Nicolas80
committed
Subcommand::List(choice) => match choice {
ListChoice::All => {
let all_account_tree_node_hierarchies =
vault_account::fetch_all_base_account_tree_node_hierarchies(db).await?;
let table =
display::compute_vault_accounts_table(&all_account_tree_node_hierarchies)?;
Nicolas80
committed
Nicolas80
committed
println!("available SS58 Addresses:");
Nicolas80
committed
println!("{table}");
}
ListChoice::Base => {
let base_account_tree_nodes =
vault_account::fetch_only_base_account_tree_nodes(db).await?;
Nicolas80
committed
let table = display::compute_vault_accounts_table(&base_account_tree_nodes)?;
Nicolas80
committed
println!("available <Base> SS58 Addresses:");
println!("{table}");
}
ListChoice::For {
address_or_vault_name,
} => {
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(&[base_account_tree_node])?;
println!(
"available SS58 Addresses linked to {}:",
account_tree_node.borrow().account
);
Nicolas80
committed
println!("{table}");
Nicolas80
committed
},
Subcommand::ListFiles => {
let vault_key_addresses = fetch_vault_key_addresses(&data).await?;
let table = display::compute_vault_key_files_table(&vault_key_addresses)?;
Nicolas80
committed
println!("available key files (needs to be migrated with command `vault migrate` in order to use them):");
Nicolas80
committed
println!("{table}");
Nicolas80
committed
Subcommand::Use {
address_or_vault_name,
} => {
let account = retrieve_vault_account(db, address_or_vault_name).await?;
Nicolas80
committed
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}");
}
Nicolas80
committed
Subcommand::Import { secret_format } => {
let vault_data_for_import =
prompt_secret_and_compute_vault_data_to_import(secret_format)?;
//Extra check for SecretFormat::G1v1 (old cesium) - showing the G1v1 cesium public key for confirmation
if secret_format == SecretFormat::G1v1 {
Nicolas80
committed
println!(
"The G1v1 public key for the provided secret is: '{}'",
compute_g1v1_public_key(&vault_data_for_import.key_pair)?
);
let confirmed = inputs::confirm_action("Is it the correct one (if not, you should try again to input G1v1 id/password) ?".to_string())?;
Nicolas80
committed
if !confirmed {
return Ok(());
}
}
Nicolas80
committed
let txn = db.begin().await?;
Nicolas80
committed
println!();
let _account =
create_base_account_for_vault_data_to_import(&txn, &vault_data_for_import, None)
.await?;
Nicolas80
committed
txn.commit().await?;
println!("Change done");
Nicolas80
committed
}
Subcommand::Derive {
Nicolas80
committed
address_or_vault_name,
} => {
let account_tree_node_to_derive =
retrieve_account_tree_node(db, address_or_vault_name).await?;
Nicolas80
committed
let account_to_derive = account_tree_node_to_derive.borrow().account.clone();
Nicolas80
committed
let base_account_tree_node =
vault_account::get_base_account_tree_node(&account_tree_node_to_derive);
Nicolas80
committed
let base_account = &base_account_tree_node.borrow().account.clone();
if base_account.crypto_scheme.is_none() {
return Err(GcliError::DatabaseError(DbErr::Custom(format!("Crypto scheme is not set for the base account:{base_account} - should never happen"))));
}
if let Some(crypto_scheme) = base_account.crypto_scheme {
if CryptoScheme::from(crypto_scheme) == CryptoScheme::Ed25519 {
println!(
Nicolas80
committed
"Only \"{}\" crypto scheme is supported for derivations.",
Into::<&str>::into(CryptoScheme::Sr25519),
);
println!();
println!(
Nicolas80
committed
"Use command `vault list base` to see available <Base> account and their crypto scheme\n\
And then use command 'vault list for' to find all accounts linked to that <Base> account"
);
return Ok(());
}
}
println!("Adding derivation to: {account_to_derive}");
Nicolas80
committed
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!("Its parent hierarchy is this:");
println!("{parent_hierarchy_table_account_to_derive}");
println!();
println!("The linked <Base> account is {base_account}");
println!("Enter password to decrypt the <Base> account key");
let password = inputs::prompt_password()?;
Nicolas80
committed
let account_to_derive_secret_suri = vault_account::compute_suri_account_tree_node(
&account_tree_node_to_derive,
password,
)?;
Nicolas80
committed
Nicolas80
committed
let derivation_path = inputs::prompt_vault_derivation_path()?;
let derivation_secret_suri =
format!("{account_to_derive_secret_suri}{derivation_path}");
Nicolas80
committed
let derivation_keypair =
compute_keypair(CryptoScheme::Sr25519, &derivation_secret_suri)?;
Nicolas80
committed
let derivation_address: String = derivation_keypair.address().to_string();
let txn = db.begin().await?;
Nicolas80
committed
println!();
let _derivation = create_derivation_account(
&txn,
&derivation_address,
&derivation_path,
&account_to_derive.address.to_string(),
)
.await?;
txn.commit().await?;
println!("Change done");
Nicolas80
committed
}
Subcommand::Rename { address } => {
let account =
vault_account::find_by_id(db, &DbAccountId::from(address.clone())).await?;
Nicolas80
committed
if account.is_none() {
Nicolas80
committed
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();
Nicolas80
committed
println!(
"Current name for address:'{address}' is {:?}",
&account.name
Nicolas80
committed
);
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?;
Nicolas80
committed
let _account = vault_account::update_account_name(db, account, name.as_ref()).await?;
println!("Rename done");
Nicolas80
committed
}
Subcommand::Remove {
address_or_vault_name,
} => {
let account_tree_node_to_delete =
retrieve_account_tree_node(db, address_or_vault_name).await?;
Nicolas80
committed
Nicolas80
committed
let txn = db.begin().await?;
Nicolas80
committed
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();
Nicolas80
committed
//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()])?;
Nicolas80
committed
println!("All addresses linked to: {account_to_delete}");
Nicolas80
committed
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
)
Nicolas80
committed
);
let confirmation_message = if account_to_delete.is_base_account() {
"Are you sure you want to delete it along with the saved key?"
};
let confirmed = inputs::confirm_action(confirmation_message.to_string())?;
Nicolas80
committed
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);
Nicolas80
committed
}
} else {
let delete_result = account_to_delete.delete(&txn).await?;
println!("Deleting {} address", delete_result.rows_affected);
Nicolas80
committed
}
txn.commit().await?;
println!("Done removing address:'{address_to_delete}'");
}
Subcommand::Inspect {
address_or_vault_name,
} => {
let account_tree_node_to_derive =
retrieve_account_tree_node(db, address_or_vault_name).await?;
println!("Enter password to decrypt the <Base> account key");
let password = inputs::prompt_password()?;
let account_to_derive_secret_suri = vault_account::compute_suri_account_tree_node(
&account_tree_node_to_derive,
password,
)?;
println!("Substrate URI: '{account_to_derive_secret_suri}'")
}
Nicolas80
committed
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)?;
Nicolas80
committed
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?;
Nicolas80
committed
if existing_account.is_some() {
Nicolas80
committed
//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 {
Nicolas80
committed
secret_format: vault_data_from_file.secret_format,
secret_suri: vault_data_from_file.secret,
Nicolas80
committed
key_pair: vault_data_from_file.key_pair,
};
Nicolas80
committed
let txn = db.begin().await?;
Nicolas80
committed
let account = create_base_account_for_vault_data_to_import(
Nicolas80
committed
&txn,
&vault_data_to_import,
Some(&vault_data_from_file.password),
Nicolas80
committed
)
Nicolas80
committed
match account {
Ok(_account) => {
txn.commit().await?;
println!("Change done");
}
Err(error) => {
println!("Error occurred: {error}");
println!("Continuing to next key");
}
}
Nicolas80
committed
}
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,
Nicolas80
committed
) -> 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);
Nicolas80
committed
Ok((prefix.to_string(), Some(derivation_path.to_string())))
Nicolas80
committed
} else {
Nicolas80
committed
}
}
/// 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) -> CryptoScheme {
Nicolas80
committed
match secret_format {
SecretFormat::Seed => CryptoScheme::Sr25519,
SecretFormat::Substrate => CryptoScheme::Sr25519,
SecretFormat::Predefined => CryptoScheme::Sr25519,
SecretFormat::G1v1 => CryptoScheme::Ed25519,
Nicolas80
committed
}
}
/// 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"
)]
Nicolas80
committed
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());
Nicolas80
committed
}
});
Ok(vault_key_addresses)
}
pub async fn retrieve_vault_account_for_name<C>(
name_input: &String,
) -> Result<vault_account::Model, GcliError>
let account_tree_node = retrieve_account_tree_node_for_name(db, name_input).await?;
Nicolas80
committed
//Need this extra step to avoid borrowing issues
let account = account_tree_node.borrow().account.clone();
Ok(account)
Nicolas80
committed
}
pub async fn retrieve_account_tree_node<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.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(),
);
Nicolas80
committed
Nicolas80
committed
} else {
//Should never happen since clap enforces exactly one of the 2 options
return Err(GcliError::Input("No address or name provided".to_string()));
};
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())?;
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
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>(
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();
Nicolas80
committed
}
fn create_vault_data_to_import<F, P>(
secret_format: SecretFormat,
prompt_fn: F,
) -> Result<VaultDataToImport, GcliError>
where
Nicolas80
committed
F: Fn() -> (String, P),
Nicolas80
committed
P: Into<KeyPair>,
{
let (secret, pair) = prompt_fn();
let key_pair = pair.into();
Ok(VaultDataToImport {
secret_format,
Nicolas80
committed
secret_suri: secret,
Nicolas80
committed
key_pair,
})
}
fn prompt_secret_and_compute_vault_data_to_import(
secret_format: SecretFormat,
) -> Result<VaultDataToImport, GcliError> {
match secret_format {
SecretFormat::Substrate => {
create_vault_data_to_import(secret_format, prompt_secret_substrate_and_compute_keypair)
}
SecretFormat::Seed => {
create_vault_data_to_import(secret_format, prompt_seed_and_compute_keypair)
}
SecretFormat::G1v1 => {
Nicolas80
committed
create_vault_data_to_import(secret_format, prompt_secret_cesium_and_compute_keypair)
}
SecretFormat::Predefined => {
create_vault_data_to_import(secret_format, 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.
Nicolas80
committed
///
/// 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,
Nicolas80
committed
vault_data: &VaultDataToImport,
password: Option<&String>,
) -> Result<vault_account::Model, GcliError>
Nicolas80
committed
where
C: ConnectionTrait,
{
Nicolas80
committed
let address_to_import = vault_data.key_pair.address().to_string();
println!("Trying to import for SS58 address :'{}'", address_to_import);
println!();
let vault_account = if let Some(existing_vault_account) =
vault_account::find_by_id(db_tx, &DbAccountId::from(address_to_import.clone())).await?
Nicolas80
committed
{
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!("{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)");
}
Nicolas80
committed
let result = inputs::select_action("Your choice?", vec!["1", "2"])?;
match result {
"2" => {
let encrypted_suri =
compute_encrypted_suri(password, vault_data.secret_suri.clone())?;
Nicolas80
committed
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
println!(
"(Optional) Enter a name for the vault entry (leave empty to remove the name)"
);
let name = inputs::prompt_vault_name_and_check_availability(
db_tx,
existing_vault_account.name.as_ref(),
)
.await?;
// 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).into(),
));
vault_account.encrypted_suri = Set(Some(encrypted_suri));
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("import canceled".into()));
}
}
} else {
//New entry
let secret_format = vault_data.secret_format;
let encrypted_suri = compute_encrypted_suri(password, vault_data.secret_suri.clone())?;
Nicolas80
committed
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
println!("(Optional) Enter a name for the vault entry");
let name = inputs::prompt_vault_name_and_check_availability(db_tx, None).await?;
let crypto_scheme = map_secret_format_to_crypto_scheme(secret_format);
let base_account = vault_account::create_base_account(
db_tx,
&address_to_import,
name.as_ref(),
crypto_scheme,
encrypted_suri,
)
.await?;
println!("Creating <Base> account {base_account}");
base_account
};
Ok(vault_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,
) -> 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!();
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
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" => {
println!(
"(Optional) Enter a name for the vault entry (leave empty to remove the name)"
);
let name = inputs::prompt_vault_name_and_check_availability(
db_tx,
existing_vault_account.name.as_ref(),
)
.await?;
// 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 {
println!("(Optional) Enter a name for the vault entry");
let name = 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 ask for password if not present and compute the encrypted suri
fn compute_encrypted_suri(
password: Option<&String>,
secret_suri: String,
) -> Result<Vec<u8>, GcliError> {
let password = match password.cloned() {
Some(password) => password,
_ => {
println!("Enter password to protect the key");
inputs::prompt_password_confirm()?
}
};
Nicolas80
committed
Ok(encrypt(secret_suri.as_bytes(), password).map_err(|e| anyhow!(e))?)
Nicolas80
committed
}
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);
Nicolas80
committed
return Ok(Some(path));
}
Ok(None)
}
/// try to get secret in keystore, prompt for the password and compute the keypair
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(),
)