Skip to main content

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	execution_mode: crate::cli::common::ExecutionMode,
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		execution_mode,
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	execution_mode: crate::cli::common::ExecutionMode,
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		execution_mode,
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	execution_mode: crate::cli::common::ExecutionMode,
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		execution_mode,
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	execution_mode: crate::cli::common::ExecutionMode,
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				execution_mode,
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 =
314				cancel_transaction(&quantus_client, &keypair, &tx_id, execution_mode).await?;
315
316			log_print!(
317				"✅ {} Cancel transaction submitted! Hash: {:?}",
318				"SUCCESS".bright_green().bold(),
319				tx_hash
320			);
321
322			Ok(())
323		},
324
325		ReversibleCommands::ScheduleTransferWithDelay {
326			to,
327			amount,
328			delay,
329			unit_blocks,
330			from,
331			password,
332			password_file,
333		} => {
334			// Parse and validate the amount
335			let quantus_client = crate::chain::client::QuantusClient::new(node_url).await?;
336			let (raw_amount, formatted_amount) =
337				crate::cli::send::validate_and_format_amount(&quantus_client, &amount).await?;
338
339			// Resolve the destination address (could be wallet name or SS58 address)
340			let resolved_address = resolve_address(&to)?;
341
342			let unit_str = if unit_blocks { "blocks" } else { "seconds" };
343			log_verbose!(
344				"🚀 {} Scheduling reversible transfer {} to {} with {} {} delay ()",
345				"REVERSIBLE".bright_cyan().bold(),
346				formatted_amount.bright_yellow().bold(),
347				resolved_address.bright_green(),
348				delay.to_string().bright_magenta(),
349				unit_str
350			);
351
352			// Get password securely for decryption
353			log_verbose!("📦 Using wallet: {}", from.bright_blue().bold());
354			let keypair = crate::wallet::load_keypair_from_wallet(&from, password, password_file)?;
355
356			// Submit transaction
357			let tx_hash = schedule_transfer_with_delay(
358				&quantus_client,
359				&keypair,
360				&resolved_address,
361				raw_amount,
362				delay,
363				unit_blocks,
364				execution_mode,
365			)
366			.await?;
367
368			log_print!(
369				"✅ {} Reversible transfer with custom delay scheduled! Hash: {:?}",
370				"SUCCESS".bright_green().bold(),
371				tx_hash
372			);
373
374			Ok(())
375		},
376	}
377}
378
379/// List all pending reversible transactions for an account
380async fn list_pending_transactions(
381	quantus_client: &crate::chain::client::QuantusClient,
382	address: Option<String>,
383	wallet_name: Option<String>,
384	password: Option<String>,
385	password_file: Option<String>,
386) -> Result<()> {
387	log_print!("📋 Listing pending reversible transactions");
388
389	// Determine which address to query
390	let target_address = match (address, wallet_name) {
391		(Some(addr), _) => {
392			// Validate the provided address
393			SpAccountId32::from_ss58check(&addr).map_err(|e| {
394				crate::error::QuantusError::Generic(format!("Invalid address: {e:?}"))
395			})?;
396			addr
397		},
398		(None, Some(wallet)) => {
399			// Load wallet and get its address
400			let keypair =
401				crate::wallet::load_keypair_from_wallet(&wallet, password, password_file)?;
402			keypair.to_account_id_ss58check()
403		},
404		(None, None) => {
405			return Err(crate::error::QuantusError::Generic(
406				"Either --address or --from must be provided".to_string(),
407			));
408		},
409	};
410
411	// Convert to AccountId32 for storage queries
412	let account_id_sp = SpAccountId32::from_ss58check(&target_address)
413		.map_err(|e| crate::error::QuantusError::Generic(format!("Invalid address: {e:?}")))?;
414	let account_id_bytes: [u8; 32] = *account_id_sp.as_ref();
415	let account_id = subxt::ext::subxt_core::utils::AccountId32::from(account_id_bytes);
416
417	log_verbose!("🔍 Querying pending transfers for: {}", target_address);
418
419	// Query pending transfers by sender (outgoing)
420	let sender_storage_address = crate::chain::quantus_subxt::api::storage()
421		.reversible_transfers()
422		.pending_transfers_by_sender(account_id);
423
424	// Get the latest block hash to read from the latest state (not finalized)
425	let latest_block_hash = quantus_client.get_latest_block().await?;
426
427	let outgoing_transfers = quantus_client
428		.client()
429		.storage()
430		.at(latest_block_hash)
431		.fetch(&sender_storage_address)
432		.await
433		.map_err(|e| crate::error::QuantusError::NetworkError(format!("Fetch error: {e:?}")))?;
434
435	let mut total_transfers = 0;
436
437	// Display outgoing transfers
438	if let Some(outgoing_hashes) = outgoing_transfers {
439		if !outgoing_hashes.0.is_empty() {
440			log_print!("📤 Outgoing pending transfers:");
441			for (i, hash) in outgoing_hashes.0.iter().enumerate() {
442				total_transfers += 1;
443				log_print!("   {}. 0x{}", i + 1, hex::encode(hash.as_ref()));
444
445				// Try to get transfer details
446				let transfer_storage_address = crate::chain::quantus_subxt::api::storage()
447					.reversible_transfers()
448					.pending_transfers(*hash);
449
450				if let Ok(Some(transfer_details)) = quantus_client
451					.client()
452					.storage()
453					.at(latest_block_hash)
454					.fetch(&transfer_storage_address)
455					.await
456					.map_err(|e| {
457						crate::error::QuantusError::NetworkError(format!("Fetch error: {e:?}"))
458					}) {
459					let formatted_amount = format_amount(transfer_details.amount);
460					log_print!("      👤 To: {}", transfer_details.to.to_quantus_ss58());
461					log_print!("      💰 Amount: {}", formatted_amount);
462					log_print!(
463						"      🔄 Interceptor: {}",
464						transfer_details.interceptor.to_quantus_ss58()
465					);
466				}
467			}
468		}
469	}
470
471	if total_transfers == 0 {
472		log_print!("📝 No pending transfers found for account: {}", target_address);
473	} else {
474		log_print!("");
475		log_print!("📊 Total pending transfers: {}", total_transfers);
476		log_print!("💡 Use transaction hash with 'quantus reversible cancel --tx-id <hash>' to cancel outgoing transfers");
477	}
478
479	Ok(())
480}
481
482/// Helper function to format amount with QUAN units
483fn format_amount(amount: u128) -> String {
484	const QUAN_DECIMALS: u128 = 1_000_000_000_000; // 10^12
485
486	if amount >= QUAN_DECIMALS {
487		let whole = amount / QUAN_DECIMALS;
488		let fractional = amount % QUAN_DECIMALS;
489
490		if fractional == 0 {
491			format!("{whole} QUAN")
492		} else {
493			// Remove trailing zeros from fractional part
494			let fractional_str = format!("{fractional:012}");
495			let trimmed = fractional_str.trim_end_matches('0');
496			format!("{whole}.{trimmed} QUAN")
497		}
498	} else {
499		format!("{amount} pico-QUAN")
500	}
501}