Skip to main content

quantus_cli/cli/
common.rs

1//! Common SubXT utilities and functions shared across CLI commands
2use crate::{chain::client::ChainConfig, error::Result, log_error, log_verbose};
3use colored::Colorize;
4use hex;
5use sp_core::crypto::{AccountId32, Ss58Codec};
6use subxt::{
7	tx::{TxProgress, TxStatus},
8	OnlineClient,
9};
10
11#[derive(Debug, Clone, Copy, Default)]
12pub struct ExecutionMode {
13	pub finalized: bool,
14	pub wait_for_transaction: bool,
15}
16
17/// Resolve address - if it's a wallet name, return the wallet's address
18/// If it's already an SS58 address, return it as is
19pub fn resolve_address(address_or_wallet_name: &str) -> Result<String> {
20	// First, try to parse as SS58 address
21	if AccountId32::from_ss58check_with_version(address_or_wallet_name).is_ok() {
22		// It's a valid SS58 address, return as is
23		return Ok(address_or_wallet_name.to_string());
24	}
25
26	// If not a valid SS58 address, try to find it as a wallet name
27	let wallet_manager = crate::wallet::WalletManager::new()?;
28	if let Some(wallet_address) = wallet_manager.find_wallet_address(address_or_wallet_name)? {
29		log_verbose!(
30			"πŸ” Found wallet '{}' with address: {}",
31			address_or_wallet_name.bright_cyan(),
32			wallet_address.bright_green()
33		);
34		return Ok(wallet_address);
35	}
36
37	// Neither a valid SS58 address nor a wallet name
38	Err(crate::error::QuantusError::Generic(format!(
39		"Invalid destination: '{address_or_wallet_name}' is neither a valid SS58 address nor a known wallet name"
40	)))
41}
42
43/// Get fresh nonce for account from the latest block using existing QuantusClient
44/// This function ensures we always get the most current nonce from the chain
45/// to avoid "Transaction is outdated" errors
46pub async fn get_fresh_nonce_with_client(
47	quantus_client: &crate::chain::client::QuantusClient,
48	from_keypair: &crate::wallet::QuantumKeyPair,
49) -> Result<u64> {
50	let (from_account_id, _version) =
51		AccountId32::from_ss58check_with_version(&from_keypair.to_account_id_ss58check()).map_err(
52			|e| crate::error::QuantusError::NetworkError(format!("Invalid from address: {e:?}")),
53		)?;
54
55	// Get nonce from the latest block (best block)
56	let latest_nonce = quantus_client
57		.get_account_nonce_from_best_block(&from_account_id)
58		.await
59		.map_err(|e| {
60			crate::error::QuantusError::NetworkError(format!(
61				"Failed to get account nonce from best block: {e:?}"
62			))
63		})?;
64
65	log_verbose!("πŸ”’ Using fresh nonce from latest block: {}", latest_nonce);
66
67	// Compare with nonce from finalized block for debugging
68	let finalized_nonce = quantus_client
69		.client()
70		.tx()
71		.account_nonce(&from_account_id)
72		.await
73		.map_err(|e| {
74			crate::error::QuantusError::NetworkError(format!(
75				"Failed to get account nonce from finalized block: {e:?}"
76			))
77		})?;
78
79	if latest_nonce != finalized_nonce {
80		log_verbose!(
81			"⚠️  Nonce difference detected! Latest: {}, Finalized: {}",
82			latest_nonce,
83			finalized_nonce
84		);
85	}
86
87	Ok(latest_nonce)
88}
89
90/// Get incremented nonce for retry scenarios from the latest block using existing QuantusClient
91/// This is useful when a transaction fails but the chain doesn't update the nonce
92pub async fn get_incremented_nonce_with_client(
93	quantus_client: &crate::chain::client::QuantusClient,
94	from_keypair: &crate::wallet::QuantumKeyPair,
95	base_nonce: u64,
96) -> Result<u64> {
97	let (from_account_id, _version) =
98		AccountId32::from_ss58check_with_version(&from_keypair.to_account_id_ss58check()).map_err(
99			|e| crate::error::QuantusError::NetworkError(format!("Invalid from address: {e:?}")),
100		)?;
101
102	// Get current nonce from the latest block
103	let current_nonce = quantus_client
104		.get_account_nonce_from_best_block(&from_account_id)
105		.await
106		.map_err(|e| {
107			crate::error::QuantusError::NetworkError(format!(
108				"Failed to get account nonce from best block: {e:?}"
109			))
110		})?;
111
112	// Use the higher of current nonce or base_nonce + 1
113	let incremented_nonce = std::cmp::max(current_nonce, base_nonce + 1);
114	log_verbose!(
115		"πŸ”’ Using incremented nonce: {} (base: {}, current from latest block: {})",
116		incremented_nonce,
117		base_nonce,
118		current_nonce
119	);
120	Ok(incremented_nonce)
121}
122
123/// Submit transaction with optional finalization check
124///
125/// By default (finalized=false), waits until transaction is in the best block (fast)
126/// With finalized=true, waits until transaction is in a finalized block (slow in PoW chains)
127/// With wait_for_transaction=false, returns immediately after submission without waiting
128pub async fn submit_transaction<Call>(
129	quantus_client: &crate::chain::client::QuantusClient,
130	from_keypair: &crate::wallet::QuantumKeyPair,
131	call: Call,
132	tip: Option<u128>,
133	execution_mode: ExecutionMode,
134) -> crate::error::Result<subxt::utils::H256>
135where
136	Call: subxt::tx::Payload,
137{
138	let signer = from_keypair.to_subxt_signer().map_err(|e| {
139		crate::error::QuantusError::NetworkError(format!("Failed to convert keypair: {e:?}"))
140	})?;
141
142	// Retry logic with automatic nonce management
143	let mut attempt = 0;
144	let mut current_nonce = None;
145
146	loop {
147		attempt += 1;
148		// Get fresh nonce for each attempt, or increment if we have a previous nonce
149		let nonce = if let Some(prev_nonce) = current_nonce {
150			// After first failure, try with incremented nonce
151			let incremented_nonce =
152				get_incremented_nonce_with_client(quantus_client, from_keypair, prev_nonce).await?;
153			log_verbose!(
154				"πŸ”’ Using incremented nonce from best block: {} (previous: {})",
155				incremented_nonce,
156				prev_nonce
157			);
158			incremented_nonce
159		} else {
160			// First attempt - get fresh nonce from best block
161			let fresh_nonce = get_fresh_nonce_with_client(quantus_client, from_keypair).await?;
162			log_verbose!("πŸ”’ Using fresh nonce from best block: {}", fresh_nonce);
163			fresh_nonce
164		};
165		current_nonce = Some(nonce);
166
167		// Get current block for logging using latest block hash
168		let latest_block_hash = quantus_client.get_latest_block().await.map_err(|e| {
169			crate::error::QuantusError::NetworkError(format!("Failed to get latest block: {e:?}"))
170		})?;
171
172		log_verbose!("πŸ”— Latest block hash: {:?}", latest_block_hash);
173
174		// Create custom params with fresh nonce and optional tip
175		use subxt::config::DefaultExtrinsicParamsBuilder;
176		let mut params_builder = DefaultExtrinsicParamsBuilder::new()
177			.mortal(256) // Value higher than our finalization - TODO: should come from config
178			.nonce(nonce);
179
180		if let Some(tip_amount) = tip {
181			params_builder = params_builder.tip(tip_amount);
182			log_verbose!("πŸ’° Using tip: {} to increase priority", tip_amount);
183		} else {
184			log_verbose!("πŸ’° No tip specified, using default priority");
185		}
186
187		// Try to get chain parameters from the client
188		// let genesis_hash = quantus_client.get_genesis_hash().await?;
189		// let (spec_version, transaction_version) = quantus_client.get_runtime_version().await?;
190
191		// log_verbose!("πŸ” Chain parameters:");
192		// log_verbose!("   Genesis hash: {:?}", genesis_hash);
193		// log_verbose!("   Spec version: {}", spec_version);
194		// log_verbose!("   Transaction version: {}", transaction_version);
195
196		// For now, just use the default params
197		let params = params_builder.build();
198
199		// Log transaction parameters for debugging
200		log_verbose!("πŸ” Transaction parameters:");
201		log_verbose!("   Nonce: {}", nonce);
202		log_verbose!("   Tip: {:?}", tip);
203		log_verbose!("   Latest block hash: {:?}", latest_block_hash);
204
205		// Get and log era information
206		log_verbose!("   Era: Using default era from SubXT");
207		log_verbose!("   Genesis hash: Using default from SubXT");
208		log_verbose!("   Spec version: Using default from SubXT");
209
210		// Log additional debugging info
211		log_verbose!("πŸ” Additional debugging:");
212		log_verbose!("   Call type: {:?}", std::any::type_name::<Call>());
213
214		let metadata = quantus_client.client().metadata();
215		let encoded_call =
216			<_ as subxt::tx::Payload>::encode_call_data(&call, &metadata).map_err(|e| {
217				crate::error::QuantusError::NetworkError(format!("Failed to encode call: {:?}", e))
218			})?;
219		crate::log_verbose!("πŸ“ Encoded call: 0x{}", hex::encode(&encoded_call));
220		crate::log_print!("πŸ“ Encoded call size: {} bytes", encoded_call.len());
221
222		if execution_mode.wait_for_transaction {
223			match quantus_client
224				.client()
225				.tx()
226				.sign_and_submit_then_watch(&call, &signer, params)
227				.await
228			{
229				Ok(mut tx_progress) => {
230					crate::log_verbose!("πŸ“‹ Transaction submitted: {:?}", tx_progress);
231
232					let tx_hash = tx_progress.extrinsic_hash();
233
234					if !execution_mode.wait_for_transaction {
235						return Ok(tx_hash);
236					}
237
238					wait_tx_inclusion(
239						&mut tx_progress,
240						quantus_client.client(),
241						&tx_hash,
242						execution_mode.finalized,
243					)
244					.await?;
245
246					return Ok(tx_hash);
247				},
248				Err(e) => {
249					let error_msg = format!("{e:?}");
250
251					// Check if it's a retryable error
252					let is_retryable = error_msg.contains("Priority is too low") ||
253						error_msg.contains("Transaction is outdated") ||
254						error_msg.contains("Transaction is temporarily banned") ||
255						error_msg.contains("Transaction has a bad signature") ||
256						error_msg.contains("Invalid Transaction");
257
258					if is_retryable && attempt < 5 {
259						log_verbose!(
260							"⚠️  Transaction error detected (attempt {}/5): {}",
261							attempt,
262							error_msg
263						);
264
265						// Exponential backoff: 2s, 4s, 8s, 16s
266						let delay = std::cmp::min(2u64.pow(attempt as u32), 16);
267						log_verbose!("⏳ Waiting {} seconds before retry...", delay);
268						tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await;
269						continue;
270					} else {
271						log_verbose!("❌ Final error after {} attempts: {}", attempt, error_msg);
272						return Err(crate::error::QuantusError::NetworkError(format!(
273							"Failed to submit transaction: {e:?}"
274						)));
275					}
276				},
277			}
278		} else {
279			match quantus_client.client().tx().sign_and_submit(&call, &signer, params).await {
280				Ok(tx_hash) => {
281					crate::log_print!("βœ… Transaction submitted: {:?}", tx_hash);
282					return Ok(tx_hash);
283				},
284				Err(e) => {
285					log_error!("❌ Failed to submit transaction: {e:?}");
286					return Err(crate::error::QuantusError::NetworkError(format!(
287						"Failed to submit transaction: {e:?}"
288					)));
289				},
290			}
291		}
292	}
293}
294
295/// Submit transaction with manual nonce (no retry logic - use exact nonce provided)
296pub async fn submit_transaction_with_nonce<Call>(
297	quantus_client: &crate::chain::client::QuantusClient,
298	from_keypair: &crate::wallet::QuantumKeyPair,
299	call: Call,
300	tip: Option<u128>,
301	nonce: u32,
302	execution_mode: ExecutionMode,
303) -> crate::error::Result<subxt::utils::H256>
304where
305	Call: subxt::tx::Payload,
306{
307	let signer = from_keypair.to_subxt_signer().map_err(|e| {
308		crate::error::QuantusError::NetworkError(format!("Failed to convert keypair: {e:?}"))
309	})?;
310
311	// Get current block for logging using latest block hash
312	let latest_block_hash = quantus_client.get_latest_block().await.map_err(|e| {
313		crate::error::QuantusError::NetworkError(format!("Failed to get latest block: {e:?}"))
314	})?;
315
316	log_verbose!("πŸ”— Latest block hash: {:?}", latest_block_hash);
317
318	// Create custom params with manual nonce and optional tip
319	use subxt::config::DefaultExtrinsicParamsBuilder;
320	let mut params_builder = DefaultExtrinsicParamsBuilder::new()
321		.mortal(256) // Value higher than our finalization - TODO: should come from config
322		.nonce(nonce.into());
323
324	if let Some(tip_amount) = tip {
325		params_builder = params_builder.tip(tip_amount);
326		log_verbose!("πŸ’° Using tip: {}", tip_amount);
327	}
328
329	let params = params_builder.build();
330
331	log_verbose!("πŸ”’ Using manual nonce: {}", nonce);
332	log_verbose!("πŸ“€ Submitting transaction with manual nonce...");
333
334	crate::log_print!("submit with wait for transaction: {}", execution_mode.wait_for_transaction);
335	// Submit the transaction with manual nonce
336
337	if execution_mode.wait_for_transaction {
338		match quantus_client
339			.client()
340			.tx()
341			.sign_and_submit_then_watch(&call, &signer, params)
342			.await
343		{
344			Ok(mut tx_progress) => {
345				let tx_hash = tx_progress.extrinsic_hash();
346				crate::log_print!("βœ… Transaction submitted: {:?}", tx_hash);
347				wait_tx_inclusion(
348					&mut tx_progress,
349					quantus_client.client(),
350					&tx_hash,
351					execution_mode.finalized,
352				)
353				.await?;
354				Ok(tx_hash)
355			},
356			Err(e) => {
357				log_error!("❌ Failed to submit transaction with manual nonce {}: {e:?}", nonce);
358				Err(crate::error::QuantusError::NetworkError(format!(
359					"Failed to submit transaction with nonce {nonce}: {e:?}"
360				)))
361			},
362		}
363	} else {
364		match quantus_client.client().tx().sign_and_submit(&call, &signer, params).await {
365			Ok(tx_hash) => {
366				crate::log_print!("βœ… Transaction submitted: {:?}", tx_hash);
367				Ok(tx_hash)
368			},
369			Err(e) => {
370				log_error!("❌ Failed to submit transaction: {e:?}");
371				Err(crate::error::QuantusError::NetworkError(format!(
372					"Failed to submit transaction: {e:?}"
373				)))
374			},
375		}
376	}
377}
378
379/// Watch transaction until it is included in the best block or finalized
380///
381/// Since Quantus network is PoW, we can't use default subxt's way of waiting for finalized block as
382/// it may take a long time. We wait for the transaction to be included in the best block and leave
383/// it up to the user to check the status of the transaction.
384async fn wait_tx_inclusion(
385	tx_progress: &mut TxProgress<ChainConfig, OnlineClient<ChainConfig>>,
386	client: &OnlineClient<ChainConfig>,
387	tx_hash: &subxt::utils::H256,
388	finalized: bool,
389) -> Result<()> {
390	use indicatif::{ProgressBar, ProgressStyle};
391
392	let start_time = std::time::Instant::now();
393
394	let spinner = if !crate::log::is_verbose() {
395		let pb = ProgressBar::new_spinner();
396		pb.set_style(
397			ProgressStyle::default_spinner()
398				.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
399				.template("{spinner:.cyan} {msg}")
400				.unwrap(),
401		);
402
403		if finalized {
404			pb.set_message("Waiting for finalized block... (0s)");
405		} else {
406			pb.set_message("Waiting for block inclusion... (0s)");
407		}
408
409		pb.enable_steady_tick(std::time::Duration::from_millis(500));
410		Some(pb)
411	} else {
412		None
413	};
414
415	while let Some(Ok(status)) = tx_progress.next().await {
416		let elapsed_secs = start_time.elapsed().as_secs();
417		crate::log_verbose!("   Transaction status: {:?} (elapsed: {}s)", status, elapsed_secs);
418
419		match status {
420			TxStatus::Validated =>
421				if let Some(ref pb) = spinner {
422					pb.set_message(format!("Transaction validated βœ“ ({}s)", elapsed_secs));
423				},
424			TxStatus::InBestBlock(tx_in_block) => {
425				let block_hash = tx_in_block.block_hash();
426				crate::log_verbose!("   Transaction included in block: {:?}", block_hash);
427				check_execution_success(client, &block_hash, tx_hash).await?;
428				if finalized {
429					if let Some(ref pb) = spinner {
430						pb.set_message(format!(
431							"In best block, waiting for finalization... ({}s)",
432							elapsed_secs
433						));
434					}
435					continue;
436				} else {
437					if let Some(pb) = spinner {
438						pb.finish_with_message(format!(
439							"βœ… Transaction included in block! ({}s)",
440							elapsed_secs
441						));
442					}
443					break;
444				};
445			},
446			TxStatus::InFinalizedBlock(tx_in_block) => {
447				let block_hash = tx_in_block.block_hash();
448				crate::log_verbose!("   Transaction finalized in block: {:?}", block_hash);
449				check_execution_success(client, &block_hash, tx_hash).await?;
450				if let Some(pb) = spinner {
451					pb.finish_with_message(format!(
452						"βœ… Transaction finalized! ({}s)",
453						elapsed_secs
454					));
455				}
456				break;
457			},
458			TxStatus::Error { message } | TxStatus::Invalid { message } => {
459				crate::log_error!("   Transaction error: {} (elapsed: {}s)", message, elapsed_secs);
460				if let Some(pb) = spinner {
461					pb.finish_with_message(format!("❌ Transaction error! ({}s)", elapsed_secs));
462				}
463				break;
464			},
465			_ => {
466				if let Some(ref pb) = spinner {
467					if finalized {
468						pb.set_message(format!(
469							"Waiting for finalized block... ({}s)",
470							elapsed_secs
471						));
472					} else {
473						pb.set_message(format!(
474							"Waiting for block inclusion... ({}s)",
475							elapsed_secs
476						));
477					}
478				}
479				continue;
480			},
481		}
482	}
483
484	Ok(())
485}
486
487fn format_dispatch_error(
488	error: &crate::chain::quantus_subxt::api::runtime_types::sp_runtime::DispatchError,
489	metadata: &subxt::Metadata,
490) -> String {
491	use crate::chain::quantus_subxt::api::runtime_types::sp_runtime::DispatchError;
492
493	match error {
494		DispatchError::Module(module_error) => {
495			let pallet_index = module_error.index;
496			let error_index = module_error.error[0];
497
498			// Try to get human-readable error name from metadata
499			if let Some(pallet) = metadata.pallet_by_index(pallet_index) {
500				let pallet_name = pallet.name();
501				// Look up the error variant name from metadata
502				if let Some(variant) = pallet.error_variant_by_index(error_index) {
503					let error_name = &variant.name;
504					let docs = variant.docs.join(" ");
505					if docs.is_empty() {
506						format!("{}::{}", pallet_name, error_name)
507					} else {
508						format!("{}::{} - {}", pallet_name, error_name, docs)
509					}
510				} else {
511					format!("{}::Error[{}]", pallet_name, error_index)
512				}
513			} else {
514				format!("Pallet[{}]::Error[{}]", pallet_index, error_index)
515			}
516		},
517		DispatchError::BadOrigin => "BadOrigin".to_string(),
518		DispatchError::CannotLookup => "CannotLookup".to_string(),
519		DispatchError::Other => "Other".to_string(),
520		_ => format!("{:?}", error),
521	}
522}
523
524async fn check_execution_success(
525	client: &OnlineClient<ChainConfig>,
526	block_hash: &subxt::utils::H256,
527	tx_hash: &subxt::utils::H256,
528) -> Result<()> {
529	use crate::chain::quantus_subxt::api::system::events::ExtrinsicFailed;
530
531	let block = client.blocks().at(*block_hash).await.map_err(|e| {
532		crate::error::QuantusError::NetworkError(format!("Failed to get block: {e:?}"))
533	})?;
534
535	let extrinsics = block.extrinsics().await.map_err(|e| {
536		crate::error::QuantusError::NetworkError(format!("Failed to get extrinsics: {e:?}"))
537	})?;
538
539	let our_extrinsic_index = extrinsics
540		.iter()
541		.enumerate()
542		.find(|(_, ext)| ext.hash() == *tx_hash)
543		.map(|(idx, _)| idx);
544
545	let events = block.events().await.map_err(|e| {
546		crate::error::QuantusError::NetworkError(format!("Failed to fetch events: {e:?}"))
547	})?;
548
549	let metadata = client.metadata();
550	if let Some(ext_idx) = our_extrinsic_index {
551		for event_result in events.iter() {
552			let event = event_result.map_err(|e| {
553				crate::error::QuantusError::NetworkError(format!("Failed to decode event: {e:?}"))
554			})?;
555
556			if let subxt::events::Phase::ApplyExtrinsic(event_ext_idx) = event.phase() {
557				if event_ext_idx == ext_idx as u32 {
558					if let Ok(Some(ExtrinsicFailed { dispatch_error, .. })) =
559						event.as_event::<ExtrinsicFailed>()
560					{
561						let error_msg = format_dispatch_error(&dispatch_error, &metadata);
562						crate::log_error!("   Transaction failed: {}", error_msg);
563						return Err(crate::error::QuantusError::NetworkError(format!(
564							"Transaction execution failed: {}",
565							error_msg
566						)));
567					}
568				}
569			}
570		}
571	}
572
573	Ok(())
574}