quantus_cli/cli/
reversible.rs

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/// Reversible transfer commands
16#[derive(Subcommand, Debug)]
17pub enum ReversibleCommands {
18	/// Schedule a transfer with default delay
19	ScheduleTransfer {
20		/// The recipient's account address
21		#[arg(short, long)]
22		to: String,
23
24		/// Amount to transfer (e.g., "10", "10.5", "0.0001")
25		#[arg(short, long)]
26		amount: String,
27
28		/// Wallet name to send from
29		#[arg(short, long)]
30		from: String,
31
32		/// Password for the wallet
33		#[arg(short, long)]
34		password: Option<String>,
35
36		/// Read password from file (for scripting)
37		#[arg(long)]
38		password_file: Option<String>,
39	},
40
41	/// Schedule a transfer with custom delay
42	ScheduleTransferWithDelay {
43		/// The recipient's account address
44		#[arg(short, long)]
45		to: String,
46
47		/// Amount to transfer (e.g., "10", "10.5", "0.0001")
48		#[arg(short, long)]
49		amount: String,
50
51		/// Delay in seconds (default) or blocks if --unit-blocks is specified
52		#[arg(short, long)]
53		delay: u64,
54
55		/// Use blocks instead of seconds for delay
56		#[arg(long)]
57		unit_blocks: bool,
58
59		/// Wallet name to send from
60		#[arg(short, long)]
61		from: String,
62
63		/// Password for the wallet
64		#[arg(short, long)]
65		password: Option<String>,
66
67		/// Read password from file (for scripting)
68		#[arg(long)]
69		password_file: Option<String>,
70	},
71
72	/// Cancel a pending reversible transaction
73	Cancel {
74		/// Transaction ID to cancel (hex hash)
75		#[arg(long)]
76		tx_id: String,
77
78		/// Wallet name to sign with
79		#[arg(short, long)]
80		from: String,
81
82		/// Password for the wallet
83		#[arg(short, long)]
84		password: Option<String>,
85
86		/// Read password from file (for scripting)
87		#[arg(long)]
88		password_file: Option<String>,
89	},
90
91	/// List all pending reversible transactions for an account
92	ListPending {
93		/// Account address to query (optional, uses wallet address if not provided)
94		#[arg(short, long)]
95		address: Option<String>,
96
97		/// Wallet name (used for address if --address not provided)
98		#[arg(short, long)]
99		from: Option<String>,
100
101		/// Password for the wallet
102		#[arg(short, long)]
103		password: Option<String>,
104
105		/// Read password from file (for scripting)
106		#[arg(long)]
107		password_file: Option<String>,
108	},
109}
110
111/// Schedule a transfer with default delay
112pub 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	// Parse the destination address
124	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	// Convert to subxt_core AccountId32
130	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	// Create the reversible transfer call using static API from quantus_subxt
136	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	// Submit the transaction
141	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
150/// Cancel a pending reversible transaction
151pub 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	// Parse transaction ID using H256::from_str
160	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	// Create the cancel transaction call using static API from quantus_subxt
167	let cancel_call = quantus_subxt::api::tx().reversible_transfers().cancel(tx_hash);
168
169	// Submit the transaction
170	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
179/// Schedule a transfer with custom delay
180pub 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	// Parse the destination address
196	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	// Convert delay to proper BlockNumberOrTimestamp
203	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		// Convert seconds to milliseconds for the runtime
207		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	// Create the schedule transfer with delay call using static API from quantus_subxt
213	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	// Submit the transaction
221	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
230/// Handle reversible transfer subxt commands
231pub 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			// Parse and validate the amount
241			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			// Resolve the destination address (could be wallet name or SS58 address)
246			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			// Get password securely for decryption
261			log_verbose!("📦 Using wallet: {}", from.bright_blue().bold());
262			let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
263
264			// Submit transaction
265			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			// Get password securely for decryption
296			log_verbose!("📦 Using wallet: {}", from.bright_blue().bold());
297			let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
298
299			// Submit cancel transaction
300			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			// Parse and validate the amount
332			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			// Resolve the destination address (could be wallet name or SS58 address)
337			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			// Get password securely for decryption
350			log_verbose!("📦 Using wallet: {}", from.bright_blue().bold());
351			let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
352
353			// Submit transaction
354			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
398/// List all pending reversible transactions for an account
399async 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	// Determine which address to query
409	let target_address = match (address, wallet_name) {
410		(Some(addr), _) => {
411			// Validate the provided address
412			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			// Load wallet and get its address
419			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	// Convert to AccountId32 for storage queries
431	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	// Query pending transfers by sender (outgoing)
439	let sender_storage_address = crate::chain::quantus_subxt::api::storage()
440		.reversible_transfers()
441		.pending_transfers_by_sender(account_id.clone());
442
443	// Get the latest block hash to read from the latest state (not finalized)
444	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	// Query pending transfers by recipient (incoming)
455	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	// Display outgoing transfers
470	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				// Try to get transfer details
478				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	// Display incoming transfers
504	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				// Try to get transfer details
515				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
551/// Helper function to format amount with QUAN units
552fn format_amount(amount: u128) -> String {
553	const QUAN_DECIMALS: u128 = 1_000_000_000_000; // 10^12
554
555	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			// Remove trailing zeros from fractional part
563			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}