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 = SpAccountId32::from_ss58check(to_address).map_err(|e| {
125		crate::error::QuantusError::NetworkError(format!("Invalid destination address: {e:?}"))
126	})?;
127
128	// Convert to subxt_core AccountId32
129	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	// Create the reversible transfer call using static API from quantus_subxt
135	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	// Submit the transaction
140	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
149/// Cancel a pending reversible transaction
150pub 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	// Parse transaction ID using H256::from_str
159	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	// Create the cancel transaction call using static API from quantus_subxt
166	let cancel_call = quantus_subxt::api::tx().reversible_transfers().cancel(tx_hash);
167
168	// Submit the transaction
169	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
178/// Schedule a transfer with custom delay
179pub 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	// Parse the destination address
195	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	// Convert delay to proper BlockNumberOrTimestamp
202	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		// Convert seconds to milliseconds for the runtime
206		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	// Create the schedule transfer with delay call using static API from quantus_subxt
212	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	// Submit the transaction
220	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
229/// Handle reversible transfer subxt commands
230pub 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			// Parse and validate the amount
240			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			// Resolve the destination address (could be wallet name or SS58 address)
245			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			// Get password securely for decryption
260			log_verbose!("📦 Using wallet: {}", from.bright_blue().bold());
261			let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
262
263			// Submit transaction
264			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			// Get password securely for decryption
295			log_verbose!("📦 Using wallet: {}", from.bright_blue().bold());
296			let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
297
298			// Submit cancel transaction
299			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			// Parse and validate the amount
331			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			// Resolve the destination address (could be wallet name or SS58 address)
336			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			// Get password securely for decryption
349			log_verbose!("📦 Using wallet: {}", from.bright_blue().bold());
350			let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
351
352			// Submit transaction
353			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
397/// List all pending reversible transactions for an account
398async 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	// Determine which address to query
408	let target_address = match (address, wallet_name) {
409		(Some(addr), _) => {
410			// Validate the provided address
411			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			// Load wallet and get its address
418			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	// Convert to AccountId32 for storage queries
430	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	// Query pending transfers by sender (outgoing)
438	let sender_storage_address = crate::chain::quantus_subxt::api::storage()
439		.reversible_transfers()
440		.pending_transfers_by_sender(account_id.clone());
441
442	// Get the latest block hash to read from the latest state (not finalized)
443	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	// Query pending transfers by recipient (incoming)
454	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	// Display outgoing transfers
469	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				// Try to get transfer details
477				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	// Display incoming transfers
503	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				// Try to get transfer details
514				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
550/// Helper function to format amount with QUAN units
551fn format_amount(amount: u128) -> String {
552	const QUAN_DECIMALS: u128 = 1_000_000_000_000; // 10^12
553
554	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			// Remove trailing zeros from fractional part
562			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}