From 8760c3bdda27db588c26911edfb6775583f1b7a9 Mon Sep 17 00:00:00 2001 From: poka <poka@p2p.legal> Date: Thu, 13 Mar 2025 23:21:46 +0100 Subject: [PATCH] use json output for all usefull commands --- src/commands/account.rs | 42 +++- src/commands/blockchain.rs | 31 ++- src/commands/oneshot.rs | 55 ++++-- src/commands/runtime.rs | 363 +++++++++++++++++++++------------- src/commands/smith.rs | 184 ++++++++++++----- src/commands/vault.rs | 104 +++++++--- src/commands/vault/display.rs | 86 ++++++++ src/conf.rs | 54 ++++- 8 files changed, 672 insertions(+), 247 deletions(-) diff --git a/src/commands/account.rs b/src/commands/account.rs index c1216b6..1d0c452 100644 --- a/src/commands/account.rs +++ b/src/commands/account.rs @@ -1,4 +1,6 @@ use crate::*; +use anyhow::anyhow; +use serde::Serialize; /// define account subcommands #[derive(Clone, Default, Debug, clap::Parser)] @@ -57,18 +59,42 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE Ok(()) } +/// Balance view for JSON serialization +#[derive(Serialize)] +struct BalanceView { + account_id: String, + free_balance: u64, + formatted_balance: String, + exists: bool, +} + /// get balance -pub async fn get_balance(data: Data) -> Result<(), subxt::Error> { +pub async fn get_balance(data: Data) -> Result<(), GcliError> { let account_id = data.address(); let account_info = get_account_info(data.client(), &account_id).await?; - if let Some(account_info) = account_info { - println!( - "{account_id} has {}", - data.format_balance(account_info.data.free) - ); - } else { - println!("account {account_id} does not exist") + + match data.args.output_format { + OutputFormat::Human => { + if let Some(account_info) = account_info { + println!( + "{account_id} has {}", + data.format_balance(account_info.data.free) + ); + } else { + println!("account {account_id} does not exist") + } + } + OutputFormat::Json => { + let view = BalanceView { + account_id: account_id.to_string(), + free_balance: account_info.as_ref().map_or(0, |info| info.data.free), + formatted_balance: account_info.as_ref().map_or("0".to_string(), |info| data.format_balance(info.data.free)), + exists: account_info.is_some(), + }; + println!("{}", serde_json::to_string(&view).map_err(|e| anyhow!(e))?); + } } + Ok(()) } diff --git a/src/commands/blockchain.rs b/src/commands/blockchain.rs index ee46955..0062071 100644 --- a/src/commands/blockchain.rs +++ b/src/commands/blockchain.rs @@ -1,4 +1,6 @@ use crate::*; +use anyhow::anyhow; +use serde::Serialize; /// define blockchain subcommands #[derive(Clone, Default, Debug, clap::Parser)] @@ -23,6 +25,14 @@ pub enum Subcommand { CreateBlock, } +/// Block information for JSON serialization +#[derive(Serialize)] +struct BlockInfoView { + endpoint: String, + finalized_block: u32, + current_block: u32, +} + /// handle blockchain commands pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { let mut data = data.build_client().await?; @@ -36,14 +46,27 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE } Subcommand::RuntimeInfo => { data = data.fetch_system_properties().await?; - commands::runtime::runtime_info(data).await; + commands::runtime::runtime_info(data).await?; } Subcommand::CurrentBlock => { let finalized_number = fetch_finalized_number(&data).await?; let current_number = fetch_latest_number_and_hash(&data).await?.0; - println!("on {}", data.cfg.duniter_endpoint); - println!("finalized block\t{}", finalized_number); - println!("current block\t{}", current_number); + + match data.args.output_format { + OutputFormat::Human => { + println!("on {}", data.cfg.duniter_endpoint); + println!("finalized block\t{}", finalized_number); + println!("current block\t{}", current_number); + } + OutputFormat::Json => { + let view = BlockInfoView { + endpoint: data.cfg.duniter_endpoint.clone(), + finalized_block: finalized_number, + current_block: current_number, + }; + println!("{}", serde_json::to_string(&view).map_err(|e| anyhow!(e))?); + } + } } Subcommand::CreateBlock => { todo!() diff --git a/src/commands/oneshot.rs b/src/commands/oneshot.rs index 44ffee8..086fc9c 100644 --- a/src/commands/oneshot.rs +++ b/src/commands/oneshot.rs @@ -1,4 +1,6 @@ use crate::*; +use anyhow::anyhow; +use serde::Serialize; /// define oneshot account subcommands #[derive(Clone, Default, Debug, clap::Parser)] @@ -64,23 +66,44 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE Ok(()) } +/// Oneshot balance view for JSON serialization +#[derive(Serialize)] +struct OneshotBalanceView { + account_id: String, + balance: u64, +} + /// get balance of oneshot account -pub async fn oneshot_account_balance(data: &Data) -> Result<(), subxt::Error> { - println!( - "balance of oneshot account {} is: {}", - data.address(), - data.client() - .storage() - .at_latest() - .await? - .fetch( - &runtime::storage() - .oneshot_account() - .oneshot_accounts(data.address()), - ) - .await? - .unwrap_or(0) - ); +pub async fn oneshot_account_balance(data: &Data) -> Result<(), GcliError> { + let account_id = data.address(); + let balance = data.client() + .storage() + .at_latest() + .await? + .fetch( + &runtime::storage() + .oneshot_account() + .oneshot_accounts(&account_id), + ) + .await? + .unwrap_or(0); + + match data.args.output_format { + OutputFormat::Human => { + println!( + "balance of oneshot account {} is: {}", + account_id, + balance + ); + } + OutputFormat::Json => { + let view = OneshotBalanceView { + account_id: account_id.to_string(), + balance, + }; + println!("{}", serde_json::to_string(&view).map_err(|e| anyhow!(e))?); + } + } Ok(()) } diff --git a/src/commands/runtime.rs b/src/commands/runtime.rs index d71b102..4045a10 100644 --- a/src/commands/runtime.rs +++ b/src/commands/runtime.rs @@ -1,6 +1,23 @@ use crate::*; +use anyhow::anyhow; +use serde::Serialize; +use std::collections::HashMap; -pub async fn runtime_info(data: Data) { +/// Runtime information for JSON serialization +#[derive(Serialize)] +struct RuntimeInfoView { + identity: HashMap<String, u32>, + certification: HashMap<String, u32>, + wot: HashMap<String, u32>, + membership: HashMap<String, u32>, + smith_members: HashMap<String, u32>, + distance: HashMap<String, serde_json::Value>, + currency: HashMap<String, String>, + provide_randomness: HashMap<String, serde_json::Value>, + universal_dividend: HashMap<String, serde_json::Value>, +} + +pub async fn runtime_info(data: Data) -> Result<(), GcliError> { let api = data.client(); let consts = runtime::constants(); // get constant u32 value @@ -12,137 +29,215 @@ pub async fn runtime_info(data: Data) { // get formatted currency value let getf = |c| data.format_balance(api.constants().at(&c).unwrap()); - // identity - println!("--- identity ---"); - println!( - "confirm period: {} blocks", - getu32(consts.identity().confirm_period()) - ); - println!( - "validation period: {} blocks", - getu32(consts.identity().validation_period()) - ); - println!( - "autorevocation period: {} blocks", - getu32(consts.identity().autorevocation_period()) - ); - println!( - "deletion period: {} blocks", - getu32(consts.identity().deletion_period()) - ); - println!( - "change owner key period: {} blocks", - getu32(consts.identity().change_owner_key_period()) - ); - println!( - "identity creation period: {} blocks", - getu32(consts.identity().idty_creation_period()) - ); - // certification - println!("--- certification ---"); - println!( - "certification period: {} blocks", - getu32(consts.certification().cert_period()) - ); - println!( - "max certs by issuer: {}", - getu32(consts.certification().max_by_issuer()) - ); - println!( - "min received cert to issue cert: {}", - getu32( - consts - .certification() - .min_received_cert_to_be_able_to_issue_cert() - ) - ); - println!( - "certification validity: {} blocks", - getu32(consts.certification().validity_period()) - ); - // wot - println!("--- wot ---"); - println!( - "first issuable on: {}", - getu32(consts.wot().first_issuable_on()) - ); - println!( - "min cert for membership: {}", - getu32(consts.wot().min_cert_for_membership()) - ); - println!( - "min cert for create identity: {}", - getu32(consts.wot().min_cert_for_create_idty_right()) - ); - // membership - println!("--- membership ---"); - println!( - "membership validity: {} blocks", - getu32(consts.membership().membership_period()) - ); - // smith members - println!("--- smith members ---"); - println!( - "max certs by issuer: {}", - getu32(consts.smith_members().max_by_issuer()) - ); - println!( - "min cert for membership: {}", - getu32(consts.smith_members().min_cert_for_membership()) - ); - println!( - "smith inactivity max duration: {}", - getu32(consts.smith_members().smith_inactivity_max_duration()) - ); - // todo membership renewal period - // distance - println!("--- distance ---"); - println!( - "max referee distance: {}", - getu32(consts.distance().max_referee_distance()) - ); - println!( - "min accessible referees: {:?}", - getp(consts.distance().min_accessible_referees()) - ); - println!( - "distance evaluation price: {}", - getf(consts.distance().evaluation_price()) - ); - // currency - println!("--- currency ---"); - println!( - "existential deposit: {}", - getf(consts.balances().existential_deposit()) - ); - // provide randomness - println!("--- provide randomness ---"); - println!( - "max requests: {}", - getu32(consts.provide_randomness().max_requests()) - ); - println!( - "request price: {}", - getf(consts.provide_randomness().request_price()) - ); - // universal dividend - println!("--- universal dividend ---"); - println!( - "max past reevals: {}", - getu32(consts.universal_dividend().max_past_reeval()) - ); - println!( - "square money growth rate: {:?}", - getp(consts.universal_dividend().square_money_growth_rate()) - ); - println!( - "UD creation period: {}", - getu64(consts.universal_dividend().ud_creation_period()) - ); - println!( - "UD reeval period: {}", - getu64(consts.universal_dividend().ud_reeval_period()) - ); - // todo treasury, technical committee, transaction payment, authority members - // consts.system().ss58_prefix() + match data.args.output_format { + OutputFormat::Human => { + // identity + println!("--- identity ---"); + println!( + "confirm period: {} blocks", + getu32(consts.identity().confirm_period()) + ); + println!( + "validation period: {} blocks", + getu32(consts.identity().validation_period()) + ); + println!( + "autorevocation period: {} blocks", + getu32(consts.identity().autorevocation_period()) + ); + println!( + "deletion period: {} blocks", + getu32(consts.identity().deletion_period()) + ); + println!( + "change owner key period: {} blocks", + getu32(consts.identity().change_owner_key_period()) + ); + println!( + "identity creation period: {} blocks", + getu32(consts.identity().idty_creation_period()) + ); + // certification + println!("--- certification ---"); + println!( + "certification period: {} blocks", + getu32(consts.certification().cert_period()) + ); + println!( + "max certs by issuer: {}", + getu32(consts.certification().max_by_issuer()) + ); + println!( + "min received cert to issue cert: {}", + getu32( + consts + .certification() + .min_received_cert_to_be_able_to_issue_cert() + ) + ); + println!( + "certification validity: {} blocks", + getu32(consts.certification().validity_period()) + ); + // wot + println!("--- wot ---"); + println!( + "first issuable on: {}", + getu32(consts.wot().first_issuable_on()) + ); + println!( + "min cert for membership: {}", + getu32(consts.wot().min_cert_for_membership()) + ); + println!( + "min cert for create identity: {}", + getu32(consts.wot().min_cert_for_create_idty_right()) + ); + // membership + println!("--- membership ---"); + println!( + "membership validity: {} blocks", + getu32(consts.membership().membership_period()) + ); + // smith members + println!("--- smith members ---"); + println!( + "max certs by issuer: {}", + getu32(consts.smith_members().max_by_issuer()) + ); + println!( + "min cert for membership: {}", + getu32(consts.smith_members().min_cert_for_membership()) + ); + println!( + "smith inactivity max duration: {}", + getu32(consts.smith_members().smith_inactivity_max_duration()) + ); + // todo membership renewal period + // distance + println!("--- distance ---"); + println!( + "max referee distance: {}", + getu32(consts.distance().max_referee_distance()) + ); + println!( + "min accessible referees: {:?}", + getp(consts.distance().min_accessible_referees()) + ); + println!( + "distance evaluation price: {}", + getf(consts.distance().evaluation_price()) + ); + // currency + println!("--- currency ---"); + println!( + "existential deposit: {}", + getf(consts.balances().existential_deposit()) + ); + // provide randomness + println!("--- provide randomness ---"); + println!( + "max requests: {}", + getu32(consts.provide_randomness().max_requests()) + ); + println!( + "request price: {}", + getf(consts.provide_randomness().request_price()) + ); + // universal dividend + println!("--- universal dividend ---"); + println!( + "max past reevals: {}", + getu32(consts.universal_dividend().max_past_reeval()) + ); + println!( + "square money growth rate: {:?}", + getp(consts.universal_dividend().square_money_growth_rate()) + ); + println!( + "UD creation period: {}", + getu64(consts.universal_dividend().ud_creation_period()) + ); + println!( + "UD reeval period: {}", + getu64(consts.universal_dividend().ud_reeval_period()) + ); + // todo treasury, technical committee, transaction payment, authority members + // consts.system().ss58_prefix() + } + OutputFormat::Json => { + // Create HashMaps for each section + let mut identity = HashMap::new(); + identity.insert("confirm_period".to_string(), getu32(consts.identity().confirm_period())); + identity.insert("validation_period".to_string(), getu32(consts.identity().validation_period())); + identity.insert("autorevocation_period".to_string(), getu32(consts.identity().autorevocation_period())); + identity.insert("deletion_period".to_string(), getu32(consts.identity().deletion_period())); + identity.insert("change_owner_key_period".to_string(), getu32(consts.identity().change_owner_key_period())); + identity.insert("idty_creation_period".to_string(), getu32(consts.identity().idty_creation_period())); + + let mut certification = HashMap::new(); + certification.insert("cert_period".to_string(), getu32(consts.certification().cert_period())); + certification.insert("max_by_issuer".to_string(), getu32(consts.certification().max_by_issuer())); + certification.insert("min_received_cert_to_be_able_to_issue_cert".to_string(), + getu32(consts.certification().min_received_cert_to_be_able_to_issue_cert())); + certification.insert("validity_period".to_string(), getu32(consts.certification().validity_period())); + + let mut wot = HashMap::new(); + wot.insert("first_issuable_on".to_string(), getu32(consts.wot().first_issuable_on())); + wot.insert("min_cert_for_membership".to_string(), getu32(consts.wot().min_cert_for_membership())); + wot.insert("min_cert_for_create_idty_right".to_string(), getu32(consts.wot().min_cert_for_create_idty_right())); + + let mut membership = HashMap::new(); + membership.insert("membership_period".to_string(), getu32(consts.membership().membership_period())); + + let mut smith_members = HashMap::new(); + smith_members.insert("max_by_issuer".to_string(), getu32(consts.smith_members().max_by_issuer())); + smith_members.insert("min_cert_for_membership".to_string(), getu32(consts.smith_members().min_cert_for_membership())); + smith_members.insert("smith_inactivity_max_duration".to_string(), getu32(consts.smith_members().smith_inactivity_max_duration())); + + let mut distance = HashMap::new(); + distance.insert("max_referee_distance".to_string(), + serde_json::to_value(getu32(consts.distance().max_referee_distance())).map_err(|e| anyhow!(e))?); + distance.insert("min_accessible_referees".to_string(), + serde_json::to_value(format!("{:?}", getp(consts.distance().min_accessible_referees()))).map_err(|e| anyhow!(e))?); + distance.insert("evaluation_price".to_string(), + serde_json::to_value(getf(consts.distance().evaluation_price())).map_err(|e| anyhow!(e))?); + + let mut currency = HashMap::new(); + currency.insert("existential_deposit".to_string(), getf(consts.balances().existential_deposit())); + + let mut provide_randomness = HashMap::new(); + provide_randomness.insert("max_requests".to_string(), + serde_json::to_value(getu32(consts.provide_randomness().max_requests())).map_err(|e| anyhow!(e))?); + provide_randomness.insert("request_price".to_string(), + serde_json::to_value(getf(consts.provide_randomness().request_price())).map_err(|e| anyhow!(e))?); + + let mut universal_dividend = HashMap::new(); + universal_dividend.insert("max_past_reeval".to_string(), + serde_json::to_value(getu32(consts.universal_dividend().max_past_reeval())).map_err(|e| anyhow!(e))?); + universal_dividend.insert("square_money_growth_rate".to_string(), + serde_json::to_value(format!("{:?}", getp(consts.universal_dividend().square_money_growth_rate()))).map_err(|e| anyhow!(e))?); + universal_dividend.insert("ud_creation_period".to_string(), + serde_json::to_value(getu64(consts.universal_dividend().ud_creation_period())).map_err(|e| anyhow!(e))?); + universal_dividend.insert("ud_reeval_period".to_string(), + serde_json::to_value(getu64(consts.universal_dividend().ud_reeval_period())).map_err(|e| anyhow!(e))?); + + // Create the view + let view = RuntimeInfoView { + identity, + certification, + wot, + membership, + smith_members, + distance, + currency, + provide_randomness, + universal_dividend, + }; + + println!("{}", serde_json::to_string_pretty(&view).map_err(|e| anyhow!(e))?); + } + } + + Ok(()) } diff --git a/src/commands/smith.rs b/src/commands/smith.rs index c2190c5..4e6789d 100644 --- a/src/commands/smith.rs +++ b/src/commands/smith.rs @@ -5,6 +5,8 @@ use commands::identity::try_get_idty_index_by_name; use runtime::runtime_types::gdev_runtime::opaque::SessionKeys as RuntimeSessionKeys; use std::collections::HashMap; use std::ops::Deref; +use anyhow::anyhow; +use serde::Serialize; type SessionKeys = [u8; 128]; @@ -187,8 +189,23 @@ pub async fn go_offline(data: &Data) -> Result<(), subxt::Error> { .await } -/// get online authorities -pub async fn online(data: &Data) -> Result<(), subxt::Error> { +/// Smith authorities view for JSON serialization +#[derive(Serialize)] +struct SmithAuthoritiesView { + online: Vec<SmithAuthorityView>, + incoming: Vec<SmithAuthorityView>, + outgoing: Vec<SmithAuthorityView>, +} + +/// Smith authority view for JSON serialization +#[derive(Serialize)] +struct SmithAuthorityView { + id: IdtyId, + name: Option<String>, +} + +/// get online smiths +pub async fn online(data: &Data) -> Result<(), GcliError> { let client = data.client(); let online_authorities = client @@ -221,62 +238,121 @@ pub async fn online(data: &Data) -> Result<(), subxt::Error> { .await? .unwrap_or_default(); - if let Some(indexer) = &data.indexer { - let mut names = HashMap::<IdtyId, String>::new(); - indexer - .names_by_indexes(&online_authorities) - .await - .into_iter() - .for_each(|e| { - names.insert(e.0, e.1); - }); - println!("Online:"); - println!( - "{}", - online_authorities - .iter() - .map(|i| names - .get(i) - .map(|n| n.to_string()) - .unwrap_or(format!("{} <no name found>", i))) - .collect::<Vec<String>>() - .join(", ") - ); + match data.args.output_format { + OutputFormat::Human => { + if let Some(indexer) = &data.indexer { + let mut names = HashMap::<IdtyId, String>::new(); + indexer + .names_by_indexes(&online_authorities) + .await + .into_iter() + .for_each(|e| { + names.insert(e.0, e.1); + }); + println!("Online:"); + println!( + "{}", + online_authorities + .iter() + .map(|i| names + .get(i) + .map(|n| n.to_string()) + .unwrap_or(format!("{} <no name found>", i))) + .collect::<Vec<String>>() + .join(", ") + ); - println!("Incoming:"); - println!( - "{}", - incoming_authorities - .iter() - .map(|i| names - .get(i) - .map(|n| n.to_string()) - .unwrap_or(format!("{} <no name found>", i))) - .collect::<Vec<String>>() - .join(", ") - ); + println!("Incoming:"); + println!( + "{}", + incoming_authorities + .iter() + .map(|i| names + .get(i) + .map(|n| n.to_string()) + .unwrap_or(format!("{} <no name found>", i))) + .collect::<Vec<String>>() + .join(", ") + ); - println!("Outgoing:"); - println!( - "{}", - outgoing_authorities - .iter() - .map(|i| names - .get(i) - .map(|n| n.to_string()) - .unwrap_or(format!("{} <no name found>", i))) - .collect::<Vec<String>>() - .join(", ") - ); - } else { - println!("Online:"); - println!("{online_authorities:?}"); + println!("Outgoing:"); + println!( + "{}", + outgoing_authorities + .iter() + .map(|i| names + .get(i) + .map(|n| n.to_string()) + .unwrap_or(format!("{} <no name found>", i))) + .collect::<Vec<String>>() + .join(", ") + ); + } else { + println!("Online:"); + println!("{online_authorities:?}"); - println!("Incoming:"); - println!("{incoming_authorities:?}"); + println!("Incoming:"); + println!("{incoming_authorities:?}"); - println!("Outgoing:"); - println!("{outgoing_authorities:?}"); + println!("Outgoing:"); + println!("{outgoing_authorities:?}"); + } + } + OutputFormat::Json => { + let mut online_view = Vec::new(); + let mut incoming_view = Vec::new(); + let mut outgoing_view = Vec::new(); + + let names = if let Some(indexer) = &data.indexer { + let mut names_map = HashMap::<IdtyId, String>::new(); + let all_authorities = [ + online_authorities.clone(), + incoming_authorities.clone(), + outgoing_authorities.clone(), + ].concat(); + + indexer + .names_by_indexes(&all_authorities) + .await + .into_iter() + .for_each(|e| { + names_map.insert(e.0, e.1); + }); + + names_map + } else { + HashMap::new() + }; + + for id in online_authorities { + online_view.push(SmithAuthorityView { + id, + name: names.get(&id).cloned(), + }); + } + + for id in incoming_authorities { + incoming_view.push(SmithAuthorityView { + id, + name: names.get(&id).cloned(), + }); + } + + for id in outgoing_authorities { + outgoing_view.push(SmithAuthorityView { + id, + name: names.get(&id).cloned(), + }); + } + + let view = SmithAuthoritiesView { + online: online_view, + incoming: incoming_view, + outgoing: outgoing_view, + }; + + println!("{}", serde_json::to_string(&view).map_err(|e| anyhow!(e))?); + } } Ok(()) diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 07b40fc..f9a21db 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -6,6 +6,7 @@ use crate::entities::vault_account::{AccountTreeNode, ActiveModel, DbAccountId}; use crate::*; use crate::keys::seed_from_cesium; use age::secrecy::Secret; +use anyhow::anyhow; use sea_orm::ActiveValue::Set; use sea_orm::{ConnectionTrait, TransactionTrait}; use sea_orm::ModelTrait; @@ -213,19 +214,33 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let all_account_tree_node_hierarchies = vault_account::fetch_all_base_account_tree_node_hierarchies(db).await?; - let table = display::compute_vault_accounts_table_with_g1v1(&all_account_tree_node_hierarchies, show_g1v1, show_type)?; - - println!("available SS58 Addresses:"); - println!("{table}"); + match data.args.output_format { + OutputFormat::Human => { + let table = display::compute_vault_accounts_table_with_g1v1(&all_account_tree_node_hierarchies, show_g1v1, show_type)?; + println!("available SS58 Addresses:"); + println!("{table}"); + } + OutputFormat::Json => { + let json_view = display::compute_vault_accounts_json(&all_account_tree_node_hierarchies, show_g1v1); + println!("{}", serde_json::to_string(&json_view).map_err(|e| anyhow!(e))?); + } + } } ListChoice::Base { show_g1v1, show_type } => { let base_account_tree_nodes = vault_account::fetch_only_base_account_tree_nodes(db).await?; - let table = display::compute_vault_accounts_table_with_g1v1(&base_account_tree_nodes, show_g1v1, show_type)?; - - println!("available <Base> SS58 Addresses:"); - println!("{table}"); + match data.args.output_format { + OutputFormat::Human => { + let table = display::compute_vault_accounts_table_with_g1v1(&base_account_tree_nodes, show_g1v1, show_type)?; + println!("available <Base> SS58 Addresses:"); + println!("{table}"); + } + OutputFormat::Json => { + let json_view = display::compute_vault_accounts_json(&base_account_tree_nodes, show_g1v1); + println!("{}", serde_json::to_string(&json_view).map_err(|e| anyhow!(e))?); + } + } } ListChoice::For { address_or_vault_name, @@ -238,29 +253,52 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE let base_account_tree_node = vault_account::get_base_account_tree_node(&account_tree_node); - let table = display::compute_vault_accounts_table_with_g1v1(&[base_account_tree_node], show_g1v1, show_type)?; - - println!( - "available SS58 Addresses linked to {}:", - account_tree_node.borrow().account - ); - println!("{table}"); + match data.args.output_format { + OutputFormat::Human => { + let table = display::compute_vault_accounts_table_with_g1v1(&[base_account_tree_node.clone()], show_g1v1, show_type)?; + println!( + "available SS58 Addresses linked to {}:", + account_tree_node.borrow().account + ); + println!("{table}"); + } + OutputFormat::Json => { + let json_view = display::compute_vault_accounts_json(&[base_account_tree_node], show_g1v1); + println!("{}", serde_json::to_string(&json_view).map_err(|e| anyhow!(e))?); + } + } } }, 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}"); + match data.args.output_format { + OutputFormat::Human => { + 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}"); + } + OutputFormat::Json => { + println!("{}", serde_json::to_string(&vault_key_addresses).map_err(|e| anyhow!(e))?); + } + } } Subcommand::Use { address_or_vault_name, } => { let account = retrieve_vault_account(db, address_or_vault_name).await?; - println!("Using: {}", account); + match data.args.output_format { + OutputFormat::Human => { + println!("Using: {}", account); + } + OutputFormat::Json => { + let json_view = serde_json::json!({ + "address": account.to_string() + }); + println!("{}", serde_json::to_string(&json_view).map_err(|e| anyhow!(e))?); + } + } let updated_cfg = conf::Config { address: Some(account.address.0), @@ -516,7 +554,17 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE password, )?; - println!("Substrate URI: '{account_to_derive_secret_suri}'") + match data.args.output_format { + OutputFormat::Human => { + println!("Substrate URI: '{account_to_derive_secret_suri}'") + } + OutputFormat::Json => { + let json_view = serde_json::json!({ + "substrate_uri": account_to_derive_secret_suri + }); + println!("{}", serde_json::to_string(&json_view).map_err(|e| anyhow!(e))?); + } + } } Subcommand::Migrate => { println!("Migrating existing key files to db"); @@ -584,7 +632,19 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE println!("Migration done"); } Subcommand::Where => { - println!("{}", data.project_dir.data_dir().to_str().unwrap()); + let data_dir = data.project_dir.data_dir().to_str().unwrap(); + + match data.args.output_format { + OutputFormat::Human => { + println!("{}", data_dir); + } + OutputFormat::Json => { + let json_view = serde_json::json!({ + "data_dir": data_dir + }); + println!("{}", serde_json::to_string(&json_view).map_err(|e| anyhow!(e))?); + } + } } }; diff --git a/src/commands/vault/display.rs b/src/commands/vault/display.rs index 5f024d6..35e952e 100644 --- a/src/commands/vault/display.rs +++ b/src/commands/vault/display.rs @@ -4,6 +4,7 @@ use crate::entities::vault_account::AccountTreeNode; use crate::keys::CryptoScheme; use crate::utils::GcliError; use comfy_table::{Cell, Table}; +use serde::Serialize; use std::cell::RefCell; use std::rc::Rc; use std::str; @@ -181,6 +182,91 @@ pub fn compute_vault_accounts_row_with_g1v1( Ok(rows) } +/// Serializable structure for JSON output +#[derive(Serialize)] +pub struct VaultAccountView { + address: String, + crypto_scheme: Option<String>, + wallet_type: Option<String>, + path: String, + name: String, + g1v1_public_key: Option<String>, + children: Vec<VaultAccountView>, +} + +/// Compute a serializable structure for JSON output +pub fn compute_vault_accounts_json( + account_tree_nodes: &[Rc<RefCell<AccountTreeNode>>], + show_g1v1: bool, +) -> Vec<VaultAccountView> { + let mut result = Vec::new(); + + for account_tree_node in account_tree_nodes { + result.push(compute_vault_account_json(account_tree_node, show_g1v1)); + } + + result +} + +/// Compute a serializable structure for a single account tree node +fn compute_vault_account_json( + account_tree_node: &Rc<RefCell<AccountTreeNode>>, + show_g1v1: bool, +) -> VaultAccountView { + let empty_string = "".to_string(); + let borrowed_node = account_tree_node.borrow(); + + let name = if let Some(name) = borrowed_node.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 (path, crypto_scheme, wallet_type, g1v1_public_key) = if let Some(path) = borrowed_node.account.path.clone() { + (path, None, None, None) + } else { + let crypto_scheme = borrowed_node.account.crypto_scheme.map(|cs| { + let scheme = CryptoScheme::from(cs); + let scheme_str: &str = scheme.into(); + scheme_str.to_string() + }); + + let wallet_type = borrowed_node.account.secret_format.map(|sf| { + match crate::keys::SecretFormat::from(sf) { + crate::keys::SecretFormat::G1v1 => "G1v1".to_string(), + crate::keys::SecretFormat::Substrate => "Mnemonic".to_string(), + crate::keys::SecretFormat::Seed => "Seed".to_string(), + crate::keys::SecretFormat::Predefined => "Predefined".to_string(), + } + }); + + let g1v1_public_key = if show_g1v1 && crypto_scheme.as_deref() == Some("ed25519") { + Some(cesium::compute_g1v1_public_key_from_ed25519_account_id(&borrowed_node.account.address.0)) + } else { + None + }; + + (format!("<{}>", borrowed_node.account.account_type()), crypto_scheme, wallet_type, g1v1_public_key) + }; + + let mut children = Vec::new(); + for child in &borrowed_node.children { + children.push(compute_vault_account_json(child, show_g1v1)); + } + + VaultAccountView { + address: borrowed_node.account.address.to_string(), + crypto_scheme, + wallet_type, + path, + name, + g1v1_public_key, + children, + } +} + #[cfg(test)] mod tests { mod vault_accounts_table_tests { diff --git a/src/conf.rs b/src/conf.rs index 509e0c5..09c83f4 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -2,6 +2,7 @@ use crate::entities::vault_account; use crate::entities::vault_account::DbAccountId; use crate::*; use serde::{Deserialize, Serialize}; +use anyhow::anyhow; const APP_NAME: &str = "gcli"; @@ -73,6 +74,15 @@ pub enum Subcommand { Default, } +/// Config view for JSON serialization +#[derive(Serialize)] +struct ConfigView { + duniter_endpoint: String, + indexer_endpoint: String, + address: Option<String>, + vault_account: Option<String>, +} + /// handle conf command pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { // match subcommand @@ -84,15 +94,41 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE ); } Subcommand::Show => { - 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); + match data.args.output_format { + OutputFormat::Human => { + 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); + } + } + } + OutputFormat::Json => { + let mut vault_account_str = None; + 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? + { + vault_account_str = Some(account.to_string()); + } + } + + let view = ConfigView { + duniter_endpoint: data.cfg.duniter_endpoint.clone(), + indexer_endpoint: data.cfg.indexer_endpoint.clone(), + address: data.cfg.address.as_ref().map(|a| a.to_string()), + vault_account: vault_account_str, + }; + + println!("{}", serde_json::to_string(&view).map_err(|e| anyhow!(e))?); } } } -- GitLab