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 = 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 let bytes: [u8; 32] = *account_id_sp.as_ref();
25 let account_id = subxt::ext::subxt_core::utils::AccountId32::from(bytes);
26
27 let storage_addr = api::storage().system().account(account_id);
29
30 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
42pub async fn get_chain_properties(quantus_client: &QuantusClient) -> Result<(String, u8)> {
44 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
62pub 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
72pub 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
96pub 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
102pub 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
140pub 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#[allow(dead_code)] pub 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
162pub 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 let resolved_address = resolve_address(to_address)?;
178 log_verbose!(" Resolved to: {}", resolved_address.bright_green());
179
180 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 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 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 let tip_to_use = tip.unwrap_or(10_000_000_000); 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
227pub async fn batch_transfer(
229 quantus_client: &QuantusClient,
230 from_keypair: &crate::wallet::QuantumKeyPair,
231 transfers: Vec<(String, u128)>, 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 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 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 let mut calls = Vec::new();
268 for (to_address, amount) in transfers {
269 log_verbose!(" To: {} Amount: {}", to_address.bright_green(), amount);
270
271 let resolved_address = crate::cli::common::resolve_address(&to_address)?;
273
274 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 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 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 let batch_call = quantus_subxt::api::tx().utility().batch(calls);
302
303 let tip_to_use = tip.unwrap_or(10_000_000_000);
305
306 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
320pub 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 let quantus_client = QuantusClient::new(node_url).await?;
335
336 let (amount, formatted_amount) =
338 validate_and_format_amount(&quantus_client, amount_str).await?;
339
340 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 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 let from_account_id = keypair.to_account_id_ss58check();
357 let balance = get_balance(&quantus_client, &from_account_id).await?;
358
359 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 log_verbose!("✍️ {} Signing transaction...", "SIGN".bright_magenta().bold());
372
373 let tip_amount = if let Some(tip_str) = &tip {
375 let (_, decimals) = get_chain_properties(&quantus_client).await?;
377 parse_amount_with_decimals(tip_str, decimals).ok()
378 } else {
379 None
380 };
381
382 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 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 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
421pub 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 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
452pub async fn get_batch_limits(quantus_client: &QuantusClient) -> Result<(u32, u32)> {
454 let constants = quantus_client.client().constants();
456
457 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); let transfer_weight = 1_500_000_000u64; let max_transfers_by_weight = (block_weight_limit / transfer_weight) as u32;
466
467 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); let transfer_size = 100u32; 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; 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}