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
10pub 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 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 let bytes: [u8; 32] = *account_id_sp.as_ref();
26 let account_id = subxt::ext::subxt_core::utils::AccountId32::from(bytes);
27
28 let storage_addr = api::storage().system().account(account_id);
30
31 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
43pub async fn get_chain_properties(quantus_client: &QuantusClient) -> Result<(String, u8)> {
45 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
63pub 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
73pub 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
97pub 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
103pub 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
141pub 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#[allow(dead_code)] pub 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
163pub 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 let resolved_address = resolve_address(to_address)?;
179 log_verbose!(" Resolved to: {}", resolved_address.bright_green());
180
181 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 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 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 let tip_to_use = tip.unwrap_or(10_000_000_000); 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
229pub async fn batch_transfer(
231 quantus_client: &QuantusClient,
232 from_keypair: &crate::wallet::QuantumKeyPair,
233 transfers: Vec<(String, u128)>, 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 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 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 let mut calls = Vec::new();
270 for (to_address, amount) in transfers {
271 log_verbose!(" To: {} Amount: {}", to_address.bright_green(), amount);
272
273 let resolved_address = crate::cli::common::resolve_address(&to_address)?;
275
276 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 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 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 let batch_call = quantus_subxt::api::tx().utility().batch(calls);
304
305 let tip_to_use = tip.unwrap_or(10_000_000_000);
307
308 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
322pub 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 let quantus_client = QuantusClient::new(node_url).await?;
337
338 let (amount, formatted_amount) =
340 validate_and_format_amount(&quantus_client, amount_str).await?;
341
342 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 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 let from_account_id = keypair.to_account_id_ss58check();
359 let balance = get_balance(&quantus_client, &from_account_id).await?;
360
361 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 log_verbose!("✍️ {} Signing transaction...", "SIGN".bright_magenta().bold());
374
375 let tip_amount = if let Some(tip_str) = &tip {
377 let (_, decimals) = get_chain_properties(&quantus_client).await?;
379 parse_amount_with_decimals(tip_str, decimals).ok()
380 } else {
381 None
382 };
383
384 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 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 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
423pub 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 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
454pub async fn get_batch_limits(quantus_client: &QuantusClient) -> Result<(u32, u32)> {
456 let constants = quantus_client.client().constants();
458
459 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); let transfer_weight = 1_500_000_000u64; let max_transfers_by_weight = (block_weight_limit / transfer_weight) as u32;
468
469 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); let transfer_size = 100u32; 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; 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}