-
Hugo Trentesaux authoredHugo Trentesaux authored
data.rs 8.07 KiB
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:4350/graphql";
const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[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://squid.gdev.coinduf.eu/v1/graphql",
"https://squid.gdev.gyroi.de/v1/graphql",
"https://gdev-squid.axiom-team.fr/v1/graphql",
];
// data derived from command arguments
/// Data of current command
/// can also include fetched information
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,
// gcli base path
pub project_dir: directories::ProjectDirs,
}
/// system properties defined in client specs
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SystemProperties {
token_decimals: u32,
token_symbol: String,
}
impl Default for Data {
fn default() -> Self {
let project_dir = directories::ProjectDirs::from("org", "duniter", "gcli").unwrap();
if !project_dir.data_dir().exists() {
std::fs::create_dir_all(project_dir.data_dir()).expect("could not create data dir");
};
Self {
project_dir,
args: Default::default(),
cfg: Default::default(),
client: Default::default(),
indexer: Default::default(),
keypair: Default::default(),
idty_index: Default::default(),
token_decimals: Default::default(),
token_symbol: Default::default(),
genesis_hash: Default::default(),
indexer_genesis_hash: Default::default(),
}
}
}
// implement helper functions for Data
impl Data {
/// --- constructor ---
pub fn new(args: Args) -> Result<Self, GcliError> {
Self {
args,
cfg: conf::load_conf(),
token_decimals: 0,
token_symbol: "tokens".into(),
..Default::default()
}
.overwrite_from_args()
}
// --- 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 => loop {
match fetch_or_get_keypair(self, self.cfg.address.clone()) {
Ok(pair) => return pair,
Err(e) => println!("{e:?} → retry"),
}
},
}
}
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) -> Result<Self, GcliError> {
// 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 let Some(secret_format) = self.args.secret_format {
let keypair = get_keypair(secret_format, self.args.secret.as_deref())?;
self.cfg.address = Some(keypair.address());
self.keypair = Some(keypair);
}
// 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.keypair = None
}
Ok(self)
}
/// build a client from url
pub async fn build_client(mut self) -> Result<Self, GcliError> {
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)
}
/// build an indexer if not disabled
pub async fn build_indexer(mut self) -> Result<Self, GcliError> {
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(format!("gcli/{PKG_VERSION}"))
.build()
.unwrap(),
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)
}
}