1use crate::{
2 chain::quantus_subxt,
3 cli::{
4 address_format::QuantusSS58, common::resolve_address,
5 progress_spinner::wait_for_tx_confirmation,
6 },
7 error::Result,
8 log_error, log_info, log_print, log_success, log_verbose,
9};
10use clap::Subcommand;
11use colored::Colorize;
12use sp_core::crypto::{AccountId32 as SpAccountId32, Ss58Codec};
13use std::str::FromStr;
14
15#[derive(Subcommand, Debug)]
17pub enum ReversibleCommands {
18 ScheduleTransfer {
20 #[arg(short, long)]
22 to: String,
23
24 #[arg(short, long)]
26 amount: String,
27
28 #[arg(short, long)]
30 from: String,
31
32 #[arg(short, long)]
34 password: Option<String>,
35
36 #[arg(long)]
38 password_file: Option<String>,
39 },
40
41 ScheduleTransferWithDelay {
43 #[arg(short, long)]
45 to: String,
46
47 #[arg(short, long)]
49 amount: String,
50
51 #[arg(short, long)]
53 delay: u64,
54
55 #[arg(long)]
57 unit_blocks: bool,
58
59 #[arg(short, long)]
61 from: String,
62
63 #[arg(short, long)]
65 password: Option<String>,
66
67 #[arg(long)]
69 password_file: Option<String>,
70 },
71
72 Cancel {
74 #[arg(long)]
76 tx_id: String,
77
78 #[arg(short, long)]
80 from: String,
81
82 #[arg(short, long)]
84 password: Option<String>,
85
86 #[arg(long)]
88 password_file: Option<String>,
89 },
90
91 ListPending {
93 #[arg(short, long)]
95 address: Option<String>,
96
97 #[arg(short, long)]
99 from: Option<String>,
100
101 #[arg(short, long)]
103 password: Option<String>,
104
105 #[arg(long)]
107 password_file: Option<String>,
108 },
109}
110
111pub async fn schedule_transfer(
113 quantus_client: &crate::chain::client::QuantusClient,
114 from_keypair: &crate::wallet::QuantumKeyPair,
115 to_address: &str,
116 amount: u128,
117) -> Result<subxt::utils::H256> {
118 log_verbose!("🔄 Creating reversible transfer...");
119 log_verbose!(" From: {}", from_keypair.to_account_id_ss58check().bright_cyan());
120 log_verbose!(" To: {}", to_address.bright_green());
121 log_verbose!(" Amount: {}", amount);
122
123 let (to_account_id_sp, _version) = SpAccountId32::from_ss58check_with_version(to_address)
125 .map_err(|e| {
126 crate::error::QuantusError::NetworkError(format!("Invalid destination address: {e:?}"))
127 })?;
128
129 let to_account_id_bytes: [u8; 32] = *to_account_id_sp.as_ref();
131 let to_account_id = subxt::ext::subxt_core::utils::AccountId32::from(to_account_id_bytes);
132
133 log_verbose!("✍️ Creating reversible transfer extrinsic...");
134
135 let transfer_call = quantus_subxt::api::tx()
137 .reversible_transfers()
138 .schedule_transfer(subxt::ext::subxt_core::utils::MultiAddress::Id(to_account_id), amount);
139
140 let tx_hash =
142 crate::cli::common::submit_transaction(quantus_client, from_keypair, transfer_call, None)
143 .await?;
144
145 log_verbose!("📋 Reversible transfer submitted: {:?}", tx_hash);
146
147 Ok(tx_hash)
148}
149
150pub async fn cancel_transaction(
152 quantus_client: &crate::chain::client::QuantusClient,
153 from_keypair: &crate::wallet::QuantumKeyPair,
154 tx_id: &str,
155) -> Result<subxt::utils::H256> {
156 log_verbose!("❌ Cancelling reversible transfer...");
157 log_verbose!(" Transaction ID: {}", tx_id.bright_yellow());
158
159 let tx_hash = subxt::utils::H256::from_str(tx_id).map_err(|e| {
161 crate::error::QuantusError::Generic(format!("Invalid transaction ID: {e:?}"))
162 })?;
163
164 log_verbose!("✍️ Creating cancel transaction extrinsic...");
165
166 let cancel_call = quantus_subxt::api::tx().reversible_transfers().cancel(tx_hash);
168
169 let tx_hash_result =
171 crate::cli::common::submit_transaction(quantus_client, from_keypair, cancel_call, None)
172 .await?;
173
174 log_verbose!("📋 Cancel transaction submitted: {:?}", tx_hash_result);
175
176 Ok(tx_hash_result)
177}
178
179pub async fn schedule_transfer_with_delay(
181 quantus_client: &crate::chain::client::QuantusClient,
182 from_keypair: &crate::wallet::QuantumKeyPair,
183 to_address: &str,
184 amount: u128,
185 delay: u64,
186 unit_blocks: bool,
187) -> Result<subxt::utils::H256> {
188 let unit_str = if unit_blocks { "blocks" } else { "seconds" };
189 log_verbose!("🔄 Creating reversible transfer with custom delay ...");
190 log_verbose!(" From: {}", from_keypair.to_account_id_ss58check().bright_cyan());
191 log_verbose!(" To: {}", to_address.bright_green());
192 log_verbose!(" Amount: {}", amount);
193 log_verbose!(" Delay: {} {}", delay, unit_str);
194
195 let to_account_id_sp = SpAccountId32::from_ss58check(to_address).map_err(|e| {
197 crate::error::QuantusError::NetworkError(format!("Invalid destination address: {e:?}"))
198 })?;
199 let to_account_id_bytes: [u8; 32] = *to_account_id_sp.as_ref();
200 let to_account_id_subxt = subxt::ext::subxt_core::utils::AccountId32::from(to_account_id_bytes);
201
202 let delay_value = if unit_blocks {
204 quantus_subxt::api::reversible_transfers::calls::types::schedule_transfer_with_delay::Delay::BlockNumber(delay as u32)
205 } else {
206 quantus_subxt::api::reversible_transfers::calls::types::schedule_transfer_with_delay::Delay::Timestamp(delay * 1000)
208 };
209
210 log_verbose!("✍️ Creating schedule_transfer_with_delay extrinsic...");
211
212 let transfer_call =
214 quantus_subxt::api::tx().reversible_transfers().schedule_transfer_with_delay(
215 subxt::ext::subxt_core::utils::MultiAddress::Id(to_account_id_subxt),
216 amount,
217 delay_value,
218 );
219
220 let tx_hash =
222 crate::cli::common::submit_transaction(quantus_client, from_keypair, transfer_call, None)
223 .await?;
224
225 log_verbose!("📋 Reversible transfer with custom delay submitted: {:?}", tx_hash);
226
227 Ok(tx_hash)
228}
229
230pub async fn handle_reversible_command(command: ReversibleCommands, node_url: &str) -> Result<()> {
232 log_print!("🔄 Reversible Transfers");
233
234 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
235
236 match command {
237 ReversibleCommands::ListPending { address, from, password, password_file } =>
238 list_pending_transactions(&quantus_client, address, from, password, password_file).await,
239 ReversibleCommands::ScheduleTransfer { to, amount, from, password, password_file } => {
240 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
242 let (raw_amount, formatted_amount) =
243 crate::cli::send::validate_and_format_amount(&quantus_client, &amount).await?;
244
245 let resolved_address = resolve_address(&to)?;
247
248 log_info!(
249 "🔄 Scheduling reversible transfer of {} to {}",
250 formatted_amount,
251 resolved_address
252 );
253 log_verbose!(
254 "🚀 {} Scheduling reversible transfer {} to {} ()",
255 "REVERSIBLE".bright_cyan().bold(),
256 formatted_amount.bright_yellow().bold(),
257 resolved_address.bright_green()
258 );
259
260 log_verbose!("📦 Using wallet: {}", from.bright_blue().bold());
262 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
263
264 let tx_hash =
266 schedule_transfer(&quantus_client, &keypair, &resolved_address, raw_amount).await?;
267
268 log_print!(
269 "✅ {} Reversible transfer scheduled! Hash: {:?}",
270 "SUCCESS".bright_green().bold(),
271 tx_hash
272 );
273
274 let success = wait_for_tx_confirmation(quantus_client.client(), tx_hash).await?;
275
276 if success {
277 log_info!("✅ Reversible transfer scheduled and confirmed on chain");
278 log_success!(
279 "🎉 {} Reversible transfer confirmed!",
280 "FINISHED".bright_green().bold()
281 );
282 } else {
283 log_error!("Transaction failed!");
284 }
285
286 Ok(())
287 },
288 ReversibleCommands::Cancel { tx_id, from, password, password_file } => {
289 log_verbose!(
290 "❌ {} Cancelling reversible transfer {} ()",
291 "CANCEL".bright_red().bold(),
292 tx_id.bright_yellow().bold()
293 );
294
295 log_verbose!("📦 Using wallet: {}", from.bright_blue().bold());
297 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
298
299 let tx_hash = cancel_transaction(&quantus_client, &keypair, &tx_id).await?;
301
302 log_print!(
303 "✅ {} Cancel transaction submitted! Hash: {:?}",
304 "SUCCESS".bright_green().bold(),
305 tx_hash
306 );
307
308 let success = wait_for_tx_confirmation(quantus_client.client(), tx_hash).await?;
309
310 if success {
311 log_success!(
312 "🎉 {} Cancel transaction confirmed!",
313 "FINISHED".bright_green().bold()
314 );
315 } else {
316 log_error!("Transaction failed!");
317 }
318
319 Ok(())
320 },
321
322 ReversibleCommands::ScheduleTransferWithDelay {
323 to,
324 amount,
325 delay,
326 unit_blocks,
327 from,
328 password,
329 password_file,
330 } => {
331 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
333 let (raw_amount, formatted_amount) =
334 crate::cli::send::validate_and_format_amount(&quantus_client, &amount).await?;
335
336 let resolved_address = resolve_address(&to)?;
338
339 let unit_str = if unit_blocks { "blocks" } else { "seconds" };
340 log_verbose!(
341 "🚀 {} Scheduling reversible transfer {} to {} with {} {} delay ()",
342 "REVERSIBLE".bright_cyan().bold(),
343 formatted_amount.bright_yellow().bold(),
344 resolved_address.bright_green(),
345 delay.to_string().bright_magenta(),
346 unit_str
347 );
348
349 log_verbose!("📦 Using wallet: {}", from.bright_blue().bold());
351 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
352
353 let tx_hash = schedule_transfer_with_delay(
355 &quantus_client,
356 &keypair,
357 &resolved_address,
358 raw_amount,
359 delay,
360 unit_blocks,
361 )
362 .await?;
363
364 log_print!(
365 "✅ {} Reversible transfer with custom delay scheduled! Hash: {:?}",
366 "SUCCESS".bright_green().bold(),
367 tx_hash
368 );
369
370 let success = wait_for_tx_confirmation(quantus_client.client(), tx_hash).await?;
371
372 if success {
373 log_success!(
374 "🎉 {} Reversible transfer with custom delay confirmed!",
375 "FINISHED".bright_green().bold()
376 );
377
378 if unit_blocks {
379 log_print!("⏰ Transfer will execute after {} {}", delay, unit_str);
380 } else {
381 let now = chrono::Local::now();
382 let completion_time = now + chrono::Duration::seconds(delay as i64);
383 log_print!(
384 "⏰ Transfer will execute in ~{} seconds, at approximately {}",
385 delay,
386 completion_time.format("%Y-%m-%d %H:%M:%S").to_string().italic().dimmed()
387 );
388 }
389 } else {
390 log_error!("Transaction failed!");
391 }
392
393 Ok(())
394 },
395 }
396}
397
398async fn list_pending_transactions(
400 quantus_client: &crate::chain::client::QuantusClient,
401 address: Option<String>,
402 wallet_name: Option<String>,
403 password: Option<String>,
404 password_file: Option<String>,
405) -> Result<()> {
406 log_print!("📋 Listing pending reversible transactions");
407
408 let target_address = match (address, wallet_name) {
410 (Some(addr), _) => {
411 SpAccountId32::from_ss58check(&addr).map_err(|e| {
413 crate::error::QuantusError::Generic(format!("Invalid address: {e:?}"))
414 })?;
415 addr
416 },
417 (None, Some(wallet)) => {
418 let keypair =
420 crate::wallet::load_keypair_from_wallet(&wallet, password, password_file)?;
421 keypair.to_account_id_ss58check()
422 },
423 (None, None) => {
424 return Err(crate::error::QuantusError::Generic(
425 "Either --address or --from must be provided".to_string(),
426 ));
427 },
428 };
429
430 let account_id_sp = SpAccountId32::from_ss58check(&target_address)
432 .map_err(|e| crate::error::QuantusError::Generic(format!("Invalid address: {e:?}")))?;
433 let account_id_bytes: [u8; 32] = *account_id_sp.as_ref();
434 let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_id_bytes);
435
436 log_verbose!("🔍 Querying pending transfers for: {}", target_address);
437
438 let sender_storage_address = crate::chain::quantus_subxt::api::storage()
440 .reversible_transfers()
441 .pending_transfers_by_sender(account_id.clone());
442
443 let latest_block_hash = quantus_client.get_latest_block().await?;
445
446 let outgoing_transfers = quantus_client
447 .client()
448 .storage()
449 .at(latest_block_hash)
450 .fetch(&sender_storage_address)
451 .await
452 .map_err(|e| crate::error::QuantusError::NetworkError(format!("Fetch error: {e:?}")))?;
453
454 let recipient_storage_address = crate::chain::quantus_subxt::api::storage()
456 .reversible_transfers()
457 .pending_transfers_by_recipient(account_id);
458
459 let incoming_transfers = quantus_client
460 .client()
461 .storage()
462 .at(latest_block_hash)
463 .fetch(&recipient_storage_address)
464 .await
465 .map_err(|e| crate::error::QuantusError::NetworkError(format!("Fetch error: {e:?}")))?;
466
467 let mut total_transfers = 0;
468
469 if let Some(outgoing_hashes) = outgoing_transfers {
471 if !outgoing_hashes.0.is_empty() {
472 log_print!("📤 Outgoing pending transfers:");
473 for (i, hash) in outgoing_hashes.0.iter().enumerate() {
474 total_transfers += 1;
475 log_print!(" {}. 0x{}", i + 1, hex::encode(hash.as_ref()));
476
477 let transfer_storage_address = crate::chain::quantus_subxt::api::storage()
479 .reversible_transfers()
480 .pending_transfers(*hash);
481
482 if let Ok(Some(transfer_details)) = quantus_client
483 .client()
484 .storage()
485 .at(latest_block_hash)
486 .fetch(&transfer_storage_address)
487 .await
488 .map_err(|e| {
489 crate::error::QuantusError::NetworkError(format!("Fetch error: {e:?}"))
490 }) {
491 let formatted_amount = format_amount(transfer_details.amount);
492 log_print!(" 👤 To: {}", transfer_details.to.to_quantus_ss58());
493 log_print!(" 💰 Amount: {}", formatted_amount);
494 log_print!(
495 " 🔄 Interceptor: {}",
496 transfer_details.interceptor.to_quantus_ss58()
497 );
498 }
499 }
500 }
501 }
502
503 if let Some(incoming_hashes) = incoming_transfers {
505 if !incoming_hashes.0.is_empty() {
506 if total_transfers > 0 {
507 log_print!("");
508 }
509 log_print!("📥 Incoming pending transfers:");
510 for (i, hash) in incoming_hashes.0.iter().enumerate() {
511 total_transfers += 1;
512 log_print!(" {}. 0x{}", i + 1, hex::encode(hash.as_ref()));
513
514 let transfer_storage_address = crate::chain::quantus_subxt::api::storage()
516 .reversible_transfers()
517 .pending_transfers(*hash);
518
519 if let Ok(Some(transfer_details)) = quantus_client
520 .client()
521 .storage()
522 .at(latest_block_hash)
523 .fetch(&transfer_storage_address)
524 .await
525 .map_err(|e| {
526 crate::error::QuantusError::NetworkError(format!("Fetch error: {e:?}"))
527 }) {
528 let formatted_amount = format_amount(transfer_details.amount);
529 log_print!(" 👤 From: {}", transfer_details.from.to_quantus_ss58());
530 log_print!(" 💰 Amount: {}", formatted_amount);
531 log_print!(
532 " 🔄 Interceptor: {}",
533 transfer_details.interceptor.to_quantus_ss58()
534 );
535 }
536 }
537 }
538 }
539
540 if total_transfers == 0 {
541 log_print!("📝 No pending transfers found for account: {}", target_address);
542 } else {
543 log_print!("");
544 log_print!("📊 Total pending transfers: {}", total_transfers);
545 log_print!("💡 Use transaction hash with 'quantus reversible cancel --tx-id <hash>' to cancel outgoing transfers");
546 }
547
548 Ok(())
549}
550
551fn format_amount(amount: u128) -> String {
553 const QUAN_DECIMALS: u128 = 1_000_000_000_000; if amount >= QUAN_DECIMALS {
556 let whole = amount / QUAN_DECIMALS;
557 let fractional = amount % QUAN_DECIMALS;
558
559 if fractional == 0 {
560 format!("{whole} QUAN")
561 } else {
562 let fractional_str = format!("{fractional:012}");
564 let trimmed = fractional_str.trim_end_matches('0');
565 format!("{whole}.{trimmed} QUAN")
566 }
567 } else {
568 format!("{amount} pico-QUAN")
569 }
570}