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