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 = SpAccountId32::from_ss58check(to_address).map_err(|e| {
125 crate::error::QuantusError::NetworkError(format!("Invalid destination address: {e:?}"))
126 })?;
127
128 let to_account_id_bytes: [u8; 32] = *to_account_id_sp.as_ref();
130 let to_account_id = subxt::ext::subxt_core::utils::AccountId32::from(to_account_id_bytes);
131
132 log_verbose!("✍️ Creating reversible transfer extrinsic...");
133
134 let transfer_call = quantus_subxt::api::tx()
136 .reversible_transfers()
137 .schedule_transfer(subxt::ext::subxt_core::utils::MultiAddress::Id(to_account_id), amount);
138
139 let tx_hash =
141 crate::cli::common::submit_transaction(quantus_client, from_keypair, transfer_call, None)
142 .await?;
143
144 log_verbose!("📋 Reversible transfer submitted: {:?}", tx_hash);
145
146 Ok(tx_hash)
147}
148
149pub async fn cancel_transaction(
151 quantus_client: &crate::chain::client::QuantusClient,
152 from_keypair: &crate::wallet::QuantumKeyPair,
153 tx_id: &str,
154) -> Result<subxt::utils::H256> {
155 log_verbose!("❌ Cancelling reversible transfer...");
156 log_verbose!(" Transaction ID: {}", tx_id.bright_yellow());
157
158 let tx_hash = subxt::utils::H256::from_str(tx_id).map_err(|e| {
160 crate::error::QuantusError::Generic(format!("Invalid transaction ID: {e:?}"))
161 })?;
162
163 log_verbose!("✍️ Creating cancel transaction extrinsic...");
164
165 let cancel_call = quantus_subxt::api::tx().reversible_transfers().cancel(tx_hash);
167
168 let tx_hash_result =
170 crate::cli::common::submit_transaction(quantus_client, from_keypair, cancel_call, None)
171 .await?;
172
173 log_verbose!("📋 Cancel transaction submitted: {:?}", tx_hash_result);
174
175 Ok(tx_hash_result)
176}
177
178pub async fn schedule_transfer_with_delay(
180 quantus_client: &crate::chain::client::QuantusClient,
181 from_keypair: &crate::wallet::QuantumKeyPair,
182 to_address: &str,
183 amount: u128,
184 delay: u64,
185 unit_blocks: bool,
186) -> Result<subxt::utils::H256> {
187 let unit_str = if unit_blocks { "blocks" } else { "seconds" };
188 log_verbose!("🔄 Creating reversible transfer with custom delay ...");
189 log_verbose!(" From: {}", from_keypair.to_account_id_ss58check().bright_cyan());
190 log_verbose!(" To: {}", to_address.bright_green());
191 log_verbose!(" Amount: {}", amount);
192 log_verbose!(" Delay: {} {}", delay, unit_str);
193
194 let to_account_id_sp = SpAccountId32::from_ss58check(to_address).map_err(|e| {
196 crate::error::QuantusError::NetworkError(format!("Invalid destination address: {e:?}"))
197 })?;
198 let to_account_id_bytes: [u8; 32] = *to_account_id_sp.as_ref();
199 let to_account_id_subxt = subxt::ext::subxt_core::utils::AccountId32::from(to_account_id_bytes);
200
201 let delay_value = if unit_blocks {
203 quantus_subxt::api::reversible_transfers::calls::types::schedule_transfer_with_delay::Delay::BlockNumber(delay as u32)
204 } else {
205 quantus_subxt::api::reversible_transfers::calls::types::schedule_transfer_with_delay::Delay::Timestamp(delay * 1000)
207 };
208
209 log_verbose!("✍️ Creating schedule_transfer_with_delay extrinsic...");
210
211 let transfer_call =
213 quantus_subxt::api::tx().reversible_transfers().schedule_transfer_with_delay(
214 subxt::ext::subxt_core::utils::MultiAddress::Id(to_account_id_subxt),
215 amount,
216 delay_value,
217 );
218
219 let tx_hash =
221 crate::cli::common::submit_transaction(quantus_client, from_keypair, transfer_call, None)
222 .await?;
223
224 log_verbose!("📋 Reversible transfer with custom delay submitted: {:?}", tx_hash);
225
226 Ok(tx_hash)
227}
228
229pub async fn handle_reversible_command(command: ReversibleCommands, node_url: &str) -> Result<()> {
231 log_print!("🔄 Reversible Transfers");
232
233 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
234
235 match command {
236 ReversibleCommands::ListPending { address, from, password, password_file } =>
237 list_pending_transactions(&quantus_client, address, from, password, password_file).await,
238 ReversibleCommands::ScheduleTransfer { to, amount, from, password, password_file } => {
239 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
241 let (raw_amount, formatted_amount) =
242 crate::cli::send::validate_and_format_amount(&quantus_client, &amount).await?;
243
244 let resolved_address = resolve_address(&to)?;
246
247 log_info!(
248 "🔄 Scheduling reversible transfer of {} to {}",
249 formatted_amount,
250 resolved_address
251 );
252 log_verbose!(
253 "🚀 {} Scheduling reversible transfer {} to {} ()",
254 "REVERSIBLE".bright_cyan().bold(),
255 formatted_amount.bright_yellow().bold(),
256 resolved_address.bright_green()
257 );
258
259 log_verbose!("📦 Using wallet: {}", from.bright_blue().bold());
261 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
262
263 let tx_hash =
265 schedule_transfer(&quantus_client, &keypair, &resolved_address, raw_amount).await?;
266
267 log_print!(
268 "✅ {} Reversible transfer scheduled! Hash: {:?}",
269 "SUCCESS".bright_green().bold(),
270 tx_hash
271 );
272
273 let success = wait_for_tx_confirmation(quantus_client.client(), tx_hash).await?;
274
275 if success {
276 log_info!("✅ Reversible transfer scheduled and confirmed on chain");
277 log_success!(
278 "🎉 {} Reversible transfer confirmed!",
279 "FINISHED".bright_green().bold()
280 );
281 } else {
282 log_error!("Transaction failed!");
283 }
284
285 Ok(())
286 },
287 ReversibleCommands::Cancel { tx_id, from, password, password_file } => {
288 log_verbose!(
289 "❌ {} Cancelling reversible transfer {} ()",
290 "CANCEL".bright_red().bold(),
291 tx_id.bright_yellow().bold()
292 );
293
294 log_verbose!("📦 Using wallet: {}", from.bright_blue().bold());
296 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
297
298 let tx_hash = cancel_transaction(&quantus_client, &keypair, &tx_id).await?;
300
301 log_print!(
302 "✅ {} Cancel transaction submitted! Hash: {:?}",
303 "SUCCESS".bright_green().bold(),
304 tx_hash
305 );
306
307 let success = wait_for_tx_confirmation(quantus_client.client(), tx_hash).await?;
308
309 if success {
310 log_success!(
311 "🎉 {} Cancel transaction confirmed!",
312 "FINISHED".bright_green().bold()
313 );
314 } else {
315 log_error!("Transaction failed!");
316 }
317
318 Ok(())
319 },
320
321 ReversibleCommands::ScheduleTransferWithDelay {
322 to,
323 amount,
324 delay,
325 unit_blocks,
326 from,
327 password,
328 password_file,
329 } => {
330 let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
332 let (raw_amount, formatted_amount) =
333 crate::cli::send::validate_and_format_amount(&quantus_client, &amount).await?;
334
335 let resolved_address = resolve_address(&to)?;
337
338 let unit_str = if unit_blocks { "blocks" } else { "seconds" };
339 log_verbose!(
340 "🚀 {} Scheduling reversible transfer {} to {} with {} {} delay ()",
341 "REVERSIBLE".bright_cyan().bold(),
342 formatted_amount.bright_yellow().bold(),
343 resolved_address.bright_green(),
344 delay.to_string().bright_magenta(),
345 unit_str
346 );
347
348 log_verbose!("📦 Using wallet: {}", from.bright_blue().bold());
350 let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
351
352 let tx_hash = schedule_transfer_with_delay(
354 &quantus_client,
355 &keypair,
356 &resolved_address,
357 raw_amount,
358 delay,
359 unit_blocks,
360 )
361 .await?;
362
363 log_print!(
364 "✅ {} Reversible transfer with custom delay scheduled! Hash: {:?}",
365 "SUCCESS".bright_green().bold(),
366 tx_hash
367 );
368
369 let success = wait_for_tx_confirmation(quantus_client.client(), tx_hash).await?;
370
371 if success {
372 log_success!(
373 "🎉 {} Reversible transfer with custom delay confirmed!",
374 "FINISHED".bright_green().bold()
375 );
376
377 if unit_blocks {
378 log_print!("⏰ Transfer will execute after {} {}", delay, unit_str);
379 } else {
380 let now = chrono::Local::now();
381 let completion_time = now + chrono::Duration::seconds(delay as i64);
382 log_print!(
383 "⏰ Transfer will execute in ~{} seconds, at approximately {}",
384 delay,
385 completion_time.format("%Y-%m-%d %H:%M:%S").to_string().italic().dimmed()
386 );
387 }
388 } else {
389 log_error!("Transaction failed!");
390 }
391
392 Ok(())
393 },
394 }
395}
396
397async fn list_pending_transactions(
399 quantus_client: &crate::chain::client::QuantusClient,
400 address: Option<String>,
401 wallet_name: Option<String>,
402 password: Option<String>,
403 password_file: Option<String>,
404) -> Result<()> {
405 log_print!("📋 Listing pending reversible transactions");
406
407 let target_address = match (address, wallet_name) {
409 (Some(addr), _) => {
410 SpAccountId32::from_ss58check(&addr).map_err(|e| {
412 crate::error::QuantusError::Generic(format!("Invalid address: {e:?}"))
413 })?;
414 addr
415 },
416 (None, Some(wallet)) => {
417 let keypair =
419 crate::wallet::load_keypair_from_wallet(&wallet, password, password_file)?;
420 keypair.to_account_id_ss58check()
421 },
422 (None, None) => {
423 return Err(crate::error::QuantusError::Generic(
424 "Either --address or --from must be provided".to_string(),
425 ));
426 },
427 };
428
429 let account_id_sp = SpAccountId32::from_ss58check(&target_address)
431 .map_err(|e| crate::error::QuantusError::Generic(format!("Invalid address: {e:?}")))?;
432 let account_id_bytes: [u8; 32] = *account_id_sp.as_ref();
433 let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_id_bytes);
434
435 log_verbose!("🔍 Querying pending transfers for: {}", target_address);
436
437 let sender_storage_address = crate::chain::quantus_subxt::api::storage()
439 .reversible_transfers()
440 .pending_transfers_by_sender(account_id.clone());
441
442 let latest_block_hash = quantus_client.get_latest_block().await?;
444
445 let outgoing_transfers = quantus_client
446 .client()
447 .storage()
448 .at(latest_block_hash)
449 .fetch(&sender_storage_address)
450 .await
451 .map_err(|e| crate::error::QuantusError::NetworkError(format!("Fetch error: {e:?}")))?;
452
453 let recipient_storage_address = crate::chain::quantus_subxt::api::storage()
455 .reversible_transfers()
456 .pending_transfers_by_recipient(account_id);
457
458 let incoming_transfers = quantus_client
459 .client()
460 .storage()
461 .at(latest_block_hash)
462 .fetch(&recipient_storage_address)
463 .await
464 .map_err(|e| crate::error::QuantusError::NetworkError(format!("Fetch error: {e:?}")))?;
465
466 let mut total_transfers = 0;
467
468 if let Some(outgoing_hashes) = outgoing_transfers {
470 if !outgoing_hashes.0.is_empty() {
471 log_print!("📤 Outgoing pending transfers:");
472 for (i, hash) in outgoing_hashes.0.iter().enumerate() {
473 total_transfers += 1;
474 log_print!(" {}. 0x{}", i + 1, hex::encode(hash.as_ref()));
475
476 let transfer_storage_address = crate::chain::quantus_subxt::api::storage()
478 .reversible_transfers()
479 .pending_transfers(*hash);
480
481 if let Ok(Some(transfer_details)) = quantus_client
482 .client()
483 .storage()
484 .at(latest_block_hash)
485 .fetch(&transfer_storage_address)
486 .await
487 .map_err(|e| {
488 crate::error::QuantusError::NetworkError(format!("Fetch error: {e:?}"))
489 }) {
490 let formatted_amount = format_amount(transfer_details.amount);
491 log_print!(" 👤 To: {}", transfer_details.to.to_quantus_ss58());
492 log_print!(" 💰 Amount: {}", formatted_amount);
493 log_print!(
494 " 🔄 Interceptor: {}",
495 transfer_details.interceptor.to_quantus_ss58()
496 );
497 }
498 }
499 }
500 }
501
502 if let Some(incoming_hashes) = incoming_transfers {
504 if !incoming_hashes.0.is_empty() {
505 if total_transfers > 0 {
506 log_print!("");
507 }
508 log_print!("📥 Incoming pending transfers:");
509 for (i, hash) in incoming_hashes.0.iter().enumerate() {
510 total_transfers += 1;
511 log_print!(" {}. 0x{}", i + 1, hex::encode(hash.as_ref()));
512
513 let transfer_storage_address = crate::chain::quantus_subxt::api::storage()
515 .reversible_transfers()
516 .pending_transfers(*hash);
517
518 if let Ok(Some(transfer_details)) = quantus_client
519 .client()
520 .storage()
521 .at(latest_block_hash)
522 .fetch(&transfer_storage_address)
523 .await
524 .map_err(|e| {
525 crate::error::QuantusError::NetworkError(format!("Fetch error: {e:?}"))
526 }) {
527 let formatted_amount = format_amount(transfer_details.amount);
528 log_print!(" 👤 From: {}", transfer_details.from.to_quantus_ss58());
529 log_print!(" 💰 Amount: {}", formatted_amount);
530 log_print!(
531 " 🔄 Interceptor: {}",
532 transfer_details.interceptor.to_quantus_ss58()
533 );
534 }
535 }
536 }
537 }
538
539 if total_transfers == 0 {
540 log_print!("📝 No pending transfers found for account: {}", target_address);
541 } else {
542 log_print!("");
543 log_print!("📊 Total pending transfers: {}", total_transfers);
544 log_print!("💡 Use transaction hash with 'quantus reversible cancel --tx-id <hash>' to cancel outgoing transfers");
545 }
546
547 Ok(())
548}
549
550fn format_amount(amount: u128) -> String {
552 const QUAN_DECIMALS: u128 = 1_000_000_000_000; if amount >= QUAN_DECIMALS {
555 let whole = amount / QUAN_DECIMALS;
556 let fractional = amount % QUAN_DECIMALS;
557
558 if fractional == 0 {
559 format!("{whole} QUAN")
560 } else {
561 let fractional_str = format!("{fractional:012}");
563 let trimmed = fractional_str.trim_end_matches('0');
564 format!("{whole}.{trimmed} QUAN")
565 }
566 } else {
567 format!("{amount} pico-QUAN")
568 }
569}