use crate::{
chain::quantus_subxt::{self},
cli::common::ExecutionMode,
log_error, log_print, log_success, log_verbose,
};
use clap::Subcommand;
use colored::Colorize;
use hex;
use sp_core::crypto::{AccountId32 as SpAccountId32, Ss58Codec};
const QUAN_DECIMALS: u128 = 1_000_000_000_000; const DEFAULT_TRANSFER_EXPIRY_BLOCKS: u32 = (2 * 60 * 60) / 10;
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct MultisigInfo {
pub address: String,
pub creator: String,
pub balance: u128,
pub threshold: u32,
pub signers: Vec<String>,
pub proposal_nonce: u32,
pub deposit: u128,
pub active_proposals: u32,
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum ProposalStatus {
Active,
Approved,
Executed,
Cancelled,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ProposalInfo {
pub id: u32,
pub proposer: String,
pub call_data: Vec<u8>,
pub expiry: u32,
pub approvals: Vec<String>,
pub deposit: u128,
pub status: ProposalStatus,
}
pub fn parse_amount(amount: &str) -> crate::error::Result<u128> {
if amount.contains('.') {
let amount_f64: f64 = amount
.parse()
.map_err(|e| crate::error::QuantusError::Generic(format!("Invalid amount: {}", e)))?;
if amount_f64 < 0.0 {
return Err(crate::error::QuantusError::Generic(
"Amount cannot be negative".to_string(),
));
}
let base_amount = (amount_f64 * QUAN_DECIMALS as f64) as u128;
Ok(base_amount)
} else {
if let Ok(raw) = amount.parse::<u128>() {
if raw >= 10_000_000_000 {
Ok(raw)
} else {
Ok(raw * QUAN_DECIMALS)
}
} else {
Err(crate::error::QuantusError::Generic(format!("Invalid amount: {}", amount)))
}
}
}
#[derive(Subcommand, Debug)]
pub enum ProposeSubcommand {
#[command(arg_required_else_help = true)]
Transfer {
#[arg(long, value_name = "MULTISIG_ADDRESS")]
address: String,
#[arg(long, value_name = "RECIPIENT")]
to: String,
#[arg(long, value_name = "AMOUNT")]
amount: String,
#[arg(long, value_name = "EXPIRY_BLOCK")]
expiry: Option<u32>,
#[arg(long, value_name = "PROPOSER_WALLET")]
from: String,
#[arg(short, long)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
Custom {
#[arg(long)]
address: String,
#[arg(long)]
pallet: String,
#[arg(long)]
call: String,
#[arg(long)]
args: Option<String>,
#[arg(long)]
expiry: u32,
#[arg(long)]
from: String,
#[arg(short, long)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
HighSecurity {
#[arg(long)]
address: String,
#[arg(long)]
interceptor: String,
#[arg(long, conflicts_with = "delay_seconds")]
delay_blocks: Option<u32>,
#[arg(long, conflicts_with = "delay_blocks")]
delay_seconds: Option<u64>,
#[arg(long)]
expiry: u32,
#[arg(long)]
from: String,
#[arg(short, long)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
}
#[derive(Subcommand, Debug)]
pub enum MultisigCommands {
#[command(
arg_required_else_help = true,
after_help = "Examples:\n quantus multisig create --signers \"5F3sa2TJ...abc,5DAAnrj7...xyz,5HGjWAeF...123\" --threshold 2 --from alice\n quantus multisig create --signers \"alice,bob,charlie\" --threshold 2 --from alice"
)]
Create {
#[arg(long, value_name = "SIGNERS_CSV")]
signers: String,
#[arg(long)]
threshold: u32,
#[arg(long, default_value = "0")]
nonce: u64,
#[arg(long)]
from: String,
#[arg(short, long)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
PredictAddress {
#[arg(long)]
signers: String,
#[arg(long)]
threshold: u32,
#[arg(long, default_value = "0")]
nonce: u64,
},
#[command(subcommand)]
Propose(ProposeSubcommand),
Approve {
#[arg(long)]
address: String,
#[arg(long)]
proposal_id: u32,
#[arg(long)]
from: String,
#[arg(short, long)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
Execute {
#[arg(long)]
address: String,
#[arg(long)]
proposal_id: u32,
#[arg(long)]
from: String,
#[arg(short, long)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
Cancel {
#[arg(long)]
address: String,
#[arg(long)]
proposal_id: u32,
#[arg(long)]
from: String,
#[arg(short, long)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
RemoveExpired {
#[arg(long)]
address: String,
#[arg(long)]
proposal_id: u32,
#[arg(long)]
from: String,
#[arg(short, long)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
ClaimDeposits {
#[arg(long)]
address: String,
#[arg(long)]
from: String,
#[arg(short, long)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
Dissolve {
#[arg(long)]
address: String,
#[arg(long)]
from: String,
#[arg(short, long)]
password: Option<String>,
#[arg(long)]
password_file: Option<String>,
},
Info {
#[arg(long)]
address: String,
#[arg(long)]
proposal_id: Option<u32>,
},
ListProposals {
#[arg(long)]
address: String,
},
#[command(subcommand)]
HighSecurity(HighSecuritySubcommands),
}
#[derive(Subcommand, Debug)]
pub enum HighSecuritySubcommands {
Status {
#[arg(long)]
address: String,
},
}
#[allow(dead_code)]
pub fn predict_multisig_address(
signers: Vec<subxt::ext::subxt_core::utils::AccountId32>,
threshold: u32,
nonce: u64,
) -> String {
use codec::Encode;
const PALLET_ID: [u8; 8] = *b"py/mltsg";
use sp_core::crypto::AccountId32 as SpAccountId32;
let sp_signers: Vec<SpAccountId32> = signers
.iter()
.map(|s| {
let bytes: [u8; 32] = *s.as_ref();
SpAccountId32::from(bytes)
})
.collect();
let mut sorted_signers = sp_signers;
sorted_signers.sort();
let mut data = Vec::new();
data.extend_from_slice(&PALLET_ID);
data.extend_from_slice(&sorted_signers.encode());
data.extend_from_slice(&threshold.encode());
data.extend_from_slice(&nonce.encode());
use codec::Decode;
use sp_core::crypto::AccountId32;
use sp_runtime::traits::{BlakeTwo256, Hash as HashT, TrailingZeroInput};
let hash = BlakeTwo256::hash(&data);
let account_id = AccountId32::decode(&mut TrailingZeroInput::new(hash.as_ref()))
.expect("TrailingZeroInput provides sufficient bytes; qed");
account_id.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189))
}
#[allow(dead_code)]
pub async fn create_multisig(
quantus_client: &crate::chain::client::QuantusClient,
creator_keypair: &crate::wallet::QuantumKeyPair,
signers: Vec<subxt::ext::subxt_core::utils::AccountId32>,
threshold: u32,
nonce: u64,
wait_for_inclusion: bool,
) -> crate::error::Result<(subxt::utils::H256, Option<String>)> {
let create_tx =
quantus_subxt::api::tx()
.multisig()
.create_multisig(signers.clone(), threshold, nonce);
let execution_mode =
ExecutionMode { finalized: false, wait_for_transaction: wait_for_inclusion };
let tx_hash = crate::cli::common::submit_transaction(
quantus_client,
creator_keypair,
create_tx,
None,
execution_mode,
)
.await?;
let multisig_address = if wait_for_inclusion {
let latest_block_hash = quantus_client.get_latest_block().await?;
let events = quantus_client.client().events().at(latest_block_hash).await?;
let mut multisig_events =
events.find::<quantus_subxt::api::multisig::events::MultisigCreated>();
let address: Option<String> = if let Some(Ok(ev)) = multisig_events.next() {
let addr_bytes: &[u8; 32] = ev.multisig_address.as_ref();
let addr = SpAccountId32::from(*addr_bytes);
Some(addr.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189)))
} else {
None
};
address
} else {
None
};
Ok((tx_hash, multisig_address))
}
#[allow(dead_code)]
pub async fn propose_transfer(
quantus_client: &crate::chain::client::QuantusClient,
proposer_keypair: &crate::wallet::QuantumKeyPair,
multisig_address: subxt::ext::subxt_core::utils::AccountId32,
to_address: subxt::ext::subxt_core::utils::AccountId32,
amount: u128,
expiry: u32,
) -> crate::error::Result<subxt::utils::H256> {
use codec::{Compact, Encode};
let pallet_index = 5u8; let call_index = 0u8;
let mut call_data = Vec::new();
call_data.push(pallet_index);
call_data.push(call_index);
call_data.push(0u8); call_data.extend_from_slice(to_address.as_ref());
Compact(amount).encode_to(&mut call_data);
let propose_tx =
quantus_subxt::api::tx().multisig().propose(multisig_address, call_data, expiry);
let execution_mode = ExecutionMode { finalized: false, wait_for_transaction: false };
let tx_hash = crate::cli::common::submit_transaction(
quantus_client,
proposer_keypair,
propose_tx,
None,
execution_mode,
)
.await?;
Ok(tx_hash)
}
#[allow(dead_code)]
pub async fn propose_custom(
quantus_client: &crate::chain::client::QuantusClient,
proposer_keypair: &crate::wallet::QuantumKeyPair,
multisig_address: subxt::ext::subxt_core::utils::AccountId32,
call_data: Vec<u8>,
expiry: u32,
) -> crate::error::Result<subxt::utils::H256> {
let propose_tx =
quantus_subxt::api::tx().multisig().propose(multisig_address, call_data, expiry);
let execution_mode = ExecutionMode { finalized: false, wait_for_transaction: false };
let tx_hash = crate::cli::common::submit_transaction(
quantus_client,
proposer_keypair,
propose_tx,
None,
execution_mode,
)
.await?;
Ok(tx_hash)
}
#[allow(dead_code)]
pub async fn approve_proposal(
quantus_client: &crate::chain::client::QuantusClient,
approver_keypair: &crate::wallet::QuantumKeyPair,
multisig_address: subxt::ext::subxt_core::utils::AccountId32,
proposal_id: u32,
) -> crate::error::Result<subxt::utils::H256> {
let approve_tx = quantus_subxt::api::tx().multisig().approve(multisig_address, proposal_id);
let execution_mode = ExecutionMode { finalized: false, wait_for_transaction: false };
let tx_hash = crate::cli::common::submit_transaction(
quantus_client,
approver_keypair,
approve_tx,
None,
execution_mode,
)
.await?;
Ok(tx_hash)
}
#[allow(dead_code)]
pub async fn cancel_proposal(
quantus_client: &crate::chain::client::QuantusClient,
proposer_keypair: &crate::wallet::QuantumKeyPair,
multisig_address: subxt::ext::subxt_core::utils::AccountId32,
proposal_id: u32,
) -> crate::error::Result<subxt::utils::H256> {
let cancel_tx = quantus_subxt::api::tx().multisig().cancel(multisig_address, proposal_id);
let execution_mode = ExecutionMode { finalized: false, wait_for_transaction: false };
let tx_hash = crate::cli::common::submit_transaction(
quantus_client,
proposer_keypair,
cancel_tx,
None,
execution_mode,
)
.await?;
Ok(tx_hash)
}
#[allow(dead_code)]
pub async fn get_multisig_info(
quantus_client: &crate::chain::client::QuantusClient,
multisig_address: subxt::ext::subxt_core::utils::AccountId32,
) -> crate::error::Result<Option<MultisigInfo>> {
let latest_block_hash = quantus_client.get_latest_block().await?;
let storage_at = quantus_client.client().storage().at(latest_block_hash);
let storage_query =
quantus_subxt::api::storage().multisig().multisigs(multisig_address.clone());
let multisig_data = storage_at.fetch(&storage_query).await?;
if let Some(data) = multisig_data {
let balance_query =
quantus_subxt::api::storage().system().account(multisig_address.clone());
let account_info = storage_at.fetch(&balance_query).await?;
let balance = account_info.map(|info| info.data.free).unwrap_or(0);
let multisig_bytes: &[u8; 32] = multisig_address.as_ref();
let multisig_sp = SpAccountId32::from(*multisig_bytes);
let address =
multisig_sp.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
let creator_bytes: &[u8; 32] = data.creator.as_ref();
let creator_sp = SpAccountId32::from(*creator_bytes);
let creator =
creator_sp.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
let signers: Vec<String> = data
.signers
.0
.iter()
.map(|signer| {
let signer_bytes: &[u8; 32] = signer.as_ref();
let signer_sp = SpAccountId32::from(*signer_bytes);
signer_sp.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189))
})
.collect();
Ok(Some(MultisigInfo {
address,
creator,
balance,
threshold: data.threshold,
signers,
proposal_nonce: data.proposal_nonce,
deposit: data.deposit,
active_proposals: data.active_proposals,
}))
} else {
Ok(None)
}
}
#[allow(dead_code)]
pub async fn get_proposal_info(
quantus_client: &crate::chain::client::QuantusClient,
multisig_address: subxt::ext::subxt_core::utils::AccountId32,
proposal_id: u32,
) -> crate::error::Result<Option<ProposalInfo>> {
let latest_block_hash = quantus_client.get_latest_block().await?;
let storage_at = quantus_client.client().storage().at(latest_block_hash);
let storage_query = quantus_subxt::api::storage()
.multisig()
.proposals(multisig_address, proposal_id);
let proposal_data = storage_at.fetch(&storage_query).await?;
if let Some(data) = proposal_data {
let proposer_bytes: &[u8; 32] = data.proposer.as_ref();
let proposer_sp = SpAccountId32::from(*proposer_bytes);
let proposer =
proposer_sp.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
let approvals: Vec<String> = data
.approvals
.0
.iter()
.map(|approver| {
let approver_bytes: &[u8; 32] = approver.as_ref();
let approver_sp = SpAccountId32::from(*approver_bytes);
approver_sp
.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189))
})
.collect();
let status = match data.status {
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Active =>
ProposalStatus::Active,
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Approved =>
ProposalStatus::Approved,
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Executed =>
ProposalStatus::Executed,
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Cancelled =>
ProposalStatus::Cancelled,
};
Ok(Some(ProposalInfo {
id: proposal_id,
proposer,
call_data: data.call.0,
expiry: data.expiry,
approvals,
deposit: data.deposit,
status,
}))
} else {
Ok(None)
}
}
#[allow(dead_code)]
pub async fn list_proposals(
quantus_client: &crate::chain::client::QuantusClient,
multisig_address: subxt::ext::subxt_core::utils::AccountId32,
) -> crate::error::Result<Vec<ProposalInfo>> {
let latest_block_hash = quantus_client.get_latest_block().await?;
let storage = quantus_client.client().storage().at(latest_block_hash);
let address = quantus_subxt::api::storage()
.multisig()
.proposals_iter1(multisig_address.clone());
let mut proposals_iter = storage.iter(address).await?;
let mut proposals = Vec::new();
while let Some(result) = proposals_iter.next().await {
if let Ok(kv) = result {
let key_bytes = kv.key_bytes;
if key_bytes.len() >= 4 {
let id_bytes = &key_bytes[key_bytes.len() - 4..];
let proposal_id =
u32::from_le_bytes([id_bytes[0], id_bytes[1], id_bytes[2], id_bytes[3]]);
let data = kv.value;
let proposer_bytes: &[u8; 32] = data.proposer.as_ref();
let proposer_sp = SpAccountId32::from(*proposer_bytes);
let proposer = proposer_sp
.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
let approvals: Vec<String> = data
.approvals
.0
.iter()
.map(|approver| {
let approver_bytes: &[u8; 32] = approver.as_ref();
let approver_sp = SpAccountId32::from(*approver_bytes);
approver_sp.to_ss58check_with_version(
sp_core::crypto::Ss58AddressFormat::custom(189),
)
})
.collect();
let status = match data.status {
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Active =>
ProposalStatus::Active,
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Approved =>
ProposalStatus::Approved,
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Executed =>
ProposalStatus::Executed,
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Cancelled =>
ProposalStatus::Cancelled,
};
proposals.push(ProposalInfo {
id: proposal_id,
proposer,
call_data: data.call.0,
expiry: data.expiry,
approvals,
deposit: data.deposit,
status,
});
}
}
}
Ok(proposals)
}
#[allow(dead_code)]
pub async fn approve_dissolve_multisig(
quantus_client: &crate::chain::client::QuantusClient,
caller_keypair: &crate::wallet::QuantumKeyPair,
multisig_address: subxt::ext::subxt_core::utils::AccountId32,
) -> crate::error::Result<subxt::utils::H256> {
let approve_tx = quantus_subxt::api::tx().multisig().approve_dissolve(multisig_address);
let execution_mode = ExecutionMode { finalized: false, wait_for_transaction: false };
let tx_hash = crate::cli::common::submit_transaction(
quantus_client,
caller_keypair,
approve_tx,
None,
execution_mode,
)
.await?;
Ok(tx_hash)
}
pub async fn handle_multisig_command(
command: MultisigCommands,
node_url: &str,
execution_mode: ExecutionMode,
) -> crate::error::Result<()> {
match command {
MultisigCommands::Create { signers, threshold, nonce, from, password, password_file } =>
handle_create_multisig(
signers,
threshold,
nonce,
from,
password,
password_file,
node_url,
execution_mode,
)
.await,
MultisigCommands::PredictAddress { signers, threshold, nonce } =>
handle_predict_address(signers, threshold, nonce).await,
MultisigCommands::Propose(subcommand) => match subcommand {
ProposeSubcommand::Transfer {
address,
to,
amount,
expiry,
from,
password,
password_file,
} =>
handle_propose_transfer(
address,
to,
amount,
expiry,
from,
password,
password_file,
node_url,
execution_mode,
)
.await,
ProposeSubcommand::Custom {
address,
pallet,
call,
args,
expiry,
from,
password,
password_file,
} =>
handle_propose(
address,
pallet,
call,
args,
expiry,
from,
password,
password_file,
node_url,
execution_mode,
)
.await,
ProposeSubcommand::HighSecurity {
address,
interceptor,
delay_blocks,
delay_seconds,
expiry,
from,
password,
password_file,
} =>
handle_high_security_set(
address,
interceptor,
delay_blocks,
delay_seconds,
expiry,
from,
password,
password_file,
node_url,
execution_mode,
)
.await,
},
MultisigCommands::Approve { address, proposal_id, from, password, password_file } =>
handle_approve(
address,
proposal_id,
from,
password,
password_file,
node_url,
execution_mode,
)
.await,
MultisigCommands::Execute { address, proposal_id, from, password, password_file } =>
handle_execute(
address,
proposal_id,
from,
password,
password_file,
node_url,
execution_mode,
)
.await,
MultisigCommands::Cancel { address, proposal_id, from, password, password_file } =>
handle_cancel(
address,
proposal_id,
from,
password,
password_file,
node_url,
execution_mode,
)
.await,
MultisigCommands::RemoveExpired { address, proposal_id, from, password, password_file } =>
handle_remove_expired(
address,
proposal_id,
from,
password,
password_file,
node_url,
execution_mode,
)
.await,
MultisigCommands::ClaimDeposits { address, from, password, password_file } =>
handle_claim_deposits(address, from, password, password_file, node_url, execution_mode)
.await,
MultisigCommands::Dissolve { address, from, password, password_file } =>
handle_dissolve(address, from, password, password_file, node_url, execution_mode).await,
MultisigCommands::Info { address, proposal_id } =>
handle_info(address, proposal_id, node_url).await,
MultisigCommands::ListProposals { address } =>
handle_list_proposals(address, node_url).await,
MultisigCommands::HighSecurity(subcommand) => match subcommand {
HighSecuritySubcommands::Status { address } =>
handle_high_security_status(address, node_url).await,
},
}
}
async fn handle_create_multisig(
signers: String,
threshold: u32,
nonce: u64,
from: String,
password: Option<String>,
password_file: Option<String>,
node_url: &str,
execution_mode: ExecutionMode,
) -> crate::error::Result<()> {
log_print!("๐ {} Creating multisig...", "MULTISIG".bright_magenta().bold());
let signer_addresses: Vec<subxt::ext::subxt_core::utils::AccountId32> = signers
.split(',')
.map(|s| s.trim())
.map(|addr| {
let ss58_str = crate::cli::common::resolve_address(addr)?;
let (account_id, _) =
SpAccountId32::from_ss58check_with_version(&ss58_str).map_err(|e| {
crate::error::QuantusError::Generic(format!(
"Invalid address '{}': {:?}",
addr, e
))
})?;
let bytes: [u8; 32] = *account_id.as_ref();
Ok(subxt::ext::subxt_core::utils::AccountId32::from(bytes))
})
.collect::<Result<Vec<_>, crate::error::QuantusError>>()?;
log_verbose!("Signers: {} addresses", signer_addresses.len());
log_verbose!("Threshold: {}", threshold);
log_verbose!("Nonce: {}", nonce);
if signer_addresses.is_empty() {
log_error!("โ At least one signer is required");
return Err(crate::error::QuantusError::Generic("No signers provided".to_string()));
}
if threshold == 0 {
log_error!("โ Threshold must be greater than zero");
return Err(crate::error::QuantusError::Generic("Invalid threshold".to_string()));
}
if threshold > signer_addresses.len() as u32 {
log_error!("โ Threshold cannot exceed number of signers");
return Err(crate::error::QuantusError::Generic("Threshold too high".to_string()));
}
let predicted_address = predict_multisig_address(signer_addresses.clone(), threshold, nonce);
log_print!("");
log_print!("๐ {} Predicted multisig address:", "DETERMINISTIC".bright_green().bold());
log_print!(" {}", predicted_address.bright_cyan().bold());
log_print!("");
let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let create_tx = quantus_subxt::api::tx().multisig().create_multisig(
signer_addresses.clone(),
threshold,
nonce,
);
let create_execution_mode = ExecutionMode {
finalized: execution_mode.finalized,
wait_for_transaction: true, };
let _tx_hash = crate::cli::common::submit_transaction(
&quantus_client,
&keypair,
create_tx,
None,
create_execution_mode,
)
.await?;
log_success!("โ
Multisig creation transaction confirmed");
{
log_print!("");
log_print!("๐ Looking for MultisigCreated event...");
let latest_block_hash = quantus_client.get_latest_block().await?;
let events = quantus_client.client().events().at(latest_block_hash).await?;
let multisig_events =
events.find::<quantus_subxt::api::multisig::events::MultisigCreated>();
let mut actual_address: Option<String> = None;
for event_result in multisig_events {
match event_result {
Ok(ev) => {
let addr_bytes: &[u8; 32] = ev.multisig_address.as_ref();
let addr = SpAccountId32::from(*addr_bytes);
actual_address = Some(addr.to_ss58check_with_version(
sp_core::crypto::Ss58AddressFormat::custom(189),
));
log_verbose!("Found MultisigCreated event");
break;
},
Err(e) => {
log_verbose!("Error parsing event: {:?}", e);
},
}
}
if let Some(address) = actual_address {
log_print!("");
log_success!("โ
Confirmed multisig address: {}", address.bright_cyan().bold());
if address == predicted_address {
log_print!(" {} Matches predicted address!", "โ".bright_green().bold());
} else {
log_verbose!(
"Predicted address differed from emitted address: predicted={}, emitted={}",
predicted_address,
address
);
}
log_print!("");
log_print!(
"๐ก {} You can now use this address to propose transactions",
"TIP".bright_blue().bold()
);
log_print!(
" Example: quantus multisig propose transfer --address {} --to recipient --amount 100",
address.bright_cyan()
);
} else {
log_error!("โ ๏ธ Couldn't find MultisigCreated event");
log_print!(" Check events manually: quantus events --latest --pallet Multisig");
}
}
log_print!("");
Ok(())
}
async fn handle_predict_address(
signers: String,
threshold: u32,
nonce: u64,
) -> crate::error::Result<()> {
log_print!("๐ฎ {} Predicting multisig address...", "PREDICT".bright_cyan().bold());
log_print!("");
let signer_addresses: Vec<subxt::ext::subxt_core::utils::AccountId32> = signers
.split(',')
.map(|s| s.trim())
.map(|addr| {
let ss58_str = crate::cli::common::resolve_address(addr)?;
let (account_id, _) =
SpAccountId32::from_ss58check_with_version(&ss58_str).map_err(|e| {
crate::error::QuantusError::Generic(format!(
"Invalid address '{}': {:?}",
addr, e
))
})?;
let bytes: [u8; 32] = *account_id.as_ref();
Ok(subxt::ext::subxt_core::utils::AccountId32::from(bytes))
})
.collect::<Result<Vec<_>, crate::error::QuantusError>>()?;
if signer_addresses.is_empty() {
log_error!("โ At least one signer is required");
return Err(crate::error::QuantusError::Generic("No signers provided".to_string()));
}
if threshold == 0 {
log_error!("โ Threshold must be greater than zero");
return Err(crate::error::QuantusError::Generic("Invalid threshold".to_string()));
}
if threshold > signer_addresses.len() as u32 {
log_error!("โ Threshold cannot exceed number of signers");
return Err(crate::error::QuantusError::Generic("Threshold too high".to_string()));
}
let predicted_address = predict_multisig_address(signer_addresses.clone(), threshold, nonce);
log_print!("๐ {} Predicted multisig address:", "RESULT".bright_green().bold());
log_print!(" {}", predicted_address.bright_cyan().bold());
log_print!("");
log_print!("โ๏ธ {} Configuration:", "PARAMS".bright_blue().bold());
log_print!(" Signers: {}", signer_addresses.len());
log_print!(" Threshold: {}", threshold);
log_print!(" Nonce: {}", nonce);
log_print!("");
log_print!("๐ก {} This address is deterministic:", "INFO".bright_yellow().bold());
log_print!(" - Same signers + threshold + nonce = same address");
log_print!(" - Order of signers doesn't matter (automatically sorted)");
log_print!(" - Use different nonce to create multiple multisigs with same signers");
log_print!("");
log_print!("๐ {} To create this multisig, run:", "NEXT".bright_magenta().bold());
log_print!(
" quantus multisig create --signers \"{}\" --threshold {} --nonce {} --from <wallet>",
signers,
threshold,
nonce
);
log_print!("");
Ok(())
}
async fn handle_propose_transfer(
multisig_address: String,
to: String,
amount: String,
expiry: Option<u32>,
from: String,
password: Option<String>,
password_file: Option<String>,
node_url: &str,
execution_mode: ExecutionMode,
) -> crate::error::Result<()> {
log_print!("๐ {} Creating transfer proposal...", "MULTISIG".bright_magenta().bold());
let to_address = crate::cli::common::resolve_address(&to)?;
let amount_u128: u128 = parse_amount(&amount)?;
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_account_id = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let latest_block_hash = quantus_client.get_latest_block().await?;
let latest_block = quantus_client.client().blocks().at(latest_block_hash).await?;
let current_block_number = latest_block.number();
let storage_at = quantus_client.client().storage().at(latest_block_hash);
let expiry = match expiry {
Some(expiry) => expiry,
None => {
let default_expiry =
current_block_number.saturating_add(DEFAULT_TRANSFER_EXPIRY_BLOCKS);
log_print!(
"โฑ๏ธ {} No --expiry provided; using block {} (head {} + {} blocks, ~2h)",
"DEFAULT".bright_blue().bold(),
default_expiry,
current_block_number,
DEFAULT_TRANSFER_EXPIRY_BLOCKS
);
default_expiry
},
};
let hs_query = quantus_subxt::api::storage()
.reversible_transfers()
.high_security_accounts(multisig_account_id);
let is_high_security = storage_at.fetch(&hs_query).await?.is_some();
if is_high_security {
log_print!(
"๐ก๏ธ {} High-Security detected: using delayed transfer (ReversibleTransfers::schedule_transfer)",
"HS".bright_green().bold()
);
let (to_id, _) = SpAccountId32::from_ss58check_with_version(&to_address).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid recipient address: {:?}", e))
})?;
let to_bytes: [u8; 32] = *to_id.as_ref();
let to_account_id = subxt::ext::subxt_core::utils::AccountId32::from(to_bytes);
let dest = subxt::utils::MultiAddress::Id(to_account_id);
let schedule_call = quantus_subxt::api::tx()
.reversible_transfers()
.schedule_transfer(dest, amount_u128);
use subxt::tx::Payload;
let call_data = schedule_call
.encode_call_data(&quantus_client.client().metadata())
.map_err(|e| {
crate::error::QuantusError::Generic(format!("Failed to encode call: {:?}", e))
})?;
handle_propose_with_call_data(
multisig_address,
call_data,
expiry,
from,
password,
password_file,
&quantus_client,
execution_mode,
)
.await
} else {
let args_json = serde_json::to_string(&vec![
serde_json::Value::String(to_address),
serde_json::Value::String(amount_u128.to_string()),
])
.map_err(|e| {
crate::error::QuantusError::Generic(format!("Failed to serialize args: {}", e))
})?;
handle_propose(
multisig_address,
"Balances".to_string(),
"transfer_allow_death".to_string(),
Some(args_json),
expiry,
from,
password,
password_file,
node_url,
execution_mode,
)
.await
}
}
async fn fetch_proposal_id(
quantus_client: &crate::chain::client::QuantusClient,
multisig_ss58: &str,
) -> Option<u32> {
let latest_block_hash = quantus_client.get_latest_block().await.ok()?;
let events = quantus_client.client().events().at(latest_block_hash).await.ok()?;
for created in events.find::<quantus_subxt::api::multisig::events::ProposalCreated>().flatten()
{
let addr_bytes: &[u8; 32] = created.multisig_address.as_ref();
let addr = SpAccountId32::from(*addr_bytes);
let addr_ss58 =
addr.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
if addr_ss58 == multisig_ss58 {
return Some(created.proposal_id);
}
}
None
}
fn log_proposal_result(multisig_ss58: &str, proposal_id: Option<u32>) {
if let Some(id) = proposal_id {
log_print!("");
log_success!("โ
Proposal #{} confirmed on-chain", id.to_string().bright_cyan().bold());
log_print!("");
log_print!("๐ {} To approve this proposal, signers run:", "NEXT".bright_blue().bold());
log_print!(
" quantus multisig approve --address {} --proposal-id {} --from <SIGNER_WALLET>",
multisig_ss58.bright_cyan(),
id
);
} else {
log_success!("โ
Proposal confirmed on-chain");
log_print!(
" Run `quantus multisig list-proposals --address {}` to find the proposal ID",
multisig_ss58
);
}
}
async fn handle_propose(
multisig_address: String,
pallet: String,
call: String,
args: Option<String>,
expiry: u32,
from: String,
password: Option<String>,
password_file: Option<String>,
node_url: &str,
execution_mode: ExecutionMode,
) -> crate::error::Result<()> {
log_print!("๐ {} Creating proposal...", "MULTISIG".bright_magenta().bold());
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
let args_vec: Vec<serde_json::Value> = if let Some(args_str) = args {
serde_json::from_str(&args_str).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid JSON for arguments: {}", e))
})?
} else {
vec![]
};
log_verbose!("Multisig: {}", multisig_ss58);
log_verbose!("Call: {}::{}", pallet, call);
log_verbose!("Expiry: block {}", expiry);
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let latest_block_hash = quantus_client.get_latest_block().await?;
let latest_block = quantus_client.client().blocks().at(latest_block_hash).await?;
let current_block_number = latest_block.number();
if expiry <= current_block_number {
log_error!(
"โ Expiry block {} is in the past (current block: {})",
expiry,
current_block_number
);
log_print!(" Use a higher block number, e.g., --expiry {}", current_block_number + 1000);
return Err(crate::error::QuantusError::Generic("Expiry must be in the future".to_string()));
}
log_verbose!("Current block: {}, expiry valid", current_block_number);
let storage_at = quantus_client.client().storage().at(latest_block_hash);
let multisig_query =
quantus_subxt::api::storage().multisig().multisigs(multisig_address.clone());
let multisig_data = storage_at.fetch(&multisig_query).await?.ok_or_else(|| {
crate::error::QuantusError::Generic(format!(
"Multisig not found at address: {}",
multisig_ss58
))
})?;
let proposer_ss58 = crate::cli::common::resolve_address(&from)?;
let (proposer_id, _) =
SpAccountId32::from_ss58check_with_version(&proposer_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid proposer address: {:?}", e))
})?;
let proposer_bytes: [u8; 32] = *proposer_id.as_ref();
let proposer_account_id = subxt::ext::subxt_core::utils::AccountId32::from(proposer_bytes);
if !multisig_data.signers.0.contains(&proposer_account_id) {
log_error!("โ Not authorized: {} is not a signer of this multisig", proposer_ss58);
return Err(crate::error::QuantusError::Generic(
"Only multisig signers can create proposals".to_string(),
));
}
let mut expired_count = 0;
let proposals_query = quantus_subxt::api::storage().multisig().proposals_iter();
let mut proposals_stream = storage_at.iter(proposals_query).await?;
while let Some(Ok(kv)) = proposals_stream.next().await {
let proposal = kv.value;
if proposal.proposer == proposer_account_id {
if proposal.expiry <= current_block_number {
expired_count += 1;
}
}
}
if expired_count > 0 {
log_print!("");
log_print!(
"๐งน {} Auto-cleanup: Runtime will remove your {} expired proposal(s)",
"INFO".bright_blue().bold(),
expired_count.to_string().bright_yellow()
);
log_print!(" This happens automatically before creating the new proposal");
log_print!("");
}
let call_data = build_runtime_call(&quantus_client, &pallet, &call, args_vec).await?;
log_verbose!("Call data size: {} bytes", call_data.len());
let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
let propose_tx =
quantus_subxt::api::tx()
.multisig()
.propose(multisig_address.clone(), call_data, expiry);
let propose_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
crate::cli::common::submit_transaction(
&quantus_client,
&keypair,
propose_tx,
None,
propose_execution_mode,
)
.await?;
let proposal_id = fetch_proposal_id(&quantus_client, &multisig_ss58).await;
log_proposal_result(&multisig_ss58, proposal_id);
Ok(())
}
async fn handle_propose_with_call_data(
multisig_address: String,
call_data: Vec<u8>,
expiry: u32,
from: String,
password: Option<String>,
password_file: Option<String>,
quantus_client: &crate::chain::client::QuantusClient,
execution_mode: ExecutionMode,
) -> crate::error::Result<()> {
log_print!("๐ {} Creating proposal...", "MULTISIG".bright_magenta().bold());
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_account_id = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
let latest_block_hash = quantus_client.get_latest_block().await?;
let latest_block = quantus_client.client().blocks().at(latest_block_hash).await?;
let current_block_number = latest_block.number();
if expiry <= current_block_number {
log_error!(
"โ Expiry block {} is in the past (current block: {})",
expiry,
current_block_number
);
log_print!(" Use a higher block number, e.g., --expiry {}", current_block_number + 1000);
return Err(crate::error::QuantusError::Generic("Expiry must be in the future".to_string()));
}
log_verbose!("Current block: {}, expiry valid", current_block_number);
log_verbose!("Call data size: {} bytes", call_data.len());
let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
let propose_tx =
quantus_subxt::api::tx()
.multisig()
.propose(multisig_account_id, call_data, expiry);
let propose_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
crate::cli::common::submit_transaction(
quantus_client,
&keypair,
propose_tx,
None,
propose_execution_mode,
)
.await?;
let proposal_id = fetch_proposal_id(quantus_client, &multisig_ss58).await;
log_proposal_result(&multisig_ss58, proposal_id);
Ok(())
}
async fn handle_approve(
multisig_address: String,
proposal_id: u32,
from: String,
password: Option<String>,
password_file: Option<String>,
node_url: &str,
execution_mode: ExecutionMode,
) -> crate::error::Result<()> {
log_print!("โ
{} Approving proposal...", "MULTISIG".bright_magenta().bold());
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
log_verbose!("Multisig: {}", multisig_ss58);
log_verbose!("Proposal ID: {}", proposal_id);
let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let latest_block_hash = quantus_client.get_latest_block().await?;
let storage_at = quantus_client.client().storage().at(latest_block_hash);
let multisig_query =
quantus_subxt::api::storage().multisig().multisigs(multisig_address.clone());
let multisig_data = storage_at.fetch(&multisig_query).await?.ok_or_else(|| {
crate::error::QuantusError::Generic(format!(
"Multisig not found at address: {}",
multisig_ss58
))
})?;
let approver_ss58 = crate::cli::common::resolve_address(&from)?;
let (approver_id, _) =
SpAccountId32::from_ss58check_with_version(&approver_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid approver address: {:?}", e))
})?;
let approver_bytes: [u8; 32] = *approver_id.as_ref();
let approver_account_id = subxt::ext::subxt_core::utils::AccountId32::from(approver_bytes);
if !multisig_data.signers.0.contains(&approver_account_id) {
log_error!("โ Not authorized: {} is not a signer of this multisig", approver_ss58);
return Err(crate::error::QuantusError::Generic(
"Only multisig signers can approve proposals".to_string(),
));
}
let proposal_query = quantus_subxt::api::storage()
.multisig()
.proposals(multisig_address.clone(), proposal_id);
let proposal_data = storage_at.fetch(&proposal_query).await?;
if proposal_data.is_none() {
log_error!("โ Proposal {} not found", proposal_id);
return Err(crate::error::QuantusError::Generic(format!(
"Proposal {} does not exist",
proposal_id
)));
}
let proposal = proposal_data.unwrap();
if proposal.approvals.0.contains(&approver_account_id) {
log_error!("โ Already approved: you have already approved this proposal");
return Err(crate::error::QuantusError::Generic(
"You have already approved this proposal".to_string(),
));
}
let approve_tx = quantus_subxt::api::tx().multisig().approve(multisig_address, proposal_id);
let approve_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
crate::cli::common::submit_transaction(
&quantus_client,
&keypair,
approve_tx,
None,
approve_execution_mode,
)
.await?;
log_success!("โ
Approval confirmed on-chain");
log_print!(
" If threshold is reached, the proposal becomes Approved. Any signer can run: quantus multisig execute --address {} --proposal-id {} --from <signer>",
multisig_ss58,
proposal_id
);
Ok(())
}
async fn handle_execute(
multisig_address: String,
proposal_id: u32,
from: String,
password: Option<String>,
password_file: Option<String>,
node_url: &str,
execution_mode: ExecutionMode,
) -> crate::error::Result<()> {
log_print!("โถ๏ธ {} Executing proposal...", "MULTISIG".bright_magenta().bold());
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_account_id = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
log_verbose!("Multisig: {}", multisig_ss58);
log_verbose!("Proposal ID: {}", proposal_id);
let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let latest_block_hash = quantus_client.get_latest_block().await?;
let storage_at = quantus_client.client().storage().at(latest_block_hash);
let multisig_query =
quantus_subxt::api::storage().multisig().multisigs(multisig_account_id.clone());
let multisig_data = storage_at.fetch(&multisig_query).await?.ok_or_else(|| {
crate::error::QuantusError::Generic(format!(
"Multisig not found at address: {}",
multisig_ss58
))
})?;
let executor_ss58 = crate::cli::common::resolve_address(&from)?;
let (executor_id, _) =
SpAccountId32::from_ss58check_with_version(&executor_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid executor address: {:?}", e))
})?;
let executor_bytes: [u8; 32] = *executor_id.as_ref();
let executor_account_id = subxt::ext::subxt_core::utils::AccountId32::from(executor_bytes);
if !multisig_data.signers.0.contains(&executor_account_id) {
log_error!("โ Not authorized: {} is not a signer of this multisig", executor_ss58);
return Err(crate::error::QuantusError::Generic(
"Only multisig signers can execute proposals".to_string(),
));
}
let execute_tx = quantus_subxt::api::tx().multisig().execute(multisig_account_id, proposal_id);
let exec_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
crate::cli::common::submit_transaction(
&quantus_client,
&keypair,
execute_tx,
None,
exec_execution_mode,
)
.await?;
log_success!("โ
Proposal executed on-chain");
Ok(())
}
async fn handle_cancel(
multisig_address: String,
proposal_id: u32,
from: String,
password: Option<String>,
password_file: Option<String>,
node_url: &str,
execution_mode: ExecutionMode,
) -> crate::error::Result<()> {
log_print!("๐ซ {} Cancelling proposal...", "MULTISIG".bright_magenta().bold());
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
log_verbose!("Proposal ID: {}", proposal_id);
let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let latest_block_hash = quantus_client.get_latest_block().await?;
let storage_at = quantus_client.client().storage().at(latest_block_hash);
let proposal_query = quantus_subxt::api::storage()
.multisig()
.proposals(multisig_address.clone(), proposal_id);
let proposal_data = storage_at.fetch(&proposal_query).await?.ok_or_else(|| {
crate::error::QuantusError::Generic(format!("Proposal {} not found", proposal_id))
})?;
let canceller_ss58 = crate::cli::common::resolve_address(&from)?;
let (canceller_id, _) =
SpAccountId32::from_ss58check_with_version(&canceller_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid canceller address: {:?}", e))
})?;
let canceller_bytes: [u8; 32] = *canceller_id.as_ref();
let canceller_account_id = subxt::ext::subxt_core::utils::AccountId32::from(canceller_bytes);
if proposal_data.proposer != canceller_account_id {
log_error!("โ Not authorized: only the proposer can cancel this proposal");
let proposer_bytes: &[u8; 32] = proposal_data.proposer.as_ref();
let proposer_sp = SpAccountId32::from(*proposer_bytes);
let proposer_ss58 =
proposer_sp.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
log_print!(" Proposer: {}", proposer_ss58);
return Err(crate::error::QuantusError::Generic(
"Only the proposer can cancel their proposal".to_string(),
));
}
let cancel_tx = quantus_subxt::api::tx().multisig().cancel(multisig_address, proposal_id);
let cancel_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
crate::cli::common::submit_transaction(
&quantus_client,
&keypair,
cancel_tx,
None,
cancel_execution_mode,
)
.await?;
log_success!("โ
Proposal cancelled and removed (confirmed on-chain)");
log_print!(" Deposit returned to proposer");
Ok(())
}
async fn handle_remove_expired(
multisig_address: String,
proposal_id: u32,
from: String,
password: Option<String>,
password_file: Option<String>,
node_url: &str,
execution_mode: ExecutionMode,
) -> crate::error::Result<()> {
log_print!("๐งน {} Removing expired proposal...", "MULTISIG".bright_magenta().bold());
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
log_verbose!("Proposal ID: {}", proposal_id);
let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let remove_tx = quantus_subxt::api::tx()
.multisig()
.remove_expired(multisig_address, proposal_id);
let remove_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
crate::cli::common::submit_transaction(
&quantus_client,
&keypair,
remove_tx,
None,
remove_execution_mode,
)
.await?;
log_success!("โ
Expired proposal removed and deposit returned (confirmed on-chain)");
Ok(())
}
async fn handle_claim_deposits(
multisig_address: String,
from: String,
password: Option<String>,
password_file: Option<String>,
node_url: &str,
execution_mode: ExecutionMode,
) -> crate::error::Result<()> {
log_print!("๐ฐ {} Claiming deposits...", "MULTISIG".bright_magenta().bold());
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let claim_tx = quantus_subxt::api::tx().multisig().claim_deposits(multisig_address);
let claim_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
crate::cli::common::submit_transaction(
&quantus_client,
&keypair,
claim_tx,
None,
claim_execution_mode,
)
.await?;
log_success!("โ
Deposits claimed (confirmed on-chain)");
log_print!(" All removable proposals have been cleaned up");
Ok(())
}
async fn handle_dissolve(
multisig_address: String,
from: String,
password: Option<String>,
password_file: Option<String>,
node_url: &str,
execution_mode: ExecutionMode,
) -> crate::error::Result<()> {
log_print!("๐๏ธ {} Approving multisig dissolution...", "MULTISIG".bright_magenta().bold());
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_address_id = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let latest_block_hash = quantus_client.get_latest_block().await?;
let storage_at = quantus_client.client().storage().at(latest_block_hash);
let multisig_query =
quantus_subxt::api::storage().multisig().multisigs(multisig_address_id.clone());
let multisig_info = storage_at.fetch(&multisig_query).await?;
let approve_tx = quantus_subxt::api::tx()
.multisig()
.approve_dissolve(multisig_address_id.clone());
let dissolve_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
crate::cli::common::submit_transaction(
&quantus_client,
&keypair,
approve_tx,
None,
dissolve_execution_mode,
)
.await?;
log_success!("โ
Dissolution approval confirmed on-chain");
if let Some(info) = multisig_info {
let creator_bytes: &[u8; 32] = info.creator.as_ref();
let creator_sp = SpAccountId32::from(*creator_bytes);
let creator_ss58 = creator_sp.to_ss58check();
log_print!(" Requires {} total approvals to dissolve", info.threshold);
log_print!("");
log_print!("๐ก {} When threshold is reached:", "INFO".bright_blue().bold());
log_print!(" - Multisig will be dissolved automatically");
log_print!(" - Deposit ({}) will be RETURNED to creator", format_balance(info.deposit));
log_print!(" - Creator: {}", creator_ss58.bright_cyan());
log_print!(" - Storage will be removed");
}
Ok(())
}
async fn handle_info(
multisig_address: String,
proposal_id: Option<u32>,
node_url: &str,
) -> crate::error::Result<()> {
if let Some(id) = proposal_id {
return handle_proposal_info(multisig_address, id, node_url).await;
}
log_print!("๐ {} Querying multisig info...", "MULTISIG".bright_magenta().bold());
log_print!("");
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
crate::log_verbose!("๐ Querying multisig with address: {}", multisig_ss58);
crate::log_verbose!("๐ Address bytes: {}", hex::encode(multisig_bytes));
let latest_block_hash = quantus_client.get_latest_block().await?;
crate::log_verbose!("๐ฆ Latest block hash: {:?}", latest_block_hash);
let storage_query =
quantus_subxt::api::storage().multisig().multisigs(multisig_address.clone());
let storage_at = quantus_client.client().storage().at(latest_block_hash);
let multisig_data = storage_at.fetch(&storage_query).await?;
crate::log_verbose!(
"๐ Fetch result: {}",
if multisig_data.is_some() { "Found" } else { "Not found" }
);
match multisig_data {
Some(data) => {
let balance_query =
quantus_subxt::api::storage().system().account(multisig_address.clone());
let account_info = storage_at.fetch(&balance_query).await?;
let (balance, reserved, frozen) = account_info
.map(|info| (info.data.free, info.data.reserved, info.data.frozen))
.unwrap_or((0, 0, 0));
let creator_bytes: &[u8; 32] = data.creator.as_ref();
let creator_sp = SpAccountId32::from(*creator_bytes);
let creator_ss58 = creator_sp.to_ss58check();
log_print!("๐ {} Information:", "MULTISIG".bright_green().bold());
log_print!(" Address: {}", multisig_ss58.bright_cyan());
log_print!(" Creator: {} (receives deposit back)", creator_ss58.bright_cyan());
if reserved == 0 && frozen == 0 {
log_print!(" Balance: {}", format_balance(balance).bright_green().bold());
} else {
log_print!(" Balance: {} (free)", format_balance(balance).bright_green().bold());
if reserved > 0 {
log_print!(
" {} (reserved)",
format_balance(reserved).bright_yellow()
);
}
if frozen > 0 {
log_print!(" {} (frozen)", format_balance(frozen).bright_yellow());
}
}
log_print!(" Threshold: {}", data.threshold.to_string().bright_yellow());
log_print!(" Signers ({}):", data.signers.0.len().to_string().bright_yellow());
for (i, signer) in data.signers.0.iter().enumerate() {
let signer_bytes: &[u8; 32] = signer.as_ref();
let signer_sp = SpAccountId32::from(*signer_bytes);
log_print!(" {}. {}", i + 1, signer_sp.to_ss58check().bright_cyan());
}
log_print!(" Proposal Nonce: {}", data.proposal_nonce);
log_print!(
" Deposit: {} (returned to creator on dissolve)",
format_balance(data.deposit)
);
log_print!(
" Active Proposals: {}",
data.active_proposals.to_string().bright_yellow()
);
if data.active_proposals > 0 {
log_print!("");
log_print!("๐ {} Active Proposals:", "PROPOSALS".bright_magenta().bold());
let proposals_query = quantus_subxt::api::storage()
.multisig()
.proposals_iter1(multisig_address.clone());
let mut proposals_stream = storage_at.iter(proposals_query).await?;
while let Some(Ok(kv)) = proposals_stream.next().await {
let proposal = kv.value;
let proposal_id = if kv.key_bytes.len() >= 4 {
let id_bytes = &kv.key_bytes[kv.key_bytes.len() - 4..];
u32::from_le_bytes([id_bytes[0], id_bytes[1], id_bytes[2], id_bytes[3]])
} else {
0
};
let status = match proposal.status {
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Active =>
"Active".bright_green(),
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Approved =>
"Approved (ready to execute)".bright_yellow(),
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Executed =>
"Executed".bright_blue(),
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Cancelled =>
"Cancelled".bright_red(),
};
let call_name = if proposal.call.0.len() >= 2 {
let pallet_idx = proposal.call.0[0];
let call_idx = proposal.call.0[1];
let metadata = quantus_client.client().metadata();
if let Some(pallet) = metadata.pallet_by_index(pallet_idx) {
if let Some(variant) = pallet.call_variant_by_index(call_idx) {
format!("{}::{}", pallet.name(), variant.name)
} else {
format!("{}::call[{}]", pallet.name(), call_idx)
}
} else {
format!("pallet[{}]::call[{}]", pallet_idx, call_idx)
}
} else {
format!("({}B encoded)", proposal.call.0.len())
};
let proposer_bytes: &[u8; 32] = proposal.proposer.as_ref();
let proposer_sp = SpAccountId32::from(*proposer_bytes);
log_print!(
" #{}: {} | {} | Approvals: {} | Proposer: {}",
proposal_id,
call_name.bright_white(),
status,
proposal.approvals.0.len(),
proposer_sp.to_ss58check().dimmed()
);
}
}
let dissolve_query = quantus_subxt::api::storage()
.multisig()
.dissolve_approvals(multisig_address.clone());
if let Some(dissolve_approvals) = storage_at.fetch(&dissolve_query).await? {
log_print!("");
log_print!("๐๏ธ {} Dissolution in progress:", "DISSOLVE".bright_red().bold());
log_print!(
" Progress: {}/{}",
dissolve_approvals.0.len().to_string().bright_yellow(),
data.threshold.to_string().bright_yellow()
);
log_print!(" Approvals:");
for (i, approver) in dissolve_approvals.0.iter().enumerate() {
let approver_bytes: &[u8; 32] = approver.as_ref();
let approver_sp = SpAccountId32::from(*approver_bytes);
log_print!(" {}. {}", i + 1, approver_sp.to_ss58check().bright_cyan());
}
let pending_signers: Vec<_> =
data.signers.0.iter().filter(|s| !dissolve_approvals.0.contains(s)).collect();
if !pending_signers.is_empty() {
log_print!(" Pending:");
for (i, signer) in pending_signers.iter().enumerate() {
let signer_bytes: &[u8; 32] = signer.as_ref();
let signer_sp = SpAccountId32::from(*signer_bytes);
log_print!(" {}. {}", i + 1, signer_sp.to_ss58check().dimmed());
}
}
log_print!("");
log_print!(" โ ๏ธ {} When threshold is reached:", "WARNING".bright_red().bold());
log_print!(" - Multisig will be dissolved IMMEDIATELY");
log_print!(
" - Deposit will be RETURNED to creator: {}",
creator_ss58.bright_cyan()
);
} else {
log_print!("");
log_print!(
" ๐ก {} Deposit ({}) will be returned to creator on dissolve",
"INFO".bright_blue().bold(),
format_balance(data.deposit)
);
}
},
None => {
log_error!("โ Multisig not found at address: {}", multisig_ss58);
},
}
log_print!("");
Ok(())
}
async fn decode_call_data(
quantus_client: &crate::chain::client::QuantusClient,
call_data: &[u8],
) -> crate::error::Result<String> {
use codec::Decode;
if call_data.len() < 2 {
return Ok(format!(" {} {} bytes (too short)", "Call Size:".dimmed(), call_data.len()));
}
let pallet_index = call_data[0];
let call_index = call_data[1];
let args = &call_data[2..];
let metadata = quantus_client.client().metadata();
let pallet_name = metadata
.pallets()
.find(|p| p.index() == pallet_index)
.map(|p| p.name())
.unwrap_or("Unknown");
match (pallet_index, call_index) {
(_, idx) if pallet_name == "Balances" && (idx == 0 || idx == 3) => {
let call_name = match idx {
0 => "transfer_allow_death",
3 => "transfer_keep_alive",
_ => unreachable!(),
};
if args.len() < 33 {
return Ok(format!(
" {} {}::{} (index {})\n {} {} bytes (too short)",
"Call:".dimmed(),
pallet_name.bright_cyan(),
call_name.bright_yellow(),
idx,
"Args:".dimmed(),
args.len()
));
}
let address_variant = args[0];
if address_variant != 0 {
return Ok(format!(
" {} {}::{} (index {})\n {} {} bytes\n {} Unknown address variant: {}",
"Call:".dimmed(),
pallet_name.bright_cyan(),
call_name.bright_yellow(),
idx,
"Args:".dimmed(),
args.len(),
"Error:".dimmed(),
address_variant
));
}
let account_bytes: [u8; 32] = args[1..33].try_into().map_err(|_| {
crate::error::QuantusError::Generic("Failed to extract account bytes".to_string())
})?;
let account_id = SpAccountId32::from(account_bytes);
let to_address = account_id.to_ss58check();
let mut cursor = &args[33..];
let amount: u128 = match codec::Compact::<u128>::decode(&mut cursor) {
Ok(compact) => compact.0,
Err(_) => {
return Ok(format!(
" {} {}::{} (index {})\n {} {}\n {} Failed to decode amount",
"Call:".dimmed(),
pallet_name.bright_cyan(),
call_name.bright_yellow(),
idx,
"To:".dimmed(),
to_address.bright_cyan(),
"Error:".dimmed()
));
},
};
Ok(format!(
" {} {}::{}\n {} {}\n {} {}",
"Call:".dimmed(),
pallet_name.bright_cyan(),
call_name.bright_yellow(),
"To:".dimmed(),
to_address.bright_cyan(),
"Amount:".dimmed(),
format_balance(amount).bright_green()
))
},
(_, idx) if pallet_name == "ReversibleTransfers" && idx == 0 => {
if args.is_empty() {
return Ok(format!(
" {} {}::set_high_security\n {} {} bytes (too short)",
"Call:".dimmed(),
pallet_name.bright_cyan(),
"Args:".dimmed(),
args.len()
));
}
let delay_variant = args[0];
let delay_str: String;
let offset: usize;
match delay_variant {
0 => {
if args.len() < 5 {
return Ok(format!(
" {} {}::set_high_security\n {} Failed to decode delay (BlockNumber)",
"Call:".dimmed(),
pallet_name.bright_cyan(),
"Error:".dimmed()
));
}
let blocks = u32::from_le_bytes([args[1], args[2], args[3], args[4]]);
delay_str = format!("{} blocks", blocks);
offset = 5;
},
1 => {
if args.len() < 9 {
return Ok(format!(
" {} {}::set_high_security\n {} Failed to decode delay (Timestamp)",
"Call:".dimmed(),
pallet_name.bright_cyan(),
"Error:".dimmed()
));
}
let millis = u64::from_le_bytes([
args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8],
]);
let seconds = millis / 1000;
delay_str = format!("{} seconds ({} ms)", seconds, millis);
offset = 9;
},
_ => {
return Ok(format!(
" {} {}::set_high_security\n {} Unknown delay variant: {}",
"Call:".dimmed(),
pallet_name.bright_cyan(),
"Error:".dimmed(),
delay_variant
));
},
}
if args.len() < offset + 32 {
return Ok(format!(
" {} {}::set_high_security\n {} {}\n {} Failed to decode interceptor",
"Call:".dimmed(),
pallet_name.bright_cyan(),
"Delay:".dimmed(),
delay_str.bright_yellow(),
"Error:".dimmed()
));
}
let interceptor_bytes: [u8; 32] =
args[offset..offset + 32].try_into().map_err(|_| {
crate::error::QuantusError::Generic(
"Failed to extract interceptor bytes".to_string(),
)
})?;
let interceptor = SpAccountId32::from(interceptor_bytes);
let interceptor_ss58 = interceptor
.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
Ok(format!(
" {} {}::set_high_security\n {} {}\n {} {}",
"Call:".dimmed(),
pallet_name.bright_cyan(),
"Delay:".dimmed(),
delay_str.bright_yellow(),
"Guardian:".dimmed(),
interceptor_ss58.bright_green()
))
},
_ => {
let call_name = metadata
.pallets()
.find(|p| p.index() == pallet_index)
.and_then(|p| {
p.call_variants().and_then(|calls| {
calls.iter().find(|v| v.index == call_index).map(|v| v.name.as_str())
})
})
.unwrap_or("unknown");
Ok(format!(
" {} {}::{} (index {}:{})\n {} {} bytes\n {} {}",
"Call:".dimmed(),
pallet_name.bright_cyan(),
call_name.bright_yellow(),
pallet_index,
call_index,
"Args:".dimmed(),
args.len(),
"Raw:".dimmed(),
hex::encode(args).bright_green()
))
},
}
}
async fn handle_proposal_info(
multisig_address: String,
proposal_id: u32,
node_url: &str,
) -> crate::error::Result<()> {
log_print!("๐ {} Querying proposal info...", "MULTISIG".bright_magenta().bold());
log_print!("");
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let latest_block_hash = quantus_client.get_latest_block().await?;
let latest_block = quantus_client.client().blocks().at(latest_block_hash).await?;
let current_block_number = latest_block.number();
let storage_query = quantus_subxt::api::storage()
.multisig()
.proposals(multisig_address.clone(), proposal_id);
let storage_at = quantus_client.client().storage().at(latest_block_hash);
let proposal_data = storage_at.fetch(&storage_query).await?;
match proposal_data {
Some(data) => {
let multisig_query =
quantus_subxt::api::storage().multisig().multisigs(multisig_address.clone());
let multisig_info = storage_at.fetch(&multisig_query).await?;
log_print!("๐ {} Information:", "PROPOSAL".bright_green().bold());
log_print!(
" Current Block: {}",
current_block_number.to_string().bright_white().bold()
);
log_print!(" Multisig: {}", multisig_ss58.bright_cyan());
if let Some(ref ms_data) = multisig_info {
let progress = format!("{}/{}", data.approvals.0.len(), ms_data.threshold);
log_print!(
" Threshold: {} (progress: {})",
ms_data.threshold.to_string().bright_yellow(),
progress.bright_cyan()
);
}
log_print!(" Proposal ID: {}", proposal_id.to_string().bright_yellow());
let proposer_bytes: &[u8; 32] = data.proposer.as_ref();
let proposer_sp = SpAccountId32::from(*proposer_bytes);
log_print!(" Proposer: {}", proposer_sp.to_ss58check().bright_cyan());
log_print!("");
match decode_call_data(&quantus_client, &data.call.0).await {
Ok(decoded) => {
log_print!("{}", decoded);
},
Err(e) => {
log_print!(" Call Size: {} bytes", data.call.0.len());
log_verbose!("Failed to decode call data: {:?}", e);
},
}
log_print!("");
if data.expiry > current_block_number {
let blocks_remaining = data.expiry - current_block_number;
log_print!(
" Expiry: block {} ({} blocks remaining)",
data.expiry,
blocks_remaining.to_string().bright_green()
);
} else {
log_print!(" Expiry: block {} ({})", data.expiry, "EXPIRED".bright_red().bold());
}
log_print!(" Deposit: {} (locked)", format_balance(data.deposit));
log_print!(
" Status: {}",
match data.status {
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Active =>
"Active".bright_green(),
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Approved =>
"Approved (ready to execute)".bright_yellow(),
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Executed =>
"Executed".bright_blue(),
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Cancelled =>
"Cancelled".bright_red(),
}
);
log_print!(" Approvals ({}):", data.approvals.0.len().to_string().bright_yellow());
for (i, approver) in data.approvals.0.iter().enumerate() {
let approver_bytes: &[u8; 32] = approver.as_ref();
let approver_sp = SpAccountId32::from(*approver_bytes);
log_print!(" {}. {}", i + 1, approver_sp.to_ss58check().bright_cyan());
}
if let Some(ms_data) = multisig_info {
let pending_signers: Vec<String> = ms_data
.signers
.0
.iter()
.filter(|s| !data.approvals.0.contains(s))
.map(|s| {
let bytes: &[u8; 32] = s.as_ref();
let sp = SpAccountId32::from(*bytes);
sp.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(
189,
))
})
.collect();
if !pending_signers.is_empty() {
log_print!("");
log_print!(
" Pending Approvals ({}):",
pending_signers.len().to_string().bright_red()
);
for (i, signer) in pending_signers.iter().enumerate() {
log_print!(" {}. {}", i + 1, signer.bright_red());
}
}
}
},
None => {
log_error!("โ Proposal not found");
},
}
log_print!("");
Ok(())
}
async fn handle_list_proposals(
multisig_address: String,
node_url: &str,
) -> crate::error::Result<()> {
log_print!("๐ {} Listing proposals...", "MULTISIG".bright_magenta().bold());
log_print!("");
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_address = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let latest_block_hash = quantus_client.get_latest_block().await?;
let storage = quantus_client.client().storage().at(latest_block_hash);
let address = quantus_subxt::api::storage().multisig().proposals_iter1(multisig_address);
let mut proposals = storage.iter(address).await?;
let mut count = 0;
let mut active_count = 0;
let mut approved_count = 0;
let mut executed_count = 0;
let mut cancelled_count = 0;
while let Some(result) = proposals.next().await {
match result {
Ok(kv) => {
count += 1;
let status_str = match kv.value.status {
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Active => {
active_count += 1;
"Active".bright_green()
},
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Approved => {
approved_count += 1;
"Approved (ready to execute)".bright_yellow()
},
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Executed => {
executed_count += 1;
"Executed".bright_blue()
},
quantus_subxt::api::runtime_types::pallet_multisig::ProposalStatus::Cancelled => {
cancelled_count += 1;
"Cancelled".bright_red()
},
};
let key_bytes = kv.key_bytes;
if key_bytes.len() >= 4 {
let id_bytes = &key_bytes[key_bytes.len() - 4..];
let proposal_id =
u32::from_le_bytes([id_bytes[0], id_bytes[1], id_bytes[2], id_bytes[3]]);
log_print!("๐ Proposal #{}", count);
log_print!(" ID: {}", proposal_id.to_string().bright_yellow());
let proposer_bytes: &[u8; 32] = kv.value.proposer.as_ref();
let proposer_sp = SpAccountId32::from(*proposer_bytes);
log_print!(" Proposer: {}", proposer_sp.to_ss58check().bright_cyan());
match decode_call_data(&quantus_client, &kv.value.call.0).await {
Ok(decoded) => {
let lines: Vec<&str> = decoded.lines().collect();
if !lines.is_empty() {
log_print!(" {}", lines[0].trim_start());
}
},
Err(_) => {
log_print!(" Call Size: {} bytes", kv.value.call.0.len());
},
}
log_print!(" Status: {}", status_str);
log_print!(" Approvals: {}", kv.value.approvals.0.len());
log_print!(" Expiry: block {}", kv.value.expiry);
log_print!("");
}
},
Err(e) => {
log_error!("Error reading proposal: {:?}", e);
},
}
}
if count == 0 {
log_print!(" No proposals found for this multisig");
} else {
log_print!("๐ {} Summary:", "PROPOSALS".bright_green().bold());
log_print!(" Total: {}", count.to_string().bright_yellow());
log_print!(" Active: {}", active_count.to_string().bright_green());
log_print!(" Approved: {}", approved_count.to_string().bright_yellow());
log_print!(" Executed: {}", executed_count.to_string().bright_blue());
log_print!(" Cancelled: {}", cancelled_count.to_string().bright_red());
}
log_print!("");
Ok(())
}
async fn build_runtime_call(
quantus_client: &crate::chain::client::QuantusClient,
pallet: &str,
call: &str,
args: Vec<serde_json::Value>,
) -> crate::error::Result<Vec<u8>> {
let metadata = quantus_client.client().metadata();
let pallet_metadata = metadata.pallet_by_name(pallet).ok_or_else(|| {
crate::error::QuantusError::Generic(format!("Pallet '{}' not found in metadata", pallet))
})?;
log_verbose!("โ
Found pallet '{}' with index {}", pallet, pallet_metadata.index());
let call_metadata = pallet_metadata.call_variant_by_name(call).ok_or_else(|| {
crate::error::QuantusError::Generic(format!(
"Call '{}' not found in pallet '{}'",
call, pallet
))
})?;
log_verbose!("โ
Found call '{}' with index {}", call, call_metadata.index);
use codec::Encode;
let mut call_data = Vec::new();
call_data.push(pallet_metadata.index());
call_data.push(call_metadata.index);
match (pallet, call) {
("Balances", "transfer_allow_death") | ("Balances", "transfer_keep_alive") => {
if args.len() != 2 {
return Err(crate::error::QuantusError::Generic(
"Balances transfer requires 2 arguments: [to_address, amount]".to_string(),
));
}
let to_address = args[0].as_str().ok_or_else(|| {
crate::error::QuantusError::Generic(
"First argument must be a string (to_address)".to_string(),
)
})?;
let amount: u128 = if let Some(amount_str) = args[1].as_str() {
amount_str.parse().map_err(|_| {
crate::error::QuantusError::Generic(
"Second argument must be a valid number (amount)".to_string(),
)
})?
} else if let Some(amount_num) = args[1].as_u64() {
amount_num as u128
} else {
return Err(crate::error::QuantusError::Generic(
"Second argument must be a number (amount)".to_string(),
));
};
let (to_account_id, _) = SpAccountId32::from_ss58check_with_version(to_address)
.map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid to_address: {:?}", e))
})?;
let to_account_id_bytes: [u8; 32] = *to_account_id.as_ref();
let to_account_id_subxt =
subxt::ext::subxt_core::utils::AccountId32::from(to_account_id_bytes);
let multi_address: subxt::ext::subxt_core::utils::MultiAddress<
subxt::ext::subxt_core::utils::AccountId32,
(),
> = subxt::ext::subxt_core::utils::MultiAddress::Id(to_account_id_subxt);
multi_address.encode_to(&mut call_data);
codec::Compact(amount).encode_to(&mut call_data);
},
("System", "remark") | ("System", "remark_with_event") => {
if args.len() != 1 {
return Err(crate::error::QuantusError::Generic(
"System remark requires 1 argument: [hex_data]".to_string(),
));
}
let hex_data = args[0].as_str().ok_or_else(|| {
crate::error::QuantusError::Generic(
"Argument must be a hex string (e.g., \"0x48656c6c6f\")".to_string(),
)
})?;
let hex_str = hex_data.trim_start_matches("0x");
let data_bytes = hex::decode(hex_str).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid hex data: {}", e))
})?;
data_bytes.encode_to(&mut call_data);
},
_ => {
return Err(crate::error::QuantusError::Generic(format!(
"Building call data for {}.{} is not yet implemented. Use a simpler approach or add support.",
pallet, call
)));
},
}
Ok(call_data)
}
fn format_balance(balance: u128) -> String {
let quan = balance / QUAN_DECIMALS;
let remainder = balance % QUAN_DECIMALS;
if remainder == 0 {
format!("{} QUAN", quan)
} else {
let decimal_str = format!("{:012}", remainder).trim_end_matches('0').to_string();
format!("{}.{} QUAN", quan, decimal_str)
}
}
async fn handle_high_security_status(
multisig_address: String,
node_url: &str,
) -> crate::error::Result<()> {
log_print!("๐ {} Checking High-Security status...", "MULTISIG".bright_magenta().bold());
log_print!("");
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_account_id = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
let latest_block_hash = quantus_client.get_latest_block().await?;
let storage_at = quantus_client.client().storage().at(latest_block_hash);
let storage_query = quantus_subxt::api::storage()
.reversible_transfers()
.high_security_accounts(multisig_account_id);
let high_security_data = storage_at.fetch(&storage_query).await?;
log_print!("๐ Multisig: {}", multisig_ss58.bright_cyan());
log_print!("");
match high_security_data {
Some(data) => {
log_success!("โ
High-Security: {}", "ENABLED".bright_green().bold());
log_print!("");
let interceptor_bytes: &[u8; 32] = data.interceptor.as_ref();
let interceptor_sp = SpAccountId32::from(*interceptor_bytes);
let interceptor_ss58 = interceptor_sp
.to_ss58check_with_version(sp_core::crypto::Ss58AddressFormat::custom(189));
log_print!("๐ก๏ธ Guardian/Interceptor: {}", interceptor_ss58.bright_green().bold());
match data.delay {
quantus_subxt::api::runtime_types::qp_scheduler::BlockNumberOrTimestamp::BlockNumber(
blocks,
) => {
log_print!("โฑ๏ธ Delay: {} blocks", blocks.to_string().bright_yellow());
},
quantus_subxt::api::runtime_types::qp_scheduler::BlockNumberOrTimestamp::Timestamp(
ms,
) => {
let seconds = ms / 1000;
log_print!("โฑ๏ธ Delay: {} seconds", seconds.to_string().bright_yellow());
},
}
log_print!("");
log_print!(
"๐ก {} All transfers from this multisig will be delayed and reversible",
"INFO".bright_blue().bold()
);
log_print!(" The guardian can intercept transactions during the delay period");
log_print!("");
log_print!(
"โ ๏ธ {} Guardian interception requires direct runtime call (not yet in CLI)",
"NOTE".bright_yellow().bold()
);
log_print!(" Use: pallet_reversible_transfers::cancel(tx_id) as guardian account");
},
None => {
log_print!("โ High-Security: {}", "DISABLED".bright_red().bold());
log_print!("");
log_print!("๐ก This multisig does not have high-security enabled.");
log_print!(" Use 'quantus multisig high-security set' to enable it via a proposal.");
},
}
log_print!("");
Ok(())
}
async fn handle_high_security_set(
multisig_address: String,
interceptor: String,
delay_blocks: Option<u32>,
delay_seconds: Option<u64>,
expiry: u32,
from: String,
password: Option<String>,
password_file: Option<String>,
node_url: &str,
execution_mode: ExecutionMode,
) -> crate::error::Result<()> {
log_print!(
"๐ก๏ธ {} Enabling High-Security (via proposal)...",
"MULTISIG".bright_magenta().bold()
);
if delay_blocks.is_none() && delay_seconds.is_none() {
log_error!("โ You must specify either --delay-blocks or --delay-seconds");
return Err(crate::error::QuantusError::Generic("Missing delay parameter".to_string()));
}
let multisig_ss58 = crate::cli::common::resolve_address(&multisig_address)?;
let (multisig_id, _) =
SpAccountId32::from_ss58check_with_version(&multisig_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid multisig address: {:?}", e))
})?;
let multisig_bytes: [u8; 32] = *multisig_id.as_ref();
let multisig_account_id = subxt::ext::subxt_core::utils::AccountId32::from(multisig_bytes);
let interceptor_ss58 = crate::cli::common::resolve_address(&interceptor)?;
let (interceptor_id, _) = SpAccountId32::from_ss58check_with_version(&interceptor_ss58)
.map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid interceptor address: {:?}", e))
})?;
let interceptor_bytes: [u8; 32] = *interceptor_id.as_ref();
let interceptor_account_id =
subxt::ext::subxt_core::utils::AccountId32::from(interceptor_bytes);
log_verbose!("Multisig: {}", multisig_ss58);
log_verbose!("Interceptor: {}", interceptor_ss58);
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
use quantus_subxt::api::reversible_transfers::calls::types::set_high_security::Delay as HsDelay;
let delay_value = if let Some(blocks) = delay_blocks {
HsDelay::BlockNumber(blocks)
} else if let Some(seconds) = delay_seconds {
HsDelay::Timestamp(seconds * 1000) } else {
return Err(crate::error::QuantusError::Generic("Missing delay parameter".to_string()));
};
log_verbose!("Delay: {:?}", delay_value);
let set_hs_call = quantus_subxt::api::tx()
.reversible_transfers()
.set_high_security(delay_value, interceptor_account_id);
use subxt::tx::Payload;
let call_data =
set_hs_call.encode_call_data(&quantus_client.client().metadata()).map_err(|e| {
crate::error::QuantusError::Generic(format!("Failed to encode call: {:?}", e))
})?;
log_verbose!("Call data size: {} bytes", call_data.len());
let latest_block_hash = quantus_client.get_latest_block().await?;
let latest_block = quantus_client.client().blocks().at(latest_block_hash).await?;
let current_block_number = latest_block.number();
if expiry <= current_block_number {
log_error!(
"โ Expiry block {} is in the past (current block: {})",
expiry,
current_block_number
);
log_print!(" Use a higher block number, e.g., --expiry {}", current_block_number + 1000);
return Err(crate::error::QuantusError::Generic("Expiry must be in the future".to_string()));
}
let storage_at = quantus_client.client().storage().at(latest_block_hash);
let multisig_query =
quantus_subxt::api::storage().multisig().multisigs(multisig_account_id.clone());
let multisig_data = storage_at.fetch(&multisig_query).await?.ok_or_else(|| {
crate::error::QuantusError::Generic(format!(
"Multisig not found at address: {}",
multisig_ss58
))
})?;
let proposer_ss58 = crate::cli::common::resolve_address(&from)?;
let (proposer_id, _) =
SpAccountId32::from_ss58check_with_version(&proposer_ss58).map_err(|e| {
crate::error::QuantusError::Generic(format!("Invalid proposer address: {:?}", e))
})?;
let proposer_bytes: [u8; 32] = *proposer_id.as_ref();
let proposer_account_id = subxt::ext::subxt_core::utils::AccountId32::from(proposer_bytes);
if !multisig_data.signers.0.contains(&proposer_account_id) {
log_error!("โ Not authorized: {} is not a signer of this multisig", proposer_ss58);
return Err(crate::error::QuantusError::Generic(
"Only multisig signers can create proposals".to_string(),
));
}
let mut expired_count = 0;
let proposals_query = quantus_subxt::api::storage().multisig().proposals_iter();
let mut proposals_stream = storage_at.iter(proposals_query).await?;
while let Some(Ok(kv)) = proposals_stream.next().await {
let proposal = kv.value;
if proposal.proposer == proposer_account_id {
if proposal.expiry <= current_block_number {
expired_count += 1;
}
}
}
if expired_count > 0 {
log_print!("");
log_print!(
"๐งน {} Auto-cleanup: Runtime will remove your {} expired proposal(s)",
"INFO".bright_blue().bold(),
expired_count.to_string().bright_yellow()
);
log_print!(" This happens automatically before creating the new proposal");
log_print!("");
}
let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
let propose_tx =
quantus_subxt::api::tx()
.multisig()
.propose(multisig_account_id, call_data, expiry);
let propose_execution_mode = ExecutionMode { wait_for_transaction: true, ..execution_mode };
crate::cli::common::submit_transaction(
&quantus_client,
&keypair,
propose_tx,
None,
propose_execution_mode,
)
.await?;
let proposal_id = fetch_proposal_id(&quantus_client, &multisig_ss58).await;
log_proposal_result(&multisig_ss58, proposal_id);
log_print!(
"๐ก {} Once this proposal reaches threshold, High-Security will be enabled",
"NEXT STEPS".bright_blue().bold()
);
log_print!(" After threshold is reached, all transfers will be delayed and reversible");
log_print!("");
Ok(())
}