pub mod queries; use crate::*; 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 { pub gql_client: reqwest::Client, pub gql_url: String, } impl Indexer { /// 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") } /// 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) } /// 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 // 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 .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] /// Check that indexer and node are on the same network /// (genesis hash, latest indexed block...) Check, } /// handle indexer commands pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { // build indexer because it is needed for all subcommands 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 => { 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 { (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}"); } }; Ok(()) }