use std::str::FromStr; use crate::*; use indexer::Indexer; // consts pub const LOCAL_DUNITER_ENDPOINT: &str = "ws://localhost:9944"; pub const LOCAL_INDEXER_ENDPOINT: &str = "http://localhost:8080/v1/graphql"; #[cfg(feature = "gdev")] pub const GDEV_DUNITER_ENDPOINTS: [&str; 5] = [ "wss://gdev.p2p.legal:443/ws", "wss://gdev.coinduf.eu:443/ws", "wss://vit.fdn.org:443/ws", "wss://gdev.cgeek.fr:443/ws", "wss://gdev.pini.fr:443/ws", ]; #[cfg(feature = "gdev")] pub const GDEV_INDEXER_ENDPOINTS: [&str; 2] = [ "https://gdev-indexer.p2p.legal/v1/graphql", "https://hasura.gdev.coinduf.eu/v1/graphql", ]; // data derived from command arguments /// Data of current command /// can also include fetched information #[derive(Default)] pub struct Data { // command line arguments pub args: Args, // config pub cfg: conf::Config, // rpc to substrate client pub client: Option<Client>, // graphql to duniter-indexer pub indexer: Option<Indexer>, // user keypair pub keypair: Option<KeyPair>, // user identity index pub idty_index: Option<IdtyId>, // token decimals pub token_decimals: u32, // token symbol pub token_symbol: String, // genesis hash pub genesis_hash: Hash, // indexer genesis hash pub indexer_genesis_hash: Hash, } /// system properties defined in client specs #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct SystemProperties { token_decimals: u32, token_symbol: String, } // implement helper functions for Data impl Data { /// --- constructor --- pub fn new(args: Args) -> Self { Self { args, cfg: conf::load_conf(), token_decimals: 0, token_symbol: "tokens".into(), ..Default::default() } .overwrite_from_args() .build_from_config() } // --- getters --- // the "unwrap" should not fail if data is well prepared pub fn client(&self) -> &Client { self.client.as_ref().expect("must build client first") } pub fn indexer(&self) -> &Indexer { self.indexer.as_ref().expect("indexer is not set up") } pub fn address(&self) -> AccountId { self.cfg.address.clone().expect("an address is needed") } pub fn keypair(&self) -> KeyPair { match self.keypair.clone() { Some(keypair) => keypair, None => prompt_secret(self.args.secret_format), } } pub fn idty_index(&self) -> IdtyId { self.idty_index.expect("must fetch idty index first") } // --- methods --- pub fn format_balance(&self, amount: Balance) -> String { let base: u32 = 10; format!( "{} {}", (amount as f32) / (base.pow(self.token_decimals) as f32), self.token_symbol ) } // --- mutators --- /// use arguments to overwrite config pub fn overwrite_from_args(mut self) -> Self { // network if let Some(network) = self.args.network.clone() { // a network was provided as arugment match &network[..] { // force local endpoints (always available) "local" => { self.cfg.duniter_endpoint = String::from(LOCAL_DUNITER_ENDPOINT); self.cfg.indexer_endpoint = String::from(LOCAL_INDEXER_ENDPOINT); } // if built with gdev feature, use gdev network #[cfg(feature = "gdev")] "gdev" => { // TODO better strategy than first self.cfg.duniter_endpoint = String::from(*GDEV_DUNITER_ENDPOINTS.first().unwrap()); self.cfg.indexer_endpoint = String::from(*GDEV_INDEXER_ENDPOINTS.first().unwrap()); } // if built with gtest feature, use gtest network #[cfg(feature = "gtest")] "gtest" => { unimplemented!(); } // if built with g1 feature, use g1 network #[cfg(feature = "g1")] "g1" => { unimplemented!(); } other => { panic!("unknown network \"{other}\""); } } } // duniter endpoint if let Some(duniter_endpoint) = self.args.url.clone() { self.cfg.duniter_endpoint = duniter_endpoint; } // indexer endpoint if let Some(indexer_endpoint) = self.args.indexer.clone() { self.cfg.indexer_endpoint = indexer_endpoint } // secret format and value if self.args.secret_format == SecretFormat::Predefined { // predefined secret format overwrites secret with mnemonic match self.args.secret.clone() { None => {} Some(derivation) => { self.cfg.secret = Some(predefined_mnemonic(&derivation)); } }; } else if self.args.secret_format == SecretFormat::Cesium { // cesium secret format also overwrites, to force valid prompt self.cfg.secret = None } else if let Some(secret) = self.args.secret.clone() { // other secret type self.cfg.secret = Some(secret); } // address if let Some(address) = self.args.address.clone() { self.cfg.address = Some(AccountId::from_str(&address).expect("invalid address")); // if giving address, cancel secret self.cfg.secret = None } self } /// build from config pub fn build_from_config(mut self) -> Self { let secret_format = self.args.secret_format; // prevent incoherent state if secret_format == SecretFormat::Cesium && self.cfg.secret.is_some() { panic!("incompatible input: secret arg with cesium format"); } // if secret format is cesium, force a prompt now and record keypair if secret_format == SecretFormat::Cesium { let keypair = prompt_secret(SecretFormat::Cesium); self.cfg.address = Some(keypair.address()); self.keypair = Some(keypair); } // if a secret is defined (format should not be cesium), build keypair and silently overwrite address if let Some(secret) = self.cfg.secret.clone() { let keypair = pair_from_secret(secret_format, &secret).expect("invalid secret"); self.cfg.address = Some(keypair.public().into()); self.keypair = Some(keypair.into()); } self } /// 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 )) })?); self.genesis_hash = commands::blockchain::fetch_genesis_hash(&self).await?; Ok(self) } /// build an indexer if not disabled pub async fn build_indexer(mut self) -> Result<Self, anyhow::Error> { if self.args.no_indexer { log::info!("called build_indexer while providing no_indexer"); self.indexer = None; } else { self.indexer = Some(Indexer { gql_client: reqwest::Client::builder() .user_agent("gcli/0.1.0") .build()?, gql_url: self.cfg.indexer_endpoint.clone(), }); self.indexer_genesis_hash = self.indexer().fetch_genesis_hash().await?; if self.client.is_some() && self.indexer_genesis_hash != self.genesis_hash { println!("⚠️ indexer does not have the same genesis hash as blockchain") } }; Ok(self) } /// get issuer index /// needs address and client first pub async fn fetch_idty_index(mut self) -> Result<Self, GcliError> { self.idty_index = Some( commands::identity::get_idty_index_by_account_id(self.client(), &self.address()) .await? .ok_or(GcliError::Logic( "you need to be member to use this command".to_string(), ))?, ); Ok(self) } /// get properties pub async fn fetch_system_properties(mut self) -> Result<Self, anyhow::Error> { let system_properties = self.legacy_rpc_methods().await.system_properties().await?; let system_properties = serde_json::from_value::<SystemProperties>( serde_json::Value::Object(system_properties), )?; self.token_decimals = system_properties.token_decimals; self.token_symbol = system_properties.token_symbol; Ok(self) } // TODO prevent awaits in async methods, prefer register requests and execute them all at once with a join // example below // example!( // use futures::join; // let addr_idty_index = runtime::storage().identity().identity_index_of(&account_id); // let addr_block_hash = runtime::storage().system().block_hash(0); // // Multiple fetches can be done in parallel. // let (idty_index, genesis_hash) = join!( // api.storage().fetch(&addr_idty_index, None), // api.storage().fetch(&addr_block_hash, None) // ); // ); } // legacy methods (see subxt changelog) use subxt::{ backend::{legacy::LegacyRpcMethods, rpc::RpcClient}, config::SubstrateConfig, }; impl Data { pub async fn legacy_rpc_methods(&self) -> LegacyRpcMethods<SubstrateConfig> { let rpc_client = RpcClient::from_url(self.cfg.duniter_endpoint.clone()) .await .expect("error"); LegacyRpcMethods::<SubstrateConfig>::new(rpc_client) } }