Newer
Older
Nicolas80
committed
use crate::commands::cesium::compute_g1v1_public_key;
Nicolas80
committed
use crate::entities::vault_account::CryptoType;
use crate::entities::{vault_account, vault_derivation};
use crate::*;
use age::secrecy::Secret;
Nicolas80
committed
use comfy_table::{Cell, Table};
use sea_orm::ActiveValue::Set;
use sea_orm::{ActiveModelTrait, EntityTrait, ModelTrait};
use sea_orm::{ColumnTrait, QueryFilter};
use sea_orm::{ConnectionTrait, TransactionTrait};
Nicolas80
committed
use std::path::PathBuf;
/// define universal dividends subcommands
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,
Nicolas80
committed
/// Import key from (substrate)mnemonic or other format with interactive prompt
#[clap(
long_about = "Import key from (substrate)mnemonic or other format with interactive prompt\n\
\n\
Nicolas80
committed
If a (substrate)mnemonic is provided with a derivation path, it will ensure the base <Account>\n\
and associated SS58 Address exists before creating the derivation; but please use command \n\
`vault derivation|derive|deriv` to add a derivation to an existing <Account> instead."
)]
Nicolas80
committed
Import {
/// Secret key format (substrate, seed, cesium)
#[clap(short = 'S', long, required = false, default_value = SecretFormat::Substrate)]
secret_format: SecretFormat,
},
Nicolas80
committed
/// Add a derivation to an existing <Account>
#[clap(long_about = "Add a derivation to an existing <Account>\n\
\n\
Only \"substrate\" and \"seed\" format are supported for derivations\n\
Nicolas80
committed
Use command `vault list account` to see available <Account> and their format")]
Nicolas80
committed
#[clap(alias = "deriv")]
#[clap(alias = "derive")]
Derivation {
#[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,
},
Nicolas80
committed
/// Remove an SS58 Address from the vault
#[clap(long_about = "Remove an SS58 Address from the vault\n\
\n\
If an <Account> Address is given it will also remove all linked derivations")]
Nicolas80
committed
Remove {
#[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)
Nicolas80
committed
ListFiles,
/// (deprecated)Migrate old key files into db (will have to provide password for each key)
Migrate,
/// Show where vault db (or old keys) is stored
Where,
}
#[derive(Clone, Default, Debug, clap::Parser)]
pub enum ListChoice {
Nicolas80
committed
/// List all <Account> and their linked derivations SS58 Addresses in the vault
Nicolas80
committed
#[default]
All,
Nicolas80
committed
/// List <Account> and derivations SS58 Addresses linked to the selected one
For {
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
},
Nicolas80
committed
/// List all <Account> SS58 Addresses in the vault
Account,
Nicolas80
committed
}
pub struct VaultDataToImport {
secret_format: SecretFormat,
Nicolas80
committed
secret_suri: String,
Nicolas80
committed
key_pair: KeyPair,
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
}
// encrypt input with passphrase
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
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 ud commands
Nicolas80
committed
pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
// match subcommand
match command {
Nicolas80
committed
Subcommand::List(choice) => match choice {
ListChoice::All => {
let derivations = vault_derivation::list_all_derivations_in_order(
data.connection.as_ref().unwrap(),
)
.await?;
let table = compute_vault_derivations_table(
data.connection.as_ref().unwrap(),
&derivations,
)
.await?;
Nicolas80
committed
Nicolas80
committed
println!("available SS58 Addresses:");
Nicolas80
committed
println!("{table}");
}
Nicolas80
committed
ListChoice::Account => {
Nicolas80
committed
let derivations = vault_derivation::list_all_root_derivations_in_order(
data.connection.as_ref().unwrap(),
)
.await?;
let table = compute_vault_derivations_table(
data.connection.as_ref().unwrap(),
&derivations,
)
.await?;
Nicolas80
committed
Nicolas80
committed
println!("available <Account> SS58 Addresses:");
println!("{table}");
}
ListChoice::For {
address_or_vault_name,
} => {
Nicolas80
committed
let selected_derivation =
retrieve_vault_derivation(&data, address_or_vault_name).await?;
let linked_derivations = vault_derivation::fetch_all_linked_derivations_in_order(
data.connection.as_ref().unwrap(),
Nicolas80
committed
&selected_derivation.root_address,
)
.await?;
let table = compute_vault_derivations_table(
data.connection.as_ref().unwrap(),
&linked_derivations,
)
.await?;
Nicolas80
committed
println!("available SS58 Addresses linked to {selected_derivation}:");
Nicolas80
committed
println!("{table}");
Nicolas80
committed
},
Subcommand::ListFiles => {
let vault_key_addresses = fetch_vault_key_addresses(&data).await?;
let table = compute_vault_key_files_table(&vault_key_addresses).await?;
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 derivation = retrieve_vault_derivation(&data, address_or_vault_name).await?;
//FIXME not sure if this is ok (but since it's a CLI; this data instance won't be used afterwards)
let mut data = data;
data.cfg.address =
Some(AccountId::from_str(&derivation.address).expect("invalid address"));
Nicolas80
committed
conf::save_config(&data);
}
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)?;
Nicolas80
committed
//Extra check for SecretFormat::Cesium / G1v1Seed - showing the G1v1 cesium public key for confirmation
if secret_format == SecretFormat::Cesium {
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 Cesium id/password) ?".to_string())?;
if !confirmed {
return Ok(());
}
}
Nicolas80
committed
let address_to_import = vault_data_for_import.key_pair.address().to_string();
Nicolas80
committed
Nicolas80
committed
println!("Trying to import for SS58 address :'{}'", address_to_import);
Nicolas80
committed
if let Some(derivation) = vault_derivation::Entity::find_by_id(&address_to_import)
.one(data.connection.as_ref().unwrap())
.await?
Nicolas80
committed
{
println!(
"Vault entry already exists for that address: {}",
derivation
Nicolas80
committed
);
let linked_derivations = vault_derivation::fetch_all_linked_derivations_in_order(
data.connection.as_ref().unwrap(),
&derivation.root_address.clone(),
)
.await?;
println!("Here are all the SS58 Addresses linked to it in the vault:");
Nicolas80
committed
let table = compute_vault_derivations_table(
data.connection.as_ref().unwrap(),
&linked_derivations,
)
.await?;
Nicolas80
committed
println!("{table}");
return Ok(());
}
println!("Enter password to protect the key");
Nicolas80
committed
let password = inputs::prompt_password_confirm()?;
println!("(Optional) Enter a name for the vault entry");
let name = inputs::prompt_vault_name()?;
let txn = data.connection.as_ref().unwrap().begin().await?;
let _derivation = create_derivation_for_vault_data_to_import(
&txn,
&vault_data_for_import,
&password,
name.as_ref(),
)
.await?;
txn.commit().await?;
println!("Import done");
}
Subcommand::Derivation {
address_or_vault_name,
} => {
let root_derivation = retrieve_vault_derivation(&data, address_or_vault_name).await?;
if root_derivation.path.is_some() {
println!("Can only add derivation on an <Account>");
Nicolas80
committed
println!(
"The selected address:'{}' already has an <Account> with address:'{}'",
Nicolas80
committed
root_derivation.address, root_derivation.root_address
);
println!("You can check for available <Account> addresses with command 'vault list account'");
Nicolas80
committed
return Ok(());
}
let vault_account = vault_account::Entity::find_by_id(&root_derivation.address)
.one(data.connection.as_ref().unwrap())
.await?
.ok_or(GcliError::Input(format!(
"Could not find vault_account for address:'{}'",
Nicolas80
committed
root_derivation.address
)))?;
Nicolas80
committed
if vault_account.crypto_type == CryptoType::G1v1Seed {
println!(
"Only \"{}\" and \"{}\" format are supported for derivations",
Into::<&str>::into(SecretFormat::Substrate),
Into::<&str>::into(SecretFormat::Seed)
);
println!(
"Use command `vault list account` to see available <Account> and their format"
);
return Ok(());
}
println!("Adding derivation to: {root_derivation}");
Nicolas80
committed
println!("Enter password to decrypt the <Account> key");
Nicolas80
committed
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
let root_secret_suri = retrieve_suri_from_vault_account(&vault_account)?;
let derivation_path = inputs::prompt_vault_derivation_path()?;
let derivation_secret_suri = format!("{root_secret_suri}{derivation_path}");
let derivation_keypair =
compute_keypair(vault_account.crypto_type, &derivation_secret_suri)?;
let derivation_address: String = derivation_keypair.address().to_string();
let check_derivation = vault_derivation::Entity::find_by_id(&derivation_address)
.one(data.connection.as_ref().unwrap())
.await?;
if check_derivation.is_some() {
println!("Derivation already exists for address:'{derivation_address}'");
return Ok(());
}
println!("(Optional) Enter a name for the new derivation");
let name = inputs::prompt_vault_name()?;
let derivation = vault_derivation::ActiveModel {
address: Set(derivation_address),
name: Set(name),
path: Set(Some(derivation_path)),
root_address: Set(root_derivation.root_address.clone()),
};
let derivation = derivation.insert(data.connection.as_ref().unwrap()).await?;
Nicolas80
committed
}
Subcommand::Rename { address } => {
let derivation = vault_derivation::Entity::find_by_id(address.to_string())
.one(data.connection.as_ref().unwrap())
.await?;
if derivation.is_none() {
println!("No vault entry found for address:'{address}'");
println!("You might want to import it first with 'vault import'");
return Ok(());
}
let derivation = derivation.unwrap();
println!(
"Current name for address:'{address}' is {:?}",
derivation.name
);
println!("Enter new name for address (leave empty to remove the name)");
Nicolas80
committed
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
let name = inputs::prompt_vault_name()?;
let old_name = derivation.name.clone();
let mut derivation: vault_derivation::ActiveModel = derivation.into();
derivation.name = Set(name.clone());
let _derivation = derivation.update(data.connection.as_ref().unwrap()).await?;
println!(
"Renamed address:'{address}' from {:?} to {:?}",
old_name, name
);
}
Subcommand::Remove {
address_or_vault_name,
} => {
let derivation = retrieve_vault_derivation(&data, address_or_vault_name).await?;
let address_to_delete = derivation.address.clone();
let txn = data.connection.as_ref().unwrap().begin().await?;
//If deleting a root derivation; also delete the vault account and all linked derivations
if derivation.path.is_none() {
let all_derivations_to_delete =
vault_derivation::fetch_all_linked_derivations_in_order(
&txn,
&address_to_delete,
)
.await?;
let table =
compute_vault_derivations_table(&txn, &all_derivations_to_delete).await?;
Nicolas80
committed
println!("All addresses linked to: {derivation}");
Nicolas80
committed
println!("{table}");
println!(
"This <Account> has {} addresses in total",
Nicolas80
committed
all_derivations_to_delete.len()
);
let confirmed = inputs::confirm_action(
"Are you sure you want to delete it along with the saved key ?".to_string(),
)?;
Nicolas80
committed
if !confirmed {
return Ok(());
}
for derivation_to_delete in all_derivations_to_delete {
let delete_result = derivation_to_delete.delete(&txn).await?;
println!("Deleted {} address", delete_result.rows_affected);
Nicolas80
committed
}
let delete_result = vault_account::Entity::delete_by_id(&address_to_delete)
.exec(&txn)
.await?;
println!("Deleted {} vault account", delete_result.rows_affected);
} else {
let delete_result = derivation.delete(&txn).await?;
println!("Deleted {} address", delete_result.rows_affected);
Nicolas80
committed
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
}
txn.commit().await?;
println!("Done removing address:'{address_to_delete}'");
}
Subcommand::Migrate => {
println!("Migrating existing key files to db");
let vault_key_addresses = fetch_vault_key_addresses(&data).await?;
let table = compute_vault_key_files_table(&vault_key_addresses).await?;
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 derivation = vault_derivation::Entity::find_by_id(&address)
.one(data.connection.as_ref().unwrap())
.await?;
if derivation.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 {
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,
};
let txn = data.connection.as_ref().unwrap().begin().await?;
let derivation = create_derivation_for_vault_data_to_import(
&txn,
&vault_data_to_import,
&vault_data_from_file.password,
None,
)
.await?;
txn.commit().await?;
println!("Import done: {}", derivation);
}
println!("Migration done");
}
Subcommand::Where => {
println!("{}", data.project_dir.data_dir().to_str().unwrap());
}
};
Ok(())
}
Nicolas80
committed
fn parse_prefix_and_derivation_path_from_string(
raw_string: String,
) -> Result<(String, Option<String>), GcliError> {
if raw_string.contains("/") {
raw_string
.find("/")
.map_or(Err(GcliError::Input("Invalid format".to_string())), |idx| {
let (prefix, derivation_path) = raw_string.split_at(idx);
Ok((prefix.to_string(), Some(derivation_path.to_string())))
})
} else {
Ok((raw_string, None))
}
}
fn map_secret_format_to_crypto_type(secret_format: SecretFormat) -> CryptoType {
match secret_format {
Nicolas80
committed
SecretFormat::Seed => vault_account::CryptoType::EntropyKdfSeed,
SecretFormat::Substrate => vault_account::CryptoType::Bip39Mnemonic,
SecretFormat::Predefined => vault_account::CryptoType::Bip39Mnemonic,
SecretFormat::Cesium => vault_account::CryptoType::G1v1Seed,
Nicolas80
committed
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
}
}
/// This method will scan files in the data directory and return the addresses of the vault keys found
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();
// To only keep the address part of the filename for names like "<ss58 address>-<secret_format>"
let potential_address = filename.split("-").next().unwrap();
// If potential_address is a valid AccountId
if AccountId::from_str(potential_address).is_ok() {
vault_key_addresses.push(potential_address.to_string());
}
});
Ok(vault_key_addresses)
}
async 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)
}
async fn compute_vault_derivations_table<C>(
db: &C,
Nicolas80
committed
derivations_ordered: &[vault_derivation::Model],
) -> Result<Table, GcliError>
where
C: ConnectionTrait,
{
Nicolas80
committed
let mut table = Table::new();
table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY);
Nicolas80
committed
table.set_header(vec!["SS58 Address", "Format", "Account/Path", "Name"]);
let empty_string = "".to_string();
Nicolas80
committed
let root_path = "<Account>".to_string();
Nicolas80
committed
let mut current_root_address = "".to_string();
let mut current_root_name: Option<String> = None;
let mut current_vault_format: Option<&str> = None;
Nicolas80
committed
for derivation in derivations_ordered {
if derivation.root_address != current_root_address {
// First entry when changing root address should be an account ("root" derivation)
Nicolas80
committed
if derivation.path.is_some() {
return Err(GcliError::Input(
"Order of derivations parameter is wrong".to_string(),
));
}
current_root_address = derivation.root_address.clone();
current_root_name = derivation.name.clone();
let vault_account = vault_account::Entity::find_by_id(current_root_address.clone())
.one(db)
.await?
.ok_or(GcliError::Input(format!(
Nicolas80
committed
"No vault <Account> found with address:'{current_root_address}'"
)))?;
current_vault_format = match vault_account.crypto_type {
Nicolas80
committed
CryptoType::Bip39Mnemonic => Some(SecretFormat::Substrate.into()),
CryptoType::EntropyKdfSeed => Some(SecretFormat::Seed.into()),
CryptoType::G1v1Seed => Some(SecretFormat::Cesium.into()),
};
Nicolas80
committed
}
let address = if derivation.path.is_none() {
derivation.address.clone()
} else {
" ".to_string() + &derivation.address
};
let (path, format) = if derivation.path.is_none() {
(root_path.clone(), current_vault_format.unwrap())
Nicolas80
committed
} else {
(derivation.path.clone().unwrap(), empty_string.as_str())
Nicolas80
committed
};
let name = if derivation.name.is_none() {
if derivation.path.is_none() {
empty_string.clone()
Nicolas80
committed
} else if let Some(current_root_name) = ¤t_root_name {
format!(
"<{}{}>",
current_root_name,
derivation.path.clone().unwrap()
)
} else {
empty_string.clone()
Nicolas80
committed
}
} else {
derivation.name.clone().unwrap()
};
table.add_row(vec![
Cell::new(&address),
Cell::new(format),
Nicolas80
committed
Cell::new(&path),
Cell::new(&name),
]);
}
Ok(table)
}
pub async fn retrieve_address_string<T: AddressOrVaultName>(
Nicolas80
committed
address_or_vault_name: T,
) -> Result<String, GcliError> {
if let Some(address) = address_or_vault_name.address() {
return Ok(address.to_string());
}
let derivation = retrieve_vault_derivation(data, address_or_vault_name).await?;
Ok(derivation.address)
}
pub async fn retrieve_vault_derivation<T: AddressOrVaultName>(
data: &Data,
address_or_vault_name: T,
) -> Result<vault_derivation::Model, GcliError> {
let derivation = if let Some(name) = address_or_vault_name.name() {
let (name, derivation_path_opt) =
parse_prefix_and_derivation_path_from_string(name.to_string())?;
let derivation = vault_derivation::Entity::find()
.filter(vault_derivation::Column::Name.eq(Some(name.clone())))
.one(data.connection.as_ref().unwrap())
.await?;
let derivation = derivation.ok_or(GcliError::Input(format!(
Nicolas80
committed
"No vault SS58 Address found with name:'{name}'"
Nicolas80
committed
)))?;
match derivation_path_opt {
None => derivation,
Some(path) => {
let sub_derivation = vault_derivation::Entity::find()
.filter(
vault_derivation::Column::RootAddress.eq(derivation.root_address.clone()),
)
.filter(vault_derivation::Column::Path.eq(Some(path.clone())))
.one(data.connection.as_ref().unwrap())
.await?;
sub_derivation.ok_or(GcliError::Input(format!(
Nicolas80
committed
"No vault derivation found with <Account> name:'{name}' and path:'{path}'"
Nicolas80
committed
)))?
}
}
} else if let Some(address) = address_or_vault_name.address() {
let derivation = vault_derivation::Entity::find_by_id(address.to_string())
.one(data.connection.as_ref().unwrap())
.await?;
derivation.ok_or(GcliError::Input(format!(
Nicolas80
committed
"No vault entry found with Address:'{address}'"
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()));
};
Ok(derivation)
}
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
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
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::Cesium => {
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 derivation and if necessary root vault account and root derivation
///
/// Does it all using "db" parameter that should better be a transaction since multiple operations can be done
pub async fn create_derivation_for_vault_data_to_import<C>(
db: &C,
vault_data: &VaultDataToImport,
password: &str,
name: Option<&String>,
) -> Result<vault_derivation::Model, GcliError>
where
C: ConnectionTrait,
{
Nicolas80
committed
let address_to_import = vault_data.key_pair.address().to_string();
Nicolas80
committed
//To be safe
Nicolas80
committed
if vault_derivation::Entity::find_by_id(&address_to_import)
Nicolas80
committed
.one(db)
.await?
.is_some()
{
return Err(GcliError::Input(format!(
"Vault entry already exists for address {}",
Nicolas80
committed
&address_to_import
Nicolas80
committed
)));
}
let secret_format = vault_data.secret_format;
let (root_secret_suri, derivation_path_opt, root_address, derivation_address) =
Nicolas80
committed
compute_root_and_derivation_data(&secret_format, vault_data.secret_suri.clone())?;
Nicolas80
committed
// Making sure the computed address is the same as the address to import
if let Some(derivation_address) = &derivation_address {
if *derivation_address != address_to_import {
return Err(GcliError::Input(format!(
"Derivation address {} does not match the expected address {}",
derivation_address, address_to_import
)));
}
} else if root_address != address_to_import {
return Err(GcliError::Input(format!(
"Derivation address {} does not match the expected address {}",
root_address, address_to_import
)));
}
Nicolas80
committed
let encrypted_suri =
Nicolas80
committed
encrypt(root_secret_suri.as_bytes(), password.to_string()).map_err(|e| anyhow!(e))?;
let crypto_type = map_secret_format_to_crypto_type(secret_format);
let _root_account =
vault_account::create_vault_account(db, &root_address, crypto_type, encrypted_suri).await?;
Nicolas80
committed
let derivation = if let Some(derivation_path) = derivation_path_opt {
777
778
779
780
781
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
let derivation_address = derivation_address.unwrap();
// Extra check of derivation path to make sure it's not linking to the same SS58 Address as root
if root_address == derivation_address {
println!("Derivation path provided:'{derivation_path}' linked to the same SS58 Address than the base suri without derivation");
let root_derivation =
vault_derivation::create_root_vault_derivation(db, &root_address, name).await?;
println!("For that reason only the base suri was imported");
println!("Created: {}", root_derivation);
root_derivation
} else {
let _root_derivation =
vault_derivation::create_root_vault_derivation(db, &root_address, None).await?;
// Compute derivation !
let derivation = vault_derivation::ActiveModel {
address: Set(derivation_address.clone()),
name: Set(name.cloned()),
path: Set(Some(derivation_path)),
root_address: Set(root_address.clone()),
};
let derivation = derivation.insert(db).await?;
println!("Created: {}", derivation);
derivation
}
Nicolas80
committed
} else {
let derivation =
vault_derivation::create_root_vault_derivation(db, &root_address, name).await?;
println!("Created: {}", derivation);
Nicolas80
committed
811
812
813
814
815
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
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
derivation
};
Ok(derivation)
}
fn compute_root_and_derivation_data(
secret_format: &SecretFormat,
secret_suri: String,
) -> Result<(String, Option<String>, String, Option<String>), GcliError> {
let (root_secret_suri, derivation_path_opt) =
parse_prefix_and_derivation_path_from_string(secret_suri)?;
let (root_address, derivation_address_opt) = match &secret_format {
SecretFormat::Cesium => match &derivation_path_opt {
None => {
let root_suri = &root_secret_suri;
let root_pair = pair_from_ed25519_str(root_suri)?;
let root_address: AccountId = root_pair.public().into();
(root_address.to_string(), None)
}
Some(derivation_path) => {
let root_suri = &root_secret_suri;
let root_pair = pair_from_ed25519_str(root_suri)?;
let root_address: AccountId = root_pair.public().into();
let derivation_suri = root_suri.clone() + derivation_path;
let derivation_pair = pair_from_ed25519_str(&derivation_suri)?;
let derivation_address: AccountId = derivation_pair.public().into();
(
root_address.to_string(),
Some(derivation_address.to_string()),
)
}
},
SecretFormat::Substrate | SecretFormat::Seed | SecretFormat::Predefined => {
match &derivation_path_opt {
None => {
let root_suri = &root_secret_suri;
let root_pair = pair_from_sr25519_str(root_suri)?;
let root_address: AccountId = root_pair.public().into();
(root_address.to_string(), None)
}
Some(derivation_path) => {
let root_suri = &root_secret_suri;
let root_pair = pair_from_sr25519_str(root_suri)?;
let root_address: AccountId = root_pair.public().into();
let derivation_suri = root_suri.clone() + derivation_path;
let derivation_pair = pair_from_sr25519_str(&derivation_suri)?;
let derivation_address: AccountId = derivation_pair.public().into();
(
root_address.to_string(),
Some(derivation_address.to_string()),
)
}
}
}
};
Ok((
root_secret_suri,
derivation_path_opt,
root_address,
derivation_address_opt,
))
}
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
890
891
892
893
894
895
896
897
898
899
900
901
902
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
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(derivation) = vault_derivation::Entity::find_by_id(address.to_string())
.one(data.connection.as_ref().unwrap())
.await?
{
if let Some(vault_account) =
vault_account::Entity::find_by_id(derivation.root_address.clone())
.one(data.connection.as_ref().unwrap())
.await?
{
let root_secret_suri = retrieve_suri_from_vault_account(&vault_account)?;
let secret_suri = if let Some(derivation_path) = derivation.path {
format!("{root_secret_suri}{derivation_path}")
} else {
root_secret_suri
};
let key_pair = compute_keypair(vault_account.crypto_type, &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)
}
} else {
Ok(None)
}
}
pub fn retrieve_suri_from_vault_account(
vault_account: &vault_account::Model,
) -> Result<String, GcliError> {
let password = inputs::prompt_password()?;
Nicolas80
committed
let cypher = &vault_account.encrypted_suri;
Nicolas80
committed
let secret_vec =
decrypt(cypher, password.clone()).map_err(|e| GcliError::Input(e.to_string()))?;
let secret_suri = String::from_utf8(secret_vec).map_err(|e| anyhow!(e))?;
Ok(secret_suri)
}
pub fn compute_keypair(crypto_type: CryptoType, secret_suri: &str) -> Result<KeyPair, GcliError> {
let key_pair = match crypto_type {
Nicolas80
committed
CryptoType::Bip39Mnemonic | CryptoType::EntropyKdfSeed => {
Nicolas80
committed
pair_from_sr25519_str(secret_suri)?.into()
}
Nicolas80
committed
CryptoType::G1v1Seed => pair_from_ed25519_str(secret_suri)?.into(),
Nicolas80
committed
};
Ok(key_pair)
}
pub struct VaultDataFromFile {
secret_format: SecretFormat,
secret: String,
Nicolas80
committed
#[allow(dead_code)]
Nicolas80
committed
path: PathBuf,
password: String,
key_pair: KeyPair,
}
/// try to get secret in keystore, prompt for the password and compute the keypair
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 = rpassword::prompt_password("Password: ")?;
Nicolas80
committed
let mut file = std::fs::OpenOptions::new().read(true).open(path.clone())?;
let mut cypher = vec![];
file.read_to_end(&mut cypher)?;
Nicolas80
committed
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)
}
}
Nicolas80
committed
#[cfg(test)]
mod tests {