Skip to main content

quantus_cli/cli/
send.rs

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