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
10pub struct AccountBalanceData {
12 pub free: u128,
13 pub reserved: u128,
14 pub frozen: u128,
15}
16
17pub 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 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 let bytes: [u8; 32] = *account_id_sp.as_ref();
36 let account_id = subxt::ext::subxt_core::utils::AccountId32::from(bytes);
37
38 let storage_addr = api::storage().system().account(account_id);
40
41 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
57pub 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
63pub async fn get_chain_properties(quantus_client: &QuantusClient) -> Result<(String, u8)> {
65 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
83pub 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
93pub 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
117pub 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
123pub 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
161pub 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#[allow(dead_code)] pub 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
185pub 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 let resolved_address = resolve_address(to_address)?;
202 log_verbose!(" Resolved to: {}", resolved_address.bright_green());
203
204 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 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 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 let tip_to_use = tip.unwrap_or(10_000_000_000); 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
254pub async fn batch_transfer(
256 quantus_client: &QuantusClient,
257 from_keypair: &crate::wallet::QuantumKeyPair,
258 transfers: Vec<(String, u128)>, 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 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 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 let mut calls = Vec::new();
296 for (to_address, amount) in transfers {
297 log_verbose!(" To: {} Amount: {}", to_address.bright_green(), amount);
298
299 let resolved_address = crate::cli::common::resolve_address(&to_address)?;
301
302 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 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 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 let batch_call = quantus_subxt::api::tx().utility().batch(calls);
330
331 let tip_to_use = tip.unwrap_or(10_000_000_000);
333
334 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
349pub 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 let quantus_client = QuantusClient::new(node_url).await?;
365
366 let (amount, formatted_amount) =
368 validate_and_format_amount(&quantus_client, amount_str).await?;
369
370 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 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 let from_account_id = keypair.to_account_id_ss58check();
387 let balance = get_balance(&quantus_client, &from_account_id).await?;
388
389 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 log_verbose!("✍️ {} Signing transaction...", "SIGN".bright_magenta().bold());
402
403 let tip_amount = if let Some(tip_str) = &tip {
405 let (_, decimals) = get_chain_properties(&quantus_client).await?;
407 parse_amount_with_decimals(tip_str, decimals).ok()
408 } else {
409 None
410 };
411
412 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 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 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
443pub 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 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
474pub async fn get_batch_limits(quantus_client: &QuantusClient) -> Result<(u32, u32)> {
476 let constants = quantus_client.client().constants();
478
479 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); let transfer_weight = 1_500_000_000u64; let max_transfers_by_weight = (block_weight_limit / transfer_weight) as u32;
488
489 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); let transfer_size = 100u32; 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; 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}