quantus_cli/cli/
tech_collective.rs

1//! `quantus tech-collective` subcommand - tech collective management
2use crate::{
3	chain::quantus_subxt,
4	cli::{address_format::QuantusSS58, common::resolve_address},
5	error::QuantusError,
6	log_print, log_success, log_verbose,
7};
8use clap::Subcommand;
9use colored::Colorize;
10use sp_core::crypto::{AccountId32, Ss58Codec};
11
12/// Tech Collective management commands
13#[derive(Subcommand, Debug)]
14pub enum TechCollectiveCommands {
15	/// Add a member to the Tech Collective
16	AddMember {
17		/// Address of the member to add
18		#[arg(short, long)]
19		who: String,
20
21		/// Wallet name to sign with (must have root or collective permissions)
22		#[arg(short, long)]
23		from: String,
24
25		/// Password for the wallet
26		#[arg(short, long)]
27		password: Option<String>,
28
29		/// Read password from file
30		#[arg(long)]
31		password_file: Option<String>,
32	},
33
34	/// Remove a member from the Tech Collective
35	RemoveMember {
36		/// Address of the member to remove
37		#[arg(short, long)]
38		who: String,
39
40		/// Wallet name to sign with (must have root permissions)
41		#[arg(short, long)]
42		from: String,
43
44		/// Password for the wallet
45		#[arg(short, long)]
46		password: Option<String>,
47
48		/// Read password from file
49		#[arg(long)]
50		password_file: Option<String>,
51	},
52
53	/// Vote on a Tech Referenda proposal
54	Vote {
55		/// Referendum index to vote on
56		#[arg(short, long)]
57		referendum_index: u32,
58
59		/// Vote (true for aye, false for nay)
60		#[arg(short, long)]
61		aye: bool,
62
63		/// Wallet name to sign with (must be a collective member)
64		#[arg(short, long)]
65		from: String,
66
67		/// Password for the wallet
68		#[arg(short, long)]
69		password: Option<String>,
70
71		/// Read password from file
72		#[arg(long)]
73		password_file: Option<String>,
74	},
75
76	/// List all Tech Collective members
77	ListMembers,
78
79	/// Check if an address is a member of the Tech Collective
80	IsMember {
81		/// Address to check
82		#[arg(short, long)]
83		address: String,
84	},
85
86	/// Check who has sudo permissions in the network
87	CheckSudo {
88		/// Address to check if it's the sudo account (optional)
89		#[arg(short, long)]
90		address: Option<String>,
91	},
92
93	/// List active Tech Referenda
94	ListReferenda,
95
96	/// Get details of a specific Tech Referendum
97	GetReferendum {
98		/// Referendum index
99		#[arg(short, long)]
100		index: u32,
101	},
102}
103
104/// Add a member to the Tech Collective using sudo
105pub async fn add_member(
106	quantus_client: &crate::chain::client::QuantusClient,
107	from_keypair: &crate::wallet::QuantumKeyPair,
108	who_address: &str,
109	finalized: bool,
110) -> crate::error::Result<subxt::utils::H256> {
111	log_verbose!("🏛️  Adding member to Tech Collective...");
112	log_verbose!("   Member: {}", who_address.bright_cyan());
113
114	// Parse the member address
115	let (member_account_sp, _) = AccountId32::from_ss58check_with_version(who_address)
116		.map_err(|e| QuantusError::Generic(format!("Invalid member address: {e:?}")))?;
117
118	// Convert to subxt_core AccountId32
119	let member_account_bytes: [u8; 32] = *member_account_sp.as_ref();
120	let member_account_id = subxt::ext::subxt_core::utils::AccountId32::from(member_account_bytes);
121
122	log_verbose!("✍️  Creating add_member transaction...");
123
124	// Create the TechCollective::add_member call as RuntimeCall enum
125	let add_member_call = quantus_subxt::api::Call::TechCollective(
126		quantus_subxt::api::tech_collective::Call::add_member {
127			who: subxt::ext::subxt_core::utils::MultiAddress::Id(member_account_id),
128		},
129	);
130
131	// Wrap in Sudo::sudo call
132	let sudo_call = quantus_subxt::api::tx().sudo().sudo(add_member_call);
133
134	let tx_hash = crate::cli::common::submit_transaction(
135		quantus_client,
136		from_keypair,
137		sudo_call,
138		None,
139		finalized,
140	)
141	.await?;
142
143	log_verbose!("📋 Add member transaction submitted: {:?}", tx_hash);
144
145	Ok(tx_hash)
146}
147
148/// Remove a member from the Tech Collective using sudo
149pub async fn remove_member(
150	quantus_client: &crate::chain::client::QuantusClient,
151	from_keypair: &crate::wallet::QuantumKeyPair,
152	who_address: &str,
153	finalized: bool,
154) -> crate::error::Result<subxt::utils::H256> {
155	log_verbose!("🏛️  Removing member from Tech Collective...");
156	log_verbose!("   Member: {}", who_address.bright_cyan());
157
158	// Parse the member address
159	let (member_account_sp, _) = AccountId32::from_ss58check_with_version(who_address)
160		.map_err(|e| QuantusError::Generic(format!("Invalid member address: {e:?}")))?;
161
162	// Convert to subxt_core AccountId32
163	let member_account_bytes: [u8; 32] = *member_account_sp.as_ref();
164	let member_account_id = subxt::ext::subxt_core::utils::AccountId32::from(member_account_bytes);
165
166	log_verbose!("✍️  Creating remove_member transaction...");
167
168	// Create the TechCollective::remove_member call as RuntimeCall enum
169	let remove_member_call = quantus_subxt::api::Call::TechCollective(
170		quantus_subxt::api::tech_collective::Call::remove_member {
171			who: subxt::ext::subxt_core::utils::MultiAddress::Id(member_account_id),
172			min_rank: 0u16, // Use rank 0 as default
173		},
174	);
175
176	// Wrap in Sudo::sudo call
177	let sudo_call = quantus_subxt::api::tx().sudo().sudo(remove_member_call);
178
179	let tx_hash = crate::cli::common::submit_transaction(
180		quantus_client,
181		from_keypair,
182		sudo_call,
183		None,
184		finalized,
185	)
186	.await?;
187
188	log_verbose!("📋 Remove member transaction submitted: {:?}", tx_hash);
189
190	Ok(tx_hash)
191}
192
193/// Vote on a Tech Referenda proposal
194pub async fn vote_on_referendum(
195	quantus_client: &crate::chain::client::QuantusClient,
196	from_keypair: &crate::wallet::QuantumKeyPair,
197	referendum_index: u32,
198	aye: bool,
199	finalized: bool,
200) -> crate::error::Result<subxt::utils::H256> {
201	log_verbose!("🗳️  Voting on referendum...");
202	log_verbose!("   Referendum: {}", referendum_index);
203	log_verbose!("   Vote: {}", if aye { "AYE" } else { "NAY" });
204
205	log_verbose!("✍️  Creating vote transaction...");
206
207	// Create the TechCollective::vote call
208	let vote_call = quantus_subxt::api::tx().tech_collective().vote(referendum_index, aye);
209
210	let tx_hash = crate::cli::common::submit_transaction(
211		quantus_client,
212		from_keypair,
213		vote_call,
214		None,
215		finalized,
216	)
217	.await?;
218
219	log_verbose!("📋 Vote transaction submitted: {:?}", tx_hash);
220
221	Ok(tx_hash)
222}
223
224/// Check if an address is a member of the Tech Collective
225pub async fn is_member(
226	quantus_client: &crate::chain::client::QuantusClient,
227	address: &str,
228) -> crate::error::Result<bool> {
229	log_verbose!("🔍 Checking membership...");
230	log_verbose!("   Address: {}", address.bright_cyan());
231
232	// Parse the address
233	let (account_sp, _) = AccountId32::from_ss58check_with_version(address)
234		.map_err(|e| QuantusError::Generic(format!("Invalid address: {e:?}")))?;
235
236	// Convert to subxt_core AccountId32
237	let account_bytes: [u8; 32] = *account_sp.as_ref();
238	let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_bytes);
239
240	// Query Members storage
241	let storage_addr = quantus_subxt::api::storage().tech_collective().members(account_id);
242
243	// Get the latest block hash to read from the latest state (not finalized)
244	let latest_block_hash = quantus_client.get_latest_block().await?;
245
246	let storage_at = quantus_client.client().storage().at(latest_block_hash);
247
248	let member_data = storage_at
249		.fetch(&storage_addr)
250		.await
251		.map_err(|e| QuantusError::NetworkError(format!("Failed to fetch member data: {e:?}")))?;
252
253	Ok(member_data.is_some())
254}
255
256/// Get member count information
257pub async fn get_member_count(
258	quantus_client: &crate::chain::client::QuantusClient,
259) -> crate::error::Result<Option<u32>> {
260	log_verbose!("🔍 Getting member count...");
261
262	// Query MemberCount storage - use rank 0 as default
263	let storage_addr = quantus_subxt::api::storage().tech_collective().member_count(0u16);
264
265	// Get the latest block hash to read from the latest state (not finalized)
266	let latest_block_hash = quantus_client.get_latest_block().await?;
267
268	let storage_at = quantus_client.client().storage().at(latest_block_hash);
269
270	let count_data = storage_at
271		.fetch(&storage_addr)
272		.await
273		.map_err(|e| QuantusError::NetworkError(format!("Failed to fetch member count: {e:?}")))?;
274
275	Ok(count_data)
276}
277
278/// Get list of all members
279pub async fn get_member_list(
280	quantus_client: &crate::chain::client::QuantusClient,
281) -> crate::error::Result<Vec<AccountId32>> {
282	log_verbose!("🔍 Getting member list...");
283
284	// Get the latest block hash to read from the latest state (not finalized)
285	let latest_block_hash = quantus_client.get_latest_block().await?;
286
287	let storage_at = quantus_client.client().storage().at(latest_block_hash);
288
289	// Query all Members storage entries
290	let members_storage = quantus_subxt::api::storage().tech_collective().members_iter();
291
292	let mut members = Vec::new();
293	let mut iter = storage_at.iter(members_storage).await.map_err(|e| {
294		QuantusError::NetworkError(format!("Failed to create members iterator: {e:?}"))
295	})?;
296
297	while let Some(result) = iter.next().await {
298		match result {
299			Ok(storage_entry) => {
300				let key = storage_entry.key_bytes;
301				// The key contains the AccountId32 after the storage prefix
302				// TechCollective Members storage key format: prefix + AccountId32
303				if key.len() >= 32 {
304					// Extract the last 32 bytes as AccountId32
305					let account_bytes: [u8; 32] =
306						key[key.len() - 32..].try_into().unwrap_or([0u8; 32]);
307					let sp_account = AccountId32::from(account_bytes);
308					log_verbose!("Found member: {}", sp_account.to_quantus_ss58());
309					members.push(sp_account);
310				}
311			},
312			Err(e) => {
313				log_verbose!("⚠️  Error reading member entry: {:?}", e);
314			},
315		}
316	}
317
318	log_verbose!("Found {} total members", members.len());
319	Ok(members)
320}
321
322/// Get sudo account information
323pub async fn get_sudo_account(
324	quantus_client: &crate::chain::client::QuantusClient,
325) -> crate::error::Result<Option<AccountId32>> {
326	log_verbose!("🔍 Getting sudo account...");
327
328	// Query Sudo::Key storage
329	let storage_addr = quantus_subxt::api::storage().sudo().key();
330
331	// Get the latest block hash to read from the latest state (not finalized)
332	let latest_block_hash = quantus_client.get_latest_block().await?;
333
334	let storage_at = quantus_client.client().storage().at(latest_block_hash);
335
336	let sudo_account = storage_at
337		.fetch(&storage_addr)
338		.await
339		.map_err(|e| QuantusError::NetworkError(format!("Failed to fetch sudo account: {e:?}")))?;
340
341	// Convert from subxt_core AccountId32 to sp_core AccountId32
342	if let Some(subxt_account) = sudo_account {
343		let account_bytes: [u8; 32] = *subxt_account.as_ref();
344		let sp_account = AccountId32::from(account_bytes);
345		Ok(Some(sp_account))
346	} else {
347		Ok(None)
348	}
349}
350
351/// Handle tech collective subxt commands
352pub async fn handle_tech_collective_command(
353	command: TechCollectiveCommands,
354	node_url: &str,
355	finalized: bool,
356) -> crate::error::Result<()> {
357	log_print!("🏛️  Tech Collective");
358
359	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
360
361	match command {
362		TechCollectiveCommands::AddMember { who, from, password, password_file } => {
363			log_print!("🏛️  Adding member to Tech Collective");
364			log_print!("   👤 Member: {}", who.bright_cyan());
365			log_print!("   🔑 Signed by: {}", from.bright_yellow());
366
367			// Load wallet
368			let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
369
370			// Submit transaction
371			let tx_hash = add_member(&quantus_client, &keypair, &who, finalized).await?;
372
373			log_print!(
374				"✅ {} Add member transaction submitted! Hash: {:?}",
375				"SUCCESS".bright_green().bold(),
376				tx_hash
377			);
378		},
379
380		TechCollectiveCommands::RemoveMember { who, from, password, password_file } => {
381			log_print!("🏛️  Removing member from Tech Collective ");
382			log_print!("   👤 Member: {}", who.bright_cyan());
383			log_print!("   🔑 Signed by: {}", from.bright_yellow());
384
385			// Load wallet
386			let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
387
388			// Submit transaction
389			let tx_hash = remove_member(&quantus_client, &keypair, &who, finalized).await?;
390
391			log_print!(
392				"✅ {} Remove member transaction submitted! Hash: {:?}",
393				"SUCCESS".bright_green().bold(),
394				tx_hash
395			);
396		},
397
398		TechCollectiveCommands::Vote { referendum_index, aye, from, password, password_file } => {
399			log_print!("🗳️  Voting on Tech Referendum #{} ", referendum_index);
400			log_print!(
401				"   📊 Vote: {}",
402				if aye { "AYE ✅".bright_green() } else { "NAY ❌".bright_red() }
403			);
404			log_print!("   🔑 Signed by: {}", from.bright_yellow());
405
406			// Load wallet
407			let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
408
409			// Submit transaction
410			let tx_hash =
411				vote_on_referendum(&quantus_client, &keypair, referendum_index, aye, finalized)
412					.await?;
413
414			log_print!(
415				"✅ {} Vote transaction submitted! Hash: {:?}",
416				"SUCCESS".bright_green().bold(),
417				tx_hash
418			);
419		},
420
421		TechCollectiveCommands::ListMembers => {
422			log_print!("🏛️  Tech Collective Members ");
423			log_print!("");
424
425			// Get actual member list
426			match get_member_list(&quantus_client).await {
427				Ok(members) =>
428					if members.is_empty() {
429						log_print!("📭 No members in Tech Collective");
430					} else {
431						log_print!("👥 Total members: {}", members.len());
432						log_print!("");
433
434						for (index, member) in members.iter().enumerate() {
435							log_print!(
436								"{}. {}",
437								(index + 1).to_string().bright_blue(),
438								member.to_quantus_ss58().bright_green()
439							);
440						}
441					},
442				Err(e) => {
443					log_verbose!("⚠️  Failed to get member list: {:?}", e);
444					// Fallback to member count
445					match get_member_count(&quantus_client).await? {
446						Some(count_data) => {
447							log_verbose!("✅ Got member count data: {:?}", count_data);
448							if count_data > 0 {
449								log_print!(
450									"👥 Total members: {} (detailed list unavailable)",
451									count_data
452								);
453							} else {
454								log_print!("📭 No members in Tech Collective");
455							}
456						},
457						None => {
458							log_print!("📭 No member data found - Tech Collective may be empty");
459						},
460					}
461				},
462			}
463
464			log_print!("");
465			log_print!("💡 To check specific membership:");
466			log_print!("   quantus tech-collective is-member --address <ADDRESS>");
467			log_print!("💡 To add a member (requires sudo):");
468			log_print!(
469				"   quantus tech-collective add-member --who <ADDRESS> --from <SUDO_WALLET>"
470			);
471		},
472
473		TechCollectiveCommands::IsMember { address } => {
474			log_print!("🔍 Checking Tech Collective membership ");
475
476			// Resolve address (could be wallet name or SS58 address)
477			let resolved_address = resolve_address(&address)?;
478
479			log_print!("   👤 Address: {}", resolved_address.bright_cyan());
480
481			if is_member(&quantus_client, &resolved_address).await? {
482				log_success!("✅ Address IS a member of Tech Collective!");
483				log_print!("👥 Member data found in storage");
484			} else {
485				log_print!("❌ Address is NOT a member of Tech Collective");
486				log_print!("💡 No membership record found for this address");
487			}
488		},
489
490		TechCollectiveCommands::CheckSudo { address } => {
491			log_print!("🏛️  Checking sudo permissions ");
492
493			match get_sudo_account(&quantus_client).await? {
494				Some(sudo_account) => {
495					let sudo_address = sudo_account.to_quantus_ss58();
496					log_verbose!("🔍 Found sudo account: {}", sudo_address);
497					log_success!("✅ Found sudo account: {}", sudo_address.bright_green());
498
499					// If an address was provided, check if it matches the sudo account
500					if let Some(check_address) = address {
501						log_verbose!("🔍 Checking if provided address is sudo...");
502
503						// Resolve address (could be wallet name or SS58 address)
504						let resolved_address = resolve_address(&check_address)?;
505						log_verbose!("   👤 Address to check: {}", resolved_address);
506
507						if sudo_address == resolved_address {
508							log_success!("✅ Provided address IS the sudo account!");
509						} else {
510							log_print!("❌ Provided address is NOT the sudo account");
511							log_verbose!("💡 Provided address: {}", resolved_address);
512							log_verbose!("💡 Actual sudo address: {}", sudo_address);
513						}
514					} else {
515						// No address provided, just show the sudo account
516						log_verbose!("💡 Use 'quantus tech-collective check-sudo --address <ADDRESS>' to check if a specific address is sudo");
517					}
518				},
519				None => {
520					log_print!("📭 No sudo account found in network");
521					log_verbose!("💡 The network may not have sudo configured");
522				},
523			}
524		},
525
526		TechCollectiveCommands::ListReferenda => {
527			log_print!("📜 Active Tech Referenda ");
528			log_print!("");
529
530			log_print!("💡 Referenda listing requires TechReferenda pallet storage queries");
531			log_print!(
532                "💡 Use 'quantus call --pallet TechReferenda --call <method>' for direct interaction"
533            );
534		},
535
536		TechCollectiveCommands::GetReferendum { index } => {
537			log_print!("📄 Tech Referendum #{} Details ", index);
538			log_print!("");
539
540			log_print!("💡 Referendum details require TechReferenda storage access");
541			log_print!("💡 Query ReferendumInfoFor storage with index {}", index);
542		},
543	};
544
545	Ok(())
546}