quantus_cli/cli/
reversible.rs

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