Skip to main content

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	execution_mode: crate::cli::common::ExecutionMode,
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		execution_mode,
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	execution_mode: crate::cli::common::ExecutionMode,
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		execution_mode,
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	execution_mode: crate::cli::common::ExecutionMode,
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		execution_mode,
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	execution_mode: crate::cli::common::ExecutionMode,
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, execution_mode).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, execution_mode).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 = vote_on_referendum(
411				&quantus_client,
412				&keypair,
413				referendum_index,
414				aye,
415				execution_mode,
416			)
417			.await?;
418
419			log_print!(
420				"✅ {} Vote transaction submitted! Hash: {:?}",
421				"SUCCESS".bright_green().bold(),
422				tx_hash
423			);
424		},
425
426		TechCollectiveCommands::ListMembers => {
427			log_print!("🏛️  Tech Collective Members ");
428			log_print!("");
429
430			// Get actual member list
431			match get_member_list(&quantus_client).await {
432				Ok(members) =>
433					if members.is_empty() {
434						log_print!("📭 No members in Tech Collective");
435					} else {
436						log_print!("👥 Total members: {}", members.len());
437						log_print!("");
438
439						for (index, member) in members.iter().enumerate() {
440							log_print!(
441								"{}. {}",
442								(index + 1).to_string().bright_blue(),
443								member.to_quantus_ss58().bright_green()
444							);
445						}
446					},
447				Err(e) => {
448					log_verbose!("⚠️  Failed to get member list: {:?}", e);
449					// Fallback to member count
450					match get_member_count(&quantus_client).await? {
451						Some(count_data) => {
452							log_verbose!("✅ Got member count data: {:?}", count_data);
453							if count_data > 0 {
454								log_print!(
455									"👥 Total members: {} (detailed list unavailable)",
456									count_data
457								);
458							} else {
459								log_print!("📭 No members in Tech Collective");
460							}
461						},
462						None => {
463							log_print!("📭 No member data found - Tech Collective may be empty");
464						},
465					}
466				},
467			}
468
469			log_print!("");
470			log_print!("💡 To check specific membership:");
471			log_print!("   quantus tech-collective is-member --address <ADDRESS>");
472			log_print!("💡 To add a member (requires sudo):");
473			log_print!(
474				"   quantus tech-collective add-member --who <ADDRESS> --from <SUDO_WALLET>"
475			);
476		},
477
478		TechCollectiveCommands::IsMember { address } => {
479			log_print!("🔍 Checking Tech Collective membership ");
480
481			// Resolve address (could be wallet name or SS58 address)
482			let resolved_address = resolve_address(&address)?;
483
484			log_print!("   👤 Address: {}", resolved_address.bright_cyan());
485
486			if is_member(&quantus_client, &resolved_address).await? {
487				log_success!("✅ Address IS a member of Tech Collective!");
488				log_print!("👥 Member data found in storage");
489			} else {
490				log_print!("❌ Address is NOT a member of Tech Collective");
491				log_print!("💡 No membership record found for this address");
492			}
493		},
494
495		TechCollectiveCommands::CheckSudo { address } => {
496			log_print!("🏛️  Checking sudo permissions ");
497
498			match get_sudo_account(&quantus_client).await? {
499				Some(sudo_account) => {
500					let sudo_address = sudo_account.to_quantus_ss58();
501					log_verbose!("🔍 Found sudo account: {}", sudo_address);
502					log_success!("✅ Found sudo account: {}", sudo_address.bright_green());
503
504					// If an address was provided, check if it matches the sudo account
505					if let Some(check_address) = address {
506						log_verbose!("🔍 Checking if provided address is sudo...");
507
508						// Resolve address (could be wallet name or SS58 address)
509						let resolved_address = resolve_address(&check_address)?;
510						log_verbose!("   👤 Address to check: {}", resolved_address);
511
512						if sudo_address == resolved_address {
513							log_success!("✅ Provided address IS the sudo account!");
514						} else {
515							log_print!("❌ Provided address is NOT the sudo account");
516							log_verbose!("💡 Provided address: {}", resolved_address);
517							log_verbose!("💡 Actual sudo address: {}", sudo_address);
518						}
519					} else {
520						// No address provided, just show the sudo account
521						log_verbose!("💡 Use 'quantus tech-collective check-sudo --address <ADDRESS>' to check if a specific address is sudo");
522					}
523				},
524				None => {
525					log_print!("📭 No sudo account found in network");
526					log_verbose!("💡 The network may not have sudo configured");
527				},
528			}
529		},
530
531		TechCollectiveCommands::ListReferenda => {
532			log_print!("📜 Active Tech Referenda ");
533			log_print!("");
534
535			log_print!("💡 Referenda listing requires TechReferenda pallet storage queries");
536			log_print!(
537                "💡 Use 'quantus call --pallet TechReferenda --call <method>' for direct interaction"
538            );
539		},
540
541		TechCollectiveCommands::GetReferendum { index } => {
542			log_print!("📄 Tech Referendum #{} Details ", index);
543			log_print!("");
544
545			log_print!("💡 Referendum details require TechReferenda storage access");
546			log_print!("💡 Query ReferendumInfoFor storage with index {}", index);
547		},
548	};
549
550	Ok(())
551}