Newer
Older
// 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,
// rpc to substrate client
pub client: Option<Client>,
// graphql to duniter-indexer
pub indexer: Option<Indexer>,
// user keypair
// 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,
token_decimals: 0,
token_symbol: "tokens".into(),
..Default::default()
}
}
// --- 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")
match self.keypair.clone() {
Some(keypair) => keypair,
None => prompt_secret(self.args.secret_format),
}
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 {
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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}\"");
}
}
}
if let Some(duniter_endpoint) = self.args.url.clone() {
self.cfg.duniter_endpoint = duniter_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);
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());
}
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)
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;
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)
}
}