mod cache; mod commands; mod indexer; mod keys; use clap::Parser; use codec::Encode; use keys::*; use serde::Deserialize; use sp_core::sr25519::Pair; use sp_core::H256; #[cfg(feature = "dev")] #[subxt::subxt( runtime_metadata_path = "res/metadata.scale", derive_for_all_types = "Debug" )] pub mod runtime { // IF NEEDED // #[subxt(substitute_type = "spcore::sr25519::Signature")] // use crate::gdev::runtime_types::sp_core::sr25519::Signature; } pub type Client = subxt::OnlineClient<Runtime>; pub type AccountId = subxt::ext::sp_runtime::AccountId32; pub type TxInBlock = subxt::tx::TxInBlock<Runtime, Client>; pub type TxProgress = subxt::tx::TxProgress<Runtime, Client>; pub type Balance = u64; pub type AccountData = runtime::runtime_types::pallet_duniter_account::types::AccountData<Balance>; pub type AccountInfo = runtime::runtime_types::frame_system::AccountInfo<u32, AccountData>; pub enum Runtime {} impl subxt::config::Config for Runtime { type Index = u32; type BlockNumber = u32; type Hash = sp_core::H256; type Hashing = subxt::ext::sp_runtime::traits::BlakeTwo256; type AccountId = AccountId; type Address = subxt::ext::sp_runtime::MultiAddress<Self::AccountId, u32>; type Header = subxt::ext::sp_runtime::generic::Header< Self::BlockNumber, subxt::ext::sp_runtime::traits::BlakeTwo256, >; type Signature = subxt::ext::sp_runtime::MultiSignature; type ExtrinsicParams = subxt::tx::BaseExtrinsicParams<Self, Tip>; } #[derive(Copy, Clone, Debug, Default, Encode)] pub struct Tip { #[codec(compact)] tip: u64, } impl Tip { pub fn new(amount: u64) -> Self { Tip { tip: amount } } } impl From<u64> for Tip { fn from(n: u64) -> Self { Self::new(n) } } #[derive(Clone, Parser, Debug, Default)] #[clap(author, version, about, long_about = None)] pub struct Args { #[clap(subcommand)] pub subcommand: Subcommand, /// Indexer URL #[clap(short, long, default_value = "http://localhost:8080/v1/graphql")] indexer: String, /// Do not use indexer #[clap(long)] no_indexer: bool, /// Secret key or BIP39 mnemonic /// (eventually followed by derivation path) #[clap(short, long)] secret: Option<String>, /// Secret key format (seed, substrate) #[clap(short = 'S', long, default_value = SecretFormat::Substrate)] secret_format: SecretFormat, /// Address #[clap(short, long)] address: Option<String>, /// Websocket RPC endpoint #[clap(short, long, default_value = "ws://localhost:9944")] url: String, } /// Data of current command /// can also include fetched information #[derive(Default)] pub struct Data { pub args: Args, pub client: Option<Client>, pub address: Option<AccountId>, pub keypair: Option<Pair>, pub idty_index: Option<u32>, pub token_decimals: u32, pub token_symbol: String, } #[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.clone().unwrap() } pub fn address(&self) -> AccountId { self.address.clone().unwrap() } pub fn keypair(&self) -> Pair { self.keypair.clone().unwrap() } pub fn idty_index(&self) -> u32 { self.idty_index.unwrap() } // --- 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 --- /// force an address if needed pub fn build_address(mut self) -> Self { self.address = Some( get_keys( self.args.secret_format, &self.args.address, &self.args.secret, NeededKeys::Public, ) .expect("needed") .0 .expect("needed"), ); self } /// force a keypair if needed pub fn build_keypair(mut self) -> Self { let (address, keypair) = get_keys( self.args.secret_format, &self.args.address, &self.args.secret, NeededKeys::Secret, ) .expect("needed"); self.address = address; self.keypair = keypair; self } /// build a client from url // TODO get client from a pre-defined list pub async fn build_client(mut self) -> Self { self.client = Some(Client::from_url(&self.args.url).await.expect("needed")); self } /// get issuer index /// needs address and client first pub async fn fetch_idty_index(mut self) -> Result<Self, anyhow::Error> { self.idty_index = Some( commands::identity::get_idty_index_by_account_id( self.client().clone(), &self.address(), ) .await? .ok_or(anyhow::anyhow!("needs to be member to use this command"))?, ); Ok(self) } /// get properties pub async fn fetch_system_properties(mut self) -> Result<Self, anyhow::Error> { let system_properties = self.client().clone().rpc().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) } } /// track progress of transaction on the network /// until it is in block with success or failure pub async fn track_progress(progress: TxProgress) -> anyhow::Result<()> { println!("submitted transaction to network, waiting 6 seconds..."); // wait for in block let tx = progress.wait_for_in_block().await?; // print result println!("{:?}", tx.wait_for_success().await?); // return empty Ok(()) } /// custom error type intended to provide more convenient error message to user #[derive(Debug)] pub enum GcliError { /// error coming from subxt Subxt(subxt::Error), /// error coming from anyhow Anyhow(anyhow::Error), } impl std::fmt::Display for GcliError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{:?}", self) } } impl std::error::Error for GcliError {} impl From<subxt::Error> for GcliError { fn from(e: subxt::Error) -> GcliError { GcliError::Subxt(e) } } impl From<anyhow::Error> for GcliError { fn from(e: anyhow::Error) -> GcliError { GcliError::Anyhow(e) } } #[derive(Clone, Debug, clap::Subcommand, Default)] pub enum Subcommand { /// Fetch account balance #[default] GetBalance, /// Create and certify an identity /// /// Caller must be member, and the target account must exist. CreateIdentity { target: sp_core::crypto::AccountId32, }, /// Confirm an identity /// /// To be called by the certified not-yet-member account, to become member. ConfirmIdentity { name: String, }, CreateOneshot { balance: u64, dest: sp_core::crypto::AccountId32, }, ConsumeOneshot { dest: sp_core::crypto::AccountId32, #[clap(long = "oneshot")] dest_oneshot: bool, }, ConsumeOneshotWithRemaining { balance: u64, dest: sp_core::crypto::AccountId32, #[clap(long = "one")] dest_oneshot: bool, remaining_to: sp_core::crypto::AccountId32, #[clap(long = "rem-one")] remaining_to_oneshot: bool, }, /// List upcoming expirations that require an action Expire { /// Show certs that expire within less than this number of blocks #[clap(short, long, default_value_t = 100800)] blocks: u32, /// Show authorities that should rotate keys within less than this number of sessions #[clap(short, long, default_value_t = 100)] sessions: u32, }, /// Fetch identity Identity { #[clap(short = 'p', long = "pubkey")] account_id: Option<sp_core::crypto::AccountId32>, #[clap(short = 'i', long = "identity")] identity_id: Option<u32>, #[clap(short = 'u', long = "username")] username: Option<String>, }, /// Generate a revocation document for the provided account GenRevocDoc, GoOffline, GoOnline, OneshotBalance { account: sp_core::crypto::AccountId32, }, /// List online authorities Online, #[clap(hide = true)] Repart { // Number of transactions per block to target target: u32, #[clap(short = 'o', long = "old-repart")] // Old/actual repartition actual_repart: Option<u32>, }, #[clap(hide = true)] SpamRoll { actual_repart: usize, }, SudoSetKey { new_key: sp_core::crypto::AccountId32, }, /// Emit a smith certification SmithCert { to: u32, }, /// List members of the technical committee TechMembers, /// List proposals to the technical committee TechProposals, /// Vote a proposal to the technical committee TechVote { /// Proposal hash hash: H256, /// Proposal index index: u32, /// Vote (0=against, 1=for) vote: u8, }, Transfer { /// Amount to transfer amount: u64, /// Destination address dest: sp_core::crypto::AccountId32, /// Prevent from going below account existential deposit #[clap(short = 'k', long = "keep-alive")] keep_alive: bool, }, /// Transfer the same amount for each space-separated address. /// If an address appears mutiple times, it will get multiple times the same amount TransferMultiple { /// Amount given to each destination address amount: u64, /// List of target addresses dests: Vec<sp_core::crypto::AccountId32>, }, /// Rotate and set session keys UpdateKeys, /// Revoke an identity RevokeIdentity, } #[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), GcliError> { env_logger::init(); let args = Args::parse(); let mut data = Data::new(args.clone()); match args.subcommand { Subcommand::GetBalance => { data = data .build_keypair() .build_client() .await .fetch_system_properties() .await?; commands::account::get_balance(data).await? } Subcommand::CreateIdentity { target } => { data = data.build_client().await.build_keypair(); let progress = commands::identity::create_identity(data.keypair(), data.client(), target).await?; track_progress(progress).await? } Subcommand::ConfirmIdentity { name } => { data = data.build_client().await.build_keypair(); let progress = commands::identity::confirm_identity(data.keypair(), data.client(), name).await?; track_progress(progress).await? } Subcommand::CreateOneshot { balance, dest } => { commands::oneshot::create_oneshot_account( get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )? .1 .unwrap(), Client::from_url(&args.url).await.unwrap(), balance, dest, ) .await? } Subcommand::ConsumeOneshot { dest, dest_oneshot } => { commands::oneshot::consume_oneshot_account( get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )? .1 .unwrap(), Client::from_url(&args.url).await.unwrap(), dest, dest_oneshot, ) .await? } Subcommand::ConsumeOneshotWithRemaining { balance, dest, dest_oneshot, remaining_to, remaining_to_oneshot, } => { commands::oneshot::consume_oneshot_account_with_remaining( get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )? .1 .unwrap(), Client::from_url(&args.url).await.unwrap(), balance, dest, dest_oneshot, remaining_to, remaining_to_oneshot, ) .await? } Subcommand::Expire { blocks, sessions } => { commands::expire::monitor_expirations( Client::from_url(&args.url).await.unwrap(), blocks, sessions, &args, ) .await? } Subcommand::Identity { ref account_id, identity_id, ref username, } => { commands::identity::get_identity( Client::from_url(&args.url).await.unwrap(), account_id.clone(), identity_id, username.clone(), &args, ) .await? } Subcommand::GenRevocDoc => { commands::revocation::generate_revoc_doc( &Client::from_url(&args.url).await.unwrap(), &get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )? .1 .unwrap(), ) .await? } Subcommand::GoOffline => { commands::smith::go_offline( get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )? .1 .unwrap(), Client::from_url(&args.url).await.unwrap(), ) .await? } Subcommand::GoOnline => { commands::smith::go_online( get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )? .1 .unwrap(), Client::from_url(&args.url).await.unwrap(), ) .await? } Subcommand::OneshotBalance { account } => { commands::oneshot::oneshot_account_balance( Client::from_url(&args.url).await.unwrap(), account, ) .await? } Subcommand::Online => { commands::smith::online(Client::from_url(&args.url).await.unwrap(), &args).await? } Subcommand::Repart { target, actual_repart, } => { commands::net_test::repart( get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )? .1 .unwrap(), Client::from_url(&args.url).await.unwrap(), target, actual_repart, ) .await? } Subcommand::SpamRoll { actual_repart } => { commands::net_test::spam_roll( get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )? .1 .unwrap(), Client::from_url(&args.url).await.unwrap(), actual_repart, ) .await? } Subcommand::SudoSetKey { new_key } => { commands::sudo::set_key( get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )? .1 .unwrap(), Client::from_url(&args.url).await.unwrap(), new_key, ) .await? } Subcommand::SmithCert { to } => { data = data .build_client() .await .build_keypair() .fetch_idty_index() .await?; commands::smith::cert(data.client(), data.keypair(), data.idty_index(), to).await? } Subcommand::TechMembers => { commands::collective::technical_committee_members( Client::from_url(&args.url).await.unwrap(), &args, ) .await? } Subcommand::TechProposals => { commands::collective::technical_committee_proposals( Client::from_url(&args.url).await.unwrap(), ) .await? } Subcommand::TechVote { hash, index, vote } => { let vote = match vote { 0 => false, 1 => true, _ => panic!("Vote must be written 0 if you disagree, or 1 if you agree."), }; commands::collective::technical_committee_vote( get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )? .1 .unwrap(), Client::from_url(&args.url).await.unwrap(), hash, //H256::from_str(&hash).expect("Invalid hash formatting"), index, vote, ) .await? } Subcommand::Transfer { amount, dest, keep_alive, } => { commands::transfer::transfer( get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )? .1 .unwrap(), Client::from_url(&args.url).await.unwrap(), amount, dest, keep_alive, ) .await? } Subcommand::TransferMultiple { amount, dests } => { commands::transfer::transfer_multiple( get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )? .1 .unwrap(), Client::from_url(&args.url).await.unwrap(), amount, dests, ) .await? } Subcommand::UpdateKeys => commands::smith::update_session_keys( get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )? .1 .unwrap(), Client::from_url(&args.url).await.unwrap(), ) .await .unwrap(), Subcommand::RevokeIdentity => { let (address, pair) = get_keys( args.secret_format, &args.address, &args.secret, NeededKeys::Secret, )?; commands::identity::revoke_identity( pair.unwrap(), Client::from_url(&args.url).await.unwrap(), address.unwrap(), ) .await? } } Ok(()) }