Skip to content
Snippets Groups Projects
main.rs 15.7 KiB
Newer Older
mod cache;
mod commands;
mod indexer;
Éloïs's avatar
Éloïs committed
use clap::Parser;
use codec::Encode;
Hugo Trentesaux's avatar
Hugo Trentesaux committed
use keys::*;
use serde::Deserialize;
Hugo Trentesaux's avatar
Hugo Trentesaux committed
use sp_core::sr25519::Pair;
use sp_core::H256;
Éloïs's avatar
Éloïs committed

Hugo Trentesaux's avatar
Hugo Trentesaux committed
#[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;
}
Éloïs's avatar
Éloïs committed

Hugo Trentesaux's avatar
Hugo Trentesaux committed
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>;
Hugo Trentesaux's avatar
Hugo Trentesaux committed
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;
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	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>;
Éloïs's avatar
Éloïs committed

#[derive(Copy, Clone, Debug, Default, Encode)]
pub struct Tip {
	#[codec(compact)]
	tip: u64,
	pub fn new(amount: u64) -> Self {
		Tip { tip: amount }
	}
}

impl From<u64> for Tip {
	fn from(n: u64) -> Self {
		Self::new(n)
	}
Hugo Trentesaux's avatar
Hugo Trentesaux committed
#[derive(Clone, Parser, Debug, Default)]
Éloïs's avatar
Éloïs committed
#[clap(author, version, about, long_about = None)]
pub struct Args {
	#[clap(subcommand)]
	pub subcommand: Subcommand,
Éloïs's avatar
Éloïs committed

	/// 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,
Éloïs's avatar
Éloïs committed
}

Hugo Trentesaux's avatar
Hugo Trentesaux committed
/// 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,
Hugo Trentesaux's avatar
Hugo Trentesaux committed
}

// implement helper functions for Data
impl Data {
	/// --- constructor ---
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	pub fn new(args: Args) -> Self {
		Self {
			args,
			token_decimals: 0,
			token_symbol: "tokens".into(),
Hugo Trentesaux's avatar
Hugo Trentesaux committed
			..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
		)
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	}
	// --- 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)
	}
Hugo Trentesaux's avatar
Hugo Trentesaux committed
}

/// 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)]
Éloïs's avatar
Éloïs committed
pub enum Subcommand {
	/// Fetch account balance
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	#[default]
	GetBalance,
	/// Create and certify an identity
	///
	/// Caller must be member, and the target account must exist.
	CreateIdentity {
		target: sp_core::crypto::AccountId32,
	},
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	/// 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,
Éloïs's avatar
Éloïs committed
}

#[tokio::main(flavor = "current_thread")]
Hugo Trentesaux's avatar
Hugo Trentesaux committed
async fn main() -> Result<(), GcliError> {
	env_logger::init();
Éloïs's avatar
Éloïs committed

	let args = Args::parse();
Hugo Trentesaux's avatar
Hugo Trentesaux committed
	let mut data = Data::new(args.clone());
Éloïs's avatar
Éloïs committed

	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 } => {
Hugo Trentesaux's avatar
Hugo Trentesaux committed
			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?
		}
Hugo Trentesaux's avatar
Hugo Trentesaux committed
		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?
		}
Éloïs's avatar
Éloïs committed

	Ok(())
Éloïs's avatar
Éloïs committed
}