use crate::{
chain::quantus_subxt, error::QuantusError, log_error, log_print, log_success, log_verbose,
wallet::QuantumKeyPair,
};
use colored::Colorize;
use serde_json::Value;
use sp_core::crypto::{AccountId32, Ss58Codec};
pub(crate) fn parse_json_u128(value: &Value, label: &str) -> crate::error::Result<u128> {
if let Some(s) = value.as_str() {
return s.parse::<u128>().map_err(|_| {
QuantusError::Generic(format!("{label} must be a number (got string '{s}')"))
});
}
if let Some(n) = value.as_u64() {
return Ok(u128::from(n));
}
if let Some(n) = value.as_number() {
return n
.to_string()
.parse::<u128>()
.map_err(|_| QuantusError::Generic(format!("{label} must be a non-negative integer")));
}
Err(QuantusError::Generic(format!("{label} must be a JSON string or number (got {value})")))
}
pub(crate) fn parse_json_u32(value: &Value, label: &str) -> crate::error::Result<u32> {
if let Some(n) = value.as_u64() {
return u32::try_from(n)
.map_err(|_| QuantusError::Generic(format!("{label} exceeds u32::MAX")));
}
if let Some(s) = value.as_str() {
return s.parse::<u32>().map_err(|_| {
QuantusError::Generic(format!("{label} must be a u32 (got string '{s}')"))
});
}
Err(QuantusError::Generic(format!(
"{label} must be a JSON number or numeric string (got {value})"
)))
}
pub(crate) fn parse_json_bool(value: &Value, label: &str) -> crate::error::Result<bool> {
value.as_bool().ok_or_else(|| {
QuantusError::Generic(format!("{label} must be a JSON boolean (got {value})"))
})
}
pub async fn execute_generic_call(
quantus_client: &crate::chain::client::QuantusClient,
pallet: &str,
call: &str,
args: Vec<Value>,
from_keypair: &QuantumKeyPair,
tip: Option<String>,
execution_mode: crate::cli::common::ExecutionMode,
) -> crate::error::Result<subxt::utils::H256> {
log_print!("🚀 Executing generic call");
log_print!("Pallet: {}", pallet.bright_green());
log_print!("Call: {}", call.bright_cyan());
log_print!("From: {}", from_keypair.try_to_account_id_ss58check()?.bright_yellow());
if let Some(tip) = &tip {
log_print!("Tip: {}", tip.bright_magenta());
}
let _signer = from_keypair
.to_subxt_signer()
.map_err(|e| QuantusError::NetworkError(format!("Failed to convert keypair: {e:?}")))?;
let metadata = quantus_client.client().metadata();
let pallet_metadata = metadata
.pallet_by_name(pallet)
.ok_or_else(|| QuantusError::Generic(format!("Pallet '{pallet}' not found in metadata")))?;
log_verbose!("✅ Found pallet '{}' with index {}", pallet, pallet_metadata.index());
let call_metadata = pallet_metadata.call_variant_by_name(call).ok_or_else(|| {
QuantusError::Generic(format!("Call '{call}' not found in pallet '{pallet}'"))
})?;
log_verbose!("✅ Found call '{}' with index {}", call, call_metadata.index);
let tip_amount = if let Some(tip_str) = &tip { tip_str.parse::<u128>().ok() } else { None };
log_print!("🔧 Creating extrinsic for {}.{}", pallet, call);
let tx_hash = match (pallet, call) {
("Balances", "transfer_allow_death") =>
submit_balance_transfer(
quantus_client,
from_keypair,
&args,
false,
tip_amount,
execution_mode,
)
.await?,
("Balances", "transfer_keep_alive") =>
submit_balance_transfer(
quantus_client,
from_keypair,
&args,
true,
tip_amount,
execution_mode,
)
.await?,
("System", "remark") =>
submit_system_remark(quantus_client, from_keypair, &args, tip_amount, execution_mode)
.await?,
("TechCollective", "add_member") =>
submit_tech_collective_add_member(quantus_client, from_keypair, &args, execution_mode)
.await?,
("TechCollective", "remove_member") =>
submit_tech_collective_remove_member(
quantus_client,
from_keypair,
&args,
execution_mode,
)
.await?,
("TechCollective", "vote") =>
submit_tech_collective_vote(quantus_client, from_keypair, &args, execution_mode).await?,
("ReversibleTransfers", "schedule_transfer") =>
submit_reversible_transfer(quantus_client, from_keypair, &args, execution_mode).await?,
(_, _) => {
log_error!(
"❌ Pallet '{}' or call '{}' is not supported yet in SubXT implementation",
pallet,
call
);
log_print!("💡 Supported pallets in SubXT:");
log_print!(" • Balances: transfer_allow_death, transfer_keep_alive");
log_print!(" • System: remark");
log_print!(" • TechCollective: add_member, remove_member, vote");
log_print!(" • ReversibleTransfers: schedule_transfer");
log_print!("💡 For other calls, use the original 'quantus call' command");
return Err(QuantusError::Generic(format!(
"Unsupported pallet/call combination in SubXT: {pallet}.{call}"
)));
},
};
log_success!("🎉 SubXT transaction submitted successfully!");
log_print!("📋 Transaction hash: {}", format!("0x{}", hex::encode(tx_hash)).bright_yellow());
Ok(tx_hash)
}
async fn submit_balance_transfer(
quantus_client: &crate::chain::client::QuantusClient,
from_keypair: &QuantumKeyPair,
args: &[Value],
keep_alive: bool,
tip: Option<u128>,
execution_mode: crate::cli::common::ExecutionMode,
) -> crate::error::Result<subxt::utils::H256> {
if args.len() != 2 {
return Err(QuantusError::Generic(
"Balances transfer requires 2 arguments: [to_address, amount]".to_string(),
));
}
let to_address = args[0].as_str().ok_or_else(|| {
QuantusError::Generic("First argument must be a string (to_address)".to_string())
})?;
let amount = parse_json_u128(&args[1], "Second argument (amount)")?;
let (to_account_id, _) = AccountId32::from_ss58check_with_version(to_address)
.map_err(|e| 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);
if keep_alive {
let transfer_call = quantus_subxt::api::tx().balances().transfer_keep_alive(
subxt::ext::subxt_core::utils::MultiAddress::Id(to_account_id_subxt),
amount,
);
crate::cli::common::submit_transaction(
quantus_client,
from_keypair,
transfer_call,
tip,
execution_mode,
)
.await
} else {
let transfer_call = quantus_subxt::api::tx().balances().transfer_allow_death(
subxt::ext::subxt_core::utils::MultiAddress::Id(to_account_id_subxt),
amount,
);
crate::cli::common::submit_transaction(
quantus_client,
from_keypair,
transfer_call,
tip,
execution_mode,
)
.await
}
}
async fn submit_system_remark(
quantus_client: &crate::chain::client::QuantusClient,
from_keypair: &QuantumKeyPair,
args: &[Value],
tip: Option<u128>,
execution_mode: crate::cli::common::ExecutionMode,
) -> crate::error::Result<subxt::utils::H256> {
if args.len() != 1 {
return Err(QuantusError::Generic(
"System remark requires 1 argument: [remark]".to_string(),
));
}
let remark = args[0]
.as_str()
.ok_or_else(|| QuantusError::Generic("Argument must be a string (remark)".to_string()))?;
let remark_call = quantus_subxt::api::tx().system().remark(remark.as_bytes().to_vec());
crate::cli::common::submit_transaction(
quantus_client,
from_keypair,
remark_call,
tip,
execution_mode,
)
.await
}
async fn submit_tech_collective_add_member(
quantus_client: &crate::chain::client::QuantusClient,
from_keypair: &QuantumKeyPair,
args: &[Value],
execution_mode: crate::cli::common::ExecutionMode,
) -> crate::error::Result<subxt::utils::H256> {
if args.len() != 1 {
return Err(QuantusError::Generic(
"TechCollective add_member requires 1 argument: [member_address]".to_string(),
));
}
let member_address = args[0].as_str().ok_or_else(|| {
QuantusError::Generic("Argument must be a string (member_address)".to_string())
})?;
let (member_account_id, _) = AccountId32::from_ss58check_with_version(member_address)
.map_err(|e| QuantusError::Generic(format!("Invalid member_address: {e:?}")))?;
let member_account_id_bytes: [u8; 32] = *member_account_id.as_ref();
let member_account_id_subxt =
subxt::ext::subxt_core::utils::AccountId32::from(member_account_id_bytes);
let call = quantus_subxt::api::tx()
.tech_collective()
.add_member(subxt::ext::subxt_core::utils::MultiAddress::Id(member_account_id_subxt));
crate::cli::common::submit_transaction(quantus_client, from_keypair, call, None, execution_mode)
.await
}
async fn submit_tech_collective_remove_member(
quantus_client: &crate::chain::client::QuantusClient,
from_keypair: &QuantumKeyPair,
args: &[Value],
execution_mode: crate::cli::common::ExecutionMode,
) -> crate::error::Result<subxt::utils::H256> {
if args.len() != 2 {
return Err(QuantusError::Generic(
"TechCollective remove_member requires 2 arguments: [member_address, min_rank]"
.to_string(),
));
}
let member_address = args[0].as_str().ok_or_else(|| {
QuantusError::Generic("First argument must be a string (member_address)".to_string())
})?;
let min_rank = args[1]
.as_u64()
.or_else(|| args[1].as_str().and_then(|rank| rank.parse::<u64>().ok()))
.ok_or_else(|| {
QuantusError::Generic("Second argument must be a number (min_rank)".to_string())
})?;
let min_rank = u16::try_from(min_rank)
.map_err(|_| QuantusError::Generic("min_rank must fit in u16".to_string()))?;
let (member_account_id, _) = AccountId32::from_ss58check_with_version(member_address)
.map_err(|e| QuantusError::Generic(format!("Invalid member_address: {e:?}")))?;
let member_account_id_bytes: [u8; 32] = *member_account_id.as_ref();
let member_account_id_subxt =
subxt::ext::subxt_core::utils::AccountId32::from(member_account_id_bytes);
let call = quantus_subxt::api::tx().tech_collective().remove_member(
subxt::ext::subxt_core::utils::MultiAddress::Id(member_account_id_subxt),
min_rank,
);
crate::cli::common::submit_transaction(quantus_client, from_keypair, call, None, execution_mode)
.await
}
async fn submit_tech_collective_vote(
quantus_client: &crate::chain::client::QuantusClient,
from_keypair: &QuantumKeyPair,
args: &[Value],
execution_mode: crate::cli::common::ExecutionMode,
) -> crate::error::Result<subxt::utils::H256> {
if args.len() != 2 {
return Err(QuantusError::Generic(
"TechCollective vote requires 2 arguments: [referendum_index, aye]".to_string(),
));
}
let referendum_index = parse_json_u32(&args[0], "First argument (referendum_index)")?;
let aye = parse_json_bool(&args[1], "Second argument (aye)")?;
let vote_call = quantus_subxt::api::tx().tech_collective().vote(referendum_index, aye);
crate::cli::common::submit_transaction(
quantus_client,
from_keypair,
vote_call,
None,
execution_mode,
)
.await
}
async fn submit_reversible_transfer(
quantus_client: &crate::chain::client::QuantusClient,
from_keypair: &QuantumKeyPair,
args: &[Value],
execution_mode: crate::cli::common::ExecutionMode,
) -> crate::error::Result<subxt::utils::H256> {
if args.len() != 2 {
return Err(QuantusError::Generic(
"ReversibleTransfers schedule_transfer requires 2 arguments: [to_address, amount]"
.to_string(),
));
}
let to_address = args[0].as_str().ok_or_else(|| {
QuantusError::Generic("First argument must be a string (to_address)".to_string())
})?;
let amount = parse_json_u128(&args[1], "Second argument (amount)")?;
let (to_account_id, _) = AccountId32::from_ss58check_with_version(to_address)
.map_err(|e| 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 schedule_call = quantus_subxt::api::tx().reversible_transfers().schedule_transfer(
subxt::ext::subxt_core::utils::MultiAddress::Id(to_account_id_subxt),
amount,
);
crate::cli::common::submit_transaction(
quantus_client,
from_keypair,
schedule_call,
None,
execution_mode,
)
.await
}
pub async fn handle_generic_call(
pallet: &str,
call: &str,
args: Vec<Value>,
keypair: &QuantumKeyPair,
tip: Option<String>,
node_url: &str,
execution_mode: crate::cli::common::ExecutionMode,
) -> crate::error::Result<()> {
log_print!("🚀 Generic Call");
let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
execute_generic_call(&quantus_client, pallet, call, args, keypair, tip, execution_mode).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parse_json_u128_accepts_string_and_number() {
assert_eq!(parse_json_u128(&json!("1000"), "amount").unwrap(), 1000);
assert_eq!(parse_json_u128(&json!(1000), "amount").unwrap(), 1000);
assert_eq!(
parse_json_u128(&json!(1_000_000_000_000u64), "amount").unwrap(),
1_000_000_000_000
);
}
#[test]
fn parse_json_u128_rejects_non_numeric_without_zero_default() {
let err = parse_json_u128(&json!(true), "amount").expect_err("bool must not become 0");
assert!(err.to_string().contains("must be a JSON string or number"), "unexpected: {err}");
let err = parse_json_u128(&json!(null), "amount").expect_err("null must not become 0");
assert!(err.to_string().contains("must be a JSON string or number"), "unexpected: {err}");
}
#[test]
fn parse_json_u32_rejects_missing_number_without_zero_default() {
let err = parse_json_u32(&json!("not-a-number"), "referendum_index")
.expect_err("invalid string must fail");
assert!(err.to_string().contains("must be a u32"), "unexpected: {err}");
let err = parse_json_u32(&json!(true), "referendum_index")
.expect_err("bool must not become referendum 0");
assert!(err.to_string().contains("must be a JSON number"), "unexpected: {err}");
assert_eq!(parse_json_u32(&json!(7), "referendum_index").unwrap(), 7);
}
#[test]
fn parse_json_bool_rejects_non_bool() {
let err = parse_json_bool(&json!(1), "aye").expect_err("number must not become false");
assert!(err.to_string().contains("boolean"), "unexpected: {err}");
assert!(parse_json_bool(&json!(true), "aye").unwrap());
}
}