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

Target

Select target project
  • clients/rust/gcli-v2s
  • d0p1/gcli-v2s
  • flebon/gcli-v2s
  • zicmama/gcli-v2s
  • Nicolas80/gcli-v2s
5 results
Show changes
use crate::*;
use sp_core::{crypto::AccountId32, sr25519::Pair};
use subxt::tx::{BaseExtrinsicParamsBuilder, PairSigner};
#[cfg(any(feature = "dev", feature = "gdev"))] // find how to get runtime calls
type Call = runtime::runtime_types::gdev_runtime::RuntimeCall;
type BalancesCall = runtime::runtime_types::pallet_balances::pallet::Call;
/// transfer balance to target
pub async fn transfer(
pair: Pair,
client: &Client,
data: &Data,
balance: u64,
dest: AccountId32,
dest: AccountId,
keep_alive: bool,
is_ud: bool,
) -> Result<(), subxt::Error> {
let progress = if keep_alive {
client
.tx()
.sign_and_submit_then_watch(
&runtime::tx().balances().transfer(dest.into(), balance),
&PairSigner::new(pair),
BaseExtrinsicParamsBuilder::new(),
)
.await?
} else {
client
.tx()
.sign_and_submit_then_watch(
match (keep_alive, is_ud) {
(true, false) => {
submit_call_and_look_event::<
runtime::balances::events::Transfer,
StaticPayload<runtime::balances::calls::types::TransferKeepAlive>,
>(
data,
&runtime::tx()
.balances()
.transfer_keep_alive(dest.into(), balance),
&PairSigner::new(pair),
BaseExtrinsicParamsBuilder::new(),
)
.await?
};
let events = track_progress(progress).await?;
if let Some(e) = events.find_first::<runtime::balances::events::Transfer>()? {
println!("{e:?}");
.await
}
(false, false) => {
submit_call_and_look_event::<
runtime::balances::events::Transfer,
StaticPayload<runtime::balances::calls::types::TransferAllowDeath>,
>(
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
pub async fn transfer_multiple(
pair: Pair,
client: &Client,
data: &Data,
amount: u64,
dests: Vec<AccountId32>,
dests: Vec<AccountId>,
) -> Result<(), subxt::Error> {
// build the list of transactions from the destination accounts
let transactions: Vec<Call> = dests
......@@ -60,21 +80,10 @@ pub async fn transfer_multiple(
})
})
.collect();
// wrap these calls in a batch call
let progress = client
.tx()
.sign_and_submit_then_watch(
&runtime::tx().utility().batch(transactions),
&PairSigner::new(pair.clone()),
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(())
submit_call_and_look_event::<
runtime::utility::events::BatchCompleted,
StaticPayload<runtime::utility::calls::types::Batch>,
>(data, &runtime::tx().utility().batch(transactions))
.await
}
......@@ -5,18 +5,17 @@ use crate::*;
pub enum Subcommand {
#[default]
/// Claim uds
ClaimUds,
Claim,
}
/// 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
let mut data = data.build_client().await?;
let data = data.build_client().await?.fetch_system_properties().await?;
// match subcommand
match command {
Subcommand::ClaimUds => {
data = data.build_keypair();
claim_ud(data).await?;
Subcommand::Claim => {
claim_ud(&data).await?;
}
};
......@@ -24,21 +23,10 @@ pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<(
}
/// claim universal dividend
pub async fn claim_ud(data: Data) -> Result<(), anyhow::Error> {
let progress = data
.client()
.tx()
.sign_and_submit_then_watch(
&runtime::tx().universal_dividend().claim_uds(),
&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(())
pub async fn claim_ud(data: &Data) -> Result<(), subxt::Error> {
submit_call_and_look_event::<
runtime::universal_dividend::events::UdsClaimed,
StaticPayload<runtime::universal_dividend::calls::types::ClaimUds>,
>(data, &runtime::tx().universal_dividend().claim_uds())
.await
}
mod display;
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;
use sea_orm::{ConnectionTrait, TransactionTrait};
use sea_orm::{DbErr, ModelTrait};
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,
},
/// Add a derivation to an existing SS58 Address
#[clap(long_about = "Add a derivation to an existing SS58 Address.\n\
\n\
Only \"sr25519\" crypto scheme is supported for derivations.\n\
\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,
},
/// 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 (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"
)]
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,
}
#[derive(Clone, Default, Debug, clap::Parser)]
pub enum ListChoice {
/// List all <Base> SS58 Addresses and their linked derivations in the vault
#[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,
}
#[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>,
}
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 => {
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)?;
println!("available SS58 Addresses:");
println!("{table}");
}
ListChoice::Base => {
let base_account_tree_nodes =
vault_account::fetch_only_base_account_tree_nodes(db).await?;
let table = display::compute_vault_accounts_table(&base_account_tree_nodes)?;
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
);
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 } => {
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 {
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())?;
if !confirmed {
return Ok(());
}
}
let txn = db.begin().await?;
println!();
let _account =
create_base_account_for_vault_data_to_import(&txn, &vault_data_for_import, None)
.await?;
txn.commit().await?;
println!("Change done");
}
Subcommand::Derive {
address_or_vault_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();
if base_account.crypto_scheme.is_none() {
return Err(GcliError::DatabaseError(DbErr::Custom(format!("Crypto scheme is not set for the base account:{base_account} - should never happen"))));
}
if let Some(crypto_scheme) = base_account.crypto_scheme {
if CryptoScheme::from(crypto_scheme) == CryptoScheme::Ed25519 {
println!(
"Only \"{}\" crypto scheme is supported for derivations.",
Into::<&str>::into(CryptoScheme::Sr25519),
);
println!();
println!(
"Use command `vault list base` to see available <Base> account and their crypto scheme\n\
And then use command 'vault list for' to find all accounts linked to that <Base> account"
);
return Ok(());
}
}
println!("Adding derivation to: {account_to_derive}");
let base_parent_hierarchy_account_tree_node_to_derive =
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}");
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!();
let derivation_path = inputs::prompt_vault_derivation_path()?;
let derivation_secret_suri =
format!("{account_to_derive_secret_suri}{derivation_path}");
let derivation_keypair =
compute_keypair(CryptoScheme::Sr25519, &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(),
)
.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_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}'")
}
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?;
let account = create_base_account_for_vault_data_to_import(
&txn,
&vault_data_to_import,
Some(&vault_data_from_file.password),
)
.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) -> CryptoScheme {
match secret_format {
SecretFormat::Seed => CryptoScheme::Sr25519,
SecretFormat::Substrate => CryptoScheme::Sr25519,
SecretFormat::Predefined => CryptoScheme::Sr25519,
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.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,
prompt_fn: F,
) -> Result<VaultDataToImport, GcliError>
where
F: Fn() -> (String, P),
P: Into<KeyPair>,
{
let (secret, pair) = prompt_fn();
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,
) -> 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 => {
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.
///
/// 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: 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!();
let vault_account = if let Some(existing_vault_account) =
vault_account::find_by_id(db_tx, &DbAccountId::from(address_to_import.clone())).await?
{
if existing_vault_account.is_base_account() {
println!("You are trying to add {address_to_import} as a <Base> account while it already exists as a <Base> account.");
println!();
println!("Do you want to:");
println!("1. keep the existing <Base> account and cancel import");
println!("2. overwrite existing <Base> account with the new encrypted key (children will be re-parented)");
} else {
// Existing derivation account
let account_tree_node_hierarchy =
vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
db_tx,
&address_to_import,
)
.await?;
let account_tree_node_for_address = vault_account::get_account_tree_node_for_address(
&account_tree_node_hierarchy,
&address_to_import,
);
let 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 encrypted_suri =
compute_encrypted_suri(password, vault_data.secret_suri.clone())?;
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())?;
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!();
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" => {
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()?
}
};
Ok(encrypt(secret_suri.as_bytes(), password).map_err(|e| anyhow!(e))?)
}
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)
}
/// 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(),
)
.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> {
let mut table = Table::new();
table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY);
table.set_header(vec![
"SS58 Address/G1v1 public key",
"Crypto",
"Path",
"Name",
]);
for account_tree_node in account_tree_nodes {
let _ = add_account_tree_node_to_table(&mut table, account_tree_node);
}
Ok(table)
}
fn add_account_tree_node_to_table(
table: &mut Table,
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
) -> Result<(), GcliError> {
let rows = compute_vault_accounts_row(account_tree_node)?;
rows.iter().for_each(|row| {
table.add_row(row.clone());
});
for child in &account_tree_node.borrow().children {
let _ = add_account_tree_node_to_table(table, child);
}
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
pub fn compute_vault_accounts_row(
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
) -> 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());
// Adding 2nd row for G1v1 public key
if CryptoScheme::Ed25519 == crypto_scheme {
rows.push(vec![Cell::new(format!(
"└ G1v1: {}",
cesium::compute_g1v1_public_key_from_ed25519_account_id(
&account_tree_node.account.address.0
)
))]);
}
let crypto_scheme_str: &str = crypto_scheme.into();
(
format!("<{}>", account_tree_node.account.account_type()),
crypto_scheme_str.to_string(),
)
};
// Adding 1st row
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;
use crate::entities::vault_account::tests::account_tree_node_tests::{
mother_account_tree_node, mother_g1v1_account_tree_node,
};
use indoc::indoc;
#[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);
}
}
}
use crate::entities::vault_account;
use crate::entities::vault_account::DbAccountId;
use crate::*;
use serde::{Deserialize, Serialize};
const APP_NAME: &str = "gcli";
/// defines structure of config file
#[derive(Serialize, Deserialize, Debug)]
pub struct Config {
// duniter endpoint
/// duniter endpoint
pub duniter_endpoint: String,
// indexer endpoint
/// indexer endpoint
pub indexer_endpoint: String,
// user address
/// user address
/// to perform actions, user must provide secret
pub address: Option<AccountId>,
}
......@@ -23,17 +27,35 @@ impl std::default::Default for Config {
}
}
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
pub fn load_conf() -> Config {
match confy::load(APP_NAME, None) {
Ok(cfg) => cfg,
Err(e) => {
log::warn!("met error while loading config file");
log::error!("{}", e);
log::info!("removing the old conf file and creating a new one");
let cfg = Config::default();
confy::store(APP_NAME, None, &cfg).expect("unable to write default config");
cfg
log::warn!(
"met error while loading config file {}",
confy::get_configuration_file_path(APP_NAME, None)
.unwrap()
.display()
);
log::error!("{:?}", e);
log::info!("using default config instead");
log::info!("call `config save` to overwrite");
Config::default()
}
}
}
......@@ -47,10 +69,12 @@ pub enum Subcommand {
Show,
/// Save config as modified by command line arguments
Save,
/// Rest config to default
Default,
}
/// 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 command {
Subcommand::Where => {
......@@ -60,12 +84,30 @@ pub fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<()> {
);
}
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 => {
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(())
}
pub fn save(cfg: &Config) {
confy::store(APP_NAME, None, cfg).expect("unable to write config");
println!("Configuration updated!");
}
use crate::commands::vault;
use crate::*;
use indexer::Indexer;
use sea_orm::DatabaseConnection;
// consts
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")]
pub const GDEV_DUNITER_ENDPOINTS: [&str; 5] = [
......@@ -15,36 +19,40 @@ pub const GDEV_DUNITER_ENDPOINTS: [&str; 5] = [
];
#[cfg(feature = "gdev")]
pub const GDEV_INDEXER_ENDPOINTS: [&str; 2] = [
"https://gdev-indexer.p2p.legal/v1/graphql",
"https://hasura.gdev.coinduf.eu/v1/graphql",
// "https://squid.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 of current command
/// can also include fetched information
#[derive(Default)]
pub struct Data {
// command line arguments
/// command line arguments
pub args: Args,
// config
/// config
pub cfg: conf::Config,
// rpc to substrate client
/// database connection
connection: Option<DatabaseConnection>,
/// rpc to substrate client
pub client: Option<Client>,
// graphql to duniter-indexer
/// graphql to duniter-indexer
pub indexer: Option<Indexer>,
// user keypair
pub keypair: Option<Pair>,
// user identity index
pub idty_index: Option<u32>,
// token decimals
/// user keypair
pub keypair: Option<KeyPair>,
/// user identity index
pub idty_index: Option<IdtyId>,
/// token decimals
pub token_decimals: u32,
// token symbol
/// token symbol
pub token_symbol: String,
// genesis hash
/// genesis hash
pub genesis_hash: Hash,
// indexer genesis hash
/// indexer genesis hash
pub indexer_genesis_hash: Hash,
/// gcli base path
pub project_dir: directories::ProjectDirs,
}
/// system properties defined in client specs
......@@ -55,21 +63,53 @@ struct SystemProperties {
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
impl Data {
/// --- constructor ---
pub fn new(args: Args) -> Self {
Self {
pub async fn new(args: Args) -> Result<Self, GcliError> {
let mut data = Self {
args,
cfg: conf::load_conf(),
token_decimals: 0,
token_symbol: "tokens".into(),
..Default::default()
}
.overwrite_from_args()
};
//Necessary to support checking "vault names" in the base arguments
data = data.build_connection().await?;
data = data.overwrite_from_args().await?;
Ok(data)
}
// --- getters ---
// 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 {
self.client.as_ref().expect("must build client first")
}
......@@ -79,24 +119,51 @@ impl Data {
pub fn address(&self) -> AccountId {
self.cfg.address.clone().expect("an address is needed")
}
pub fn keypair(&self) -> Pair {
self.keypair.clone().expect("a keypair is needed")
pub async fn keypair(&self) -> KeyPair {
match self.keypair.clone() {
Some(keypair) => keypair,
None => loop {
match fetch_or_get_keypair(self, self.cfg.address.clone()).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());
}
_ => {}
}
}
println!("{e:?} → retry")
}
}
},
}
}
pub fn idty_index(&self) -> u32 {
pub fn idty_index(&self) -> IdtyId {
self.idty_index.expect("must fetch idty index first")
}
// --- methods ---
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!(
"{} {}",
(amount as f32) / (base.pow(self.token_decimals) as f32),
self.token_symbol
"{}.{:0left_padding$} {}",
integer_part,
fractional_part,
self.token_symbol,
left_padding = self.token_decimals as usize
)
}
// --- mutators ---
/// 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
if let Some(network) = self.args.network.clone() {
// a network was provided as arugment
......@@ -138,71 +205,51 @@ impl Data {
if let Some(indexer_endpoint) = self.args.indexer.clone() {
self.cfg.indexer_endpoint = indexer_endpoint
}
// secret
if self.args.secret.is_some() {
self = self.build_keypair();
// secret format and value
if let Some(secret_format) = self.args.secret_format {
let keypair = get_keypair(secret_format, self.args.secret.as_deref())?;
self.cfg.address = Some(keypair.address());
self.keypair = Some(keypair);
}
// address
if self.args.address.is_some() {
self = self.build_address();
if let Some(address) = self.args.address.clone() {
self.cfg.address = Some(address.clone());
// if giving address, cancel secret
self.keypair = None
}
self
}
/// ask user to input an address if needed
pub fn build_address(mut self) -> Self {
if self.cfg.address.is_none() {
self.cfg.address = Some(
get_keys(
self.args.secret_format,
&self.args.address,
&self.args.secret,
NeededKeys::Public,
)
.expect("needed")
.0
.expect("needed"),
);
}
self
}
/// ask user to input a keypair if needed
pub fn build_keypair(mut self) -> Self {
if self.keypair.is_none() {
let (address, keypair) = get_keys(
self.args.secret_format,
&self.args.address,
&self.args.secret,
NeededKeys::Secret,
)
.expect("needed");
self.cfg.address = address;
self.keypair = keypair;
// (vault)name
if let Some(name) = self.args.name.clone() {
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
pub async fn build_client(mut self) -> Result<Self, GcliError> {
let duniter_endpoint = self.cfg.duniter_endpoint.clone();
self.client = Some(Client::from_url(&duniter_endpoint).await.map_err(|e| {
GcliError::Duniter(format!(
"could not establish connection with the server {}, due to error {}",
duniter_endpoint,
dbg!(e)
))
})?);
let duniter_endpoint = &self.cfg.duniter_endpoint;
let client = Client::from_url(duniter_endpoint).await.map_err(|e| {
// to get more details TODO fixme, see issue #18
dbg!(e);
GcliError::Duniter(format!("can not connect to duniter {duniter_endpoint}",))
})?;
self.client = Some(client);
self.genesis_hash = commands::blockchain::fetch_genesis_hash(&self).await?;
Ok(self)
}
/// 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 {
log::info!("called build_indexer while providing no_indexer");
self.indexer = None;
} else {
self.indexer = Some(Indexer {
gql_client: reqwest::Client::builder()
.user_agent("gcli/0.1.0")
.build()?,
.user_agent(format!("gcli/{PKG_VERSION}"))
.build()
.unwrap(),
gql_url: self.cfg.indexer_endpoint.clone(),
});
self.indexer_genesis_hash = self.indexer().fetch_genesis_hash().await?;
......@@ -212,22 +259,37 @@ impl Data {
};
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
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(
commands::identity::get_idty_index_by_account_id(self.client(), &self.address())
.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)
}
/// get properties
pub async fn fetch_system_properties(mut self) -> Result<Self, anyhow::Error> {
let system_properties = self.client().rpc().system_properties().await?;
pub async fn fetch_system_properties(mut self) -> Result<Self, GcliError> {
let system_properties = self.legacy_rpc_methods().await.system_properties().await?;
let system_properties = serde_json::from_value::<SystemProperties>(
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_symbol = system_properties.token_symbol;
Ok(self)
......@@ -245,3 +307,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(e.to_string()))
})?))
}
}
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))
}
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::Address)
.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;
use graphql_client::GraphQLQuery;
pub mod queries;
use crate::*;
// type used in parameters query
#[allow(non_camel_case_types)]
type jsonb = serde_json::Value;
#[derive(GraphQLQuery)]
#[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;
use comfy_table::*;
use comfy_table::{ContentArrangement, Table};
use graphql_client::reqwest::post_graphql;
use graphql_client::GraphQLQuery;
use queries::*;
// use sp_core::Bytes;
#[derive(Clone, Debug)]
pub struct Indexer {
......@@ -42,116 +15,235 @@ pub struct Indexer {
}
impl Indexer {
pub async fn username_by_pubkey(&self, pubkey: &str) -> anyhow::Result<Option<String>> {
Ok(post_graphql::<IdentityNameByPubkey, _>(
&self.gql_client,
&self.gql_url,
identity_name_by_pubkey::Variables {
pubkey: pubkey.to_string(),
},
)
.await?
.data
.and_then(|data| data.identity.into_iter().next().map(|idty| idty.name)))
/// graphql query without error management
async fn query<T: GraphQLQuery>(
&self,
var: <T as GraphQLQuery>::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")
}
pub async fn pubkey_by_username(&self, username: &str) -> anyhow::Result<Option<String>> {
Ok(post_graphql::<IdentityPubkeyByName, _>(
&self.gql_client,
self.gql_url.clone(),
identity_pubkey_by_name::Variables {
name: username.to_string(),
},
)
.await?
.data
.and_then(|data| data.identity_by_pk.map(|idty| idty.pubkey)))
/// 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)
}
/// fetch latest block number
pub async fn fetch_latest_block(&self) -> Result<u64, anyhow::Error> {
Ok(post_graphql::<LatestBlock, _>(
&self.gql_client,
self.gql_url.clone(),
latest_block::Variables {},
)
.await?
.data
.unwrap() // must have a data field
.parameters
.first()
.unwrap() // must have one and only one parameter matching request
.value
.clone()
.unwrap() // must have a value field
.as_u64()
.unwrap()) // must be a Number of blocks
/// 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(),
})
.await
.identity
.pop()
.map(|idty| idty.name)
}
/// pubkey → was name
pub async fn wasname_by_pubkey(&self, pubkey: &str) -> Option<String> {
self.query::<WasIdentityNameByPubkey>(was_identity_name_by_pubkey::Variables {
pubkey: pubkey.to_string(),
})
.await
.account_by_pk
.and_then(|mut acc| acc.was_identity.pop())
.map(|idty| idty.identity.unwrap().name)
}
/// index → info
pub async fn identity_info(&self, index: u32) -> Option<identity_info::IdentityInfoIdentity> {
self.query::<IdentityInfo>(identity_info::Variables {
index: index.into(),
})
.await
.identity
.pop()
}
/// fetch latest block
pub async fn fetch_latest_block(&self) -> Option<latest_block::LatestBlockBlock> {
self.query::<LatestBlock>(latest_block::Variables {})
.await
.block
.pop()
}
/// 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
pub async fn fetch_genesis_hash(&self) -> Result<Hash, anyhow::Error> {
Ok(post_graphql::<GenesisHash, _>(
// since this is always called before any other indexer request, check errors in a more detailed way
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_url.clone(),
genesis_hash::Variables {},
)
.await?
.data
.ok_or(GcliError::Indexer("could not reach indexer".to_string()))?
.block
.first()
.unwrap() // must have one and only one block matching request
.hash
.clone()
.parse::<Hash>()
.unwrap())
.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
.ok_or(GcliError::Indexer(
"no field 'data' when getting genesis hash".to_string(),
))?
.block
.first()
.ok_or_else(|| GcliError::Indexer("genesis block not yet indexed".to_string()))?
.hash
.clone();
// 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
#[derive(Clone, Default, Debug, clap::Parser)]
pub enum Subcommand {
#[default]
/// Show indexer endpoint
ShowEndpoint,
/// Fetch latest indexed block
LatestBlock,
/// 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,
}
pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<()> {
/// handle indexer commands
pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
// 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 command {
Subcommand::ShowEndpoint => {
println!("indexer endpoint: {}", data.indexer().gql_url);
}
Subcommand::LatestBlock => {
println!(
"latest block indexed by {} is: {}",
data.cfg.indexer_endpoint,
data.indexer().fetch_latest_block().await?
);
}
Subcommand::Check => {
data = data.build_client().await?;
if data.genesis_hash == data.indexer_genesis_hash {
println!(
"{} and {} have the same genesis hash: {}",
data.cfg.duniter_endpoint,
data.indexer().gql_url,
data.genesis_hash
);
let d_url = &data.cfg.duniter_endpoint;
let i_url = &indexer.gql_url;
let d_gen_hash = &data.genesis_hash.to_string();
let i_gen_hash = &data.indexer_genesis_hash.to_string();
let (d_finalized_n, d_finalized_h) =
commands::blockchain::fetch_finalized_number_and_hash(&data).await?;
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 {
println!(
"⚠️ {} ({}) and {} ({}) do not share same genesis",
data.cfg.duniter_endpoint,
data.genesis_hash,
data.indexer().gql_url,
data.indexer_genesis_hash
);
(None, None)
};
let (d_latest_n, d_latest_h) =
commands::blockchain::fetch_latest_number_and_hash(&data).await?;
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;
fn color(x: bool) -> Color {
match x {
true => Color::Green,
false => Color::Red,
}
}
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_confirm() -> Result<String, GcliError> {
prompt_password_query_confirm("Password")
}
pub fn prompt_password_query(query: impl ToString) -> Result<String, GcliError> {
inquire::Password::new(query.to_string().as_str())
.without_confirmation()
.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()))
}
pub fn prompt_password_query_confirm(query: impl ToString) -> Result<String, GcliError> {
inquire::Password::new(query.to_string().as_str())
.prompt()
.map_err(|e| GcliError::Input(e.to_string()))
}
/// Prompt for a (direct) vault name (cannot contain derivation path)
///
/// Also preventing to use '<' and '>' as those are used in the display
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| {
if input.contains('<') || input.contains('>') || input.contains('/') {
return Ok(Validation::Invalid(
"Name cannot contain characters '<', '>', '/'".into(),
));
}
Ok(Validation::Valid)
}
});
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 = if name.trim().is_empty() {
None
} else {
Some(name.trim().to_string())
};
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()
);
}
}
/// Prompt for a derivation path
pub fn prompt_vault_derivation_path() -> Result<String, GcliError> {
inquire::Text::new("Derivation path:")
.with_validator(|input: &str| {
if !input.starts_with("/") {
Ok(Validation::Invalid(
"derivation path needs to start with one or more '/'".into(),
))
} else {
match vault::parse_prefix_and_derivation_path_from_suri(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 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 anyhow::{anyhow, Result};
use clap::builder::OsStr;
use sp_core::{
crypto::{AccountId32, Pair as _, Ss58Codec},
sr25519::Pair,
};
use std::str::FromStr;
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NeededKeys {
None,
Public,
Secret,
}
use crate::commands::vault;
use crate::*;
use sp_core::ed25519;
use sp_core::sr25519;
pub const SUBSTRATE_MNEMONIC: &str =
"bottom drive obey lake curtain smoke basket hold race lonely fit walk";
/// secret format
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
pub enum SecretFormat {
/// Raw 32B seed
......@@ -21,6 +14,10 @@ pub enum SecretFormat {
/// Substrate secret key or BIP39 mnemonic (optionally followed by derivation path)
#[default]
Substrate,
/// Predefined (Alice, Bob, ...)
Predefined,
/// G1v1 id+secret using (scrypt + ed25519)
G1v1,
}
impl FromStr for SecretFormat {
......@@ -30,98 +27,853 @@ impl FromStr for SecretFormat {
match s {
"seed" => Ok(SecretFormat::Seed),
"substrate" => Ok(SecretFormat::Substrate),
"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)),
}
}
}
impl From<SecretFormat> for &'static str {
fn from(val: SecretFormat) -> &'static str {
match val {
SecretFormat::Seed => "seed",
SecretFormat::Substrate => "substrate",
SecretFormat::Predefined => "predefined",
SecretFormat::G1v1 => "g1v1",
}
}
}
impl From<SecretFormat> for OsStr {
fn from(val: SecretFormat) -> OsStr {
OsStr::from(Into::<&str>::into(val))
}
}
pub fn pair_from_str(secret_format: SecretFormat, secret: &str) -> Result<Pair> {
match secret_format {
SecretFormat::Seed => {
let mut seed = [0; 32];
hex::decode_to_slice(secret, &mut seed).map_err(|_| anyhow!("Invalid secret"))?;
let pair = Pair::from_seed(&seed);
Ok(pair)
/// The crypto scheme to use - partial copy from sc_cli::arg_enums::CryptoScheme
///
/// 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,
}
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)),
}
SecretFormat::Substrate => {
Pair::from_string(secret, None).map_err(|_| anyhow!("Invalid secret"))
}
}
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))
}
}
pub fn prompt_secret(secret_format: SecretFormat) -> Pair {
/// 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>,
) -> Result<KeyPair, GcliError> {
match (secret_format, secret) {
(SecretFormat::Predefined, Some(deriv)) => pair_from_predefined(deriv).map(|v| v.into()),
(secret_format, None) => Ok(prompt_secret(secret_format)),
(_, Some(secret)) => Ok(pair_from_secret(secret_format, secret)?.into()),
}
}
/// get keypair from given secret
/// if secret is predefined, secret should contain the predefined value
pub fn pair_from_secret(
secret_format: SecretFormat,
secret: &str,
) -> Result<sr25519::Pair, GcliError> {
match secret_format {
SecretFormat::Substrate => pair_from_sr25519_str(secret),
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)
}
/// get keypair from given ed25519 string secret (used for cesium)
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)
}
/// get mnemonic from predefined derivation path
pub fn predefined_mnemonic(deriv: &str) -> String {
format!("{SUBSTRATE_MNEMONIC}//{deriv}")
}
/// get keypair from predefined secret
pub fn pair_from_predefined(deriv: &str) -> Result<sr25519::Pair, GcliError> {
pair_from_sr25519_str(&predefined_mnemonic(deriv))
}
/// 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
}
/// ask user to input a secret
pub fn prompt_secret_substrate() -> sr25519::Pair {
// Only interested in the keypair which is the second element of the tuple
prompt_secret_substrate_and_compute_keypair().1
}
pub fn prompt_secret_substrate_and_compute_keypair() -> (String, sr25519::Pair) {
loop {
match pair_from_str(
secret_format,
&rpassword::prompt_password(format!("Secret key ({secret_format:?}): ")).unwrap(),
) {
Ok(pair) => return pair,
println!("Substrate URI can be a mnemonic or a mini-secret ('0x' prefixed seed) together with optional derivation path");
let substrate_suri = inputs::prompt_password_query("Substrate URI: ").unwrap();
match pair_from_sr25519_str(&substrate_suri) {
Ok(pair) => return (substrate_suri, pair),
Err(_) => println!("Invalid secret"),
}
}
}
pub fn get_keys(
secret_format: SecretFormat,
address: &Option<String>,
secret: &Option<String>,
needed_keys: NeededKeys,
) -> Result<(Option<AccountId32>, Option<Pair>)> {
// Get from args
let mut account_id = match (address, secret) {
(Some(address), Some(secret)) => {
let pair = pair_from_str(secret_format, secret)?;
let address = AccountId32::from_string(address)
.map_err(|_| anyhow!("Invalid address {}", address))?;
assert_eq!(
address,
pair.public().into(),
"Secret and address do not match."
);
return Ok((Some(pair.public().into()), Some(pair)));
}
(None, Some(secret)) => {
let pair = pair_from_str(secret_format, secret)?;
return Ok((Some(pair.public().into()), Some(pair)));
}
(Some(address), None) => Some(
AccountId32::from_str(address).map_err(|_| anyhow!("Invalid address {}", address))?,
),
(None, None) => None,
};
// Prompt
if needed_keys == NeededKeys::Secret
|| (account_id.is_none() && needed_keys == NeededKeys::Public)
{
loop {
let pair = prompt_secret(secret_format);
if let Some(account_id) = &account_id {
if account_id != &pair.public().into() {
println!("Secret and address do not match.");
}
/// ask user pass (Cesium format)
pub fn prompt_secret_cesium() -> ed25519::Pair {
// Only interested in the keypair which is the second element of the tuple
prompt_secret_cesium_and_compute_keypair().1
}
pub fn prompt_secret_cesium_and_compute_keypair() -> (String, ed25519::Pair) {
let id = inputs::prompt_password_query("G1v1 id: ").unwrap();
let pwd = inputs::prompt_password_query("G1v1 password: ").unwrap();
let seed = seed_from_cesium(&id, &pwd);
let secret_suri = format!("0x{}", hex::encode(seed));
match pair_from_ed25519_str(&secret_suri) {
Ok(pair) => (secret_suri, pair),
Err(_) => panic!("Could not compute KeyPair from G1v1 id/pwd"),
}
}
/// ask user to input a seed
pub fn prompt_seed() -> sr25519::Pair {
// Only interested in the keypair which is the second element of the tuple
prompt_seed_and_compute_keypair().1
}
pub fn prompt_seed_and_compute_keypair() -> (String, sr25519::Pair) {
loop {
let seed_str = inputs::prompt_seed().unwrap();
let secret_suri = format!("0x{}", seed_str);
match pair_from_sr25519_str(&secret_suri) {
Ok(pair) => return (secret_suri, pair),
Err(_) => println!("Invalid seed"),
}
}
}
/// ask user pass (Cesium format)
pub fn prompt_predefined() -> sr25519::Pair {
// Only interested in the keypair which is the second element of the tuple
prompt_predefined_and_compute_keypair().1
}
pub fn prompt_predefined_and_compute_keypair() -> (String, sr25519::Pair) {
let deriv = inputs::prompt_password_query("Enter derivation path: ").unwrap();
(
predefined_mnemonic(&deriv),
pair_from_predefined(&deriv).expect("invalid secret"),
)
}
/// ask user secret in relevant format
pub fn prompt_secret(secret_format: SecretFormat) -> KeyPair {
match secret_format {
SecretFormat::Substrate => prompt_secret_substrate().into(),
SecretFormat::G1v1 => prompt_secret_cesium().into(),
SecretFormat::Seed => prompt_seed().into(),
SecretFormat::Predefined => prompt_predefined().into(),
}
}
/// get the secret from user, trying first keystore then input
pub async fn fetch_or_get_keypair(
data: &Data,
address: Option<AccountId>,
) -> Result<KeyPair, GcliError> {
if let Some(address) = address {
// if address corresponds to predefined, (for example saved to config)
// keypair is already known (useful for dev mode)
if let Some(d) = catch_known(&address.to_string()) {
return Ok(pair_from_predefined(d).unwrap().into());
};
// look for corresponding KeyPair in keystore
if let Some(key_pair) = commands::vault::try_fetch_key_pair(data, address).await? {
return Ok(key_pair);
};
}
// at the moment, there is no way to confg gcli to use an other kind of secret
// without telling explicitly each time
Ok(prompt_secret(SecretFormat::Substrate))
}
// catch known addresses
fn catch_known(address: &str) -> Option<&str> {
match address {
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" => Some("Alice"),
"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" => Some("Bob"),
"5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y" => Some("Charlie"),
"5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" => Some("Dave"),
"5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" => Some("Eve"),
_ => 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 {
account_id = Some(pair.public().into());
return Ok((account_id, Some(pair)));
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::*;
/// 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()
);
}
}
Ok((account_id, None))
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 cesium {
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_cesium_v1_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_cesium_v1_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 cache;
mod commands;
mod conf;
mod data;
mod database;
mod display;
mod entities;
mod indexer;
mod inputs;
mod keys;
mod runtime_config;
mod utils;
use anyhow::anyhow;
use clap::builder::OsStr;
use clap::Parser;
use codec::Encode;
use colored::Colorize;
use data::*;
use display::DisplayEvent;
use keys::*;
use serde::Deserialize;
use sp_core::{sr25519::Pair, Pair as _, H256};
use subxt::blocks::ExtrinsicEvents;
use subxt::tx::{BaseExtrinsicParamsBuilder, PairSigner, TxStatus};
use runtime_config::*;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use subxt::{
blocks::ExtrinsicEvents,
config::DefaultExtrinsicParamsBuilder,
events::StaticEvent,
ext::sp_core::{sr25519, Pair as _},
tx::{DefaultPayload, PairSigner, Payload, TxStatus},
};
use utils::*;
#[cfg(feature = "gdev")]
#[subxt::subxt(
runtime_metadata_path = "res/metadata.scale",
derive_for_all_types = "Debug"
)]
pub mod runtime {
// IF NEEDED
// #[subxt(substitute_type = "spcore::sr25519::Signature")]
// use crate::gdev::runtime_types::sp_core::sr25519::Signature;
}
// declare custom types
pub type Client = subxt::OnlineClient<Runtime>;
pub type AccountId = subxt::ext::sp_runtime::AccountId32;
pub type TxInBlock = subxt::tx::TxInBlock<Runtime, Client>;
pub type TxProgress = subxt::tx::TxProgress<Runtime, Client>;
pub type Balance = u64;
pub type AccountData = runtime::runtime_types::pallet_duniter_account::types::AccountData<Balance>;
pub type AccountInfo = runtime::runtime_types::frame_system::AccountInfo<u32, AccountData>;
pub type Hash = sp_core::H256;
// declare runtime types
pub enum Runtime {}
impl subxt::config::Config for Runtime {
type Index = u32;
type BlockNumber = u32;
type Hash = Hash;
type Hashing = subxt::ext::sp_runtime::traits::BlakeTwo256;
type AccountId = AccountId;
type Address = subxt::ext::sp_runtime::MultiAddress<Self::AccountId, u32>;
type Header = subxt::ext::sp_runtime::generic::Header<
Self::BlockNumber,
subxt::ext::sp_runtime::traits::BlakeTwo256,
>;
type Signature = subxt::ext::sp_runtime::MultiSignature;
type ExtrinsicParams = subxt::tx::BaseExtrinsicParams<Self, Tip>;
}
// alias
pub type StaticPayload<Calldata> = DefaultPayload<Calldata>;
// Tip for transaction fee
#[derive(Copy, Clone, Debug, Default, codec::Encode)]
pub struct Tip {
#[codec(compact)]
tip: u64,
}
impl Tip {
pub fn new(amount: u64) -> Self {
Tip { tip: amount }
}
}
impl From<u64> for Tip {
fn from(n: u64) -> Self {
Self::new(n)
}
}
// define command line arguments
/// define command line arguments
#[derive(Clone, clap::Parser, Debug, Default)]
#[clap(author, version, about, long_about = None)]
pub struct Args {
......@@ -78,455 +41,163 @@ pub struct Args {
#[clap(subcommand)]
pub subcommand: Subcommand,
/// Overwrite indexer endpoint
#[clap(short, long)]
#[clap(short, long, conflicts_with_all=["no_indexer","network"])]
indexer: Option<String>,
/// Do not use indexer
#[clap(long)]
no_indexer: bool,
/// Secret key or BIP39 mnemonic
/// Secret key or BIP39 mnemonic (only used when secret format is compatible)
/// (eventually followed by derivation path)
#[clap(short, long)]
secret: Option<String>,
/// Secret key format (seed, substrate)
#[clap(short = 'S', long, default_value = SecretFormat::Substrate)]
secret_format: SecretFormat,
/// Address
#[clap(short, long)]
address: Option<String>,
/// Secret key format (seed, substrate, g1v1)
#[clap(short = 'S', long)]
secret_format: Option<SecretFormat>,
/// SS58 Address
#[clap(short, conflicts_with = "name")]
address: Option<AccountId>,
/// Name of an SS58 Address in the vault
#[clap(short = 'v')]
name: Option<String>,
/// Overwrite duniter websocket RPC endpoint
#[clap(short, long)]
#[clap(short, long, conflicts_with = "network")]
url: Option<String>,
/// Chose target network
/// Target network (local, gdev, gtest...)
#[clap(short, long)]
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,
}
/// track progress of transaction on the network
/// until it is in block with success or failure
pub async fn track_progress(
mut progress: TxProgress,
) -> Result<ExtrinsicEvents<Runtime>, subxt::Error> {
loop {
if let Some(status) = progress.next_item().await {
match status? {
TxStatus::Ready => {
println!("transaction submitted to the network, waiting 6 seconds...");
}
TxStatus::InBlock(in_block) => break in_block,
TxStatus::Invalid => {
println!("Invalid");
}
_ => continue,
}
}
}
.wait_for_success()
.await
// 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;
/// custom error type intended to provide more convenient error message to user
#[derive(Debug)]
pub enum GcliError {
/// error coming from subxt
Subxt(subxt::Error),
/// error coming from duniter
Duniter(String),
/// error coming from indexer
Indexer(String),
/// logic error (illegal operation or security)
Logic(String),
/// error coming from anyhow
Anyhow(anyhow::Error),
}
impl std::fmt::Display for GcliError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
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 std::error::Error for GcliError {}
impl From<subxt::Error> for GcliError {
fn from(e: subxt::Error) -> GcliError {
GcliError::Subxt(e)
impl From<OutputFormat> for &'static str {
fn from(val: OutputFormat) -> &'static str {
match val {
OutputFormat::Human => "human",
OutputFormat::Json => "json",
}
}
}
impl From<anyhow::Error> for GcliError {
fn from(e: anyhow::Error) -> GcliError {
GcliError::Anyhow(e)
impl From<OutputFormat> for OsStr {
fn from(val: OutputFormat) -> OsStr {
OsStr::from(Into::<&str>::into(val))
}
}
/// define subcommands
#[derive(Clone, Debug, clap::Subcommand, Default)]
pub enum Subcommand {
/// Fetch account balance TODO also oneshot account
/// Nothing
#[default]
GetBalance,
/// Show address corresponding to given arguments
ShowAddress,
/// Fetch identity
Identity {
#[clap(short = 'p', long = "pubkey")]
account_id: Option<AccountId>,
#[clap(short = 'i', long = "identity")]
identity_id: Option<u32>,
#[clap(short = 'u', long = "username")]
username: Option<String>,
},
/// Create and certify an identity
///
/// Caller must be member, and the target account must exist.
CreateIdentity {
target: AccountId,
},
/// Confirm an identity
///
/// To be called by the certified not-yet-member account, to become member.
ConfirmIdentity {
name: String,
},
/// Generate a revocation document for the provided account
GenRevocDoc,
/// Revoke an identity immediately
RevokeIdentity,
/// List upcoming expirations that require an action
Expire {
/// Show certs that expire within less than this number of blocks
#[clap(short, long, default_value_t = 100800)]
blocks: u32,
/// Show authorities that should rotate keys within less than this number of sessions
#[clap(short, long, default_value_t = 100)]
sessions: u32,
},
GoOffline,
GoOnline,
/// List online authorities
Online,
#[clap(hide = true)]
Repart {
// Number of transactions per block to target
target: u32,
#[clap(short = 'o', long = "old-repart")]
// Old/actual repartition
actual_repart: Option<u32>,
},
Nothing,
/// Account (balance, transfer...)
#[clap(subcommand)]
Account(commands::account::Subcommand),
/// Identity (get, create, confirm, revoke...)
#[clap(subcommand)]
Identity(commands::identity::Subcommand),
/// Smith (certify, go-online, go-offline...)
#[clap(subcommand)]
Smith(commands::smith::Subcommand),
/// Sudo (set key, sudo calls...)
#[clap(hide = true)]
SpamRoll {
actual_repart: usize,
},
SudoSetKey {
new_key: AccountId,
},
/// Emit a smith certification
SmithCert {
to: u32,
},
/// List members of the technical committee
TechMembers,
/// List proposals to the technical committee
TechProposals,
/// Vote a proposal to the technical committee
TechVote {
/// Proposal hash
hash: H256,
/// Proposal index
index: u32,
/// Vote (0=against, 1=for)
vote: u8,
},
Transfer {
/// Amount to transfer
amount: u64,
/// Destination address
dest: AccountId,
/// Prevent from going below account existential deposit
#[clap(short = 'k', long = "keep-alive")]
keep_alive: bool,
},
/// Transfer the same amount for each space-separated address.
/// If an address appears mutiple times, it will get multiple times the same amount
TransferMultiple {
/// Amount given to each destination address
amount: u64,
/// List of target addresses
dests: Vec<AccountId>,
},
/// Rotate and set session keys
UpdateKeys,
/// Get information about runtime
RuntimeInfo,
/// Check current block
CurrentBlock,
/// Oneshot account subcommands
#[clap(subcommand)]
Oneshot(commands::oneshot::Subcommand),
/// Universal Dividend subcommands
Sudo(commands::sudo::Subcommand),
/// Tech (list members, proposals, vote...)
#[clap(subcommand)]
Tech(commands::collective::Subcommand),
/// Universal Dividend (claim...)
#[clap(subcommand)]
Ud(commands::ud::Subcommand),
/// Indexer subcommands
/// Oneshot account (balance, create, consume...)
#[clap(subcommand)]
Oneshot(commands::oneshot::Subcommand),
/// Blockchain (current block, runtime info...)
#[clap(subcommand)]
Blockchain(commands::blockchain::Subcommand),
/// Indexer (check, latest block)
#[clap(subcommand)]
Indexer(indexer::Subcommand),
/// Config subcommands
/// Config (show, save...)
#[clap(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,
}
/// main function
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), GcliError> {
// init logger
env_logger::init();
// parse argument and initialize data
let args = Args::parse();
let mut data = Data::new(args.clone());
let data = Data::new(Args::parse()).await?;
match args.subcommand {
Subcommand::GetBalance => {
data = data
.build_address()
.build_client()
.await?
.fetch_system_properties()
.await?;
commands::account::get_balance(data).await?
}
Subcommand::ShowAddress => {
data = data.build_address();
println!("address is: {}", data.address());
}
Subcommand::CreateIdentity { target } => {
data = data.build_client().await?.build_keypair();
commands::identity::create_identity(data.keypair(), data.client(), target).await?;
}
Subcommand::ConfirmIdentity { name } => {
data = data.build_client().await?.build_keypair();
commands::identity::confirm_identity(data.keypair(), data.client(), name).await?;
}
Subcommand::RevokeIdentity => {
data = data
.build_client()
.await?
.build_keypair()
.fetch_idty_index()
.await?;
commands::identity::revoke_identity(data).await?;
}
Subcommand::Expire { blocks, sessions } => {
data = data.build_client().await?;
commands::expire::monitor_expirations(&data, blocks, sessions).await?
}
Subcommand::Identity {
ref account_id,
identity_id,
ref username,
} => {
data = data.build_client().await?;
commands::identity::get_identity(
&data,
account_id.clone(),
identity_id,
username.clone(),
)
.await?
}
Subcommand::GenRevocDoc => {
data = data
.build_client()
.await?
.build_keypair()
.fetch_idty_index()
.await?;
commands::revocation::print_revoc_sig(&data)
}
Subcommand::GoOffline => {
data = data.build_client().await?;
commands::smith::go_offline(
get_keys(
args.secret_format,
&args.address,
&args.secret,
NeededKeys::Secret,
)?
.1
.unwrap(),
data.client(),
)
.await?;
}
Subcommand::GoOnline => {
data = data.build_client().await?;
commands::smith::go_online(
get_keys(
args.secret_format,
&args.address,
&args.secret,
NeededKeys::Secret,
)?
.1
.unwrap(),
data.client(),
)
.await?;
// match subcommands
let result = match data.args.subcommand.clone() {
Subcommand::Nothing => Ok(()),
Subcommand::Account(subcommand) => {
commands::account::handle_command(data, subcommand).await
}
Subcommand::Online => {
data = data.build_client().await?;
commands::smith::online(&data).await?
Subcommand::Identity(subcommand) => {
commands::identity::handle_command(data, subcommand).await
}
Subcommand::Repart {
target,
actual_repart,
} => {
data = data.build_client().await?;
commands::net_test::repart(
get_keys(
args.secret_format,
&args.address,
&args.secret,
NeededKeys::Secret,
)?
.1
.unwrap(),
data.client(),
target,
actual_repart,
)
.await?
}
Subcommand::SpamRoll { actual_repart } => {
data = data.build_client().await?;
commands::net_test::spam_roll(
get_keys(
args.secret_format,
&args.address,
&args.secret,
NeededKeys::Secret,
)?
.1
.unwrap(),
data.client(),
actual_repart,
)
.await?
}
Subcommand::SudoSetKey { new_key } => {
data = data.build_keypair().build_client().await?;
commands::sudo::set_key(data.keypair(), data.client(), new_key).await?;
}
Subcommand::SmithCert { to } => {
data = data
.build_client()
.await?
.build_keypair()
.fetch_idty_index()
.await?;
commands::smith::cert(data.client(), data.keypair(), data.idty_index(), to).await?
}
Subcommand::TechMembers => {
data = data.build_client().await?;
commands::collective::technical_committee_members(&data).await?
}
Subcommand::TechProposals => {
data = data.build_client().await?;
commands::collective::technical_committee_proposals(data.client()).await?
}
Subcommand::TechVote { hash, index, vote } => {
data = data.build_client().await?;
let vote = match vote {
0 => false,
1 => true,
_ => panic!("Vote must be written 0 if you disagree, or 1 if you agree."),
};
commands::collective::technical_committee_vote(
get_keys(
args.secret_format,
&args.address,
&args.secret,
NeededKeys::Secret,
)?
.1
.unwrap(),
data.client(),
hash, //H256::from_str(&hash).expect("Invalid hash formatting"),
index,
vote,
)
.await?;
}
Subcommand::Transfer {
amount,
dest,
keep_alive,
} => {
data = data.build_client().await?;
commands::transfer::transfer(
get_keys(
args.secret_format,
&args.address,
&args.secret,
NeededKeys::Secret,
)?
.1
.unwrap(),
data.client(),
amount,
dest,
keep_alive,
)
.await?;
}
Subcommand::TransferMultiple { amount, dests } => {
data = data.build_client().await?;
commands::transfer::transfer_multiple(
get_keys(
args.secret_format,
&args.address,
&args.secret,
NeededKeys::Secret,
)?
.1
.unwrap(),
data.client(),
amount,
dests,
)
.await?;
}
Subcommand::UpdateKeys => {
data = data.build_client().await?;
commands::smith::update_session_keys(
get_keys(
args.secret_format,
&args.address,
&args.secret,
NeededKeys::Secret,
)?
.1
.unwrap(),
data.client(),
)
.await?;
}
Subcommand::RuntimeInfo => {
data = data.build_client().await?.fetch_system_properties().await?;
commands::runtime::runtime_info(data).await;
}
Subcommand::CurrentBlock => {
data = data.build_client().await?;
println!(
"current block on {}: {}",
data.cfg.duniter_endpoint,
data.client()
.storage()
.fetch(&runtime::storage().system().number(), None)
.await?
.unwrap()
);
Subcommand::Smith(subcommand) => commands::smith::handle_command(data, subcommand).await,
Subcommand::Sudo(subcommand) => commands::sudo::handle_command(data, subcommand).await,
Subcommand::Tech(subcommand) => {
commands::collective::handle_command(data, subcommand).await
}
Subcommand::Ud(subcommand) => commands::ud::handle_command(data, subcommand).await,
Subcommand::Oneshot(subcommand) => {
commands::oneshot::handle_command(data, subcommand).await?
}
Subcommand::Ud(subcommand) => commands::ud::handle_command(data, subcommand).await?,
Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await?,
Subcommand::Config(subcommand) => conf::handle_command(data, subcommand)?,
commands::oneshot::handle_command(data, subcommand).await
}
Subcommand::Blockchain(subcommand) => {
commands::blockchain::handle_command(data, subcommand).await
}
Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await,
Subcommand::Config(subcommand) => conf::handle_command(data, subcommand).await,
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")]
#[subxt::subxt(
runtime_metadata_path = "res/metadata.scale",
derive_for_all_types = "Debug"
)]
pub mod runtime {}
// declare custom types
pub type Client = subxt::OnlineClient<Runtime>;
pub type AccountId = subxt::utils::AccountId32;
pub type IdtyId = u32;
pub type BlockNumber = u32;
pub type TxProgress = subxt::tx::TxProgress<Runtime, Client>;
pub type Balance = u64;
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 Hash = sp_core::H256;
// declare runtime types
pub enum Runtime {}
impl subxt::config::Config for Runtime {
type AssetId = ();
type Hash = Hash;
type AccountId = AccountId;
type Address = sp_runtime::MultiAddress<Self::AccountId, u32>;
type Signature = sp_runtime::MultiSignature;
type Hasher = subxt::config::substrate::BlakeTwo256;
type Header = subxt::config::substrate::SubstrateHeader<BlockNumber, Self::Hasher>;
type ExtrinsicParams = subxt::config::DefaultExtrinsicParams<Self>;
}
// Tip for transaction fee
#[derive(Copy, Clone, Debug, Default, codec::Encode)]
pub struct Tip {
#[codec(compact)]
tip: u64,
}
impl Tip {
pub fn new(amount: u64) -> Self {
Tip { tip: amount }
}
}
impl From<u64> for Tip {
fn from(n: u64) -> Self {
Self::new(n)
}
}
use crate::*;
use sea_orm::DbErr;
/// track progress of transaction on the network
/// until it is in block with success or failure
pub async fn track_progress(
mut progress: TxProgress,
) -> Result<ExtrinsicEvents<Runtime>, subxt::Error> {
loop {
if let Some(status) = progress.next().await {
match status? {
TxStatus::Validated => {
println!("transaction submitted to the network, waiting 6 seconds...");
}
TxStatus::InBestBlock(in_block) => break in_block,
TxStatus::Invalid { message } => {
println!("Invalid {message}");
}
_ => continue,
}
}
}
.wait_for_success()
.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
#[derive(Debug)]
pub enum GcliError {
/// error coming from subxt
Subxt(subxt::Error),
/// error coming from duniter
#[allow(dead_code)]
Duniter(String),
/// error coming from indexer
#[allow(dead_code)]
Indexer(String),
/// error coming from database
#[allow(dead_code)]
DatabaseError(DbErr),
/// logic error (illegal operation or security)
#[allow(dead_code)]
Logic(String),
/// input error
#[allow(dead_code)]
Input(String),
/// error coming from anyhow (to be removed)
#[allow(dead_code)]
Anyhow(anyhow::Error),
/// error coming from io
#[allow(dead_code)]
IoError(std::io::Error),
}
impl std::fmt::Display for GcliError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
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 From<subxt::Error> for GcliError {
fn from(e: subxt::Error) -> GcliError {
GcliError::Subxt(e)
}
}
impl From<anyhow::Error> for GcliError {
fn from(e: anyhow::Error) -> GcliError {
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)
}
}