Skip to content
Snippets Groups Projects
indexer.rs 6.77 KiB
Newer Older
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::*;

#[derive(Clone, Debug)]
Hugo Trentesaux's avatar
Hugo Trentesaux committed
pub struct Indexer {
	pub gql_client: reqwest::Client,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	pub gql_url: String,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
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)
			.expect("indexer connexion error");
		if let Some(errs) = response.errors {
			log::debug!("{:?}", errs)
		}
		response.data.expect("indexer error")
	pub async fn username_by_index(&self, index: u32) -> Option<String> {
		self.query::<IdentityNameByIndex>(identity_name_by_index::Variables {
			index: index.into(),
		})
		.await
		.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
		.into_iter()
		.map(|idty| (idty.index as IdtyId, idty.name))
		.collect()
	pub async fn username_by_pubkey(&self, pubkey: &str) -> Option<String> {
		self.query::<IdentityNameByPubkey>(identity_name_by_pubkey::Variables {
			pubkey: pubkey.to_string(),
		})
		.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(),
		.and_then(|mut acc| acc.was_identity.pop())
		.map(|idty| idty.identity.unwrap().name)
	pub async fn identity_info(&self, index: u32) -> Option<identity_info::IdentityInfoIdentity> {
		self.query::<IdentityInfo>(identity_info::Variables {
			index: index.into(),
		})
	/// fetch latest block
	pub async fn fetch_latest_block(&self) -> Option<latest_block::LatestBlockBlock> {
		self.query::<LatestBlock>(latest_block::Variables {})
			.await
	}

	/// 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(),
		})

	/// 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 {},
		)
			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(),
			))?
			.ok_or_else(|| GcliError::Indexer("genesis block not yet indexed".to_string()))?
		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();
/// 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...)
/// 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))
				(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}");