Skip to content
Snippets Groups Projects
data.rs 8.35 KiB
Newer Older
use std::str::FromStr;

Hugo Trentesaux's avatar
Hugo Trentesaux committed
use crate::*;
use indexer::Indexer;
Hugo Trentesaux's avatar
Hugo Trentesaux committed

// 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",
];

Hugo Trentesaux's avatar
Hugo Trentesaux committed
// 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,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	// config
	pub cfg: conf::Config,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	// rpc to substrate client
	pub client: Option<Client>,
	// graphql to duniter-indexer
	pub indexer: Option<Indexer>,
	// user keypair
	pub keypair: Option<KeyPair>,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	// user identity index
	pub idty_index: Option<IdtyId>,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	// 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,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
}

/// 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(),
Hugo Trentesaux's avatar
Hugo Trentesaux committed
			token_decimals: 0,
			token_symbol: "tokens".into(),
			..Default::default()
		}
		.overwrite_from_args()
		.build_from_config()
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	}
	// --- 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")
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	}
	pub fn keypair(&self) -> KeyPair {
		match self.keypair.clone() {
			Some(keypair) => keypair,
			None => prompt_secret(self.args.secret_format),
		}
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	}
	pub fn idty_index(&self) -> IdtyId {
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		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
	/// 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());
		}
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		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)
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	}
	/// 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;
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		} else {
			self.indexer = Some(Indexer {
Hugo Trentesaux's avatar
Hugo Trentesaux committed
				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")
			}
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		};
		Ok(self)
	}
	/// get issuer index
	/// needs address and client first
	pub async fn fetch_idty_index(mut self) -> Result<Self, GcliError> {
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		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(),
				))?,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		);
		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?;
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		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)
	}
}