From b7990182049771014697506fbf7e08864f6f1a3d Mon Sep 17 00:00:00 2001 From: Hugo Trentesaux <hugo.trentesaux@lilo.org> Date: Mon, 19 Feb 2024 14:03:49 +0100 Subject: [PATCH] indexer check (clients/rust/gcli-v2s!24) * v0.2.8 * improve current block display * clippy fmt * display names of migrated identities in tech members * minor indexer improvements * add finalized / latest info * improve readability of indexer check --- Cargo.lock | 49 ++++++++- Cargo.toml | 3 +- res/indexer-queries.graphql | 18 ++++ src/cache.rs | 2 +- src/commands/blockchain.rs | 72 ++++++++++--- src/commands/collective.rs | 54 +++++----- src/commands/vault.rs | 2 +- src/data.rs | 20 ++-- src/indexer.rs | 207 +++++++++++++++++++++++++++--------- 9 files changed, 324 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e61c32..7a491fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1083,6 +1083,18 @@ dependencies = [ "unreachable", ] +[[package]] +name = "comfy-table" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c64043d6c7b7a4c58e39e7efccfdea7b93d885a795d0c054a69dbbf4dd52686" +dependencies = [ + "crossterm 0.27.0", + "strum", + "strum_macros", + "unicode-width", +] + [[package]] name = "common" version = "0.1.0" @@ -1264,6 +1276,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.2", + "crossterm_winapi", + "libc", + "parking_lot", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -2073,13 +2098,14 @@ dependencies = [ [[package]] name = "gcli" -version = "0.2.7" +version = "0.2.8" dependencies = [ "age", "anyhow", "bip39", "bs58", "clap", + "comfy-table", "confy", "directories 5.0.1", "env_logger", @@ -2620,7 +2646,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33e7c1ddeb15c9abcbfef6029d8e29f69b52b6d6c891031b88ed91b5065803b" dependencies = [ "bitflags 1.3.2", - "crossterm", + "crossterm 0.25.0", "dyn-clone", "lazy_static", "newline-converter", @@ -5148,6 +5174,25 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.48", +] + [[package]] name = "substrate-bip39" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index bd3166d..70db2d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ rust-version = "1.75.0" license = "AGPL-3.0-only" name = "gcli" repository = "https://git.duniter.org/clients/rust/gcli-v2s" -version = "0.2.7" +version = "0.2.8" [dependencies] # subxt is main dependency @@ -41,6 +41,7 @@ confy = "^0.5.1" bs58 = "^0.5.0" inquire = "^0.6.2" directories = "^5.0.1" +comfy-table = "^7.1.0" # crypto scrypt = { version = "^0.11", default-features = false } # for old-style key generation diff --git a/res/indexer-queries.graphql b/res/indexer-queries.graphql index bf7bb86..b02516a 100644 --- a/res/indexer-queries.graphql +++ b/res/indexer-queries.graphql @@ -39,9 +39,27 @@ query IdentityNameByPubkey($pubkey: String!) { } } +query WasIdentityNameByPubkey($pubkey: String!) { + accountById(id: $pubkey) { + wasIdentity { + identity { + name + } + } + } +} + query LatestBlock { blocks(limit: 1, orderBy: height_DESC) { height + hash + } +} + +query BlockByNumber($number: Int!) { + blocks(where: { height_eq: $number }) { + height + hash } } diff --git a/src/cache.rs b/src/cache.rs index 7f8f647..94316b5 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -38,7 +38,7 @@ impl IdentityCache { format!( "“ {} â€", if let Some(indexer) = &self.indexer { - if let Ok(Some(username)) = indexer.username_by_pubkey(&pubkey).await { + if let Some(username) = indexer.username_by_pubkey(&pubkey).await { username } else { pubkey diff --git a/src/commands/blockchain.rs b/src/commands/blockchain.rs index 2938229..ee46955 100644 --- a/src/commands/blockchain.rs +++ b/src/commands/blockchain.rs @@ -39,17 +39,11 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE commands::runtime::runtime_info(data).await; } Subcommand::CurrentBlock => { - println!( - "current block on {}: {}", - data.cfg.duniter_endpoint, - data.client() - .storage() - .at_latest() - .await? - .fetch(&runtime::storage().system().number()) - .await? - .unwrap() - ); + 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); } Subcommand::CreateBlock => { todo!() @@ -63,7 +57,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE } /// get genesis hash -pub async fn fetch_genesis_hash(data: &Data) -> Result<Hash, anyhow::Error> { +pub async fn fetch_genesis_hash(data: &Data) -> Result<Hash, subxt::Error> { Ok(data .client() .storage() @@ -73,3 +67,57 @@ pub async fn fetch_genesis_hash(data: &Data) -> Result<Hash, anyhow::Error> { .await? .unwrap()) } + +/// get finalized number +pub async fn fetch_finalized_number(data: &Data) -> Result<BlockNumber, subxt::Error> { + Ok(data + .client() + .storage() + .at_latest() + .await? + .fetch(&runtime::storage().system().number()) + .await? + .unwrap()) +} + +/// get finalized hash and number (require legacy) +pub async fn fetch_finalized_number_and_hash( + data: &Data, +) -> Result<(BlockNumber, Hash), subxt::Error> { + let hash = data + .legacy_rpc_methods() + .await + .chain_get_finalized_head() + .await?; + let number = data + .legacy_rpc_methods() + .await + .chain_get_block(Some(hash)) + .await? + .unwrap() + .block + .header + .number; + + Ok((number, hash)) +} + +/// get latest hash and number +pub async fn fetch_latest_number_and_hash( + data: &Data, +) -> Result<(BlockNumber, Hash), subxt::Error> { + let number = data + .legacy_rpc_methods() + .await + .chain_get_header(None) + .await? + .unwrap() + .number; + let hash = data + .legacy_rpc_methods() + .await + .chain_get_block_hash(Some(number.into())) + .await? + .unwrap(); + Ok((number, hash)) +} diff --git a/src/commands/collective.rs b/src/commands/collective.rs index 9717d66..4c77443 100644 --- a/src/commands/collective.rs +++ b/src/commands/collective.rs @@ -1,7 +1,5 @@ use crate::*; -use anyhow::Result; - /// define technical committee subcommands #[derive(Clone, Default, Debug, clap::Parser)] pub enum Subcommand { @@ -24,8 +22,8 @@ pub enum Subcommand { } /// handle technical committee commands -pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { - let data = data.build_client().await?; +pub async fn handle_command(data: Data, command: Subcommand) -> anyhow::Result<(), GcliError> { + let data = data.build_client().await?.build_indexer().await?; match command { Subcommand::Members => technical_committee_members(&data).await?, Subcommand::Propose { hex } => technical_committee_propose(&data, &hex).await?, @@ -48,43 +46,42 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE } /// list technical committee members -pub async fn technical_committee_members(data: &Data) -> Result<()> { +pub async fn technical_committee_members(data: &Data) -> Result<(), anyhow::Error> { let client = data.client(); - let indexer = data.indexer.clone(); + let indexer = &data.indexer; - let parent_hash = client + for account_id in client .storage() .at_latest() .await? - .fetch(&runtime::storage().system().parent_hash()) - .await? - .unwrap(); - - for account_id in client - .storage() - .at(parent_hash) .fetch(&runtime::storage().technical_committee().members()) .await? .unwrap_or_default() { println!( "{}", - if let Some(indexer) = &indexer { - indexer - .username_by_pubkey(&account_id.to_string()) - .await - .ok() - .flatten() + if let Some(indexer) = indexer { + // indexer is set, we can get the name + let name = indexer.username_by_pubkey(&account_id.to_string()).await; + if name.is_some() { + name + } else { + indexer + .wasname_by_pubkey(&account_id.to_string()) + .await + .map(|name| format!("{name}\t(old account)")) + } } else { + // indexer is not set, we can get the idty index by accountid client .storage() - .at(parent_hash) + .at_latest() + .await? .fetch(&runtime::storage().identity().identity_index_of(&account_id)) - .await - .ok() - .flatten() + .await? .map(|identity_id| format!("{identity_id}")) } + // no idty found, display account_id .unwrap_or_else(|| account_id.to_string(),) ); } @@ -96,7 +93,7 @@ pub async fn technical_committee_members(data: &Data) -> Result<()> { // TODO: // * better formatting (format pubkeys to SS58 and add usernames) // * display proposals indices -pub async fn technical_committee_proposals(client: &Client) -> Result<()> { +pub async fn technical_committee_proposals(client: &Client) -> anyhow::Result<()> { let parent_hash = client .storage() .at_latest() @@ -125,7 +122,7 @@ pub async fn technical_committee_vote( proposal_hash: Hash, proposal_index: u32, vote: bool, -) -> Result<(), subxt::Error> { +) -> anyhow::Result<(), subxt::Error> { submit_call_and_look_event::< runtime::technical_committee::events::Voted, Payload<runtime::technical_committee::calls::types::Vote>, @@ -140,7 +137,10 @@ pub async fn technical_committee_vote( /// propose call given as hexadecimal /// can be generated with `subxt explore` for example -pub async fn technical_committee_propose(data: &Data, proposal: &str) -> Result<(), subxt::Error> { +pub async fn technical_committee_propose( + data: &Data, + proposal: &str, +) -> anyhow::Result<(), subxt::Error> { let raw_call = hex::decode(proposal).expect("invalid hex"); let call = codec::decode_from_bytes(raw_call.into()).expect("invalid call"); let payload = runtime::tx().technical_committee().propose(5, call, 100); diff --git a/src/commands/vault.rs b/src/commands/vault.rs index 14c3766..b6e09b7 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -81,7 +81,7 @@ pub fn store_mnemonic( password: String, ) -> Result<AccountId, GcliError> { // check validity by deriving keypair - let keypair = pair_from_str(&mnemonic)?; + let keypair = pair_from_str(mnemonic)?; let address = keypair.public(); // write encrypted mnemonic in file identified by pubkey let path = data.project_dir.data_dir().join(address.to_string()); diff --git a/src/data.rs b/src/data.rs index ce0e06c..16cb048 100644 --- a/src/data.rs +++ b/src/data.rs @@ -6,6 +6,7 @@ use indexer::Indexer; // consts pub const LOCAL_DUNITER_ENDPOINT: &str = "ws://localhost:9944"; pub const LOCAL_INDEXER_ENDPOINT: &str = "http://localhost:4350/graphql"; +const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); #[cfg(feature = "gdev")] pub const GDEV_DUNITER_ENDPOINTS: [&str; 5] = [ @@ -187,14 +188,13 @@ impl Data { } /// 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) // needed to get more details TODO fixme - )) - })?); + 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) } @@ -206,9 +206,9 @@ impl Data { } else { self.indexer = Some(Indexer { gql_client: reqwest::Client::builder() - .user_agent("gcli/0.1.0") + .user_agent(format!("gcli/{PKG_VERSION}")) .build() - .map_err(|e| anyhow!(e))?, + .unwrap(), gql_url: self.cfg.indexer_endpoint.clone(), }); self.indexer_genesis_hash = self.indexer().fetch_genesis_hash().await?; diff --git a/src/indexer.rs b/src/indexer.rs index 00e9959..495a99a 100644 --- a/src/indexer.rs +++ b/src/indexer.rs @@ -1,8 +1,9 @@ -use graphql_client::{reqwest::post_graphql, GraphQLQuery}; -use sp_core::Bytes; - use crate::*; +use comfy_table::*; +use comfy_table::{ContentArrangement, Table}; +use graphql_client::{reqwest::post_graphql, GraphQLQuery}; use identity_info::*; +use sp_core::Bytes; // type used in parameters query // #[allow(non_camel_case_types)] @@ -32,6 +33,14 @@ pub struct IdentityInfo; )] pub struct IdentityNameByPubkey; +// pubkey → wasidentity +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "res/indexer-schema.json", + query_path = "res/indexer-queries.graphql" +)] +pub struct WasIdentityNameByPubkey; + #[derive(GraphQLQuery)] #[graphql( schema_path = "res/indexer-schema.json", @@ -39,6 +48,13 @@ pub struct IdentityNameByPubkey; )] pub struct LatestBlock; +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "res/indexer-schema.json", + query_path = "res/indexer-queries.graphql" +)] +pub struct BlockByNumber; + #[derive(GraphQLQuery, Debug)] #[graphql( schema_path = "res/indexer-schema.json", @@ -68,17 +84,37 @@ impl Indexer { } /// pubkey → name - pub async fn username_by_pubkey(&self, pubkey: &str) -> anyhow::Result<Option<String>> { - Ok(post_graphql::<IdentityNameByPubkey, _>( + pub async fn username_by_pubkey(&self, pubkey: &str) -> Option<String> { + post_graphql::<IdentityNameByPubkey, _>( &self.gql_client, &self.gql_url, identity_name_by_pubkey::Variables { pubkey: pubkey.to_string(), }, ) - .await? + .await + .expect("indexer connexion error") .data - .and_then(move |mut data| data.identities.pop().map(|idty| idty.name))) + .and_then(move |mut data| data.identities.pop().map(|idty| idty.name)) + } + + /// pubkey → was name + pub async fn wasname_by_pubkey(&self, pubkey: &str) -> Option<String> { + post_graphql::<WasIdentityNameByPubkey, _>( + &self.gql_client, + &self.gql_url, + was_identity_name_by_pubkey::Variables { + pubkey: pubkey.to_string(), + }, + ) + .await + .expect("indexer connexion error") + .data + .and_then(move |data| { + data.account_by_id + .and_then(|mut acc| acc.was_identity.pop()) + .map(|idty| idty.identity.name) + }) } /// index → info @@ -91,27 +127,40 @@ impl Indexer { }, ) .await - .expect("problem") + .expect("indexer connexion error") .data .and_then(move |mut data| data.identities.pop()) } - /// fetch latest block number - pub async fn fetch_latest_block(&self) -> Result<u64, anyhow::Error> { - Ok(post_graphql::<LatestBlock, _>( + /// fetch latest block + pub async fn fetch_latest_block(&self) -> Option<latest_block::LatestBlockBlocks> { + post_graphql::<LatestBlock, _>( &self.gql_client, self.gql_url.clone(), latest_block::Variables {}, ) - .await? + .await + .expect("indexer connexion error") .data - .unwrap() // must have a data field - .blocks - .first() - .unwrap() // must have one and only one parameter matching request - .height - .try_into() - .unwrap()) + .and_then(move |mut data| data.blocks.pop()) + } + + /// fetch block by number + pub async fn fetch_block_by_number( + &self, + number: BlockNumber, + ) -> Option<block_by_number::BlockByNumberBlocks> { + post_graphql::<BlockByNumber, _>( + &self.gql_client, + self.gql_url.clone(), + block_by_number::Variables { + number: number.into(), + }, + ) + .await + .expect("indexer connexion error") + .data + .and_then(move |mut data| data.blocks.pop()) } /// fetch genesis hash @@ -124,7 +173,10 @@ impl Indexer { genesis_hash::Variables {}, ) .await - .map_err(|e| anyhow!(e))?; + .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)); @@ -137,57 +189,114 @@ impl Indexer { ))? .blocks .first() - .unwrap() // must have one and only one block matching request + .ok_or_else(|| GcliError::Indexer("genesis block not yet indexed".to_string()))? .hash .clone(); // convert it - let hash = TryInto::<[u8; 32]>::try_into(hash.as_ref()).unwrap(); - Ok(hash.into()) + Ok(convert_hash(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() +} + /// define indexer subcommands #[derive(Clone, Default, Debug, clap::Parser)] pub enum Subcommand { #[default] - /// 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, - /// Fetch latest indexed block - LatestBlock, } /// 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::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(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(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, + } } - } - Subcommand::LatestBlock => { - println!( - "latest block indexed by {} is: {}", - data.cfg.indexer_endpoint, - data.indexer().fetch_latest_block().await? - ); + + 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}"); } }; -- GitLab