Skip to main content

quantus_cli/cli/
multisig.rs

1use crate::{
2	chain::quantus_subxt::{self},
3	cli::common::ExecutionMode,
4	log_error, log_print, log_success, log_verbose,
5};
6use clap::Subcommand;
7use colored::Colorize;
8use hex;
9use sp_core::crypto::{AccountId32 as SpAccountId32, Ss58Codec};
10
11// Base unit (QUAN) decimals for amount conversions
12const QUAN_DECIMALS: u128 = 1_000_000_000_000; // 10^12
13
14// ============================================================================
15// PUBLIC LIBRARY API - Data Structures
16// ============================================================================
17
18/// Multisig account information
19#[allow(dead_code)]
20#[derive(Debug, Clone)]
21pub struct MultisigInfo {
22	/// Multisig address (SS58 format)
23	pub address: String,
24	/// Creator address (SS58 format) - receives deposit back on dissolve
25	pub creator: String,
26	/// Current balance (spendable)
27	pub balance: u128,
28	/// Approval threshold
29	pub threshold: u32,
30	/// List of signer addresses (SS58 format)
31	pub signers: Vec<String>,
32	/// Next proposal ID
33	pub proposal_nonce: u32,
34	/// Locked deposit amount (returned to creator on dissolve)
35	pub deposit: u128,
36	/// Number of active proposals
37	pub active_proposals: u32,
38}
39
40/// Proposal status
41#[allow(dead_code)]
42#[derive(Debug, Clone, PartialEq)]
43pub enum ProposalStatus {
44	Active,
45	/// Threshold reached; any signer can call execute to dispatch
46	Approved,
47	Executed,
48	Cancelled,
49}
50
51/// Proposal information
52#[allow(dead_code)]
53#[derive(Debug, Clone)]
54pub struct ProposalInfo {
55	/// Proposal ID
56	pub id: u32,
57	/// Proposer address (SS58 format)
58	pub proposer: String,
59	/// Encoded call data
60	pub call_data: Vec<u8>,
61	/// Expiry block number
62	pub expiry: u32,
63	/// List of approver addresses (SS58 format)
64	pub approvals: Vec<String>,
65	/// Locked deposit amount
66	pub deposit: u128,
67	/// Proposal status
68	pub status: ProposalStatus,
69}
70
71// ============================================================================
72// PUBLIC LIBRARY API - Helper Functions
73// ============================================================================
74
75/// Parse amount from human-readable format (e.g., "10", "10.5", "0.001")
76/// or raw format (e.g., "10000000000000")
77pub fn parse_amount(amount: &str) -> crate::error::Result<u128> {
78	// If contains decimal point, parse as float and multiply by QUAN_DECIMALS
79	if amount.contains('.') {
80		let amount_f64: f64 = amount
81			.parse()
82			.map_err(|e| crate::error::QuantusError::Generic(format!("Invalid amount: {}", e)))?;
83
84		if amount_f64 < 0.0 {
85			return Err(crate::error::QuantusError::Generic(
86				"Amount cannot be negative".to_string(),
87			));
88		}
89
90		// Multiply by decimals and convert to u128
91		let base_amount = (amount_f64 * QUAN_DECIMALS as f64) as u128;
92		Ok(base_amount)
93	} else {
94		// Try parsing as u128 first (raw format)
95		if let Ok(raw) = amount.parse::<u128>() {
96			// If the number is very large (>= 10^10), assume it's already in base units
97			if raw >= 10_000_000_000 {
98				Ok(raw)
99			} else {
100				// Otherwise assume it's in QUAN and convert
101				Ok(raw * QUAN_DECIMALS)
102			}
103		} else {
104			Err(crate::error::QuantusError::Generic(format!("Invalid amount: {}", amount)))
105		}
106	}
107}
108
109/// Subcommands for proposing transactions
110#[derive(Subcommand, Debug)]
111pub enum ProposeSubcommand {
112	/// Propose a simple transfer (most common case)
113	Transfer {
114		/// Multisig account address (SS58 format)
115		#[arg(long)]
116		address: String,
117
118		/// Recipient address (SS58 format)
119		#[arg(long)]
120		to: String,
121
122		/// Amount to transfer (e.g., "10", "10.5", or raw "10000000000000")
123		#[arg(long)]
124		amount: String,
125
126		/// Expiry block number (when this proposal expires)
127		#[arg(long)]
128		expiry: u32,
129
130		/// Proposer wallet name (must be a signer)
131		#[arg(long)]
132		from: String,
133
134		/// Password for the wallet
135		#[arg(short, long)]
136		password: Option<String>,
137
138		/// Read password from file
139		#[arg(long)]
140		password_file: Option<String>,
141	},
142
143	/// Propose a custom transaction (full flexibility)
144	Custom {
145		/// Multisig account address (SS58 format)
146		#[arg(long)]
147		address: String,
148
149		/// Pallet name for the call (e.g., "Balances")
150		#[arg(long)]
151		pallet: String,
152
153		/// Call/function name (e.g., "transfer_allow_death")
154		#[arg(long)]
155		call: String,
156
157		/// Arguments as JSON array (e.g., '["5GrwvaEF...", "1000000000000"]')
158		#[arg(long)]
159		args: Option<String>,
160
161		/// Expiry block number (when this proposal expires)
162		#[arg(long)]
163		expiry: u32,
164
165		/// Proposer wallet name (must be a signer)
166		#[arg(long)]
167		from: String,
168
169		/// Password for the wallet
170		#[arg(short, long)]
171		password: Option<String>,
172
173		/// Read password from file
174		#[arg(long)]
175		password_file: Option<String>,
176	},
177
178	/// Propose to enable high-security for this multisig
179	HighSecurity {
180		/// Multisig account address (SS58 format)
181		#[arg(long)]
182		address: String,
183
184		/// Guardian/Interceptor account (SS58 or wallet name)
185		#[arg(long)]
186		interceptor: String,
187
188		/// Delay in blocks (mutually exclusive with --delay-seconds)
189		#[arg(long, conflicts_with = "delay_seconds")]
190		delay_blocks: Option<u32>,
191
192		/// Delay in seconds (mutually exclusive with --delay-blocks)
193		#[arg(long, conflicts_with = "delay_blocks")]
194		delay_seconds: Option<u64>,
195
196		/// Expiry block number (when this proposal expires)
197		#[arg(long)]
198		expiry: u32,
199
200		/// Proposer wallet name (must be a signer)
201		#[arg(long)]
202		from: String,
203
204		/// Password for the wallet
205		#[arg(short, long)]
206		password: Option<String>,
207
208		/// Read password from file
209		#[arg(long)]
210		password_file: Option<String>,
211	},
212}
213
214/// Multisig-related commands
215#[derive(Subcommand, Debug)]
216pub enum MultisigCommands {
217	/// Create a new multisig account
218	Create {
219		/// List of signer addresses (SS58 or wallet names), comma-separated
220		#[arg(long)]
221		signers: String,
222
223		/// Number of approvals required to execute transactions
224		#[arg(long)]
225		threshold: u32,
226
227		/// Nonce for deterministic address generation (allows creating multiple multisigs with
228		/// same signers)
229		#[arg(long, default_value = "0")]
230		nonce: u64,
231
232		/// Wallet name to pay for multisig creation
233		#[arg(long)]
234		from: String,
235
236		/// Password for the wallet
237		#[arg(short, long)]
238		password: Option<String>,
239
240		/// Read password from file (for scripting)
241		#[arg(long)]
242		password_file: Option<String>,
243	},
244
245	/// Predict multisig address without creating it (deterministic calculation)
246	PredictAddress {
247		/// List of signer addresses (SS58 or wallet names), comma-separated
248		#[arg(long)]
249		signers: String,
250
251		/// Number of approvals required to execute transactions
252		#[arg(long)]
253		threshold: u32,
254
255		/// Nonce for deterministic address generation
256		#[arg(long, default_value = "0")]
257		nonce: u64,
258	},
259
260	/// Propose a transaction to be executed by the multisig
261	#[command(subcommand)]
262	Propose(ProposeSubcommand),
263
264	/// Approve a proposed transaction
265	Approve {
266		/// Multisig account address
267		#[arg(long)]
268		address: String,
269
270		/// Proposal ID (u32 nonce)
271		#[arg(long)]
272		proposal_id: u32,
273
274		/// Approver wallet name (must be a signer)
275		#[arg(long)]
276		from: String,
277
278		/// Password for the wallet
279		#[arg(short, long)]
280		password: Option<String>,
281
282		/// Read password from file
283		#[arg(long)]
284		password_file: Option<String>,
285	},
286
287	/// Execute an approved proposal (any signer; proposal must have reached threshold)
288	Execute {
289		/// Multisig account address
290		#[arg(long)]
291		address: String,
292
293		/// Proposal ID (u32 nonce) to execute
294		#[arg(long)]
295		proposal_id: u32,
296
297		/// Wallet name (must be a signer)
298		#[arg(long)]
299		from: String,
300
301		/// Password for the wallet
302		#[arg(short, long)]
303		password: Option<String>,
304
305		/// Read password from file
306		#[arg(long)]
307		password_file: Option<String>,
308	},
309
310	/// Cancel a proposed transaction (only by proposer)
311	Cancel {
312		/// Multisig account address
313		#[arg(long)]
314		address: String,
315
316		/// Proposal ID (u32 nonce) to cancel
317		#[arg(long)]
318		proposal_id: u32,
319
320		/// Wallet name (must be the proposer)
321		#[arg(long)]
322		from: String,
323
324		/// Password for the wallet
325		#[arg(short, long)]
326		password: Option<String>,
327
328		/// Read password from file
329		#[arg(long)]
330		password_file: Option<String>,
331	},
332
333	/// Remove an expired proposal
334	RemoveExpired {
335		/// Multisig account address
336		#[arg(long)]
337		address: String,
338
339		/// Proposal ID (u32 nonce) to remove
340		#[arg(long)]
341		proposal_id: u32,
342
343		/// Wallet name (must be a signer)
344		#[arg(long)]
345		from: String,
346
347		/// Password for the wallet
348		#[arg(short, long)]
349		password: Option<String>,
350
351		/// Read password from file
352		#[arg(long)]
353		password_file: Option<String>,
354	},
355
356	/// Claim all deposits from removable proposals (batch operation)
357	ClaimDeposits {
358		/// Multisig account address
359		#[arg(long)]
360		address: String,
361
362		/// Wallet name (must be the proposer)
363		#[arg(long)]
364		from: String,
365
366		/// Password for the wallet
367		#[arg(short, long)]
368		password: Option<String>,
369
370		/// Read password from file
371		#[arg(long)]
372		password_file: Option<String>,
373	},
374
375	/// Dissolve a multisig and recover the creation deposit
376	Dissolve {
377		/// Multisig account address
378		#[arg(long)]
379		address: String,
380
381		/// Wallet name (must be creator or a signer)
382		#[arg(long)]
383		from: String,
384
385		/// Password for the wallet
386		#[arg(short, long)]
387		password: Option<String>,
388
389		/// Read password from file
390		#[arg(long)]
391		password_file: Option<String>,
392	},
393
394	/// Query multisig information (or specific proposal if --proposal-id provided)
395	Info {
396		/// Multisig account address
397		#[arg(long)]
398		address: String,
399
400		/// Optional: Query specific proposal by ID
401		#[arg(long)]
402		proposal_id: Option<u32>,
403	},
404
405	/// List all proposals for a multisig
406	ListProposals {
407		/// Multisig account address
408		#[arg(long)]
409		address: String,
410	},
411
412	/// High-Security operations for multisig accounts
413	#[command(subcommand)]
414	HighSecurity(HighSecuritySubcommands),
415}
416
417/// High-Security subcommands for multisig (query only)
418#[derive(Subcommand, Debug)]
419pub enum HighSecuritySubcommands {
420	/// Check if multisig has high-security enabled
421	Status {
422		/// Multisig account address
423		#[arg(long)]
424		address: String,
425	},
426}
427
428// ============================================================================
429// PUBLIC LIBRARY API - Core Functions
430// ============================================================================
431// Note: These functions are public library API and may not be used by the CLI binary
432
433/// Predict multisig address deterministically
434///
435/// This function calculates what the multisig address will be BEFORE creating it.
436/// The address is computed as: hash(pallet_id || sorted_signers || threshold || nonce)
437///
438/// # Arguments
439/// * `signers` - List of signer AccountId32 (order doesn't matter - will be sorted)
440/// * `threshold` - Number of approvals required
441/// * `nonce` - Nonce for uniqueness (allows multiple multisigs with same signers)
442///
443/// # Returns
444/// Predicted multisig address in SS58 format
445#[allow(dead_code)]
446pub fn predict_multisig_address(
447	signers: Vec<subxt::ext::subxt_core::utils::AccountId32>,
448	threshold: u32,
449	nonce: u64,
450) -> String {
451	use codec::Encode;
452
453	// Pallet ID from runtime: py/mltsg
454	const PALLET_ID: [u8; 8] = *b"py/mltsg";
455
456	// Convert subxt AccountId32 to sp_core AccountId32 for consistent encoding
457	use sp_core::crypto::AccountId32 as SpAccountId32;
458	let sp_signers: Vec<SpAccountId32> = signers
459		.iter()
460		.map(|s| {
461			let bytes: [u8; 32] = *s.as_ref();
462			SpAccountId32::from(bytes)
463		})
464		.collect();
465
466	// Sort signers for deterministic address (same as runtime does)
467	let mut sorted_signers = sp_signers;
468	sorted_signers.sort();
469
470	// Build data to hash: pallet_id || sorted_signers || threshold || nonce
471	// IMPORTANT: Must match runtime encoding exactly
472	let mut data = Vec::new();
473	data.extend_from_slice(&PALLET_ID);
474	// Encode Vec<sp_core::AccountId32> - same as runtime!
475	data.extend_from_slice(&sorted_signers.encode());
476	data.extend_from_slice(&threshold.encode());
477	data.extend_from_slice(&nonce.encode());
478
479	// Hash the data and map it deterministically into an AccountId
480	// CRITICAL: Use PoseidonHasher (same as runtime!) and TrailingZeroInput
481	use codec::Decode;
482	use qp_poseidon::PoseidonHasher;
483	use sp_core::crypto::AccountId32;
484	use sp_runtime::traits::{Hash as HashT, TrailingZeroInput};
485
486	let hash = PoseidonHasher::hash(&data);
487	let account_id = AccountId32::decode(&mut TrailingZeroInput::new(hash.as_ref()))
488		.expect("TrailingZeroInput provides sufficient bytes; qed");
489
490	// Convert to SS58 format (network 189 for Quantus)
491	account_id.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189))
492}
493
494/// Create a multisig account
495///
496/// # Arguments
497/// * `quantus_client` - Connected Quantus client
498/// * `creator_keypair` - Keypair of the account creating the multisig
499/// * `signers` - List of signer addresses (AccountId32)
500/// * `threshold` - Number of approvals required
501/// * `nonce` - Nonce for deterministic address (allows multiple multisigs with same signers)
502/// * `wait_for_inclusion` - Whether to wait for transaction inclusion
503///
504/// # Returns
505/// Transaction hash and optionally the multisig address (if wait_for_inclusion=true)
506#[allow(dead_code)]
507pub async fn create_multisig(
508	quantus_client: &crate::chain::client::QuantusClient,
509	creator_keypair: &crate::wallet::QuantumKeyPair,
510	signers: Vec<subxt::ext::subxt_core::utils::AccountId32>,
511	threshold: u32,
512	nonce: u64,
513	wait_for_inclusion: bool,
514) -> crate::error::Result<(subxt::utils::H256, Option<String>)> {
515	// Build transaction with nonce
516	let create_tx =
517		quantus_subxt::api::tx()
518			.multisig()
519			.create_multisig(signers.clone(), threshold, nonce);
520
521	// Submit transaction
522	let execution_mode =
523		ExecutionMode { finalized: false, wait_for_transaction: wait_for_inclusion };
524	let tx_hash = crate::cli::common::submit_transaction(
525		quantus_client,
526		creator_keypair,
527		create_tx,
528		None,
529		execution_mode,
530	)
531	.await?;
532
533	// If waiting, extract address from events
534	let multisig_address = if wait_for_inclusion {
535		let latest_block_hash = quantus_client.get_latest_block().await?;
536		let events = quantus_client.client().events().at(latest_block_hash).await?;
537
538		let mut multisig_events =
539			events.find::<quantus_subxt::api::multisig::events::MultisigCreated>();
540
541		let address: Option<String> = if let Some(Ok(ev)) = multisig_events.next() {
542			let addr_bytes: &[u8; 32] = ev.multisig_address.as_ref();
543			let addr = SpAccountId32::from(*addr_bytes);
544			Some(addr.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189)))
545		} else {
546			None
547		};
548		address
549	} else {
550		None
551	};
552
553	Ok((tx_hash, multisig_address))
554}
555
556/// Propose a transfer from multisig
557///
558/// # Returns
559/// Transaction hash
560#[allow(dead_code)]
561pub async fn propose_transfer(
562	quantus_client: &crate::chain::client::QuantusClient,
563	proposer_keypair: &crate::wallet::QuantumKeyPair,
564	multisig_address: subxt::ext::subxt_core::utils::AccountId32,
565	to_address: subxt::ext::subxt_core::utils::AccountId32,
566	amount: u128,
567	expiry: u32,
568) -> crate::error::Result<subxt::utils::H256> {
569	use codec::{Compact, Encode};
570
571	// Build Balances::transfer_allow_death call
572	let pallet_index = 5u8; // Balances pallet
573	let call_index = 0u8; // transfer_allow_death
574
575	let mut call_data = Vec::new();
576	call_data.push(pallet_index);
577	call_data.push(call_index);
578
579	// Encode destination (MultiAddress::Id)
580	call_data.push(0u8); // MultiAddress::Id variant
581	call_data.extend_from_slice(to_address.as_ref());
582
583	// Encode amount (Compact<u128>)
584	Compact(amount).encode_to(&mut call_data);
585
586	// Build propose transaction
587	let propose_tx =
588		quantus_subxt::api::tx().multisig().propose(multisig_address, call_data, expiry);
589
590	// Submit transaction
591	let execution_mode = ExecutionMode { finalized: false, wait_for_transaction: false };
592	let tx_hash = crate::cli::common::submit_transaction(
593		quantus_client,
594		proposer_keypair,
595		propose_tx,
596		None,
597		execution_mode,
598	)
599	.await?;
600
601	Ok(tx_hash)
602}
603
604/// Propose a custom call from multisig
605///
606/// # Returns
607/// Transaction hash
608#[allow(dead_code)]
609pub async fn propose_custom(
610	quantus_client: &crate::chain::client::QuantusClient,
611	proposer_keypair: &crate::wallet::QuantumKeyPair,
612	multisig_address: subxt::ext::subxt_core::utils::AccountId32,
613	call_data: Vec<u8>,
614	expiry: u32,
615) -> crate::error::Result<subxt::utils::H256> {
616	// Build propose transaction
617	let propose_tx =
618		quantus_subxt::api::tx().multisig().propose(multisig_address, call_data, expiry);
619
620	// Submit transaction
621	let execution_mode = ExecutionMode { finalized: false, wait_for_transaction: false };
622	let tx_hash = crate::cli::common::submit_transaction(
623		quantus_client,
624		proposer_keypair,
625		propose_tx,
626		None,
627		execution_mode,
628	)
629	.await?;
630
631	Ok(tx_hash)
632}
633
634/// Approve a proposal
635///
636/// # Returns
637/// Transaction hash
638#[allow(dead_code)]
639pub async fn approve_proposal(
640	quantus_client: &crate::chain::client::QuantusClient,
641	approver_keypair: &crate::wallet::QuantumKeyPair,
642	multisig_address: subxt::ext::subxt_core::utils::AccountId32,
643	proposal_id: u32,
644) -> crate::error::Result<subxt::utils::H256> {
645	let approve_tx = quantus_subxt::api::tx().multisig().approve(multisig_address, proposal_id);
646
647	let execution_mode = ExecutionMode { finalized: false, wait_for_transaction: false };
648	let tx_hash = crate::cli::common::submit_transaction(
649		quantus_client,
650		approver_keypair,
651		approve_tx,
652		None,
653		execution_mode,
654	)
655	.await?;
656
657	Ok(tx_hash)
658}
659
660/// Cancel a proposal (only by proposer)
661///
662/// # Returns
663/// Transaction hash
664#[allow(dead_code)]
665pub async fn cancel_proposal(
666	quantus_client: &crate::chain::client::QuantusClient,
667	proposer_keypair: &crate::wallet::QuantumKeyPair,
668	multisig_address: subxt::ext::subxt_core::utils::AccountId32,
669	proposal_id: u32,
670) -> crate::error::Result<subxt::utils::H256> {
671	let cancel_tx = quantus_subxt::api::tx().multisig().cancel(multisig_address, proposal_id);
672
673	let execution_mode = ExecutionMode { finalized: false, wait_for_transaction: false };
674	let tx_hash = crate::cli::common::submit_transaction(
675		quantus_client,
676		proposer_keypair,
677		cancel_tx,
678		None,
679		execution_mode,
680	)
681	.await?;
682
683	Ok(tx_hash)
684}
685
686/// Get multisig information
687///
688/// # Returns
689/// Multisig information or None if not found
690#[allow(dead_code)]
691pub async fn get_multisig_info(
692	quantus_client: &crate::chain::client::QuantusClient,
693	multisig_address: subxt::ext::subxt_core::utils::AccountId32,
694) -> crate::error::Result<Option<MultisigInfo>> {
695	let latest_block_hash = quantus_client.get_latest_block().await?;
696	let storage_at = quantus_client.client().storage().at(latest_block_hash);
697
698	// Query multisig data
699	let storage_query =
700		quantus_subxt::api::storage().multisig().multisigs(multisig_address.clone());
701	let multisig_data = storage_at.fetch(&storage_query).await?;
702
703	if let Some(data) = multisig_data {
704		// Query balance
705		let balance_query =
706			quantus_subxt::api::storage().system().account(multisig_address.clone());
707		let account_info = storage_at.fetch(&balance_query).await?;
708		let balance = account_info.map(|info| info.data.free).unwrap_or(0);
709
710		// Convert to SS58
711		let multisig_bytes: &[u8; 32] = multisig_address.as_ref();
712		let multisig_sp = SpAccountId32::from(*multisig_bytes);
713		let address =
714			multisig_sp.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
715
716		// Convert creator to SS58
717		let creator_bytes: &[u8; 32] = data.creator.as_ref();
718		let creator_sp = SpAccountId32::from(*creator_bytes);
719		let creator =
720			creator_sp.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
721
722		let signers: Vec<String> = data
723			.signers
724			.0
725			.iter()
726			.map(|signer| {
727				let signer_bytes: &[u8; 32] = signer.as_ref();
728				let signer_sp = SpAccountId32::from(*signer_bytes);
729				signer_sp.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189))
730			})
731			.collect();
732
733		Ok(Some(MultisigInfo {
734			address,
735			creator,
736			balance,
737			threshold: data.threshold,
738			signers,
739			proposal_nonce: data.proposal_nonce,
740			deposit: data.deposit,
741			active_proposals: data.active_proposals,
742		}))
743	} else {
744		Ok(None)
745	}
746}
747
748/// Get proposal information
749///
750/// # Returns
751/// Proposal information or None if not found
752#[allow(dead_code)]
753pub async fn get_proposal_info(
754	quantus_client: &crate::chain::client::QuantusClient,
755	multisig_address: subxt::ext::subxt_core::utils::AccountId32,
756	proposal_id: u32,
757) -> crate::error::Result<Option<ProposalInfo>> {
758	let latest_block_hash = quantus_client.get_latest_block().await?;
759	let storage_at = quantus_client.client().storage().at(latest_block_hash);
760
761	let storage_query = quantus_subxt::api::storage()
762		.multisig()
763		.proposals(multisig_address, proposal_id);
764
765	let proposal_data = storage_at.fetch(&storage_query).await?;
766
767	if let Some(data) = proposal_data {
768		let proposer_bytes: &[u8; 32] = data.proposer.as_ref();
769		let proposer_sp = SpAccountId32::from(*proposer_bytes);
770		let proposer =
771			proposer_sp.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
772
773		let approvals: Vec<String> = data
774			.approvals
775			.0
776			.iter()
777			.map(|approver| {
778				let approver_bytes: &[u8; 32] = approver.as_ref();
779				let approver_sp = SpAccountId32::from(*approver_bytes);
780				approver_sp
781					.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189))
782			})
783			.collect();
784
785		let status = match data.status {
786			quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Active =>
787				ProposalStatus::Active,
788			quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Approved =>
789				ProposalStatus::Approved,
790			quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Executed =>
791				ProposalStatus::Executed,
792			quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Cancelled =>
793				ProposalStatus::Cancelled,
794		};
795
796		Ok(Some(ProposalInfo {
797			id: proposal_id,
798			proposer,
799			call_data: data.call.0,
800			expiry: data.expiry,
801			approvals,
802			deposit: data.deposit,
803			status,
804		}))
805	} else {
806		Ok(None)
807	}
808}
809
810/// List all proposals for a multisig
811///
812/// # Returns
813/// List of proposals
814#[allow(dead_code)]
815pub async fn list_proposals(
816	quantus_client: &crate::chain::client::QuantusClient,
817	multisig_address: subxt::ext::subxt_core::utils::AccountId32,
818) -> crate::error::Result<Vec<ProposalInfo>> {
819	let latest_block_hash = quantus_client.get_latest_block().await?;
820	let storage = quantus_client.client().storage().at(latest_block_hash);
821
822	let address = quantus_subxt::api::storage()
823		.multisig()
824		.proposals_iter1(multisig_address.clone());
825	let mut proposals_iter = storage.iter(address).await?;
826
827	let mut proposals = Vec::new();
828
829	while let Some(result) = proposals_iter.next().await {
830		if let Ok(kv) = result {
831			// Extract proposal_id from key
832			let key_bytes = kv.key_bytes;
833			if key_bytes.len() >= 4 {
834				let id_bytes = &key_bytes[key_bytes.len() - 4..];
835				let proposal_id =
836					u32::from_le_bytes([id_bytes[0], id_bytes[1], id_bytes[2], id_bytes[3]]);
837
838				// Use value directly from iterator (more efficient)
839				let data = kv.value;
840
841				let proposer_bytes: &[u8; 32] = data.proposer.as_ref();
842				let proposer_sp = SpAccountId32::from(*proposer_bytes);
843				let proposer = proposer_sp
844					.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
845
846				let approvals: Vec<String> = data
847					.approvals
848					.0
849					.iter()
850					.map(|approver| {
851						let approver_bytes: &[u8; 32] = approver.as_ref();
852						let approver_sp = SpAccountId32::from(*approver_bytes);
853						approver_sp.to_ss58check_with_version(
854							sp_core::crypto::Ss58AddressFormat::custom(189),
855						)
856					})
857					.collect();
858
859				let status = match data.status {
860					quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Active =>
861						ProposalStatus::Active,
862					quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Approved =>
863						ProposalStatus::Approved,
864					quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Executed =>
865						ProposalStatus::Executed,
866					quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Cancelled =>
867						ProposalStatus::Cancelled,
868				};
869
870				proposals.push(ProposalInfo {
871					id: proposal_id,
872					proposer,
873					call_data: data.call.0,
874					expiry: data.expiry,
875					approvals,
876					deposit: data.deposit,
877					status,
878				});
879			}
880		}
881	}
882
883	Ok(proposals)
884}
885
886/// Approve dissolving a multisig
887///
888/// Requires threshold approvals. When threshold is reached, multisig is dissolved.
889/// Requirements:
890/// - No proposals exist (active, executed, or cancelled)
891/// - Multisig account balance must be zero
892/// - Deposit is returned to creator
893///
894/// # Returns
895/// Transaction hash
896#[allow(dead_code)]
897pub async fn approve_dissolve_multisig(
898	quantus_client: &crate::chain::client::QuantusClient,
899	caller_keypair: &crate::wallet::QuantumKeyPair,
900	multisig_address: subxt::ext::subxt_core::utils::AccountId32,
901) -> crate::error::Result<subxt::utils::H256> {
902	let approve_tx = quantus_subxt::api::tx().multisig().approve_dissolve(multisig_address);
903
904	let execution_mode = ExecutionMode { finalized: false, wait_for_transaction: false };
905	let tx_hash = crate::cli::common::submit_transaction(
906		quantus_client,
907		caller_keypair,
908		approve_tx,
909		None,
910		execution_mode,
911	)
912	.await?;
913
914	Ok(tx_hash)
915}
916
917// ============================================================================
918// CLI HANDLERS (Internal)
919// ============================================================================
920
921/// Handle multisig command
922pub async fn handle_multisig_command(
923	command: MultisigCommands,
924	node_url: &str,
925	execution_mode: ExecutionMode,
926) -> crate::error::Result<()> {
927	match command {
928		MultisigCommands::Create { signers, threshold, nonce, from, password, password_file } =>
929			handle_create_multisig(
930				signers,
931				threshold,
932				nonce,
933				from,
934				password,
935				password_file,
936				node_url,
937				execution_mode,
938			)
939			.await,
940		MultisigCommands::PredictAddress { signers, threshold, nonce } =>
941			handle_predict_address(signers, threshold, nonce).await,
942		MultisigCommands::Propose(subcommand) => match subcommand {
943			ProposeSubcommand::Transfer {
944				address,
945				to,
946				amount,
947				expiry,
948				from,
949				password,
950				password_file,
951			} =>
952				handle_propose_transfer(
953					address,
954					to,
955					amount,
956					expiry,
957					from,
958					password,
959					password_file,
960					node_url,
961					execution_mode,
962				)
963				.await,
964			ProposeSubcommand::Custom {
965				address,
966				pallet,
967				call,
968				args,
969				expiry,
970				from,
971				password,
972				password_file,
973			} =>
974				handle_propose(
975					address,
976					pallet,
977					call,
978					args,
979					expiry,
980					from,
981					password,
982					password_file,
983					node_url,
984					execution_mode,
985				)
986				.await,
987			ProposeSubcommand::HighSecurity {
988				address,
989				interceptor,
990				delay_blocks,
991				delay_seconds,
992				expiry,
993				from,
994				password,
995				password_file,
996			} =>
997				handle_high_security_set(
998					address,
999					interceptor,
1000					delay_blocks,
1001					delay_seconds,
1002					expiry,
1003					from,
1004					password,
1005					password_file,
1006					node_url,
1007					execution_mode,
1008				)
1009				.await,
1010		},
1011		MultisigCommands::Approve { address, proposal_id, from, password, password_file } =>
1012			handle_approve(
1013				address,
1014				proposal_id,
1015				from,
1016				password,
1017				password_file,
1018				node_url,
1019				execution_mode,
1020			)
1021			.await,
1022		MultisigCommands::Execute { address, proposal_id, from, password, password_file } =>
1023			handle_execute(
1024				address,
1025				proposal_id,
1026				from,
1027				password,
1028				password_file,
1029				node_url,
1030				execution_mode,
1031			)
1032			.await,
1033		MultisigCommands::Cancel { address, proposal_id, from, password, password_file } =>
1034			handle_cancel(
1035				address,
1036				proposal_id,
1037				from,
1038				password,
1039				password_file,
1040				node_url,
1041				execution_mode,
1042			)
1043			.await,
1044		MultisigCommands::RemoveExpired { address, proposal_id, from, password, password_file } =>
1045			handle_remove_expired(
1046				address,
1047				proposal_id,
1048				from,
1049				password,
1050				password_file,
1051				node_url,
1052				execution_mode,
1053			)
1054			.await,
1055		MultisigCommands::ClaimDeposits { address, from, password, password_file } =>
1056			handle_claim_deposits(address, from, password, password_file, node_url, execution_mode)
1057				.await,
1058		MultisigCommands::Dissolve { address, from, password, password_file } =>
1059			handle_dissolve(address, from, password, password_file, node_url, execution_mode).await,
1060		MultisigCommands::Info { address, proposal_id } =>
1061			handle_info(address, proposal_id, node_url).await,
1062		MultisigCommands::ListProposals { address } =>
1063			handle_list_proposals(address, node_url).await,
1064		MultisigCommands::HighSecurity(subcommand) => match subcommand {
1065			HighSecuritySubcommands::Status { address } =>
1066				handle_high_security_status(address, node_url).await,
1067		},
1068	}
1069}
1070
1071/// Create a new multisig account
1072async fn handle_create_multisig(
1073	signers: String,
1074	threshold: u32,
1075	nonce: u64,
1076	from: String,
1077	password: Option<String>,
1078	password_file: Option<String>,
1079	node_url: &str,
1080	execution_mode: ExecutionMode,
1081) -> crate::error::Result<()> {
1082	log_print!("๐Ÿ” {} Creating multisig...", "MULTISIG".bright_magenta().bold());
1083
1084	// Parse signers - convert to AccountId32
1085	let signer_addresses: Vec<subxt::ext::subxt_core::utils::AccountId32> = signers
1086		.split(',')
1087		.map(|s| s.trim())
1088		.map(|addr| {
1089			// Resolve wallet name or SS58 address to SS58 string
1090			let ss58_str = crate::cli::common::resolve_address(addr)?;
1091			// Convert SS58 to AccountId32
1092			let (account_id, _) =
1093				SpAccountId32::from_ss58check_with_version(&ss58_str).map_err(|e| {
1094					crate::error::QuantusError::Generic(format!(
1095						"Invalid address '{}': {:?}",
1096						addr, e
1097					))
1098				})?;
1099			// Convert to subxt AccountId32
1100			let bytes: [u8; 32] = *account_id.as_ref();
1101			Ok(subxt::ext::subxt_core::utils::AccountId32::from(bytes))
1102		})
1103		.collect::<Result<Vec<_>, crate::error::QuantusError>>()?;
1104
1105	log_verbose!("Signers: {} addresses", signer_addresses.len());
1106	log_verbose!("Threshold: {}", threshold);
1107	log_verbose!("Nonce: {}", nonce);
1108
1109	// Validate inputs
1110	if signer_addresses.is_empty() {
1111		log_error!("โŒ At least one signer is required");
1112		return Err(crate::error::QuantusError::Generic("No signers provided".to_string()));
1113	}
1114
1115	if threshold == 0 {
1116		log_error!("โŒ Threshold must be greater than zero");
1117		return Err(crate::error::QuantusError::Generic("Invalid threshold".to_string()));
1118	}
1119
1120	if threshold > signer_addresses.len() as u32 {
1121		log_error!("โŒ Threshold cannot exceed number of signers");
1122		return Err(crate::error::QuantusError::Generic("Threshold too high".to_string()));
1123	}
1124
1125	// Calculate predicted address BEFORE submission (deterministic)
1126	let predicted_address = predict_multisig_address(signer_addresses.clone(), threshold, nonce);
1127
1128	log_print!("");
1129	log_print!("๐Ÿ“ {} Predicted multisig address:", "DETERMINISTIC".bright_green().bold());
1130	log_print!("   {}", predicted_address.bright_cyan().bold());
1131	log_print!("");
1132
1133	// Load keypair
1134	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1135
1136	// Connect to chain
1137	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1138
1139	// Build transaction with nonce
1140	let create_tx = quantus_subxt::api::tx().multisig().create_multisig(
1141		signer_addresses.clone(),
1142		threshold,
1143		nonce,
1144	);
1145
1146	// Always wait for transaction to confirm creation
1147	let create_execution_mode = ExecutionMode {
1148		finalized: execution_mode.finalized,
1149		wait_for_transaction: true, // Always wait to confirm address
1150	};
1151
1152	let _tx_hash = crate::cli::common::submit_transaction(
1153		&quantus_client,
1154		&keypair,
1155		create_tx,
1156		None,
1157		create_execution_mode,
1158	)
1159	.await?;
1160
1161	log_success!("โœ… Multisig creation transaction confirmed");
1162
1163	// Extract and verify address from events
1164	{
1165		log_print!("");
1166		log_print!("๐Ÿ” Looking for MultisigCreated event...");
1167
1168		// Query latest block events
1169		let latest_block_hash = quantus_client.get_latest_block().await?;
1170		let events = quantus_client.client().events().at(latest_block_hash).await?;
1171
1172		// Find MultisigCreated event
1173		let multisig_events =
1174			events.find::<quantus_subxt::api::multisig::events::MultisigCreated>();
1175
1176		let mut actual_address: Option<String> = None;
1177		for event_result in multisig_events {
1178			match event_result {
1179				Ok(ev) => {
1180					let addr_bytes: &[u8; 32] = ev.multisig_address.as_ref();
1181					let addr = SpAccountId32::from(*addr_bytes);
1182					actual_address = Some(addr.to_ss58check_with_version(
1183						sp_core::crypto::Ss58AddressFormat::custom(189),
1184					));
1185					log_verbose!("Found MultisigCreated event");
1186					break;
1187				},
1188				Err(e) => {
1189					log_verbose!("Error parsing event: {:?}", e);
1190				},
1191			}
1192		}
1193
1194		if let Some(address) = actual_address {
1195			log_print!("");
1196
1197			// Verify address matches prediction
1198			if address == predicted_address {
1199				log_success!("โœ… Confirmed multisig address: {}", address.bright_cyan().bold());
1200				log_print!("   {} Matches predicted address!", "โœ“".bright_green().bold());
1201			} else {
1202				log_error!("โš ๏ธ  Address mismatch!");
1203				log_print!("   Expected: {}", predicted_address.bright_yellow());
1204				log_print!("   Got:      {}", address.bright_red());
1205				log_print!("   This should never happen with deterministic addresses!");
1206			}
1207
1208			log_print!("");
1209			log_print!(
1210				"๐Ÿ’ก {} You can now use this address to propose transactions",
1211				"TIP".bright_blue().bold()
1212			);
1213			log_print!(
1214				"   Example: quantus multisig propose transfer --address {} --to recipient --amount 100",
1215				address.bright_cyan()
1216			);
1217		} else {
1218			log_error!("โš ๏ธ  Couldn't find MultisigCreated event");
1219			log_print!("   Check events manually: quantus events --latest --pallet Multisig");
1220		}
1221	}
1222
1223	log_print!("");
1224
1225	Ok(())
1226}
1227
1228/// Predict multisig address without creating it
1229async fn handle_predict_address(
1230	signers: String,
1231	threshold: u32,
1232	nonce: u64,
1233) -> crate::error::Result<()> {
1234	log_print!("๐Ÿ”ฎ {} Predicting multisig address...", "PREDICT".bright_cyan().bold());
1235	log_print!("");
1236
1237	// Parse signers - convert to AccountId32
1238	let signer_addresses: Vec<subxt::ext::subxt_core::utils::AccountId32> = signers
1239		.split(',')
1240		.map(|s| s.trim())
1241		.map(|addr| {
1242			// Resolve wallet name or SS58 address to SS58 string
1243			let ss58_str = crate::cli::common::resolve_address(addr)?;
1244			// Convert SS58 to AccountId32
1245			let (account_id, _) =
1246				SpAccountId32::from_ss58check_with_version(&ss58_str).map_err(|e| {
1247					crate::error::QuantusError::Generic(format!(
1248						"Invalid address '{}': {:?}",
1249						addr, e
1250					))
1251				})?;
1252			// Convert to subxt AccountId32
1253			let bytes: [u8; 32] = *account_id.as_ref();
1254			Ok(subxt::ext::subxt_core::utils::AccountId32::from(bytes))
1255		})
1256		.collect::<Result<Vec<_>, crate::error::QuantusError>>()?;
1257
1258	// Validate inputs
1259	if signer_addresses.is_empty() {
1260		log_error!("โŒ At least one signer is required");
1261		return Err(crate::error::QuantusError::Generic("No signers provided".to_string()));
1262	}
1263
1264	if threshold == 0 {
1265		log_error!("โŒ Threshold must be greater than zero");
1266		return Err(crate::error::QuantusError::Generic("Invalid threshold".to_string()));
1267	}
1268
1269	if threshold > signer_addresses.len() as u32 {
1270		log_error!("โŒ Threshold cannot exceed number of signers");
1271		return Err(crate::error::QuantusError::Generic("Threshold too high".to_string()));
1272	}
1273
1274	// Calculate deterministic address
1275	let predicted_address = predict_multisig_address(signer_addresses.clone(), threshold, nonce);
1276
1277	log_print!("๐Ÿ“ {} Predicted multisig address:", "RESULT".bright_green().bold());
1278	log_print!("   {}", predicted_address.bright_cyan().bold());
1279	log_print!("");
1280	log_print!("โš™๏ธ  {} Configuration:", "PARAMS".bright_blue().bold());
1281	log_print!("   Signers: {}", signer_addresses.len());
1282	log_print!("   Threshold: {}", threshold);
1283	log_print!("   Nonce: {}", nonce);
1284	log_print!("");
1285	log_print!("๐Ÿ’ก {} This address is deterministic:", "INFO".bright_yellow().bold());
1286	log_print!("   - Same signers + threshold + nonce = same address");
1287	log_print!("   - Order of signers doesn't matter (automatically sorted)");
1288	log_print!("   - Use different nonce to create multiple multisigs with same signers");
1289	log_print!("");
1290	log_print!("๐Ÿš€ {} To create this multisig, run:", "NEXT".bright_magenta().bold());
1291	log_print!(
1292		"   quantus multisig create --signers \"{}\" --threshold {} --nonce {} --from <wallet>",
1293		signers,
1294		threshold,
1295		nonce
1296	);
1297	log_print!("");
1298
1299	Ok(())
1300}
1301
1302/// Propose a transaction
1303/// Propose a transfer transaction (simplified interface)
1304async fn handle_propose_transfer(
1305	multisig_address: String,
1306	to: String,
1307	amount: String,
1308	expiry: u32,
1309	from: String,
1310	password: Option<String>,
1311	password_file: Option<String>,
1312	node_url: &str,
1313	execution_mode: ExecutionMode,
1314) -> crate::error::Result<()> {
1315	log_print!("๐Ÿ“ {} Creating transfer proposal...", "MULTISIG".bright_magenta().bold());
1316
1317	// Resolve recipient address (wallet name or SS58)
1318	let to_address = crate::cli::common::resolve_address(&to)?;
1319
1320	// Parse amount (supports both human format "10" and raw "10000000000000")
1321	let amount_u128: u128 = parse_amount(&amount)?;
1322
1323	// Resolve multisig address to check HS status
1324	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
1325	let (multisig_id, _) =
1326		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
1327			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
1328		})?;
1329	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
1330	let multisig_account_id = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
1331
1332	// Connect to chain and check if multisig has High Security enabled
1333	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1334	let latest_block_hash = quantus_client.get_latest_block().await?;
1335	let storage_at = quantus_client.client().storage().at(latest_block_hash);
1336
1337	let hs_query = quantus_subxt::api::storage()
1338		.reversible_transfers()
1339		.high_security_accounts(multisig_account_id);
1340	let is_high_security = storage_at.fetch(&hs_query).await?.is_some();
1341
1342	if is_high_security {
1343		// HS enabled: use ReversibleTransfers::schedule_transfer (delayed + reversible)
1344		log_print!(
1345			"๐Ÿ›ก๏ธ  {} High-Security detected: using delayed transfer (ReversibleTransfers::schedule_transfer)",
1346			"HS".bright_green().bold()
1347		);
1348
1349		// Build schedule_transfer call data directly
1350		let (to_id, _) = SpAccountId32::from_ss58check_with_version(&to_address).map_err(|e| {
1351			crate::error::QuantusError::Generic(format!("Invalid recipient address: {:?}", e))
1352		})?;
1353		let to_bytes: [u8; 32] = *to_id.as_ref();
1354		let to_account_id = subxt::ext::subxt_core::utils::AccountId32::from(to_bytes);
1355		let dest = subxt::utils::MultiAddress::Id(to_account_id);
1356
1357		let schedule_call = quantus_subxt::api::tx()
1358			.reversible_transfers()
1359			.schedule_transfer(dest, amount_u128);
1360
1361		use subxt::tx::Payload;
1362		let call_data = schedule_call
1363			.encode_call_data(&quantus_client.client().metadata())
1364			.map_err(|e| {
1365				crate::error::QuantusError::Generic(format!("Failed to encode call: {:?}", e))
1366			})?;
1367
1368		// Submit as multisig proposal with pre-built call data
1369		handle_propose_with_call_data(
1370			multisig_address,
1371			call_data,
1372			expiry,
1373			from,
1374			password,
1375			password_file,
1376			&quantus_client,
1377			execution_mode,
1378		)
1379		.await
1380	} else {
1381		// No HS: use standard Balances::transfer_allow_death
1382		let args_json = serde_json::to_string(&vec![
1383			serde_json::Value::String(to_address),
1384			serde_json::Value::String(amount_u128.to_string()),
1385		])
1386		.map_err(|e| {
1387			crate::error::QuantusError::Generic(format!("Failed to serialize args: {}", e))
1388		})?;
1389
1390		handle_propose(
1391			multisig_address,
1392			"Balances".to_string(),
1393			"transfer_allow_death".to_string(),
1394			Some(args_json),
1395			expiry,
1396			from,
1397			password,
1398			password_file,
1399			node_url,
1400			execution_mode,
1401		)
1402		.await
1403	}
1404}
1405
1406/// Propose a custom transaction
1407async fn handle_propose(
1408	multisig_address: String,
1409	pallet: String,
1410	call: String,
1411	args: Option<String>,
1412	expiry: u32,
1413	from: String,
1414	password: Option<String>,
1415	password_file: Option<String>,
1416	node_url: &str,
1417	execution_mode: ExecutionMode,
1418) -> crate::error::Result<()> {
1419	log_print!("๐Ÿ“ {} Creating proposal...", "MULTISIG".bright_magenta().bold());
1420
1421	// Resolve multisig address
1422	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
1423	let (multisig_id, _) =
1424		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
1425			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
1426		})?;
1427	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
1428	let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
1429
1430	// Parse arguments
1431	let args_vec: Vec<serde_json::Value> = if let Some(args_str) = args {
1432		serde_json::from_str(&args_str).map_err(|e| {
1433			crate::error::QuantusError::Generic(format!("Invalid JSON for arguments: {}", e))
1434		})?
1435	} else {
1436		vec![]
1437	};
1438
1439	log_verbose!("Multisig: {}", multisig_ss58);
1440	log_verbose!("Call: {}::{}", pallet, call);
1441	log_verbose!("Expiry: block {}", expiry);
1442
1443	// Connect to chain
1444	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1445
1446	// Validate expiry is in the future (client-side check)
1447	let latest_block_hash = quantus_client.get_latest_block().await?;
1448	let latest_block = quantus_client.client().blocks().at(latest_block_hash).await?;
1449	let current_block_number = latest_block.number();
1450
1451	if expiry <= current_block_number {
1452		log_error!(
1453			"โŒ Expiry block {} is in the past (current block: {})",
1454			expiry,
1455			current_block_number
1456		);
1457		log_print!("   Use a higher block number, e.g., --expiry {}", current_block_number + 1000);
1458		return Err(crate::error::QuantusError::Generic("Expiry must be in the future".to_string()));
1459	}
1460
1461	log_verbose!("Current block: {}, expiry valid", current_block_number);
1462
1463	// Validate proposer is a signer (client-side check before submitting)
1464	let storage_at = quantus_client.client().storage().at(latest_block_hash);
1465	let multisig_query =
1466		quantus_subxt::api::storage().multisig().multisigs(multisig_address.clone());
1467	let multisig_data = storage_at.fetch(&multisig_query).await?.ok_or_else(|| {
1468		crate::error::QuantusError::Generic(format!(
1469			"Multisig not found at address: {}",
1470			multisig_ss58
1471		))
1472	})?;
1473
1474	// Resolve proposer address
1475	let proposer_ss58 = crate::cli::common::resolve_address(&from)?;
1476	let (proposer_id, _) =
1477		SpAccountId32::from_ss58check_with_version(&proposer_ss58).map_err(|e| {
1478			crate::error::QuantusError::Generic(format!("Invalid proposer address: {:?}", e))
1479		})?;
1480	let proposer_bytes: [u8; 32] = *proposer_id.as_ref();
1481	let proposer_account_id = subxt::ext::subxt_core::utils::AccountId32::from(proposer_bytes);
1482
1483	// Check if proposer is in signers list
1484	if !multisig_data.signers.0.contains(&proposer_account_id) {
1485		log_error!("โŒ Not authorized: {} is not a signer of this multisig", proposer_ss58);
1486		return Err(crate::error::QuantusError::Generic(
1487			"Only multisig signers can create proposals".to_string(),
1488		));
1489	}
1490
1491	// Check for expired proposals from this proposer (will be auto-cleaned by runtime)
1492	let mut expired_count = 0;
1493	let proposals_query = quantus_subxt::api::storage().multisig().proposals_iter();
1494	let mut proposals_stream = storage_at.iter(proposals_query).await?;
1495
1496	while let Some(Ok(kv)) = proposals_stream.next().await {
1497		let proposal = kv.value;
1498		// Check if this proposal belongs to our proposer
1499		if proposal.proposer == proposer_account_id {
1500			// Check if expired
1501			if proposal.expiry <= current_block_number {
1502				expired_count += 1;
1503			}
1504		}
1505	}
1506
1507	if expired_count > 0 {
1508		log_print!("");
1509		log_print!(
1510			"๐Ÿงน {} Auto-cleanup: Runtime will remove your {} expired proposal(s)",
1511			"INFO".bright_blue().bold(),
1512			expired_count.to_string().bright_yellow()
1513		);
1514		log_print!("   This happens automatically before creating the new proposal");
1515		log_print!("");
1516	}
1517
1518	// Build the call data using runtime metadata
1519	let call_data = build_runtime_call(&quantus_client, &pallet, &call, args_vec).await?;
1520
1521	log_verbose!("Call data size: {} bytes", call_data.len());
1522
1523	// Load keypair
1524	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1525
1526	// Build transaction
1527	let propose_tx =
1528		quantus_subxt::api::tx()
1529			.multisig()
1530			.propose(multisig_address.clone(), call_data, expiry);
1531
1532	// Always wait for transaction confirmation
1533	let propose_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
1534
1535	// Submit transaction and wait for on-chain confirmation
1536	crate::cli::common::submit_transaction(
1537		&quantus_client,
1538		&keypair,
1539		propose_tx,
1540		None,
1541		propose_execution_mode,
1542	)
1543	.await?;
1544
1545	log_success!("โœ… Proposal confirmed on-chain");
1546
1547	Ok(())
1548}
1549
1550/// Submit a multisig proposal with pre-built call data (used for HS transfers)
1551async fn handle_propose_with_call_data(
1552	multisig_address: String,
1553	call_data: Vec<u8>,
1554	expiry: u32,
1555	from: String,
1556	password: Option<String>,
1557	password_file: Option<String>,
1558	quantus_client: &crate::chain::client::QuantusClient,
1559	execution_mode: ExecutionMode,
1560) -> crate::error::Result<()> {
1561	log_print!("๐Ÿ“ {} Creating proposal...", "MULTISIG".bright_magenta().bold());
1562
1563	// Resolve multisig address
1564	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
1565	let (multisig_id, _) =
1566		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
1567			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
1568		})?;
1569	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
1570	let multisig_account_id = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
1571
1572	// Validate expiry is in the future (client-side check)
1573	let latest_block_hash = quantus_client.get_latest_block().await?;
1574	let latest_block = quantus_client.client().blocks().at(latest_block_hash).await?;
1575	let current_block_number = latest_block.number();
1576
1577	if expiry <= current_block_number {
1578		log_error!(
1579			"โŒ Expiry block {} is in the past (current block: {})",
1580			expiry,
1581			current_block_number
1582		);
1583		log_print!("   Use a higher block number, e.g., --expiry {}", current_block_number + 1000);
1584		return Err(crate::error::QuantusError::Generic("Expiry must be in the future".to_string()));
1585	}
1586
1587	log_verbose!("Current block: {}, expiry valid", current_block_number);
1588	log_verbose!("Call data size: {} bytes", call_data.len());
1589
1590	// Load keypair
1591	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1592
1593	// Build transaction
1594	let propose_tx =
1595		quantus_subxt::api::tx()
1596			.multisig()
1597			.propose(multisig_account_id, call_data, expiry);
1598
1599	// Always wait for transaction confirmation
1600	let propose_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
1601
1602	// Submit transaction and wait for on-chain confirmation
1603	crate::cli::common::submit_transaction(
1604		quantus_client,
1605		&keypair,
1606		propose_tx,
1607		None,
1608		propose_execution_mode,
1609	)
1610	.await?;
1611
1612	log_success!("โœ… Proposal confirmed on-chain");
1613
1614	Ok(())
1615}
1616
1617/// Approve a proposal
1618async fn handle_approve(
1619	multisig_address: String,
1620	proposal_id: u32,
1621	from: String,
1622	password: Option<String>,
1623	password_file: Option<String>,
1624	node_url: &str,
1625	execution_mode: ExecutionMode,
1626) -> crate::error::Result<()> {
1627	log_print!("โœ… {} Approving proposal...", "MULTISIG".bright_magenta().bold());
1628
1629	// Resolve multisig address
1630	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
1631	let (multisig_id, _) =
1632		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
1633			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
1634		})?;
1635	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
1636	let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
1637
1638	log_verbose!("Multisig: {}", multisig_ss58);
1639	log_verbose!("Proposal ID: {}", proposal_id);
1640
1641	// Load keypair
1642	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1643
1644	// Connect to chain
1645	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1646
1647	// Validate approver is a signer (client-side check before submitting)
1648	let latest_block_hash = quantus_client.get_latest_block().await?;
1649	let storage_at = quantus_client.client().storage().at(latest_block_hash);
1650
1651	let multisig_query =
1652		quantus_subxt::api::storage().multisig().multisigs(multisig_address.clone());
1653	let multisig_data = storage_at.fetch(&multisig_query).await?.ok_or_else(|| {
1654		crate::error::QuantusError::Generic(format!(
1655			"Multisig not found at address: {}",
1656			multisig_ss58
1657		))
1658	})?;
1659
1660	// Resolve approver address
1661	let approver_ss58 = crate::cli::common::resolve_address(&from)?;
1662	let (approver_id, _) =
1663		SpAccountId32::from_ss58check_with_version(&approver_ss58).map_err(|e| {
1664			crate::error::QuantusError::Generic(format!("Invalid approver address: {:?}", e))
1665		})?;
1666	let approver_bytes: [u8; 32] = *approver_id.as_ref();
1667	let approver_account_id = subxt::ext::subxt_core::utils::AccountId32::from(approver_bytes);
1668
1669	// Check if approver is in signers list
1670	if !multisig_data.signers.0.contains(&approver_account_id) {
1671		log_error!("โŒ Not authorized: {} is not a signer of this multisig", approver_ss58);
1672		return Err(crate::error::QuantusError::Generic(
1673			"Only multisig signers can approve proposals".to_string(),
1674		));
1675	}
1676
1677	// Check if proposal exists
1678	let proposal_query = quantus_subxt::api::storage()
1679		.multisig()
1680		.proposals(multisig_address.clone(), proposal_id);
1681	let proposal_data = storage_at.fetch(&proposal_query).await?;
1682	if proposal_data.is_none() {
1683		log_error!("โŒ Proposal {} not found", proposal_id);
1684		return Err(crate::error::QuantusError::Generic(format!(
1685			"Proposal {} does not exist",
1686			proposal_id
1687		)));
1688	}
1689	let proposal = proposal_data.unwrap();
1690
1691	// Check if already approved by this signer
1692	if proposal.approvals.0.contains(&approver_account_id) {
1693		log_error!("โŒ Already approved: you have already approved this proposal");
1694		return Err(crate::error::QuantusError::Generic(
1695			"You have already approved this proposal".to_string(),
1696		));
1697	}
1698
1699	// Build transaction
1700	let approve_tx = quantus_subxt::api::tx().multisig().approve(multisig_address, proposal_id);
1701
1702	// Always wait for transaction confirmation
1703	let approve_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
1704
1705	// Submit transaction and wait for on-chain confirmation
1706	crate::cli::common::submit_transaction(
1707		&quantus_client,
1708		&keypair,
1709		approve_tx,
1710		None,
1711		approve_execution_mode,
1712	)
1713	.await?;
1714
1715	log_success!("โœ… Approval confirmed on-chain");
1716	log_print!(
1717		"   If threshold is reached, the proposal becomes Approved. Any signer can run: quantus multisig execute --address {} --proposal-id {} --from <signer>",
1718		multisig_ss58,
1719		proposal_id
1720	);
1721
1722	Ok(())
1723}
1724
1725/// Execute an approved proposal (any signer)
1726async fn handle_execute(
1727	multisig_address: String,
1728	proposal_id: u32,
1729	from: String,
1730	password: Option<String>,
1731	password_file: Option<String>,
1732	node_url: &str,
1733	execution_mode: ExecutionMode,
1734) -> crate::error::Result<()> {
1735	log_print!("โ–ถ๏ธ  {} Executing proposal...", "MULTISIG".bright_magenta().bold());
1736
1737	// Resolve multisig address
1738	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
1739	let (multisig_id, _) =
1740		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
1741			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
1742		})?;
1743	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
1744	let multisig_account_id = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
1745
1746	log_verbose!("Multisig: {}", multisig_ss58);
1747	log_verbose!("Proposal ID: {}", proposal_id);
1748
1749	// Load keypair
1750	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1751
1752	// Connect to chain
1753	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1754
1755	// Validate executor is a signer (client-side check)
1756	let latest_block_hash = quantus_client.get_latest_block().await?;
1757	let storage_at = quantus_client.client().storage().at(latest_block_hash);
1758
1759	let multisig_query =
1760		quantus_subxt::api::storage().multisig().multisigs(multisig_account_id.clone());
1761	let multisig_data = storage_at.fetch(&multisig_query).await?.ok_or_else(|| {
1762		crate::error::QuantusError::Generic(format!(
1763			"Multisig not found at address: {}",
1764			multisig_ss58
1765		))
1766	})?;
1767
1768	let executor_ss58 = crate::cli::common::resolve_address(&from)?;
1769	let (executor_id, _) =
1770		SpAccountId32::from_ss58check_with_version(&executor_ss58).map_err(|e| {
1771			crate::error::QuantusError::Generic(format!("Invalid executor address: {:?}", e))
1772		})?;
1773	let executor_bytes: [u8; 32] = *executor_id.as_ref();
1774	let executor_account_id = subxt::ext::subxt_core::utils::AccountId32::from(executor_bytes);
1775
1776	if !multisig_data.signers.0.contains(&executor_account_id) {
1777		log_error!("โŒ Not authorized: {} is not a signer of this multisig", executor_ss58);
1778		return Err(crate::error::QuantusError::Generic(
1779			"Only multisig signers can execute proposals".to_string(),
1780		));
1781	}
1782
1783	// Build transaction
1784	let execute_tx = quantus_subxt::api::tx().multisig().execute(multisig_account_id, proposal_id);
1785
1786	let exec_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
1787
1788	crate::cli::common::submit_transaction(
1789		&quantus_client,
1790		&keypair,
1791		execute_tx,
1792		None,
1793		exec_execution_mode,
1794	)
1795	.await?;
1796
1797	log_success!("โœ… Proposal executed on-chain");
1798
1799	Ok(())
1800}
1801
1802/// Cancel a proposal
1803async fn handle_cancel(
1804	multisig_address: String,
1805	proposal_id: u32,
1806	from: String,
1807	password: Option<String>,
1808	password_file: Option<String>,
1809	node_url: &str,
1810	execution_mode: ExecutionMode,
1811) -> crate::error::Result<()> {
1812	log_print!("๐Ÿšซ {} Cancelling proposal...", "MULTISIG".bright_magenta().bold());
1813
1814	// Resolve multisig address
1815	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
1816	let (multisig_id, _) =
1817		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
1818			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
1819		})?;
1820	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
1821	let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
1822
1823	log_verbose!("Proposal ID: {}", proposal_id);
1824
1825	// Load keypair
1826	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1827
1828	// Connect to chain
1829	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1830
1831	// Validate caller is the proposer (client-side check before submitting)
1832	let latest_block_hash = quantus_client.get_latest_block().await?;
1833	let storage_at = quantus_client.client().storage().at(latest_block_hash);
1834
1835	let proposal_query = quantus_subxt::api::storage()
1836		.multisig()
1837		.proposals(multisig_address.clone(), proposal_id);
1838	let proposal_data = storage_at.fetch(&proposal_query).await?.ok_or_else(|| {
1839		crate::error::QuantusError::Generic(format!("Proposal {} not found", proposal_id))
1840	})?;
1841
1842	// Resolve canceller address
1843	let canceller_ss58 = crate::cli::common::resolve_address(&from)?;
1844	let (canceller_id, _) =
1845		SpAccountId32::from_ss58check_with_version(&canceller_ss58).map_err(|e| {
1846			crate::error::QuantusError::Generic(format!("Invalid canceller address: {:?}", e))
1847		})?;
1848	let canceller_bytes: [u8; 32] = *canceller_id.as_ref();
1849	let canceller_account_id = subxt::ext::subxt_core::utils::AccountId32::from(canceller_bytes);
1850
1851	// Check if caller is the proposer
1852	if proposal_data.proposer != canceller_account_id {
1853		log_error!("โŒ Not authorized: only the proposer can cancel this proposal");
1854		let proposer_bytes: &[u8; 32] = proposal_data.proposer.as_ref();
1855		let proposer_sp = SpAccountId32::from(*proposer_bytes);
1856		let proposer_ss58 =
1857			proposer_sp.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
1858		log_print!("   Proposer: {}", proposer_ss58);
1859		return Err(crate::error::QuantusError::Generic(
1860			"Only the proposer can cancel their proposal".to_string(),
1861		));
1862	}
1863
1864	// Build transaction
1865	let cancel_tx = quantus_subxt::api::tx().multisig().cancel(multisig_address, proposal_id);
1866
1867	// Always wait for transaction confirmation
1868	let cancel_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
1869
1870	// Submit transaction and wait for on-chain confirmation
1871	crate::cli::common::submit_transaction(
1872		&quantus_client,
1873		&keypair,
1874		cancel_tx,
1875		None,
1876		cancel_execution_mode,
1877	)
1878	.await?;
1879
1880	log_success!("โœ… Proposal cancelled and removed (confirmed on-chain)");
1881	log_print!("   Deposit returned to proposer");
1882
1883	Ok(())
1884}
1885
1886/// Remove an expired proposal
1887async fn handle_remove_expired(
1888	multisig_address: String,
1889	proposal_id: u32,
1890	from: String,
1891	password: Option<String>,
1892	password_file: Option<String>,
1893	node_url: &str,
1894	execution_mode: ExecutionMode,
1895) -> crate::error::Result<()> {
1896	log_print!("๐Ÿงน {} Removing expired proposal...", "MULTISIG".bright_magenta().bold());
1897
1898	// Resolve multisig address
1899	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
1900	let (multisig_id, _) =
1901		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
1902			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
1903		})?;
1904	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
1905	let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
1906
1907	log_verbose!("Proposal ID: {}", proposal_id);
1908
1909	// Load keypair
1910	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1911
1912	// Connect to chain
1913	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1914
1915	// Build transaction
1916	let remove_tx = quantus_subxt::api::tx()
1917		.multisig()
1918		.remove_expired(multisig_address, proposal_id);
1919
1920	// Always wait for transaction confirmation
1921	let remove_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
1922
1923	// Submit transaction and wait for on-chain confirmation
1924	crate::cli::common::submit_transaction(
1925		&quantus_client,
1926		&keypair,
1927		remove_tx,
1928		None,
1929		remove_execution_mode,
1930	)
1931	.await?;
1932
1933	log_success!("โœ… Expired proposal removed and deposit returned (confirmed on-chain)");
1934
1935	Ok(())
1936}
1937
1938/// Claim all deposits (batch cleanup)
1939async fn handle_claim_deposits(
1940	multisig_address: String,
1941	from: String,
1942	password: Option<String>,
1943	password_file: Option<String>,
1944	node_url: &str,
1945	execution_mode: ExecutionMode,
1946) -> crate::error::Result<()> {
1947	log_print!("๐Ÿ’ฐ {} Claiming deposits...", "MULTISIG".bright_magenta().bold());
1948
1949	// Resolve multisig address
1950	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
1951	let (multisig_id, _) =
1952		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
1953			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
1954		})?;
1955	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
1956	let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
1957
1958	// Load keypair
1959	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1960
1961	// Connect to chain
1962	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1963
1964	// Build transaction
1965	let claim_tx = quantus_subxt::api::tx().multisig().claim_deposits(multisig_address);
1966
1967	// Always wait for transaction confirmation
1968	let claim_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
1969
1970	// Submit transaction and wait for on-chain confirmation
1971	crate::cli::common::submit_transaction(
1972		&quantus_client,
1973		&keypair,
1974		claim_tx,
1975		None,
1976		claim_execution_mode,
1977	)
1978	.await?;
1979
1980	log_success!("โœ… Deposits claimed (confirmed on-chain)");
1981	log_print!("   All removable proposals have been cleaned up");
1982
1983	Ok(())
1984}
1985
1986/// Approve dissolving a multisig
1987async fn handle_dissolve(
1988	multisig_address: String,
1989	from: String,
1990	password: Option<String>,
1991	password_file: Option<String>,
1992	node_url: &str,
1993	execution_mode: ExecutionMode,
1994) -> crate::error::Result<()> {
1995	log_print!("๐Ÿ—‘๏ธ  {} Approving multisig dissolution...", "MULTISIG".bright_magenta().bold());
1996
1997	// Resolve multisig address
1998	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
1999	let (multisig_id, _) =
2000		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
2001			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
2002		})?;
2003	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
2004	let multisig_address_id = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
2005
2006	// Load keypair
2007	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
2008
2009	// Connect to chain
2010	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
2011
2012	// Get threshold for info message
2013	let latest_block_hash = quantus_client.get_latest_block().await?;
2014	let storage_at = quantus_client.client().storage().at(latest_block_hash);
2015	let multisig_query =
2016		quantus_subxt::api::storage().multisig().multisigs(multisig_address_id.clone());
2017	let multisig_info = storage_at.fetch(&multisig_query).await?;
2018
2019	// Build transaction
2020	let approve_tx = quantus_subxt::api::tx()
2021		.multisig()
2022		.approve_dissolve(multisig_address_id.clone());
2023
2024	// Always wait for transaction confirmation - runtime validates all conditions
2025	let dissolve_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
2026
2027	// Submit transaction and wait for on-chain confirmation
2028	crate::cli::common::submit_transaction(
2029		&quantus_client,
2030		&keypair,
2031		approve_tx,
2032		None,
2033		dissolve_execution_mode,
2034	)
2035	.await?;
2036
2037	log_success!("โœ… Dissolution approval confirmed on-chain");
2038
2039	if let Some(info) = multisig_info {
2040		// Convert creator to SS58
2041		let creator_bytes: &[u8; 32] = info.creator.as_ref();
2042		let creator_sp = SpAccountId32::from(*creator_bytes);
2043		let creator_ss58 = creator_sp.to_ss58check();
2044
2045		log_print!("   Requires {} total approvals to dissolve", info.threshold);
2046		log_print!("");
2047		log_print!("๐Ÿ’ก {} When threshold is reached:", "INFO".bright_blue().bold());
2048		log_print!("   - Multisig will be dissolved automatically");
2049		log_print!("   - Deposit ({}) will be RETURNED to creator", format_balance(info.deposit));
2050		log_print!("   - Creator: {}", creator_ss58.bright_cyan());
2051		log_print!("   - Storage will be removed");
2052	}
2053
2054	Ok(())
2055}
2056
2057/// Query multisig information (or specific proposal if proposal_id provided)
2058async fn handle_info(
2059	multisig_address: String,
2060	proposal_id: Option<u32>,
2061	node_url: &str,
2062) -> crate::error::Result<()> {
2063	// If proposal_id is provided, delegate to handle_proposal_info
2064	if let Some(id) = proposal_id {
2065		return handle_proposal_info(multisig_address, id, node_url).await;
2066	}
2067
2068	log_print!("๐Ÿ” {} Querying multisig info...", "MULTISIG".bright_magenta().bold());
2069	log_print!("");
2070
2071	// Resolve multisig address
2072	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
2073	let (multisig_id, _) =
2074		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
2075			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
2076		})?;
2077	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
2078	let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
2079
2080	// Connect to chain
2081	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
2082
2083	// Query storage using direct fetch with explicit block hash
2084	crate::log_verbose!("๐Ÿ” Querying multisig with address: {}", multisig_ss58);
2085	crate::log_verbose!("๐Ÿ” Address bytes: {}", hex::encode(multisig_bytes));
2086
2087	// Get latest block hash explicitly
2088	let latest_block_hash = quantus_client.get_latest_block().await?;
2089	crate::log_verbose!("๐Ÿ“ฆ Latest block hash: {:?}", latest_block_hash);
2090
2091	let storage_query =
2092		quantus_subxt::api::storage().multisig().multisigs(multisig_address.clone());
2093
2094	let storage_at = quantus_client.client().storage().at(latest_block_hash);
2095
2096	let multisig_data = storage_at.fetch(&storage_query).await?;
2097
2098	crate::log_verbose!(
2099		"๐Ÿ” Fetch result: {}",
2100		if multisig_data.is_some() { "Found" } else { "Not found" }
2101	);
2102
2103	match multisig_data {
2104		Some(data) => {
2105			// Query balance for multisig address
2106			let balance_query =
2107				quantus_subxt::api::storage().system().account(multisig_address.clone());
2108			let account_info = storage_at.fetch(&balance_query).await?;
2109			let (balance, reserved, frozen) = account_info
2110				.map(|info| (info.data.free, info.data.reserved, info.data.frozen))
2111				.unwrap_or((0, 0, 0));
2112
2113			// Convert creator to SS58
2114			let creator_bytes: &[u8; 32] = data.creator.as_ref();
2115			let creator_sp = SpAccountId32::from(*creator_bytes);
2116			let creator_ss58 = creator_sp.to_ss58check();
2117
2118			log_print!("๐Ÿ“‹ {} Information:", "MULTISIG".bright_green().bold());
2119			log_print!("   Address: {}", multisig_ss58.bright_cyan());
2120			log_print!("   Creator: {} (receives deposit back)", creator_ss58.bright_cyan());
2121			if reserved == 0 && frozen == 0 {
2122				log_print!("   Balance: {}", format_balance(balance).bright_green().bold());
2123			} else {
2124				log_print!("   Balance: {} (free)", format_balance(balance).bright_green().bold());
2125				if reserved > 0 {
2126					log_print!(
2127						"            {} (reserved)",
2128						format_balance(reserved).bright_yellow()
2129					);
2130				}
2131				if frozen > 0 {
2132					log_print!("            {} (frozen)", format_balance(frozen).bright_yellow());
2133				}
2134			}
2135			log_print!("   Threshold: {}", data.threshold.to_string().bright_yellow());
2136			log_print!("   Signers ({}):", data.signers.0.len().to_string().bright_yellow());
2137			for (i, signer) in data.signers.0.iter().enumerate() {
2138				// Convert subxt AccountId32 to SS58
2139				let signer_bytes: &[u8; 32] = signer.as_ref();
2140				let signer_sp = SpAccountId32::from(*signer_bytes);
2141				log_print!("     {}. {}", i + 1, signer_sp.to_ss58check().bright_cyan());
2142			}
2143			log_print!("   Proposal Nonce: {}", data.proposal_nonce);
2144			log_print!(
2145				"   Deposit: {} (returned to creator on dissolve)",
2146				format_balance(data.deposit)
2147			);
2148			log_print!(
2149				"   Active Proposals: {}",
2150				data.active_proposals.to_string().bright_yellow()
2151			);
2152
2153			// Show active proposals summary if any exist
2154			if data.active_proposals > 0 {
2155				log_print!("");
2156				log_print!("๐Ÿ“ {} Active Proposals:", "PROPOSALS".bright_magenta().bold());
2157				let proposals_query = quantus_subxt::api::storage()
2158					.multisig()
2159					.proposals_iter1(multisig_address.clone());
2160				let mut proposals_stream = storage_at.iter(proposals_query).await?;
2161				while let Some(Ok(kv)) = proposals_stream.next().await {
2162					let proposal = kv.value;
2163					// Extract proposal ID from key_bytes (last 4 bytes = u32 LE)
2164					let proposal_id = if kv.key_bytes.len() >= 4 {
2165						let id_bytes = &kv.key_bytes[kv.key_bytes.len() - 4..];
2166						u32::from_le_bytes([id_bytes[0], id_bytes[1], id_bytes[2], id_bytes[3]])
2167					} else {
2168						0
2169					};
2170					let status = match proposal.status {
2171						quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Active =>
2172							"Active".bright_green(),
2173						quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Approved =>
2174							"Approved (ready to execute)".bright_yellow(),
2175						quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Executed =>
2176							"Executed".bright_blue(),
2177						quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Cancelled =>
2178							"Cancelled".bright_red(),
2179					};
2180					// Decode call name from the call field
2181					let call_name = if proposal.call.0.len() >= 2 {
2182						let pallet_idx = proposal.call.0[0];
2183						let call_idx = proposal.call.0[1];
2184						let metadata = quantus_client.client().metadata();
2185						if let Some(pallet) = metadata.pallet_by_index(pallet_idx) {
2186							if let Some(variant) = pallet.call_variant_by_index(call_idx) {
2187								format!("{}::{}", pallet.name(), variant.name)
2188							} else {
2189								format!("{}::call[{}]", pallet.name(), call_idx)
2190							}
2191						} else {
2192							format!("pallet[{}]::call[{}]", pallet_idx, call_idx)
2193						}
2194					} else {
2195						format!("({}B encoded)", proposal.call.0.len())
2196					};
2197					let proposer_bytes: &[u8; 32] = proposal.proposer.as_ref();
2198					let proposer_sp = SpAccountId32::from(*proposer_bytes);
2199					log_print!(
2200						"   #{}: {} | {} | Approvals: {} | Proposer: {}",
2201						proposal_id,
2202						call_name.bright_white(),
2203						status,
2204						proposal.approvals.0.len(),
2205						proposer_sp.to_ss58check().dimmed()
2206					);
2207				}
2208			}
2209
2210			// Check for dissolution progress
2211			let dissolve_query = quantus_subxt::api::storage()
2212				.multisig()
2213				.dissolve_approvals(multisig_address.clone());
2214			if let Some(dissolve_approvals) = storage_at.fetch(&dissolve_query).await? {
2215				log_print!("");
2216				log_print!("๐Ÿ—‘๏ธ  {} Dissolution in progress:", "DISSOLVE".bright_red().bold());
2217				log_print!(
2218					"   Progress: {}/{}",
2219					dissolve_approvals.0.len().to_string().bright_yellow(),
2220					data.threshold.to_string().bright_yellow()
2221				);
2222				log_print!("   Approvals:");
2223				for (i, approver) in dissolve_approvals.0.iter().enumerate() {
2224					let approver_bytes: &[u8; 32] = approver.as_ref();
2225					let approver_sp = SpAccountId32::from(*approver_bytes);
2226					log_print!("     {}. {}", i + 1, approver_sp.to_ss58check().bright_cyan());
2227				}
2228
2229				// Show pending approvals
2230				let pending_signers: Vec<_> =
2231					data.signers.0.iter().filter(|s| !dissolve_approvals.0.contains(s)).collect();
2232
2233				if !pending_signers.is_empty() {
2234					log_print!("   Pending:");
2235					for (i, signer) in pending_signers.iter().enumerate() {
2236						let signer_bytes: &[u8; 32] = signer.as_ref();
2237						let signer_sp = SpAccountId32::from(*signer_bytes);
2238						log_print!("     {}. {}", i + 1, signer_sp.to_ss58check().dimmed());
2239					}
2240				}
2241
2242				log_print!("");
2243				log_print!("   โš ๏ธ  {} When threshold is reached:", "WARNING".bright_red().bold());
2244				log_print!("      - Multisig will be dissolved IMMEDIATELY");
2245				log_print!(
2246					"      - Deposit will be RETURNED to creator: {}",
2247					creator_ss58.bright_cyan()
2248				);
2249			} else {
2250				log_print!("");
2251				log_print!(
2252					"   ๐Ÿ’ก {} Deposit ({}) will be returned to creator on dissolve",
2253					"INFO".bright_blue().bold(),
2254					format_balance(data.deposit)
2255				);
2256			}
2257		},
2258		None => {
2259			log_error!("โŒ Multisig not found at address: {}", multisig_ss58);
2260		},
2261	}
2262
2263	log_print!("");
2264	Ok(())
2265}
2266
2267/// Decode call data into human-readable format
2268async fn decode_call_data(
2269	quantus_client: &crate::chain::client::QuantusClient,
2270	call_data: &[u8],
2271) -> crate::error::Result<String> {
2272	use codec::Decode;
2273
2274	if call_data.len() < 2 {
2275		return Ok(format!("   {}  {} bytes (too short)", "Call Size:".dimmed(), call_data.len()));
2276	}
2277
2278	let pallet_index = call_data[0];
2279	let call_index = call_data[1];
2280	let args = &call_data[2..];
2281
2282	// Get metadata to find pallet and call names
2283	let metadata = quantus_client.client().metadata();
2284
2285	// Try to find pallet by index
2286	let pallet_name = metadata
2287		.pallets()
2288		.find(|p| p.index() == pallet_index)
2289		.map(|p| p.name())
2290		.unwrap_or("Unknown");
2291
2292	// Try to decode based on known patterns
2293	match (pallet_index, call_index) {
2294		// Balances pallet transfers
2295		// transfer_allow_death (0) or transfer_keep_alive (3)
2296		(_, idx) if pallet_name == "Balances" && (idx == 0 || idx == 3) => {
2297			let call_name = match idx {
2298				0 => "transfer_allow_death",
2299				3 => "transfer_keep_alive",
2300				_ => unreachable!(),
2301			};
2302
2303			if args.len() < 33 {
2304				return Ok(format!(
2305					"   {}  {}::{} (index {})\n   {}  {} bytes (too short)",
2306					"Call:".dimmed(),
2307					pallet_name.bright_cyan(),
2308					call_name.bright_yellow(),
2309					idx,
2310					"Args:".dimmed(),
2311					args.len()
2312				));
2313			}
2314
2315			// Decode MultiAddress::Id (first byte is variant, 0x00 = Id)
2316			// Then 32 bytes for AccountId32
2317			let address_variant = args[0];
2318			if address_variant != 0 {
2319				return Ok(format!(
2320					"   {}  {}::{} (index {})\n   {}  {} bytes\n   {}  Unknown address variant: {}",
2321					"Call:".dimmed(),
2322					pallet_name.bright_cyan(),
2323					call_name.bright_yellow(),
2324					idx,
2325					"Args:".dimmed(),
2326					args.len(),
2327					"Error:".dimmed(),
2328					address_variant
2329				));
2330			}
2331
2332			let account_bytes: [u8; 32] = args[1..33].try_into().map_err(|_| {
2333				crate::error::QuantusError::Generic("Failed to extract account bytes".to_string())
2334			})?;
2335			let account_id = SpAccountId32::from(account_bytes);
2336			let to_address = account_id.to_ss58check();
2337
2338			// Decode amount (Compact<u128>)
2339			let mut cursor = &args[33..];
2340			let amount: u128 = match codec::Compact::<u128>::decode(&mut cursor) {
2341				Ok(compact) => compact.0,
2342				Err(_) => {
2343					return Ok(format!(
2344						"   {}  {}::{} (index {})\n   {}  {}\n   {}  Failed to decode amount",
2345						"Call:".dimmed(),
2346						pallet_name.bright_cyan(),
2347						call_name.bright_yellow(),
2348						idx,
2349						"To:".dimmed(),
2350						to_address.bright_cyan(),
2351						"Error:".dimmed()
2352					));
2353				},
2354			};
2355
2356			Ok(format!(
2357				"   {}  {}::{}\n   {}  {}\n   {}  {}",
2358				"Call:".dimmed(),
2359				pallet_name.bright_cyan(),
2360				call_name.bright_yellow(),
2361				"To:".dimmed(),
2362				to_address.bright_cyan(),
2363				"Amount:".dimmed(),
2364				format_balance(amount).bright_green()
2365			))
2366		},
2367		// ReversibleTransfers::set_high_security
2368		(_, idx) if pallet_name == "ReversibleTransfers" && idx == 0 => {
2369			// set_high_security has: delay (enum), interceptor (AccountId32)
2370			if args.is_empty() {
2371				return Ok(format!(
2372					"   {}  {}::set_high_security\n   {}  {} bytes (too short)",
2373					"Call:".dimmed(),
2374					pallet_name.bright_cyan(),
2375					"Args:".dimmed(),
2376					args.len()
2377				));
2378			}
2379
2380			// Decode delay (BlockNumberOrTimestamp enum)
2381			let delay_variant = args[0];
2382			let delay_str: String;
2383			let offset: usize;
2384
2385			match delay_variant {
2386				0 => {
2387					// BlockNumber(u32)
2388					if args.len() < 5 {
2389						return Ok(format!(
2390							"   {}  {}::set_high_security\n   {}  Failed to decode delay (BlockNumber)",
2391							"Call:".dimmed(),
2392							pallet_name.bright_cyan(),
2393							"Error:".dimmed()
2394						));
2395					}
2396					let blocks = u32::from_le_bytes([args[1], args[2], args[3], args[4]]);
2397					delay_str = format!("{} blocks", blocks);
2398					offset = 5;
2399				},
2400				1 => {
2401					// Timestamp(u64)
2402					if args.len() < 9 {
2403						return Ok(format!(
2404							"   {}  {}::set_high_security\n   {}  Failed to decode delay (Timestamp)",
2405							"Call:".dimmed(),
2406							pallet_name.bright_cyan(),
2407							"Error:".dimmed()
2408						));
2409					}
2410					let millis = u64::from_le_bytes([
2411						args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8],
2412					]);
2413					let seconds = millis / 1000;
2414					delay_str = format!("{} seconds ({} ms)", seconds, millis);
2415					offset = 9;
2416				},
2417				_ => {
2418					return Ok(format!(
2419						"   {}  {}::set_high_security\n   {}  Unknown delay variant: {}",
2420						"Call:".dimmed(),
2421						pallet_name.bright_cyan(),
2422						"Error:".dimmed(),
2423						delay_variant
2424					));
2425				},
2426			}
2427
2428			// Decode interceptor (AccountId32)
2429			if args.len() < offset + 32 {
2430				return Ok(format!(
2431					"   {}  {}::set_high_security\n   {}  {}\n   {}  Failed to decode interceptor",
2432					"Call:".dimmed(),
2433					pallet_name.bright_cyan(),
2434					"Delay:".dimmed(),
2435					delay_str.bright_yellow(),
2436					"Error:".dimmed()
2437				));
2438			}
2439
2440			let interceptor_bytes: [u8; 32] =
2441				args[offset..offset + 32].try_into().map_err(|_| {
2442					crate::error::QuantusError::Generic(
2443						"Failed to extract interceptor bytes".to_string(),
2444					)
2445				})?;
2446			let interceptor = SpAccountId32::from(interceptor_bytes);
2447			let interceptor_ss58 = interceptor
2448				.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
2449
2450			Ok(format!(
2451				"   {}  {}::set_high_security\n   {}  {}\n   {}  {}",
2452				"Call:".dimmed(),
2453				pallet_name.bright_cyan(),
2454				"Delay:".dimmed(),
2455				delay_str.bright_yellow(),
2456				"Guardian:".dimmed(),
2457				interceptor_ss58.bright_green()
2458			))
2459		},
2460		_ => {
2461			// Try to get call name from metadata
2462			let call_name = metadata
2463				.pallets()
2464				.find(|p| p.index() == pallet_index)
2465				.and_then(|p| {
2466					p.call_variants().and_then(|calls| {
2467						calls.iter().find(|v| v.index == call_index).map(|v| v.name.as_str())
2468					})
2469				})
2470				.unwrap_or("unknown");
2471
2472			Ok(format!(
2473				"   {}  {}::{} (index {}:{})\n   {}  {} bytes\n   {}  {}",
2474				"Call:".dimmed(),
2475				pallet_name.bright_cyan(),
2476				call_name.bright_yellow(),
2477				pallet_index,
2478				call_index,
2479				"Args:".dimmed(),
2480				args.len(),
2481				"Raw:".dimmed(),
2482				hex::encode(args).bright_green()
2483			))
2484		},
2485	}
2486}
2487
2488/// Query proposal information
2489async fn handle_proposal_info(
2490	multisig_address: String,
2491	proposal_id: u32,
2492	node_url: &str,
2493) -> crate::error::Result<()> {
2494	log_print!("๐Ÿ” {} Querying proposal info...", "MULTISIG".bright_magenta().bold());
2495	log_print!("");
2496
2497	// Resolve multisig address
2498	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
2499	let (multisig_id, _) =
2500		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
2501			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
2502		})?;
2503	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
2504	let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
2505
2506	// Connect to chain
2507	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
2508
2509	// Get latest block hash explicitly
2510	let latest_block_hash = quantus_client.get_latest_block().await?;
2511
2512	// Get current block number
2513	let latest_block = quantus_client.client().blocks().at(latest_block_hash).await?;
2514	let current_block_number = latest_block.number();
2515
2516	// Query storage by proposal ID
2517	let storage_query = quantus_subxt::api::storage()
2518		.multisig()
2519		.proposals(multisig_address.clone(), proposal_id);
2520
2521	let storage_at = quantus_client.client().storage().at(latest_block_hash);
2522	let proposal_data = storage_at.fetch(&storage_query).await?;
2523
2524	match proposal_data {
2525		Some(data) => {
2526			// Get multisig info for context
2527			let multisig_query =
2528				quantus_subxt::api::storage().multisig().multisigs(multisig_address.clone());
2529			let multisig_info = storage_at.fetch(&multisig_query).await?;
2530
2531			log_print!("๐Ÿ“ {} Information:", "PROPOSAL".bright_green().bold());
2532			log_print!(
2533				"   Current Block: {}",
2534				current_block_number.to_string().bright_white().bold()
2535			);
2536			log_print!("   Multisig: {}", multisig_ss58.bright_cyan());
2537
2538			// Show threshold and approval progress
2539			if let Some(ref ms_data) = multisig_info {
2540				let progress = format!("{}/{}", data.approvals.0.len(), ms_data.threshold);
2541				log_print!(
2542					"   Threshold: {} (progress: {})",
2543					ms_data.threshold.to_string().bright_yellow(),
2544					progress.bright_cyan()
2545				);
2546			}
2547
2548			log_print!("   Proposal ID: {}", proposal_id.to_string().bright_yellow());
2549			// Convert proposer to SS58
2550			let proposer_bytes: &[u8; 32] = data.proposer.as_ref();
2551			let proposer_sp = SpAccountId32::from(*proposer_bytes);
2552			log_print!("   Proposer: {}", proposer_sp.to_ss58check().bright_cyan());
2553
2554			// Decode and display call data
2555			log_print!("");
2556			match decode_call_data(&quantus_client, &data.call.0).await {
2557				Ok(decoded) => {
2558					log_print!("{}", decoded);
2559				},
2560				Err(e) => {
2561					log_print!("   Call Size: {} bytes", data.call.0.len());
2562					log_verbose!("Failed to decode call data: {:?}", e);
2563				},
2564			}
2565			log_print!("");
2566
2567			// Calculate blocks remaining until expiry
2568			if data.expiry > current_block_number {
2569				let blocks_remaining = data.expiry - current_block_number;
2570				log_print!(
2571					"   Expiry: block {} ({} blocks remaining)",
2572					data.expiry,
2573					blocks_remaining.to_string().bright_green()
2574				);
2575			} else {
2576				log_print!("   Expiry: block {} ({})", data.expiry, "EXPIRED".bright_red().bold());
2577			}
2578			log_print!("   Deposit: {} (locked)", format_balance(data.deposit));
2579			log_print!(
2580				"   Status: {}",
2581				match data.status {
2582					quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Active =>
2583						"Active".bright_green(),
2584					quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Approved =>
2585						"Approved (ready to execute)".bright_yellow(),
2586					quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Executed =>
2587						"Executed".bright_blue(),
2588					quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Cancelled =>
2589						"Cancelled".bright_red(),
2590				}
2591			);
2592			log_print!("   Approvals ({}):", data.approvals.0.len().to_string().bright_yellow());
2593			for (i, approver) in data.approvals.0.iter().enumerate() {
2594				// Convert approver to SS58
2595				let approver_bytes: &[u8; 32] = approver.as_ref();
2596				let approver_sp = SpAccountId32::from(*approver_bytes);
2597				log_print!("     {}. {}", i + 1, approver_sp.to_ss58check().bright_cyan());
2598			}
2599
2600			// Show which signers haven't approved yet
2601			if let Some(ms_data) = multisig_info {
2602				let pending_signers: Vec<String> = ms_data
2603					.signers
2604					.0
2605					.iter()
2606					.filter(|s| !data.approvals.0.contains(s))
2607					.map(|s| {
2608						let bytes: &[u8; 32] = s.as_ref();
2609						let sp = SpAccountId32::from(*bytes);
2610						sp.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(
2611							189,
2612						))
2613					})
2614					.collect();
2615
2616				if !pending_signers.is_empty() {
2617					log_print!("");
2618					log_print!(
2619						"   Pending Approvals ({}):",
2620						pending_signers.len().to_string().bright_red()
2621					);
2622					for (i, signer) in pending_signers.iter().enumerate() {
2623						log_print!("     {}. {}", i + 1, signer.bright_red());
2624					}
2625				}
2626			}
2627		},
2628		None => {
2629			log_error!("โŒ Proposal not found");
2630		},
2631	}
2632
2633	log_print!("");
2634	Ok(())
2635}
2636
2637/// List all proposals for a multisig
2638async fn handle_list_proposals(
2639	multisig_address: String,
2640	node_url: &str,
2641) -> crate::error::Result<()> {
2642	log_print!("๐Ÿ“‹ {} Listing proposals...", "MULTISIG".bright_magenta().bold());
2643	log_print!("");
2644
2645	// Resolve multisig address
2646	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
2647	let (multisig_id, _) =
2648		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
2649			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
2650		})?;
2651	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
2652	let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
2653
2654	// Connect to chain
2655	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
2656
2657	// Get latest block hash explicitly
2658	let latest_block_hash = quantus_client.get_latest_block().await?;
2659
2660	// Query all proposals for this multisig using prefix iteration
2661	let storage = quantus_client.client().storage().at(latest_block_hash);
2662
2663	// Use iter_key_values to iterate over the double map
2664	let address = quantus_subxt::api::storage().multisig().proposals_iter1(multisig_address);
2665	let mut proposals = storage.iter(address).await?;
2666
2667	let mut count = 0;
2668	let mut active_count = 0;
2669	let mut approved_count = 0;
2670	let mut executed_count = 0;
2671	let mut cancelled_count = 0;
2672
2673	while let Some(result) = proposals.next().await {
2674		match result {
2675			Ok(kv) => {
2676				count += 1;
2677
2678				let status_str = match kv.value.status {
2679					quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Active => {
2680						active_count += 1;
2681						"Active".bright_green()
2682					},
2683					quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Approved => {
2684						approved_count += 1;
2685						"Approved (ready to execute)".bright_yellow()
2686					},
2687					quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Executed => {
2688						executed_count += 1;
2689						"Executed".bright_blue()
2690					},
2691					quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Cancelled => {
2692						cancelled_count += 1;
2693						"Cancelled".bright_red()
2694					},
2695				};
2696
2697				// Extract proposal ID from key_bytes (u32, 4 bytes with Twox64Concat hasher)
2698				// The key_bytes contains:
2699				// [storage_prefix][Blake2_128Concat(multisig)][Twox64Concat(u32)] Twox64Concat
2700				// encoding: [8-byte hash][4-byte value] We need the last 4 bytes as
2701				// little-endian u32
2702				let key_bytes = kv.key_bytes;
2703				if key_bytes.len() >= 4 {
2704					let id_bytes = &key_bytes[key_bytes.len() - 4..];
2705					let proposal_id =
2706						u32::from_le_bytes([id_bytes[0], id_bytes[1], id_bytes[2], id_bytes[3]]);
2707
2708					log_print!("๐Ÿ“ Proposal #{}", count);
2709					log_print!("   ID: {}", proposal_id.to_string().bright_yellow());
2710
2711					// Convert proposer to SS58
2712					let proposer_bytes: &[u8; 32] = kv.value.proposer.as_ref();
2713					let proposer_sp = SpAccountId32::from(*proposer_bytes);
2714					log_print!("   Proposer: {}", proposer_sp.to_ss58check().bright_cyan());
2715
2716					// Decode and display call data (compact format for list)
2717					match decode_call_data(&quantus_client, &kv.value.call.0).await {
2718						Ok(decoded) => {
2719							// Extract just the call info line for compact display
2720							let lines: Vec<&str> = decoded.lines().collect();
2721							if !lines.is_empty() {
2722								log_print!("   {}", lines[0].trim_start());
2723							}
2724						},
2725						Err(_) => {
2726							log_print!("   Call Size: {} bytes", kv.value.call.0.len());
2727						},
2728					}
2729
2730					log_print!("   Status: {}", status_str);
2731					log_print!("   Approvals: {}", kv.value.approvals.0.len());
2732					log_print!("   Expiry: block {}", kv.value.expiry);
2733					log_print!("");
2734				}
2735			},
2736			Err(e) => {
2737				log_error!("Error reading proposal: {:?}", e);
2738			},
2739		}
2740	}
2741
2742	if count == 0 {
2743		log_print!("   No proposals found for this multisig");
2744	} else {
2745		log_print!("๐Ÿ“Š {} Summary:", "PROPOSALS".bright_green().bold());
2746		log_print!("   Total: {}", count.to_string().bright_yellow());
2747		log_print!("   Active: {}", active_count.to_string().bright_green());
2748		log_print!("   Approved: {}", approved_count.to_string().bright_yellow());
2749		log_print!("   Executed: {}", executed_count.to_string().bright_blue());
2750		log_print!("   Cancelled: {}", cancelled_count.to_string().bright_red());
2751	}
2752
2753	log_print!("");
2754	Ok(())
2755}
2756
2757/// Build runtime call data from pallet, call name, and arguments
2758async fn build_runtime_call(
2759	quantus_client: &crate::chain::client::QuantusClient,
2760	pallet: &str,
2761	call: &str,
2762	args: Vec<serde_json::Value>,
2763) -> crate::error::Result<Vec<u8>> {
2764	// Validate pallet/call exists in metadata
2765	let metadata = quantus_client.client().metadata();
2766	let pallet_metadata = metadata.pallet_by_name(pallet).ok_or_else(|| {
2767		crate::error::QuantusError::Generic(format!("Pallet '{}' not found in metadata", pallet))
2768	})?;
2769
2770	log_verbose!("โœ… Found pallet '{}' with index {}", pallet, pallet_metadata.index());
2771
2772	// Find the call in the pallet
2773	let call_metadata = pallet_metadata.call_variant_by_name(call).ok_or_else(|| {
2774		crate::error::QuantusError::Generic(format!(
2775			"Call '{}' not found in pallet '{}'",
2776			call, pallet
2777		))
2778	})?;
2779
2780	log_verbose!("โœ… Found call '{}' with index {}", call, call_metadata.index);
2781
2782	// For now, we'll construct a basic call using the generic approach
2783	// This is a simplified implementation - in production, you'd want to handle all argument types
2784	use codec::Encode;
2785
2786	let mut call_data = Vec::new();
2787	// Pallet index
2788	call_data.push(pallet_metadata.index());
2789	// Call index
2790	call_data.push(call_metadata.index);
2791
2792	// Encode arguments based on call type
2793	// This is a simplified version - in production you'd need proper argument encoding
2794	match (pallet, call) {
2795		("Balances", "transfer_allow_death") | ("Balances", "transfer_keep_alive") => {
2796			if args.len() != 2 {
2797				return Err(crate::error::QuantusError::Generic(
2798					"Balances transfer requires 2 arguments: [to_address, amount]".to_string(),
2799				));
2800			}
2801
2802			let to_address = args[0].as_str().ok_or_else(|| {
2803				crate::error::QuantusError::Generic(
2804					"First argument must be a string (to_address)".to_string(),
2805				)
2806			})?;
2807
2808			// Parse amount - can be either string or number in JSON
2809			let amount: u128 = if let Some(amount_str) = args[1].as_str() {
2810				// If it's a string, parse it
2811				amount_str.parse().map_err(|_| {
2812					crate::error::QuantusError::Generic(
2813						"Second argument must be a valid number (amount)".to_string(),
2814					)
2815				})?
2816			} else if let Some(amount_num) = args[1].as_u64() {
2817				// If it's a number, use it directly
2818				amount_num as u128
2819			} else {
2820				// Try as_i64 for negative numbers (though we'll reject them)
2821				return Err(crate::error::QuantusError::Generic(
2822					"Second argument must be a number (amount)".to_string(),
2823				));
2824			};
2825
2826			// Convert to AccountId32
2827			let (to_account_id, _) = SpAccountId32::from_ss58check_with_version(to_address)
2828				.map_err(|e| {
2829					crate::error::QuantusError::Generic(format!("Invalid to_address: {:?}", e))
2830				})?;
2831
2832			// Convert to subxt AccountId32
2833			let to_account_id_bytes: [u8; 32] = *to_account_id.as_ref();
2834			let to_account_id_subxt =
2835				subxt::ext::subxt_core::utils::AccountId32::from(to_account_id_bytes);
2836
2837			// Encode as MultiAddress::Id
2838			let multi_address: subxt::ext::subxt_core::utils::MultiAddress<
2839				subxt::ext::subxt_core::utils::AccountId32,
2840				(),
2841			> = subxt::ext::subxt_core::utils::MultiAddress::Id(to_account_id_subxt);
2842
2843			multi_address.encode_to(&mut call_data);
2844			// Amount must be Compact encoded for Balance type
2845			codec::Compact(amount).encode_to(&mut call_data);
2846		},
2847		("System", "remark") | ("System", "remark_with_event") => {
2848			// System::remark takes a Vec<u8> argument
2849			if args.len() != 1 {
2850				return Err(crate::error::QuantusError::Generic(
2851					"System remark requires 1 argument: [hex_data]".to_string(),
2852				));
2853			}
2854
2855			let hex_data = args[0].as_str().ok_or_else(|| {
2856				crate::error::QuantusError::Generic(
2857					"Argument must be a hex string (e.g., \"0x48656c6c6f\")".to_string(),
2858				)
2859			})?;
2860
2861			// Remove 0x prefix if present
2862			let hex_str = hex_data.trim_start_matches("0x");
2863
2864			// Decode hex to bytes
2865			let data_bytes = hex::decode(hex_str).map_err(|e| {
2866				crate::error::QuantusError::Generic(format!("Invalid hex data: {}", e))
2867			})?;
2868
2869			// Encode as Vec<u8> (with length prefix)
2870			data_bytes.encode_to(&mut call_data);
2871		},
2872		_ => {
2873			return Err(crate::error::QuantusError::Generic(format!(
2874			"Building call data for {}.{} is not yet implemented. Use a simpler approach or add support.",
2875			pallet, call
2876		)));
2877		},
2878	}
2879
2880	Ok(call_data)
2881}
2882
2883/// Format balance for display
2884fn format_balance(balance: u128) -> String {
2885	let quan = balance / QUAN_DECIMALS;
2886	let remainder = balance % QUAN_DECIMALS;
2887
2888	if remainder == 0 {
2889		format!("{} QUAN", quan)
2890	} else {
2891		// Show up to 12 decimal places, removing trailing zeros
2892		let decimal_str = format!("{:012}", remainder).trim_end_matches('0').to_string();
2893		format!("{}.{} QUAN", quan, decimal_str)
2894	}
2895}
2896
2897// ============================================================================
2898// HIGH SECURITY HANDLERS
2899// ============================================================================
2900
2901/// Check high-security status for a multisig
2902async fn handle_high_security_status(
2903	multisig_address: String,
2904	node_url: &str,
2905) -> crate::error::Result<()> {
2906	log_print!("๐Ÿ” {} Checking High-Security status...", "MULTISIG".bright_magenta().bold());
2907	log_print!("");
2908
2909	// Resolve multisig address
2910	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
2911	let (multisig_id, _) =
2912		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
2913			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
2914		})?;
2915	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
2916	let multisig_account_id = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
2917
2918	// Connect to chain
2919	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
2920
2921	// Query high-security status
2922	let latest_block_hash = quantus_client.get_latest_block().await?;
2923	let storage_at = quantus_client.client().storage().at(latest_block_hash);
2924
2925	let storage_query = quantus_subxt::api::storage()
2926		.reversible_transfers()
2927		.high_security_accounts(multisig_account_id);
2928
2929	let high_security_data = storage_at.fetch(&storage_query).await?;
2930
2931	log_print!("๐Ÿ“‹ Multisig: {}", multisig_ss58.bright_cyan());
2932	log_print!("");
2933
2934	match high_security_data {
2935		Some(data) => {
2936			log_success!("โœ… High-Security: {}", "ENABLED".bright_green().bold());
2937			log_print!("");
2938
2939			// Convert interceptor to SS58
2940			let interceptor_bytes: &[u8; 32] = data.interceptor.as_ref();
2941			let interceptor_sp = SpAccountId32::from(*interceptor_bytes);
2942			let interceptor_ss58 = interceptor_sp
2943				.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
2944
2945			log_print!("๐Ÿ›ก๏ธ  Guardian/Interceptor: {}", interceptor_ss58.bright_green().bold());
2946
2947			// Format delay display
2948			match data.delay {
2949				quantus_subxt::api::runtime_types::qp_scheduler::BlockNumberOrTimestamp::BlockNumber(
2950					blocks,
2951				) => {
2952					log_print!("โฑ๏ธ  Delay: {} blocks", blocks.to_string().bright_yellow());
2953				},
2954				quantus_subxt::api::runtime_types::qp_scheduler::BlockNumberOrTimestamp::Timestamp(
2955					ms,
2956				) => {
2957					let seconds = ms / 1000;
2958					log_print!("โฑ๏ธ  Delay: {} seconds", seconds.to_string().bright_yellow());
2959				},
2960			}
2961
2962			log_print!("");
2963			log_print!(
2964				"๐Ÿ’ก {} All transfers from this multisig will be delayed and reversible",
2965				"INFO".bright_blue().bold()
2966			);
2967			log_print!("   The guardian can intercept transactions during the delay period");
2968			log_print!("");
2969			log_print!(
2970				"โš ๏ธ  {} Guardian interception requires direct runtime call (not yet in CLI)",
2971				"NOTE".bright_yellow().bold()
2972			);
2973			log_print!("   Use: pallet_reversible_transfers::cancel(tx_id) as guardian account");
2974		},
2975		None => {
2976			log_print!("โŒ High-Security: {}", "DISABLED".bright_red().bold());
2977			log_print!("");
2978			log_print!("๐Ÿ’ก This multisig does not have high-security enabled.");
2979			log_print!("   Use 'quantus multisig high-security set' to enable it via a proposal.");
2980		},
2981	}
2982
2983	log_print!("");
2984	Ok(())
2985}
2986
2987/// Enable high-security for a multisig (via proposal)
2988async fn handle_high_security_set(
2989	multisig_address: String,
2990	interceptor: String,
2991	delay_blocks: Option<u32>,
2992	delay_seconds: Option<u64>,
2993	expiry: u32,
2994	from: String,
2995	password: Option<String>,
2996	password_file: Option<String>,
2997	node_url: &str,
2998	execution_mode: ExecutionMode,
2999) -> crate::error::Result<()> {
3000	log_print!(
3001		"๐Ÿ›ก๏ธ  {} Enabling High-Security (via proposal)...",
3002		"MULTISIG".bright_magenta().bold()
3003	);
3004
3005	// Validate delay parameters
3006	if delay_blocks.is_none() && delay_seconds.is_none() {
3007		log_error!("โŒ You must specify either --delay-blocks or --delay-seconds");
3008		return Err(crate::error::QuantusError::Generic("Missing delay parameter".to_string()));
3009	}
3010
3011	// Resolve multisig address
3012	let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
3013	let (multisig_id, _) =
3014		SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
3015			crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
3016		})?;
3017	let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
3018	let multisig_account_id = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
3019
3020	// Resolve interceptor address
3021	let interceptor_ss58 = crate::cli::common::resolve_address(&interceptor)?;
3022	let (interceptor_id, _) = SpAccountId32::from_ss58check_with_version(&interceptor_ss58)
3023		.map_err(|e| {
3024			crate::error::QuantusError::Generic(format!("Invalid interceptor address: {:?}", e))
3025		})?;
3026	let interceptor_bytes: [u8; 32] = *interceptor_id.as_ref();
3027	let interceptor_account_id =
3028		subxt::ext::subxt_core::utils::AccountId32::from(interceptor_bytes);
3029
3030	log_verbose!("Multisig: {}", multisig_ss58);
3031	log_verbose!("Interceptor: {}", interceptor_ss58);
3032
3033	// Connect to chain
3034	let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
3035
3036	// Build the set_high_security call
3037	use quantus_subxt::api::reversible_transfers::calls::types::set_high_security::Delay as HsDelay;
3038
3039	let delay_value = if let Some(blocks) = delay_blocks {
3040		HsDelay::BlockNumber(blocks)
3041	} else if let Some(seconds) = delay_seconds {
3042		HsDelay::Timestamp(seconds * 1000) // Convert seconds to milliseconds
3043	} else {
3044		return Err(crate::error::QuantusError::Generic("Missing delay parameter".to_string()));
3045	};
3046
3047	log_verbose!("Delay: {:?}", delay_value);
3048
3049	// Build the runtime call
3050	let set_hs_call = quantus_subxt::api::tx()
3051		.reversible_transfers()
3052		.set_high_security(delay_value, interceptor_account_id);
3053
3054	// Encode the call
3055	use subxt::tx::Payload;
3056	let call_data =
3057		set_hs_call.encode_call_data(&quantus_client.client().metadata()).map_err(|e| {
3058			crate::error::QuantusError::Generic(format!("Failed to encode call: {:?}", e))
3059		})?;
3060
3061	log_verbose!("Call data size: {} bytes", call_data.len());
3062
3063	// Validate expiry is in the future (client-side check)
3064	let latest_block_hash = quantus_client.get_latest_block().await?;
3065	let latest_block = quantus_client.client().blocks().at(latest_block_hash).await?;
3066	let current_block_number = latest_block.number();
3067
3068	if expiry <= current_block_number {
3069		log_error!(
3070			"โŒ Expiry block {} is in the past (current block: {})",
3071			expiry,
3072			current_block_number
3073		);
3074		log_print!("   Use a higher block number, e.g., --expiry {}", current_block_number + 1000);
3075		return Err(crate::error::QuantusError::Generic("Expiry must be in the future".to_string()));
3076	}
3077
3078	// Validate proposer is a signer
3079	let storage_at = quantus_client.client().storage().at(latest_block_hash);
3080	let multisig_query =
3081		quantus_subxt::api::storage().multisig().multisigs(multisig_account_id.clone());
3082	let multisig_data = storage_at.fetch(&multisig_query).await?.ok_or_else(|| {
3083		crate::error::QuantusError::Generic(format!(
3084			"Multisig not found at address: {}",
3085			multisig_ss58
3086		))
3087	})?;
3088
3089	// Resolve proposer address
3090	let proposer_ss58 = crate::cli::common::resolve_address(&from)?;
3091	let (proposer_id, _) =
3092		SpAccountId32::from_ss58check_with_version(&proposer_ss58).map_err(|e| {
3093			crate::error::QuantusError::Generic(format!("Invalid proposer address: {:?}", e))
3094		})?;
3095	let proposer_bytes: [u8; 32] = *proposer_id.as_ref();
3096	let proposer_account_id = subxt::ext::subxt_core::utils::AccountId32::from(proposer_bytes);
3097
3098	// Check if proposer is in signers list
3099	if !multisig_data.signers.0.contains(&proposer_account_id) {
3100		log_error!("โŒ Not authorized: {} is not a signer of this multisig", proposer_ss58);
3101		return Err(crate::error::QuantusError::Generic(
3102			"Only multisig signers can create proposals".to_string(),
3103		));
3104	}
3105
3106	// Check for expired proposals from this proposer (will be auto-cleaned by runtime)
3107	let mut expired_count = 0;
3108	let proposals_query = quantus_subxt::api::storage().multisig().proposals_iter();
3109	let mut proposals_stream = storage_at.iter(proposals_query).await?;
3110
3111	while let Some(Ok(kv)) = proposals_stream.next().await {
3112		let proposal = kv.value;
3113		// Check if this proposal belongs to our proposer
3114		if proposal.proposer == proposer_account_id {
3115			// Check if expired
3116			if proposal.expiry <= current_block_number {
3117				expired_count += 1;
3118			}
3119		}
3120	}
3121
3122	if expired_count > 0 {
3123		log_print!("");
3124		log_print!(
3125			"๐Ÿงน {} Auto-cleanup: Runtime will remove your {} expired proposal(s)",
3126			"INFO".bright_blue().bold(),
3127			expired_count.to_string().bright_yellow()
3128		);
3129		log_print!("   This happens automatically before creating the new proposal");
3130		log_print!("");
3131	}
3132
3133	// Load keypair
3134	let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
3135
3136	// Build propose transaction
3137	let propose_tx =
3138		quantus_subxt::api::tx()
3139			.multisig()
3140			.propose(multisig_account_id, call_data, expiry);
3141
3142	// Always wait for transaction confirmation
3143	let propose_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
3144
3145	// Submit transaction and wait for on-chain confirmation
3146	crate::cli::common::submit_transaction(
3147		&quantus_client,
3148		&keypair,
3149		propose_tx,
3150		None,
3151		propose_execution_mode,
3152	)
3153	.await?;
3154
3155	log_print!("");
3156	log_success!("โœ… High-Security proposal confirmed on-chain!");
3157	log_print!("");
3158	log_print!(
3159		"๐Ÿ’ก {} Once this proposal reaches threshold, High-Security will be enabled",
3160		"NEXT STEPS".bright_blue().bold()
3161	);
3162	log_print!(
3163		"   - Other signers need to approve: quantus multisig approve --address {} --proposal-id <ID> --from <SIGNER>",
3164		multisig_ss58.bright_cyan()
3165	);
3166	log_print!("   - After threshold is reached, all transfers will be delayed and reversible");
3167	log_print!("");
3168
3169	Ok(())
3170}