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
11const QUAN_DECIMALS: u128 = 1_000_000_000_000; #[allow(dead_code)]
20#[derive(Debug, Clone)]
21pub struct MultisigInfo {
22 pub address: String,
24 pub creator: String,
26 pub balance: u128,
28 pub threshold: u32,
30 pub signers: Vec<String>,
32 pub proposal_nonce: u32,
34 pub deposit: u128,
36 pub active_proposals: u32,
38}
39
40#[allow(dead_code)]
42#[derive(Debug, Clone, PartialEq)]
43pub enum ProposalStatus {
44 Active,
45 Approved,
47 Executed,
48 Cancelled,
49}
50
51#[allow(dead_code)]
53#[derive(Debug, Clone)]
54pub struct ProposalInfo {
55 pub id: u32,
57 pub proposer: String,
59 pub call_data: Vec<u8>,
61 pub expiry: u32,
63 pub approvals: Vec<String>,
65 pub deposit: u128,
67 pub status: ProposalStatus,
69}
70
71pub fn parse_amount(amount: &str) -> crate::error::Result<u128> {
78 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 let base_amount = (amount_f64 * QUAN_DECIMALS as f64) as u128;
92 Ok(base_amount)
93 } else {
94 if let Ok(raw) = amount.parse::<u128>() {
96 if raw >= 10_000_000_000 {
98 Ok(raw)
99 } else {
100 Ok(raw * QUAN_DECIMALS)
102 }
103 } else {
104 Err(crate::error::QuantusError::Generic(format!("Invalid amount: {}", amount)))
105 }
106 }
107}
108
109#[derive(Subcommand, Debug)]
111pub enum ProposeSubcommand {
112 Transfer {
114 #[arg(long)]
116 address: String,
117
118 #[arg(long)]
120 to: String,
121
122 #[arg(long)]
124 amount: String,
125
126 #[arg(long)]
128 expiry: u32,
129
130 #[arg(long)]
132 from: String,
133
134 #[arg(short, long)]
136 password: Option<String>,
137
138 #[arg(long)]
140 password_file: Option<String>,
141 },
142
143 Custom {
145 #[arg(long)]
147 address: String,
148
149 #[arg(long)]
151 pallet: String,
152
153 #[arg(long)]
155 call: String,
156
157 #[arg(long)]
159 args: Option<String>,
160
161 #[arg(long)]
163 expiry: u32,
164
165 #[arg(long)]
167 from: String,
168
169 #[arg(short, long)]
171 password: Option<String>,
172
173 #[arg(long)]
175 password_file: Option<String>,
176 },
177
178 HighSecurity {
180 #[arg(long)]
182 address: String,
183
184 #[arg(long)]
186 interceptor: String,
187
188 #[arg(long, conflicts_with = "delay_seconds")]
190 delay_blocks: Option<u32>,
191
192 #[arg(long, conflicts_with = "delay_blocks")]
194 delay_seconds: Option<u64>,
195
196 #[arg(long)]
198 expiry: u32,
199
200 #[arg(long)]
202 from: String,
203
204 #[arg(short, long)]
206 password: Option<String>,
207
208 #[arg(long)]
210 password_file: Option<String>,
211 },
212}
213
214#[derive(Subcommand, Debug)]
216pub enum MultisigCommands {
217 Create {
219 #[arg(long)]
221 signers: String,
222
223 #[arg(long)]
225 threshold: u32,
226
227 #[arg(long, default_value = "0")]
230 nonce: u64,
231
232 #[arg(long)]
234 from: String,
235
236 #[arg(short, long)]
238 password: Option<String>,
239
240 #[arg(long)]
242 password_file: Option<String>,
243 },
244
245 PredictAddress {
247 #[arg(long)]
249 signers: String,
250
251 #[arg(long)]
253 threshold: u32,
254
255 #[arg(long, default_value = "0")]
257 nonce: u64,
258 },
259
260 #[command(subcommand)]
262 Propose(ProposeSubcommand),
263
264 Approve {
266 #[arg(long)]
268 address: String,
269
270 #[arg(long)]
272 proposal_id: u32,
273
274 #[arg(long)]
276 from: String,
277
278 #[arg(short, long)]
280 password: Option<String>,
281
282 #[arg(long)]
284 password_file: Option<String>,
285 },
286
287 Execute {
289 #[arg(long)]
291 address: String,
292
293 #[arg(long)]
295 proposal_id: u32,
296
297 #[arg(long)]
299 from: String,
300
301 #[arg(short, long)]
303 password: Option<String>,
304
305 #[arg(long)]
307 password_file: Option<String>,
308 },
309
310 Cancel {
312 #[arg(long)]
314 address: String,
315
316 #[arg(long)]
318 proposal_id: u32,
319
320 #[arg(long)]
322 from: String,
323
324 #[arg(short, long)]
326 password: Option<String>,
327
328 #[arg(long)]
330 password_file: Option<String>,
331 },
332
333 RemoveExpired {
335 #[arg(long)]
337 address: String,
338
339 #[arg(long)]
341 proposal_id: u32,
342
343 #[arg(long)]
345 from: String,
346
347 #[arg(short, long)]
349 password: Option<String>,
350
351 #[arg(long)]
353 password_file: Option<String>,
354 },
355
356 ClaimDeposits {
358 #[arg(long)]
360 address: String,
361
362 #[arg(long)]
364 from: String,
365
366 #[arg(short, long)]
368 password: Option<String>,
369
370 #[arg(long)]
372 password_file: Option<String>,
373 },
374
375 Dissolve {
377 #[arg(long)]
379 address: String,
380
381 #[arg(long)]
383 from: String,
384
385 #[arg(short, long)]
387 password: Option<String>,
388
389 #[arg(long)]
391 password_file: Option<String>,
392 },
393
394 Info {
396 #[arg(long)]
398 address: String,
399
400 #[arg(long)]
402 proposal_id: Option<u32>,
403 },
404
405 ListProposals {
407 #[arg(long)]
409 address: String,
410 },
411
412 #[command(subcommand)]
414 HighSecurity(HighSecuritySubcommands),
415}
416
417#[derive(Subcommand, Debug)]
419pub enum HighSecuritySubcommands {
420 Status {
422 #[arg(long)]
424 address: String,
425 },
426}
427
428#[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 const PALLET_ID: [u8; 8] = *b"py/mltsg";
455
456 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 let mut sorted_signers = sp_signers;
468 sorted_signers.sort();
469
470 let mut data = Vec::new();
473 data.extend_from_slice(&PALLET_ID);
474 data.extend_from_slice(&sorted_signers.encode());
476 data.extend_from_slice(&threshold.encode());
477 data.extend_from_slice(&nonce.encode());
478
479 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 account_id.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189))
492}
493
494#[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 let create_tx =
517 quantus_subxt::api::tx()
518 .multisig()
519 .create_multisig(signers.clone(), threshold, nonce);
520
521 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 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#[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 let pallet_index = 5u8; let call_index = 0u8; let mut call_data = Vec::new();
576 call_data.push(pallet_index);
577 call_data.push(call_index);
578
579 call_data.push(0u8); call_data.extend_from_slice(to_address.as_ref());
582
583 Compact(amount).encode_to(&mut call_data);
585
586 let propose_tx =
588 quantus_subxt::api::tx().multisig().propose(multisig_address, call_data, expiry);
589
590 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#[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 let propose_tx =
618 quantus_subxt::api::tx().multisig().propose(multisig_address, call_data, expiry);
619
620 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#[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#[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#[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 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 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 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 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#[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#[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 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 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#[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
917pub 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
1071async 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 let signer_addresses: Vec<subxt::ext::subxt_core::utils::AccountId32> = signers
1086 .split(',')
1087 .map(|s| s.trim())
1088 .map(|addr| {
1089 let ss58_str = crate::cli::common::resolve_address(addr)?;
1091 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 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 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 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 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1135
1136 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1138
1139 let create_tx = quantus_subxt::api::tx().multisig().create_multisig(
1141 signer_addresses.clone(),
1142 threshold,
1143 nonce,
1144 );
1145
1146 let create_execution_mode = ExecutionMode {
1148 finalized: execution_mode.finalized,
1149 wait_for_transaction: true, };
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 {
1165 log_print!("");
1166 log_print!("๐ Looking for MultisigCreated event...");
1167
1168 let latest_block_hash = quantus_client.get_latest_block().await?;
1170 let events = quantus_client.client().events().at(latest_block_hash).await?;
1171
1172 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 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
1228async 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 let signer_addresses: Vec<subxt::ext::subxt_core::utils::AccountId32> = signers
1239 .split(',')
1240 .map(|s| s.trim())
1241 .map(|addr| {
1242 let ss58_str = crate::cli::common::resolve_address(addr)?;
1244 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 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 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 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
1302async 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 let to_address = crate::cli::common::resolve_address(&to)?;
1319
1320 let amount_u128: u128 = parse_amount(&amount)?;
1322
1323 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 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 log_print!(
1345 "๐ก๏ธ {} High-Security detected: using delayed transfer (ReversibleTransfers::schedule_transfer)",
1346 "HS".bright_green().bold()
1347 );
1348
1349 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 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 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
1406async 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 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 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 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1445
1446 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 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 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 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 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 if proposal.proposer == proposer_account_id {
1500 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 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 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1525
1526 let propose_tx =
1528 quantus_subxt::api::tx()
1529 .multisig()
1530 .propose(multisig_address.clone(), call_data, expiry);
1531
1532 let propose_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
1534
1535 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
1550async 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 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 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 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1592
1593 let propose_tx =
1595 quantus_subxt::api::tx()
1596 .multisig()
1597 .propose(multisig_account_id, call_data, expiry);
1598
1599 let propose_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
1601
1602 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
1617async 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 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 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1643
1644 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1646
1647 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 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 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 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 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 let approve_tx = quantus_subxt::api::tx().multisig().approve(multisig_address, proposal_id);
1701
1702 let approve_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
1704
1705 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
1725async 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 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 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1751
1752 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1754
1755 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 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
1802async 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 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 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1827
1828 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1830
1831 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 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 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 let cancel_tx = quantus_subxt::api::tx().multisig().cancel(multisig_address, proposal_id);
1866
1867 let cancel_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
1869
1870 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
1886async 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 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 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1911
1912 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1914
1915 let remove_tx = quantus_subxt::api::tx()
1917 .multisig()
1918 .remove_expired(multisig_address, proposal_id);
1919
1920 let remove_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
1922
1923 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
1938async 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 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 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
1960
1961 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
1963
1964 let claim_tx = quantus_subxt::api::tx().multisig().claim_deposits(multisig_address);
1966
1967 let claim_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
1969
1970 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
1986async 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 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 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
2008
2009 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
2011
2012 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 let approve_tx = quantus_subxt::api::tx()
2021 .multisig()
2022 .approve_dissolve(multisig_address_id.clone());
2023
2024 let dissolve_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
2026
2027 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 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
2057async fn handle_info(
2059 multisig_address: String,
2060 proposal_id: Option<u32>,
2061 node_url: &str,
2062) -> crate::error::Result<()> {
2063 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 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 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
2082
2083 crate::log_verbose!("๐ Querying multisig with address: {}", multisig_ss58);
2085 crate::log_verbose!("๐ Address bytes: {}", hex::encode(multisig_bytes));
2086
2087 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 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 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 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 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 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 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 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 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
2267async 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 let metadata = quantus_client.client().metadata();
2284
2285 let pallet_name = metadata
2287 .pallets()
2288 .find(|p| p.index() == pallet_index)
2289 .map(|p| p.name())
2290 .unwrap_or("Unknown");
2291
2292 match (pallet_index, call_index) {
2294 (_, 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 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 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 (_, idx) if pallet_name == "ReversibleTransfers" && idx == 0 => {
2369 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 let delay_variant = args[0];
2382 let delay_str: String;
2383 let offset: usize;
2384
2385 match delay_variant {
2386 0 => {
2387 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 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 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 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
2488async 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 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 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
2508
2509 let latest_block_hash = quantus_client.get_latest_block().await?;
2511
2512 let latest_block = quantus_client.client().blocks().at(latest_block_hash).await?;
2514 let current_block_number = latest_block.number();
2515
2516 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 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 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 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 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 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 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 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
2637async 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 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 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
2656
2657 let latest_block_hash = quantus_client.get_latest_block().await?;
2659
2660 let storage = quantus_client.client().storage().at(latest_block_hash);
2662
2663 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 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 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 match decode_call_data(&quantus_client, &kv.value.call.0).await {
2718 Ok(decoded) => {
2719 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
2757async 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 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 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 use codec::Encode;
2785
2786 let mut call_data = Vec::new();
2787 call_data.push(pallet_metadata.index());
2789 call_data.push(call_metadata.index);
2791
2792 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 let amount: u128 = if let Some(amount_str) = args[1].as_str() {
2810 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 amount_num as u128
2819 } else {
2820 return Err(crate::error::QuantusError::Generic(
2822 "Second argument must be a number (amount)".to_string(),
2823 ));
2824 };
2825
2826 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 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 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 codec::Compact(amount).encode_to(&mut call_data);
2846 },
2847 ("System", "remark") | ("System", "remark_with_event") => {
2848 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 let hex_str = hex_data.trim_start_matches("0x");
2863
2864 let data_bytes = hex::decode(hex_str).map_err(|e| {
2866 crate::error::QuantusError::Generic(format!("Invalid hex data: {}", e))
2867 })?;
2868
2869 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
2883fn 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 let decimal_str = format!("{:012}", remainder).trim_end_matches('0').to_string();
2893 format!("{}.{} QUAN", quan, decimal_str)
2894 }
2895}
2896
2897async 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 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 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
2920
2921 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 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 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
2987async 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 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 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 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 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
3035
3036 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) } else {
3044 return Err(crate::error::QuantusError::Generic("Missing delay parameter".to_string()));
3045 };
3046
3047 log_verbose!("Delay: {:?}", delay_value);
3048
3049 let set_hs_call = quantus_subxt::api::tx()
3051 .reversible_transfers()
3052 .set_high_security(delay_value, interceptor_account_id);
3053
3054 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 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 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 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 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 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 if proposal.proposer == proposer_account_id {
3115 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 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
3135
3136 let propose_tx =
3138 quantus_subxt::api::tx()
3139 .multisig()
3140 .propose(multisig_account_id, call_data, expiry);
3141
3142 let propose_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
3144
3145 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}