quantus_cli/cli/
send.rs

1use crate::{
2	chain::{client::QuantusClient, quantus_subxt},
3	cli::{common::resolve_address, progress_spinner::wait_for_tx_confirmation},
4	error::Result,
5	log_error, log_info, log_print, log_success, log_verbose,
6};
7use colored::Colorize;
8use sp_core::crypto::{AccountId32 as SpAccountId32, Ss58Codec};
9
10/// Get the `free` balance for the given account using on-chain storage.
11pub async fn get_balance(quantus_client: &QuantusClient, account_address: &str) -> Result<u128> {
12	use quantus_subxt::api;
13
14	log_verbose!("💰 Querying balance for account: {}", account_address.bright_green());
15
16	// Decode the SS58 address into `AccountId32` (sp-core) first …
17	let account_id_sp = SpAccountId32::from_ss58check(account_address).map_err(|e| {
18		crate::error::QuantusError::Generic(format!(
19			"Invalid account address '{account_address}': {e:?}"
20		))
21	})?;
22
23	// … then convert into the `subxt` representation expected by the generated API.
24	let bytes: [u8; 32] = *account_id_sp.as_ref();
25	let account_id = subxt::ext::subxt_core::utils::AccountId32::from(bytes);
26
27	// Build the storage key for `System::Account` and fetch (or default-init) it.
28	let storage_addr = api::storage().system().account(account_id);
29
30	// Get the latest block hash to read from the latest state (not finalized)
31	let latest_block_hash = quantus_client.get_latest_block().await?;
32
33	let storage_at = quantus_client.client().storage().at(latest_block_hash);
34
35	let account_info = storage_at.fetch_or_default(&storage_addr).await.map_err(|e| {
36		crate::error::QuantusError::NetworkError(format!("Failed to fetch account info: {e:?}"))
37	})?;
38
39	Ok(account_info.data.free)
40}
41
42/// Get chain properties for formatting (uses system.rs ChainHead API)
43pub async fn get_chain_properties(quantus_client: &QuantusClient) -> Result<(String, u8)> {
44	// Use the shared ChainHead API from system.rs to avoid duplication
45	match crate::cli::system::get_complete_chain_info(quantus_client.node_url()).await {
46		Ok(chain_info) => {
47			log_verbose!(
48				"💰 Token: {} with {} decimals",
49				chain_info.token.symbol,
50				chain_info.token.decimals
51			);
52
53			Ok((chain_info.token.symbol, chain_info.token.decimals))
54		},
55		Err(e) => {
56			log_verbose!("❌ ChainHead API failed: {:?}", e);
57			Err(e)
58		},
59	}
60}
61
62/// Format balance with token symbol
63pub async fn format_balance_with_symbol(
64	quantus_client: &QuantusClient,
65	amount: u128,
66) -> Result<String> {
67	let (symbol, decimals) = get_chain_properties(quantus_client).await?;
68	let formatted_amount = format_balance(amount, decimals);
69	Ok(format!("{formatted_amount} {symbol}"))
70}
71
72/// Format balance with proper decimals
73pub fn format_balance(amount: u128, decimals: u8) -> String {
74	if decimals == 0 {
75		return amount.to_string();
76	}
77
78	let divisor = 10_u128.pow(decimals as u32);
79	let whole_part = amount / divisor;
80	let fractional_part = amount % divisor;
81
82	if fractional_part == 0 {
83		whole_part.to_string()
84	} else {
85		let fractional_str = format!("{:0width$}", fractional_part, width = decimals as usize);
86		let fractional_str = fractional_str.trim_end_matches('0');
87
88		if fractional_str.is_empty() {
89			whole_part.to_string()
90		} else {
91			format!("{whole_part}.{fractional_str}")
92		}
93	}
94}
95
96/// Parse human-readable amount string to raw chain units
97pub async fn parse_amount(quantus_client: &QuantusClient, amount_str: &str) -> Result<u128> {
98	let (_, decimals) = get_chain_properties(quantus_client).await?;
99	parse_amount_with_decimals(amount_str, decimals)
100}
101
102/// Parse amount string with specific decimals
103pub fn parse_amount_with_decimals(amount_str: &str, decimals: u8) -> Result<u128> {
104	let amount_part = amount_str.split_whitespace().next().unwrap_or("");
105
106	if amount_part.is_empty() {
107		return Err(crate::error::QuantusError::Generic("Amount cannot be empty".to_string()));
108	}
109
110	let parsed_amount: f64 = amount_part.parse().map_err(|_| {
111		crate::error::QuantusError::Generic(format!(
112			"Invalid amount format: '{amount_part}'. Use formats like '10', '10.5', '0.0001'"
113		))
114	})?;
115
116	if parsed_amount < 0.0 {
117		return Err(crate::error::QuantusError::Generic("Amount cannot be negative".to_string()));
118	}
119
120	if let Some(decimal_part) = amount_part.split('.').nth(1) {
121		if decimal_part.len() > decimals as usize {
122			return Err(crate::error::QuantusError::Generic(format!(
123				"Too many decimal places. Maximum {decimals} decimal places allowed for this chain"
124			)));
125		}
126	}
127
128	let multiplier = 10_f64.powi(decimals as i32);
129	let raw_amount = (parsed_amount * multiplier).round() as u128;
130
131	if raw_amount == 0 {
132		return Err(crate::error::QuantusError::Generic(
133			"Amount too small to represent in chain units".to_string(),
134		));
135	}
136
137	Ok(raw_amount)
138}
139
140/// Validate and format amount for display before sending
141pub async fn validate_and_format_amount(
142	quantus_client: &QuantusClient,
143	amount_str: &str,
144) -> Result<(u128, String)> {
145	let raw_amount = parse_amount(quantus_client, amount_str).await?;
146	let formatted = format_balance_with_symbol(quantus_client, raw_amount).await?;
147	Ok((raw_amount, formatted))
148}
149
150/// Transfer tokens with automatic nonce
151#[allow(dead_code)] // Used by external libraries via lib.rs export
152pub async fn transfer(
153	quantus_client: &QuantusClient,
154	from_keypair: &crate::wallet::QuantumKeyPair,
155	to_address: &str,
156	amount: u128,
157	tip: Option<u128>,
158) -> Result<subxt::utils::H256> {
159	transfer_with_nonce(quantus_client, from_keypair, to_address, amount, tip, None).await
160}
161
162/// Transfer tokens with manual nonce override
163pub async fn transfer_with_nonce(
164	quantus_client: &QuantusClient,
165	from_keypair: &crate::wallet::QuantumKeyPair,
166	to_address: &str,
167	amount: u128,
168	tip: Option<u128>,
169	nonce: Option<u32>,
170) -> Result<subxt::utils::H256> {
171	log_verbose!("🚀 Creating transfer transaction...");
172	log_verbose!("   From: {}", from_keypair.to_account_id_ss58check().bright_cyan());
173	log_verbose!("   To: {}", to_address.bright_green());
174	log_verbose!("   Amount: {}", amount);
175
176	// Resolve the destination address (could be wallet name or SS58 address)
177	let resolved_address = resolve_address(to_address)?;
178	log_verbose!("   Resolved to: {}", resolved_address.bright_green());
179
180	// Parse the destination address
181	let to_account_id_sp = SpAccountId32::from_ss58check(&resolved_address).map_err(|e| {
182		crate::error::QuantusError::NetworkError(format!("Invalid destination address: {e:?}"))
183	})?;
184
185	// Convert to subxt_core AccountId32
186	let to_account_id_bytes: [u8; 32] = *to_account_id_sp.as_ref();
187	let to_account_id = subxt::ext::subxt_core::utils::AccountId32::from(to_account_id_bytes);
188
189	log_verbose!("✍️  Creating balance transfer extrinsic...");
190
191	// Create the transfer call using static API from quantus_subxt
192	let transfer_call = quantus_subxt::api::tx().balances().transfer_allow_death(
193		subxt::ext::subxt_core::utils::MultiAddress::Id(to_account_id.clone()),
194		amount,
195	);
196
197	// Use provided tip or default tip of 10 DEV to increase priority and avoid temporarily
198	// banned errors
199	let tip_to_use = tip.unwrap_or(10_000_000_000); // Use provided tip or default 10 DEV
200
201	// Submit the transaction with optional manual nonce
202	let tx_hash = if let Some(manual_nonce) = nonce {
203		log_verbose!("🔢 Using manual nonce: {}", manual_nonce);
204		crate::cli::common::submit_transaction_with_nonce(
205			quantus_client,
206			from_keypair,
207			transfer_call,
208			Some(tip_to_use),
209			manual_nonce,
210		)
211		.await?
212	} else {
213		crate::cli::common::submit_transaction(
214			quantus_client,
215			from_keypair,
216			transfer_call,
217			Some(tip_to_use),
218		)
219		.await?
220	};
221
222	log_verbose!("📋 Transaction submitted: {:?}", tx_hash);
223
224	Ok(tx_hash)
225}
226
227/// Batch transfer tokens to multiple recipients in a single transaction
228pub async fn batch_transfer(
229	quantus_client: &QuantusClient,
230	from_keypair: &crate::wallet::QuantumKeyPair,
231	transfers: Vec<(String, u128)>, // (to_address, amount) pairs
232	tip: Option<u128>,
233) -> Result<subxt::utils::H256> {
234	log_verbose!("🚀 Creating batch transfer transaction with {} transfers...", transfers.len());
235	log_verbose!("   From: {}", from_keypair.to_account_id_ss58check().bright_cyan());
236
237	if transfers.is_empty() {
238		return Err(crate::error::QuantusError::Generic(
239			"No transfers provided for batch".to_string(),
240		));
241	}
242
243	// Get dynamic limits from chain
244	let (safe_limit, recommended_limit) =
245		get_batch_limits(quantus_client).await.unwrap_or((500, 1000));
246
247	if transfers.len() as u32 > recommended_limit {
248		return Err(crate::error::QuantusError::Generic(format!(
249			"Too many transfers in batch ({}) - chain limit is ~{} (safe: {})",
250			transfers.len(),
251			recommended_limit,
252			safe_limit
253		)));
254	}
255
256	// Warn about large batches
257	if transfers.len() as u32 > safe_limit {
258		log_verbose!(
259			"⚠️  Large batch ({} transfers) - approaching chain limits (safe: {}, max: {})",
260			transfers.len(),
261			safe_limit,
262			recommended_limit
263		);
264	}
265
266	// Prepare all transfer calls as RuntimeCall
267	let mut calls = Vec::new();
268	for (to_address, amount) in transfers {
269		log_verbose!("   To: {} Amount: {}", to_address.bright_green(), amount);
270
271		// Resolve the destination address
272		let resolved_address = crate::cli::common::resolve_address(&to_address)?;
273
274		// Parse the destination address
275		let to_account_id_sp = SpAccountId32::from_ss58check(&resolved_address).map_err(|e| {
276			crate::error::QuantusError::NetworkError(format!(
277				"Invalid destination address {resolved_address}: {e:?}"
278			))
279		})?;
280
281		// Convert to subxt_core AccountId32
282		let to_account_id_bytes: [u8; 32] = *to_account_id_sp.as_ref();
283		let to_account_id = subxt::ext::subxt_core::utils::AccountId32::from(to_account_id_bytes);
284
285		// Create the transfer call as RuntimeCall
286		use quantus_subxt::api::runtime_types::{
287			pallet_balances::pallet::Call as BalancesCall, quantus_runtime::RuntimeCall,
288		};
289
290		let transfer_call = RuntimeCall::Balances(BalancesCall::transfer_allow_death {
291			dest: subxt::ext::subxt_core::utils::MultiAddress::Id(to_account_id),
292			value: amount,
293		});
294
295		calls.push(transfer_call);
296	}
297
298	log_verbose!("✍️  Creating batch extrinsic with {} calls...", calls.len());
299
300	// Create the batch call using utility pallet
301	let batch_call = quantus_subxt::api::tx().utility().batch(calls);
302
303	// Use provided tip or default tip
304	let tip_to_use = tip.unwrap_or(10_000_000_000);
305
306	// Submit the batch transaction
307	let tx_hash = crate::cli::common::submit_transaction(
308		quantus_client,
309		from_keypair,
310		batch_call,
311		Some(tip_to_use),
312	)
313	.await?;
314
315	log_verbose!("📋 Batch transaction submitted: {:?}", tx_hash);
316
317	Ok(tx_hash)
318}
319
320// (Removed custom `AccountData` struct – we now use the runtime-generated type)
321
322/// Handle the send command
323pub async fn handle_send_command(
324	from_wallet: String,
325	to_address: String,
326	amount_str: &str,
327	node_url: &str,
328	password: Option<String>,
329	password_file: Option<String>,
330	tip: Option<String>,
331	nonce: Option<u32>,
332) -> Result<()> {
333	// Create quantus chain client
334	let quantus_client = QuantusClient::new(node_url).await?;
335
336	// Parse and validate the amount
337	let (amount, formatted_amount) =
338		validate_and_format_amount(&quantus_client, amount_str).await?;
339
340	// Resolve the destination address (could be wallet name or SS58 address)
341	let resolved_address = resolve_address(&to_address)?;
342
343	log_info!("🚀 Initiating transfer of {} to {}", formatted_amount, resolved_address);
344	log_verbose!(
345		"🚀 {} Sending {} to {}",
346		"SEND".bright_cyan().bold(),
347		formatted_amount.bright_yellow().bold(),
348		resolved_address.bright_green()
349	);
350
351	// Get password securely for decryption
352	log_verbose!("📦 Using wallet: {}", from_wallet.bright_blue().bold());
353	let keypair = crate::wallet::load_keypair_from_wallet(&from_wallet, password, password_file)?;
354
355	// Get account information
356	let from_account_id = keypair.to_account_id_ss58check();
357	let balance = get_balance(&quantus_client, &from_account_id).await?;
358
359	// Get formatted balance with proper decimals
360	let formatted_balance = format_balance_with_symbol(&quantus_client, balance).await?;
361	log_verbose!("💰 Current balance: {}", formatted_balance.bright_yellow());
362
363	if balance < amount {
364		return Err(crate::error::QuantusError::InsufficientBalance {
365			available: balance,
366			required: amount,
367		});
368	}
369
370	// Create and submit transaction
371	log_verbose!("✍️  {} Signing transaction...", "SIGN".bright_magenta().bold());
372
373	// Parse tip amount if provided
374	let tip_amount = if let Some(tip_str) = &tip {
375		// Get chain properties for proper decimal parsing
376		let (_, decimals) = get_chain_properties(&quantus_client).await?;
377		parse_amount_with_decimals(tip_str, decimals).ok()
378	} else {
379		None
380	};
381
382	// Submit transaction
383	let tx_hash = transfer_with_nonce(
384		&quantus_client,
385		&keypair,
386		&resolved_address,
387		amount,
388		tip_amount,
389		nonce,
390	)
391	.await?;
392
393	log_print!("✅ {} Transaction submitted! Hash: {:?}", "SUCCESS".bright_green().bold(), tx_hash);
394
395	let success = wait_for_tx_confirmation(quantus_client.client(), tx_hash).await?;
396
397	if success {
398		log_info!("✅ Transaction confirmed and finalized on chain");
399		log_success!("🎉 {} Transaction confirmed!", "FINISHED".bright_green().bold());
400
401		// Show updated balance with proper formatting
402		let new_balance = get_balance(&quantus_client, &from_account_id).await?;
403		let formatted_new_balance =
404			format_balance_with_symbol(&quantus_client, new_balance).await?;
405
406		// Calculate and display transaction fee in verbose mode
407		let fee_paid = balance.saturating_sub(new_balance).saturating_sub(amount);
408		if fee_paid > 0 {
409			let formatted_fee = format_balance_with_symbol(&quantus_client, fee_paid).await?;
410			log_verbose!("💸 Transaction fee: {}", formatted_fee.bright_cyan());
411		}
412
413		log_print!("💰 New balance: {}", formatted_new_balance.bright_yellow());
414	} else {
415		log_error!("Transaction failed!");
416	}
417
418	Ok(())
419}
420
421/// Load transfers from JSON file
422pub async fn load_transfers_from_file(file_path: &str) -> Result<Vec<(String, u128)>> {
423	use serde_json;
424	use std::fs;
425
426	#[derive(serde::Deserialize)]
427	struct TransferEntry {
428		to: String,
429		amount: String,
430	}
431
432	let content = fs::read_to_string(file_path).map_err(|e| {
433		crate::error::QuantusError::Generic(format!("Failed to read batch file: {e:?}"))
434	})?;
435
436	let entries: Vec<TransferEntry> = serde_json::from_str(&content).map_err(|e| {
437		crate::error::QuantusError::Generic(format!("Failed to parse batch file JSON: {e:?}"))
438	})?;
439
440	let mut transfers = Vec::new();
441	for entry in entries {
442		// Parse amount as raw units (no decimals conversion here)
443		let amount = entry.amount.parse::<u128>().map_err(|e| {
444			crate::error::QuantusError::Generic(format!("Invalid amount '{}': {e:?}", entry.amount))
445		})?;
446		transfers.push((entry.to, amount));
447	}
448
449	Ok(transfers)
450}
451
452/// Get chain constants for batch limits
453pub async fn get_batch_limits(quantus_client: &QuantusClient) -> Result<(u32, u32)> {
454	// Try to get actual chain constants
455	let constants = quantus_client.client().constants();
456
457	// Get block weight limit
458	let block_weight_limit = constants
459		.at(&quantus_subxt::api::constants().system().block_weights())
460		.map(|weights| weights.max_block.ref_time)
461		.unwrap_or(2_000_000_000_000); // Default 2 trillion weight units
462
463	// Estimate transfers per block (rough calculation)
464	let transfer_weight = 1_500_000_000u64; // Rough estimate per transfer
465	let max_transfers_by_weight = (block_weight_limit / transfer_weight) as u32;
466
467	// Get max extrinsic length
468	let max_extrinsic_length = constants
469		.at(&quantus_subxt::api::constants().system().block_length())
470		.map(|length| length.max.normal)
471		.unwrap_or(5_242_880); // Default 5MB
472
473	// Estimate transfers per extrinsic size (very rough)
474	let transfer_size = 100u32; // Rough estimate per transfer in bytes
475	let max_transfers_by_size = max_extrinsic_length / transfer_size;
476
477	let recommended_limit = std::cmp::min(max_transfers_by_weight, max_transfers_by_size);
478	let safe_limit = recommended_limit / 2; // Be conservative
479
480	log_verbose!(
481		"📊 Chain limits: weight allows ~{}, size allows ~{}",
482		max_transfers_by_weight,
483		max_transfers_by_size
484	);
485	log_verbose!("📊 Recommended batch size: {} (safe: {})", recommended_limit, safe_limit);
486
487	Ok((safe_limit, recommended_limit))
488}