Skip to content
Snippets Groups Projects
Commit b7990182 authored by Hugo Trentesaux's avatar Hugo Trentesaux
Browse files

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
parent 124bd9c2
No related branches found
No related tags found
No related merge requests found
......@@ -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"
......
......@@ -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
......
......@@ -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
}
}
......
......@@ -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
......
......@@ -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))
}
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);
......
......@@ -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());
......
......@@ -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?;
......
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}");
}
};
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment