Skip to content
Snippets Groups Projects
Commit badf52ce authored by Nicolas80's avatar Nicolas80
Browse files

Adding db persistence for all SecretFormat of vault keys as well as supporting derivations

* Added "/.idea" exclusion in .gitignore (for when using JetBrains IDEs)
* Added dialoguer dependency for easier user input handling (see in inputs.rs)
* Added sea-orm dependency to allow having DB entity mappings and use a local sqlite file database
* Added rstest test dependency for parameterized tests support
* Added derivation tests for each SecretFormat (including cesium v1 key derivation, using sp_core::ed25519::Pair)
* Made a lot of changes to add vault_account and vault_derivation db tables to persist vault keys & derivations
* Added support for KeyPair::Ed25519 linking to sp_core::ed25519::Pair which can be created from secret seed retrieved from nacl::sign::Keypair (which is created from cesium id + secret)
** This was necessary to allow deriving keys from "cesium v1" keys (to be reviewed - it might be a bad idea to permit that from a security point of view)
* Only kept original (substrate) keyfiles support for migration (use "vault list-files" and "vault migrate")
* Added possibility to give either "-a" Address or "-v" Vault Name as general option
* Added extra commands in Vault
** list-files: (deprecated)List available key files (needs to be migrated with command "vault migrate" in order to use them)
** migrate: (deprecated)Migrate old key files into db (will have to provide password for each key)
** 'list' now has sub-commands 'all' or 'root' to show all keys or only root keys (without derivation path)
** use: "Use specific vault key (changes the config address)", which will have the same behaviour as `gcli <-a <Address>|-v <VaultName>> config save` (left a FIXME in there to review)
** derivation: Add a derivation to an existing (root) vault key
** rename: Give a meaningful vault name to a vault key or derivation
** remove: Remove a vault key (and potential derivations if it's a root key)
* Had to bubble up "await" and "async" in a lot of places
* ...
parent 8f67e583
Branches
Tags
1 merge request!41Adding db persistence for all SecretFormat of vault keys as well as supporting derivations
/target /target
/.idea
\ No newline at end of file
This diff is collapsed.
...@@ -37,6 +37,7 @@ reqwest = { version = "^0.11.27", default-features = false, features = [ ...@@ -37,6 +37,7 @@ reqwest = { version = "^0.11.27", default-features = false, features = [
"rustls-tls", "rustls-tls",
] } ] }
rpassword = "^7.3.1" rpassword = "^7.3.1"
dialoguer = "0.11.0"
serde = { version = "^1.0", features = ["derive"] } serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0.128" serde_json = "^1.0.128"
tokio = { version = "^1.40.0", features = ["macros"] } tokio = { version = "^1.40.0", features = ["macros"] }
...@@ -45,6 +46,7 @@ bs58 = "^0.5.1" ...@@ -45,6 +46,7 @@ bs58 = "^0.5.1"
inquire = "^0.6.2" inquire = "^0.6.2"
directories = "^5.0.1" directories = "^5.0.1"
comfy-table = "^7.1.1" comfy-table = "^7.1.1"
sea-orm = { version = "1.1.0", features = [ "sqlx-sqlite", "runtime-tokio-native-tls", "macros" ] }
# crypto # crypto
scrypt = { version = "^0.11", default-features = false } # for old-style key generation scrypt = { version = "^0.11", default-features = false } # for old-style key generation
...@@ -54,6 +56,9 @@ age = { default-features = false, version = "^0.10.0", features = ["armor"] } ...@@ -54,6 +56,9 @@ age = { default-features = false, version = "^0.10.0", features = ["armor"] }
bip39 = { version = "^2.0.0", features = ["rand"] } # mnemonic bip39 = { version = "^2.0.0", features = ["rand"] } # mnemonic
colored = "2.1.0" colored = "2.1.0"
# Tests
rstest = "0.23.0"
# allows to build gcli for different runtimes and with different predefined networks # allows to build gcli for different runtimes and with different predefined networks
[features] [features]
default = ["gdev"] # default runtime is "gdev", gdev network is available default = ["gdev"] # default runtime is "gdev", gdev network is available
......
...@@ -124,7 +124,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE ...@@ -124,7 +124,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
} }
Subcommand::GenRevocDoc => { Subcommand::GenRevocDoc => {
data = data.fetch_idty_index().await?; data = data.fetch_idty_index().await?;
commands::revocation::print_revoc_sig(&data) commands::revocation::print_revoc_sig(&data).await
} }
Subcommand::MemberCount => { Subcommand::MemberCount => {
println!( println!(
...@@ -488,7 +488,7 @@ pub async fn confirm_identity(data: &Data, name: String) -> Result<(), subxt::Er ...@@ -488,7 +488,7 @@ pub async fn confirm_identity(data: &Data, name: String) -> Result<(), subxt::Er
/// generate revokation document and submit it immediately /// generate revokation document and submit it immediately
pub async fn revoke_identity(data: &Data) -> Result<(), subxt::Error> { pub async fn revoke_identity(data: &Data) -> Result<(), subxt::Error> {
let (_payload, signature) = generate_revoc_doc(data); let (_payload, signature) = generate_revoc_doc(data).await;
// Transform signature to MultiSignature // Transform signature to MultiSignature
// TODO: allow other signature formats // TODO: allow other signature formats
...@@ -519,6 +519,11 @@ pub fn generate_link_account( ...@@ -519,6 +519,11 @@ pub fn generate_link_account(
let signature = keypair.sign(&payload); let signature = keypair.sign(&payload);
(payload, Signature::Sr25519(signature)) (payload, Signature::Sr25519(signature))
} }
KeyPair::Ed25519(keypair) => {
let signature = keypair.sign(&payload);
(payload, Signature::Ed25519(signature))
}
//FIXME Cleanup
KeyPair::Nacl(keypair) => { KeyPair::Nacl(keypair) => {
let signature = nacl::sign::signature(&payload, &keypair.skey).expect("could not sign"); let signature = nacl::sign::signature(&payload, &keypair.skey).expect("could not sign");
(payload, Signature::Nacl(signature)) (payload, Signature::Nacl(signature))
...@@ -545,6 +550,11 @@ pub fn generate_chok_payload( ...@@ -545,6 +550,11 @@ pub fn generate_chok_payload(
let signature = keypair.sign(&payload); let signature = keypair.sign(&payload);
(payload, Signature::Sr25519(signature)) (payload, Signature::Sr25519(signature))
} }
KeyPair::Ed25519(keypair) => {
let signature = keypair.sign(&payload);
(payload, Signature::Ed25519(signature))
}
//FIXME Cleanup
KeyPair::Nacl(keypair) => { KeyPair::Nacl(keypair) => {
// should not migrate to Nacl // should not migrate to Nacl
let signature = nacl::sign::signature(&payload, &keypair.skey).expect("could not sign"); let signature = nacl::sign::signature(&payload, &keypair.skey).expect("could not sign");
...@@ -564,6 +574,8 @@ pub async fn link_account( ...@@ -564,6 +574,8 @@ pub async fn link_account(
// TODO cleaner way to manage signature // TODO cleaner way to manage signature
let signature = match signature { let signature = match signature {
Signature::Sr25519(signature) => MultiSignature::Sr25519(signature.into()), Signature::Sr25519(signature) => MultiSignature::Sr25519(signature.into()),
Signature::Ed25519(signature) => MultiSignature::Ed25519(signature.into()),
//FIXME Cleanup
Signature::Nacl(signature) => MultiSignature::Ed25519(signature.try_into().unwrap()), Signature::Nacl(signature) => MultiSignature::Ed25519(signature.try_into().unwrap()),
}; };
...@@ -588,6 +600,8 @@ pub async fn change_owner_key( ...@@ -588,6 +600,8 @@ pub async fn change_owner_key(
// TODO cleaner way to manage signature // TODO cleaner way to manage signature
let signature = match signature { let signature = match signature {
Signature::Sr25519(signature) => MultiSignature::Sr25519(signature.into()), Signature::Sr25519(signature) => MultiSignature::Sr25519(signature.into()),
Signature::Ed25519(signature) => MultiSignature::Ed25519(signature.into()),
//FIXME Cleanup
Signature::Nacl(signature) => MultiSignature::Ed25519(signature.try_into().unwrap()), Signature::Nacl(signature) => MultiSignature::Ed25519(signature.try_into().unwrap()),
}; };
......
...@@ -4,7 +4,7 @@ use sp_core::DeriveJunction; ...@@ -4,7 +4,7 @@ use sp_core::DeriveJunction;
use subxt::ext::sp_runtime::MultiAddress; use subxt::ext::sp_runtime::MultiAddress;
pub async fn repart(data: &Data, target: u32, actual_repart: Option<u32>) -> anyhow::Result<()> { pub async fn repart(data: &Data, target: u32, actual_repart: Option<u32>) -> anyhow::Result<()> {
let KeyPair::Sr25519(keypair) = data.keypair() else { let KeyPair::Sr25519(keypair) = data.keypair().await else {
panic!("Cesium keys not implemented there") panic!("Cesium keys not implemented there")
}; };
let mut pairs = Vec::new(); let mut pairs = Vec::new();
...@@ -43,7 +43,7 @@ pub async fn repart(data: &Data, target: u32, actual_repart: Option<u32>) -> any ...@@ -43,7 +43,7 @@ pub async fn repart(data: &Data, target: u32, actual_repart: Option<u32>) -> any
} }
pub async fn spam_roll(data: &Data, actual_repart: usize) -> anyhow::Result<()> { pub async fn spam_roll(data: &Data, actual_repart: usize) -> anyhow::Result<()> {
let KeyPair::Sr25519(keypair) = data.keypair() else { let KeyPair::Sr25519(keypair) = data.keypair().await else {
panic!("Cesium keys not implemented there") panic!("Cesium keys not implemented there")
}; };
let client = data.client(); let client = data.client();
......
...@@ -4,15 +4,15 @@ use crate::*; ...@@ -4,15 +4,15 @@ use crate::*;
// use crate::runtime::runtime_types::pallet_identity::types::RevocationPayload; // use crate::runtime::runtime_types::pallet_identity::types::RevocationPayload;
type EncodedRevocationPayload = Vec<u8>; type EncodedRevocationPayload = Vec<u8>;
pub fn print_revoc_sig(data: &Data) { pub async fn print_revoc_sig(data: &Data) {
let (_, signature) = generate_revoc_doc(data); let (_, signature) = generate_revoc_doc(data).await;
println!("revocation payload signature"); println!("revocation payload signature");
println!("0x{}", hex::encode(signature)); println!("0x{}", hex::encode(signature));
} }
pub fn generate_revoc_doc(data: &Data) -> (EncodedRevocationPayload, sr25519::Signature) { pub async fn generate_revoc_doc(data: &Data) -> (EncodedRevocationPayload, sr25519::Signature) {
let payload = (b"revo", data.genesis_hash, data.idty_index()).encode(); let payload = (b"revo", data.genesis_hash, data.idty_index()).encode();
let KeyPair::Sr25519(keypair) = data.keypair() else { let KeyPair::Sr25519(keypair) = data.keypair().await else {
panic!("Cesium keys not implemented there") panic!("Cesium keys not implemented there")
}; };
let signature = keypair.sign(&payload); let signature = keypair.sign(&payload);
......
This diff is collapsed.
use crate::entities::vault_derivation;
use crate::*; use crate::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
...@@ -72,7 +73,7 @@ pub enum Subcommand { ...@@ -72,7 +73,7 @@ pub enum Subcommand {
} }
/// handle conf command /// handle conf command
pub fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
// match subcommand // match subcommand
match command { match command {
Subcommand::Where => { Subcommand::Where => {
...@@ -83,10 +84,19 @@ pub fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> ...@@ -83,10 +84,19 @@ pub fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError>
} }
Subcommand::Show => { Subcommand::Show => {
println!("{}", data.cfg); println!("{}", data.cfg);
if let Some(account_id) = data.cfg.address {
if let Some(derivation) = vault_derivation::fetch_vault_derivation(
data.connection.as_ref().unwrap(),
account_id.to_string().as_str(),
)
.await?
{
println!("(Vault entry: {})", derivation);
}
}
} }
Subcommand::Save => { Subcommand::Save => {
confy::store(APP_NAME, None, &data.cfg).expect("unable to write config"); save_config(&data);
println!("Configuration updated!");
} }
Subcommand::Default => { Subcommand::Default => {
confy::store(APP_NAME, None, Config::default()).expect("unable to write config"); confy::store(APP_NAME, None, Config::default()).expect("unable to write config");
...@@ -95,3 +105,8 @@ pub fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> ...@@ -95,3 +105,8 @@ pub fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError>
Ok(()) Ok(())
} }
pub fn save_config(data: &Data) {
confy::store(APP_NAME, None, &data.cfg).expect("unable to write config");
println!("Configuration updated!");
}
use std::str::FromStr; use crate::commands::vault;
use crate::*; use crate::*;
use indexer::Indexer; use indexer::Indexer;
use sea_orm::DatabaseConnection;
use std::str::FromStr;
// consts // consts
pub const LOCAL_DUNITER_ENDPOINT: &str = "ws://localhost:9944"; pub const LOCAL_DUNITER_ENDPOINT: &str = "ws://localhost:9944";
pub const LOCAL_INDEXER_ENDPOINT: &str = "http://localhost:4350/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"); const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(feature = "gdev")] #[cfg(feature = "gdev")]
...@@ -28,27 +30,29 @@ pub const GDEV_INDEXER_ENDPOINTS: [&str; 2] = [ ...@@ -28,27 +30,29 @@ pub const GDEV_INDEXER_ENDPOINTS: [&str; 2] = [
/// Data of current command /// Data of current command
/// can also include fetched information /// can also include fetched information
pub struct Data { pub struct Data {
// command line arguments /// command line arguments
pub args: Args, pub args: Args,
// config /// config
pub cfg: conf::Config, pub cfg: conf::Config,
// rpc to substrate client /// database connection
pub connection: Option<DatabaseConnection>,
/// rpc to substrate client
pub client: Option<Client>, pub client: Option<Client>,
// graphql to duniter-indexer /// graphql to duniter-indexer
pub indexer: Option<Indexer>, pub indexer: Option<Indexer>,
// user keypair /// user keypair
pub keypair: Option<KeyPair>, pub keypair: Option<KeyPair>,
// user identity index /// user identity index
pub idty_index: Option<IdtyId>, pub idty_index: Option<IdtyId>,
// token decimals /// token decimals
pub token_decimals: u32, pub token_decimals: u32,
// token symbol /// token symbol
pub token_symbol: String, pub token_symbol: String,
// genesis hash /// genesis hash
pub genesis_hash: Hash, pub genesis_hash: Hash,
// indexer genesis hash /// indexer genesis hash
pub indexer_genesis_hash: Hash, pub indexer_genesis_hash: Hash,
// gcli base path /// gcli base path
pub project_dir: directories::ProjectDirs, pub project_dir: directories::ProjectDirs,
} }
...@@ -70,6 +74,7 @@ impl Default for Data { ...@@ -70,6 +74,7 @@ impl Default for Data {
project_dir, project_dir,
args: Default::default(), args: Default::default(),
cfg: Default::default(), cfg: Default::default(),
connection: Default::default(),
client: Default::default(), client: Default::default(),
indexer: Default::default(), indexer: Default::default(),
keypair: Default::default(), keypair: Default::default(),
...@@ -85,15 +90,18 @@ impl Default for Data { ...@@ -85,15 +90,18 @@ impl Default for Data {
// implement helper functions for Data // implement helper functions for Data
impl Data { impl Data {
/// --- constructor --- /// --- constructor ---
pub fn new(args: Args) -> Result<Self, GcliError> { pub async fn new(args: Args) -> Result<Self, GcliError> {
Self { let mut data = Self {
args, args,
cfg: conf::load_conf(), cfg: conf::load_conf(),
token_decimals: 0, token_decimals: 0,
token_symbol: "tokens".into(), token_symbol: "tokens".into(),
..Default::default() ..Default::default()
} };
.overwrite_from_args() //Necessary to support checking "vault names" in the base arguments
data = data.build_connection().await?;
data = data.overwrite_from_args().await?;
Ok(data)
} }
// --- getters --- // --- getters ---
// the "unwrap" should not fail if data is well prepared // the "unwrap" should not fail if data is well prepared
...@@ -106,11 +114,11 @@ impl Data { ...@@ -106,11 +114,11 @@ impl Data {
pub fn address(&self) -> AccountId { pub fn address(&self) -> AccountId {
self.cfg.address.clone().expect("an address is needed") self.cfg.address.clone().expect("an address is needed")
} }
pub fn keypair(&self) -> KeyPair { pub async fn keypair(&self) -> KeyPair {
match self.keypair.clone() { match self.keypair.clone() {
Some(keypair) => keypair, Some(keypair) => keypair,
None => loop { None => loop {
match fetch_or_get_keypair(self, self.cfg.address.clone()) { match fetch_or_get_keypair(self, self.cfg.address.clone()).await {
Ok(pair) => return pair, Ok(pair) => return pair,
Err(e) => println!("{e:?} → retry"), Err(e) => println!("{e:?} → retry"),
} }
...@@ -136,7 +144,7 @@ impl Data { ...@@ -136,7 +144,7 @@ impl Data {
} }
// --- mutators --- // --- mutators ---
/// use arguments to overwrite config /// use arguments to overwrite config
pub fn overwrite_from_args(mut self) -> Result<Self, GcliError> { pub async fn overwrite_from_args(mut self) -> Result<Self, GcliError> {
// network // network
if let Some(network) = self.args.network.clone() { if let Some(network) = self.args.network.clone() {
// a network was provided as arugment // a network was provided as arugment
...@@ -184,8 +192,10 @@ impl Data { ...@@ -184,8 +192,10 @@ impl Data {
self.cfg.address = Some(keypair.address()); self.cfg.address = Some(keypair.address());
self.keypair = Some(keypair); self.keypair = Some(keypair);
} }
// address // address or vault name
if let Some(address) = self.args.address.clone() { if let Some(address_or_vault_name) = self.args.address_or_vault_name.clone() {
let address = vault::retrieve_address_string(&self, address_or_vault_name).await?;
self.cfg.address = Some(AccountId::from_str(&address).expect("invalid address")); self.cfg.address = Some(AccountId::from_str(&address).expect("invalid address"));
// if giving address, cancel secret // if giving address, cancel secret
self.keypair = None self.keypair = None
...@@ -224,7 +234,16 @@ impl Data { ...@@ -224,7 +234,16 @@ impl Data {
}; };
Ok(self) Ok(self)
} }
/// get issuer index
/// build a database connection
pub async fn build_connection(mut self) -> Result<Self, GcliError> {
let data_dir = self.project_dir.data_dir();
let connection = database::build_sqlite_connection(data_dir, SQLITE_DB_FILENAME).await?;
self.connection = Some(connection);
Ok(self)
}
/// get issuer index<br>
/// needs address and client first /// needs address and client first
pub async fn fetch_idty_index(mut self) -> Result<Self, GcliError> { pub async fn fetch_idty_index(mut self) -> Result<Self, GcliError> {
self.idty_index = Some( self.idty_index = Some(
......
use crate::entities::{vault_account, vault_derivation};
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?;
create_table_if_not_exists(&db, &schema, vault_derivation::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(())
}
pub mod vault_account;
pub mod vault_derivation;
use crate::inputs;
use crate::utils::GcliError;
use sea_orm::prelude::StringLen;
use sea_orm::ActiveValue::Set;
use sea_orm::{
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, EnumIter, Related, RelationDef,
RelationTrait,
};
use sea_orm::{ActiveModelTrait, ConnectionTrait, PrimaryKeyTrait};
use sea_orm::{DeriveActiveEnum, EntityTrait};
use std::fmt::Display;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "vault_account")]
pub struct Model {
/// SS58 Address of (root) account
#[sea_orm(primary_key, auto_increment = false)]
pub address: String,
pub crypto_type: CryptoType,
pub encrypted_private_key: Vec<u8>,
}
impl Display for Model {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[address:\"{}\"]", self.address)
}
}
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
#[sea_orm(
rs_type = "String",
db_type = "String(StringLen::None)",
rename_all = "PascalCase"
)]
pub enum CryptoType {
/// The secret key or BIP39 mnemonic
Sr25519Mnemonic,
/// The 32B SR25519 seed without "0x" prefix
Sr25519Seed,
/// The 32B ED25519 seed without "0x" prefix (for cesium)
Ed25519Seed,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Derivation,
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::Derivation => Entity::has_many(super::vault_derivation::Entity).into(),
}
}
}
// `Related` trait has to be implemented by hand
impl Related<super::vault_derivation::Entity> for Entity {
fn to() -> RelationDef {
Relation::Derivation.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
/// Creates a (root) vault account (if it doesn't exist) and returns it
pub async fn create_vault_account<C>(
db: &C,
address: &str,
crypto_type: CryptoType,
encrypted_private_key: Vec<u8>,
) -> Result<Model, GcliError>
where
C: ConnectionTrait,
{
let vault_account = Entity::find_by_id(address.to_owned()).one(db).await?;
Ok(match vault_account {
Some(vault_account) => {
println!("Already existing (root) vault account {vault_account}");
let overwrite_key =
inputs::confirm_action("Do you want to overwrite with the new encrypted key ?")?;
if overwrite_key {
let mut vault_account: ActiveModel = vault_account.into();
vault_account.encrypted_private_key = Set(encrypted_private_key);
let vault_account = vault_account.update(db).await?;
println!("Updated (root) vault account {vault_account}");
vault_account
} else {
vault_account
}
}
None => {
let vault_account = ActiveModel {
address: Set(address.to_owned()),
crypto_type: Set(crypto_type),
encrypted_private_key: Set(encrypted_private_key),
};
let vault_account = vault_account.insert(db).await?;
println!("Created vault account {}", vault_account);
vault_account
}
})
}
use crate::utils::GcliError;
use sea_orm::sea_query::NullOrdering;
use sea_orm::ActiveValue::Set;
use sea_orm::PrimaryKeyTrait;
use sea_orm::QueryFilter;
use sea_orm::{
ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, EnumIter, Related, RelationDef,
RelationTrait,
};
use sea_orm::{ActiveModelTrait, ColumnTrait};
use sea_orm::{ConnectionTrait, EntityTrait, Order, QueryOrder};
use std::fmt::Display;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "vault_derivation")]
pub struct Model {
/// SS58 Address
#[sea_orm(primary_key, auto_increment = false)]
pub address: String,
/// Optional name for the derivation
#[sea_orm(unique)]
pub name: Option<String>,
/// derivation path - None if for the root account
pub path: Option<String>,
/// ForeignKey to root vault_account SS58 Address
pub root_address: String,
}
impl Display for Model {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[address:\"{}\", name:{:?}, path:{:?}, root_address:\"{}\"]",
self.address, self.name, self.path, self.root_address
)
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
RootAccount,
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::RootAccount => Entity::belongs_to(super::vault_account::Entity)
.from(Column::RootAddress)
.to(super::vault_account::Column::Address)
.into(),
}
}
}
// `Related` trait has to be implemented by hand
impl Related<super::vault_account::Entity> for Entity {
fn to() -> RelationDef {
Relation::RootAccount.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
/// Creates a root derivation (if it doesn't exist) and returns it
pub async fn create_root_vault_derivation<C>(
db: &C,
root_address: &str,
root_name: Option<&String>,
) -> Result<Model, GcliError>
where
C: ConnectionTrait,
{
create_vault_derivation(db, root_address, root_address, root_name, None).await
}
/// Creates a derivation (if it doesn't exist) and returns it
pub async fn create_vault_derivation<C>(
db: &C,
address: &str,
root_address: &str,
name: Option<&String>,
path: Option<&String>,
) -> Result<Model, GcliError>
where
C: ConnectionTrait,
{
let derivation = Entity::find_by_id(root_address.to_owned()).one(db).await?;
Ok(match derivation {
Some(derivation) => derivation,
None => {
let derivation = ActiveModel {
address: Set(address.to_owned()),
name: Set(name.cloned()),
path: Set(path.cloned()),
root_address: Set(root_address.to_owned()),
};
derivation.insert(db).await?
}
})
}
pub async fn fetch_vault_derivation<C>(db: &C, address: &str) -> Result<Option<Model>, GcliError>
where
C: ConnectionTrait,
{
let derivation = Entity::find_by_id(address.to_owned()).one(db).await?;
Ok(derivation)
}
pub async fn fetch_all_linked_derivations_in_order<C>(
db: &C,
root_address: &str,
) -> Result<Vec<Model>, GcliError>
where
C: ConnectionTrait,
{
let linked_derivations = Entity::find()
.filter(Column::RootAddress.eq(root_address.to_owned()))
.order_by_with_nulls(Column::Path, Order::Asc, NullOrdering::First)
.all(db)
.await?;
Ok(linked_derivations)
}
pub async fn list_all_derivations_in_order<C>(db: &C) -> Result<Vec<Model>, GcliError>
where
C: ConnectionTrait,
{
let derivations = Entity::find()
.order_by_asc(Column::RootAddress)
.order_by_with_nulls(Column::Path, Order::Asc, NullOrdering::First)
.all(db)
.await?;
Ok(derivations)
}
pub async fn list_all_root_derivations_in_order<C>(db: &C) -> Result<Vec<Model>, GcliError>
where
C: ConnectionTrait,
{
let derivations = Entity::find()
.filter(Column::Path.is_null())
.order_by_asc(Column::RootAddress)
.all(db)
.await?;
Ok(derivations)
}
use crate::utils::GcliError;
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> {
dialoguer::Password::default()
.with_prompt(query.to_string())
.allow_empty_password(true)
.interact()
.map_err(|e| GcliError::Input(e.to_string()))
}
pub fn prompt_password_query_confirm(query: impl ToString) -> Result<String, GcliError> {
dialoguer::Password::new()
.with_prompt(query.to_string())
.allow_empty_password(true)
.with_confirmation(
format!("Repeat {}", query.to_string()),
"Error: the values do not match.",
)
.interact()
.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 of
pub fn prompt_vault_name() -> Result<Option<String>, GcliError> {
let name = dialoguer::Input::new()
.with_prompt("Name")
.validate_with({
move |input: &String| -> Result<(), &str> {
if input.contains('<') || input.contains('>') || input.contains('/') {
Err("Name cannot contain characters '<', '>', '/'")
} else {
Ok(())
}
}
})
.allow_empty(true)
.interact_text()
.map_err(|e| GcliError::Input(e.to_string()))?;
let name = if name.trim().is_empty() {
None
} else {
Some(name.trim().to_string())
};
Ok(name)
}
/// Prompt for a derivation path
pub fn prompt_vault_derivation_path() -> Result<String, GcliError> {
let path = dialoguer::Input::new()
.with_prompt("Derivation path")
.validate_with({
move |input: &String| -> Result<(), &str> {
if !input.starts_with("/") {
Err("derivation path needs to start with one or more '/'")
} else {
Ok(())
}
}
})
.allow_empty(false)
.interact_text()
.map_err(|e| GcliError::Input(e.to_string()))?;
Ok(path)
}
pub fn confirm_action(query: impl ToString) -> Result<bool, GcliError> {
dialoguer::Confirm::new()
.with_prompt(query.to_string())
//.default(false)
.interact()
.map_err(|e| GcliError::Input(e.to_string()))
}
This diff is collapsed.
mod commands; mod commands;
mod conf; mod conf;
mod data; mod data;
mod database;
mod display; mod display;
mod entities;
mod indexer; mod indexer;
mod inputs;
mod keys; mod keys;
mod runtime_config; mod runtime_config;
mod utils; mod utils;
...@@ -51,8 +54,8 @@ pub struct Args { ...@@ -51,8 +54,8 @@ pub struct Args {
#[clap(short = 'S', long)] #[clap(short = 'S', long)]
secret_format: Option<SecretFormat>, secret_format: Option<SecretFormat>,
/// Address /// Address
#[clap(short, long)] #[clap(flatten)]
address: Option<String>, address_or_vault_name: Option<AddressOrVaultNameGroupOptional>,
/// Overwrite duniter websocket RPC endpoint /// Overwrite duniter websocket RPC endpoint
#[clap(short, long)] #[clap(short, long)]
url: Option<String>, url: Option<String>,
...@@ -67,6 +70,51 @@ pub struct Args { ...@@ -67,6 +70,51 @@ pub struct Args {
output_format: OutputFormat, output_format: OutputFormat,
} }
trait AddressOrVaultName {
fn address(&self) -> Option<&AccountId>;
fn name(&self) -> Option<&String>;
}
#[derive(Debug, clap::Args, Clone)]
#[group(required = false, multiple = false)]
pub struct AddressOrVaultNameGroupOptional {
/// Account Address
#[clap(short)]
address: Option<AccountId>,
/// Vault name for that account
#[clap(short = 'v')]
name: Option<String>,
}
impl AddressOrVaultName for AddressOrVaultNameGroupOptional {
fn address(&self) -> Option<&AccountId> {
self.address.as_ref()
}
fn name(&self) -> Option<&String> {
self.name.as_ref()
}
}
#[derive(Debug, clap::Args, Clone)]
#[group(required = true, multiple = false)]
pub struct AddressOrVaultNameGroup {
/// Account Address
#[clap(short)]
address: Option<AccountId>,
/// Vault name for that account
#[clap(short = 'v')]
name: Option<String>,
}
impl AddressOrVaultName for AddressOrVaultNameGroup {
fn address(&self) -> Option<&AccountId> {
self.address.as_ref()
}
fn name(&self) -> Option<&String> {
self.name.as_ref()
}
}
// TODO derive the fromstr implementation // TODO derive the fromstr implementation
/// secret format /// secret format
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
...@@ -158,7 +206,7 @@ async fn main() -> Result<(), GcliError> { ...@@ -158,7 +206,7 @@ async fn main() -> Result<(), GcliError> {
env_logger::init(); env_logger::init();
// parse argument and initialize data // parse argument and initialize data
let data = Data::new(Args::parse())?; let data = Data::new(Args::parse()).await?;
// match subcommands // match subcommands
let result = match data.args.subcommand.clone() { let result = match data.args.subcommand.clone() {
...@@ -182,8 +230,8 @@ async fn main() -> Result<(), GcliError> { ...@@ -182,8 +230,8 @@ async fn main() -> Result<(), GcliError> {
commands::blockchain::handle_command(data, subcommand).await commands::blockchain::handle_command(data, subcommand).await
} }
Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await, Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await,
Subcommand::Config(subcommand) => conf::handle_command(data, subcommand), Subcommand::Config(subcommand) => conf::handle_command(data, subcommand).await,
Subcommand::Vault(subcommand) => commands::vault::handle_command(data, subcommand), Subcommand::Vault(subcommand) => commands::vault::handle_command(data, subcommand).await,
Subcommand::Cesium(subcommand) => commands::cesium::handle_command(data, subcommand).await, Subcommand::Cesium(subcommand) => commands::cesium::handle_command(data, subcommand).await,
Subcommand::Publish => commands::publish::handle_command().await, Subcommand::Publish => commands::publish::handle_command().await,
}; };
......
use crate::*; use crate::*;
use sea_orm::DbErr;
/// track progress of transaction on the network /// track progress of transaction on the network
/// until it is in block with success or failure /// until it is in block with success or failure
...@@ -56,14 +57,19 @@ pub async fn submit_call<TxPayload: Payload>( ...@@ -56,14 +57,19 @@ pub async fn submit_call<TxPayload: Payload>(
.await?; .await?;
// sign and submit // sign and submit
match data.keypair() { match data.keypair().await {
// sr25519 key pair // sr25519 key pair
KeyPair::Sr25519(keypair) => data.client().tx().create_signed_offline( KeyPair::Sr25519(keypair) => data.client().tx().create_signed_offline(
payload, payload,
&PairSigner::<Runtime, sp_core::sr25519::Pair>::new(keypair), &PairSigner::<Runtime, sp_core::sr25519::Pair>::new(keypair),
DefaultExtrinsicParamsBuilder::new().nonce(nonce).build(), DefaultExtrinsicParamsBuilder::new().nonce(nonce).build(),
), ),
// nacl key pair KeyPair::Ed25519(keypair) => data.client().tx().create_signed_offline(
payload,
&PairSigner::<Runtime, sp_core::ed25519::Pair>::new(keypair),
DefaultExtrinsicParamsBuilder::new().nonce(nonce).build(),
),
//FIXME cleanup
KeyPair::Nacl(keypair) => data.client().tx().create_signed_offline( KeyPair::Nacl(keypair) => data.client().tx().create_signed_offline(
payload, payload,
&commands::cesium::CesiumSigner::new(keypair), &commands::cesium::CesiumSigner::new(keypair),
...@@ -93,19 +99,29 @@ pub fn look_event<E: std::fmt::Debug + StaticEvent + DisplayEvent>( ...@@ -93,19 +99,29 @@ pub fn look_event<E: std::fmt::Debug + StaticEvent + DisplayEvent>(
/// custom error type intended to provide more convenient error message to user /// custom error type intended to provide more convenient error message to user
#[derive(Debug)] #[derive(Debug)]
pub enum GcliError { pub enum GcliError {
//TODO Check allowing dead code is ok for those (we are using the values only in case of exception)
/// error coming from subxt /// error coming from subxt
Subxt(subxt::Error), Subxt(subxt::Error),
/// error coming from duniter /// error coming from duniter
#[allow(dead_code)]
Duniter(String), Duniter(String),
/// error coming from indexer /// error coming from indexer
#[allow(dead_code)]
Indexer(String), Indexer(String),
/// error coming from database
#[allow(dead_code)]
DatabaseError(DbErr),
/// logic error (illegal operation or security) /// logic error (illegal operation or security)
#[allow(dead_code)]
Logic(String), Logic(String),
/// input error /// input error
#[allow(dead_code)]
Input(String), Input(String),
/// error coming from anyhow (to be removed) /// error coming from anyhow (to be removed)
#[allow(dead_code)]
Anyhow(anyhow::Error), Anyhow(anyhow::Error),
/// error coming from io /// error coming from io
#[allow(dead_code)]
IoError(std::io::Error), IoError(std::io::Error),
} }
impl std::fmt::Display for GcliError { impl std::fmt::Display for GcliError {
...@@ -141,3 +157,8 @@ impl From<std::io::Error> for GcliError { ...@@ -141,3 +157,8 @@ impl From<std::io::Error> for GcliError {
GcliError::IoError(error) GcliError::IoError(error)
} }
} }
impl From<sea_orm::DbErr> for GcliError {
fn from(error: sea_orm::DbErr) -> Self {
GcliError::DatabaseError(error)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment