quantus_cli/cli/
tech_collective.rs

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