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