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 sp_core::crypto::{AccountId32, Ss58Codec};
5use subxt::{
6	tx::{TxProgress, TxStatus},
7	OnlineClient,
8};
9
10/// Resolve address - if it's a wallet name, return the wallet's address
11/// If it's already an SS58 address, return it as is
12pub fn resolve_address(address_or_wallet_name: &str) -> Result<String> {
13	// First, try to parse as SS58 address
14	if AccountId32::from_ss58check_with_version(address_or_wallet_name).is_ok() {
15		// It's a valid SS58 address, return as is
16		return Ok(address_or_wallet_name.to_string());
17	}
18
19	// If not a valid SS58 address, try to find it as a wallet name
20	let wallet_manager = crate::wallet::WalletManager::new()?;
21	if let Some(wallet_address) = wallet_manager.find_wallet_address(address_or_wallet_name)? {
22		log_verbose!(
23			"πŸ” Found wallet '{}' with address: {}",
24			address_or_wallet_name.bright_cyan(),
25			wallet_address.bright_green()
26		);
27		return Ok(wallet_address);
28	}
29
30	// Neither a valid SS58 address nor a wallet name
31	Err(crate::error::QuantusError::Generic(format!(
32		"Invalid destination: '{address_or_wallet_name}' is neither a valid SS58 address nor a known wallet name"
33	)))
34}
35
36/// Get fresh nonce for account from the latest block using existing QuantusClient
37/// This function ensures we always get the most current nonce from the chain
38/// to avoid "Transaction is outdated" errors
39pub async fn get_fresh_nonce_with_client(
40	quantus_client: &crate::chain::client::QuantusClient,
41	from_keypair: &crate::wallet::QuantumKeyPair,
42) -> Result<u64> {
43	let (from_account_id, _version) =
44		AccountId32::from_ss58check_with_version(&from_keypair.to_account_id_ss58check()).map_err(
45			|e| crate::error::QuantusError::NetworkError(format!("Invalid from address: {e:?}")),
46		)?;
47
48	// Get nonce from the latest block (best block)
49	let latest_nonce = quantus_client
50		.get_account_nonce_from_best_block(&from_account_id)
51		.await
52		.map_err(|e| {
53			crate::error::QuantusError::NetworkError(format!(
54				"Failed to get account nonce from best block: {e:?}"
55			))
56		})?;
57
58	log_verbose!("πŸ”’ Using fresh nonce from latest block: {}", latest_nonce);
59
60	// Compare with nonce from finalized block for debugging
61	let finalized_nonce = quantus_client
62		.client()
63		.tx()
64		.account_nonce(&from_account_id)
65		.await
66		.map_err(|e| {
67			crate::error::QuantusError::NetworkError(format!(
68				"Failed to get account nonce from finalized block: {e:?}"
69			))
70		})?;
71
72	if latest_nonce != finalized_nonce {
73		log_verbose!(
74			"⚠️  Nonce difference detected! Latest: {}, Finalized: {}",
75			latest_nonce,
76			finalized_nonce
77		);
78	}
79
80	Ok(latest_nonce)
81}
82
83/// Get incremented nonce for retry scenarios from the latest block using existing QuantusClient
84/// This is useful when a transaction fails but the chain doesn't update the nonce
85pub async fn get_incremented_nonce_with_client(
86	quantus_client: &crate::chain::client::QuantusClient,
87	from_keypair: &crate::wallet::QuantumKeyPair,
88	base_nonce: u64,
89) -> Result<u64> {
90	let (from_account_id, _version) =
91		AccountId32::from_ss58check_with_version(&from_keypair.to_account_id_ss58check()).map_err(
92			|e| crate::error::QuantusError::NetworkError(format!("Invalid from address: {e:?}")),
93		)?;
94
95	// Get current nonce from the latest block
96	let current_nonce = quantus_client
97		.get_account_nonce_from_best_block(&from_account_id)
98		.await
99		.map_err(|e| {
100			crate::error::QuantusError::NetworkError(format!(
101				"Failed to get account nonce from best block: {e:?}"
102			))
103		})?;
104
105	// Use the higher of current nonce or base_nonce + 1
106	let incremented_nonce = std::cmp::max(current_nonce, base_nonce + 1);
107	log_verbose!(
108		"πŸ”’ Using incremented nonce: {} (base: {}, current from latest block: {})",
109		incremented_nonce,
110		base_nonce,
111		current_nonce
112	);
113	Ok(incremented_nonce)
114}
115
116/// Submit transaction with optional finalization check
117///
118/// By default (finalized=false), waits until transaction is in the best block (fast)
119/// With finalized=true, waits until transaction is in a finalized block (slow in PoW chains)
120pub async fn submit_transaction<Call>(
121	quantus_client: &crate::chain::client::QuantusClient,
122	from_keypair: &crate::wallet::QuantumKeyPair,
123	call: Call,
124	tip: Option<u128>,
125	finalized: bool,
126) -> crate::error::Result<subxt::utils::H256>
127where
128	Call: subxt::tx::Payload,
129{
130	let signer = from_keypair.to_subxt_signer().map_err(|e| {
131		crate::error::QuantusError::NetworkError(format!("Failed to convert keypair: {e:?}"))
132	})?;
133
134	// Retry logic with automatic nonce management
135	let mut attempt = 0;
136	let mut current_nonce = None;
137
138	loop {
139		attempt += 1;
140
141		// Get fresh nonce for each attempt, or increment if we have a previous nonce
142		let nonce = if let Some(prev_nonce) = current_nonce {
143			// After first failure, try with incremented nonce
144			let incremented_nonce =
145				get_incremented_nonce_with_client(quantus_client, from_keypair, prev_nonce).await?;
146			log_verbose!(
147				"πŸ”’ Using incremented nonce from best block: {} (previous: {})",
148				incremented_nonce,
149				prev_nonce
150			);
151			incremented_nonce
152		} else {
153			// First attempt - get fresh nonce from best block
154			let fresh_nonce = get_fresh_nonce_with_client(quantus_client, from_keypair).await?;
155			log_verbose!("πŸ”’ Using fresh nonce from best block: {}", fresh_nonce);
156			fresh_nonce
157		};
158		current_nonce = Some(nonce);
159
160		// Get current block for logging using latest block hash
161		let latest_block_hash = quantus_client.get_latest_block().await.map_err(|e| {
162			crate::error::QuantusError::NetworkError(format!("Failed to get latest block: {e:?}"))
163		})?;
164
165		log_verbose!("πŸ”— Latest block hash: {:?}", latest_block_hash);
166
167		// Create custom params with fresh nonce and optional tip
168		use subxt::config::DefaultExtrinsicParamsBuilder;
169		let mut params_builder = DefaultExtrinsicParamsBuilder::new()
170			.mortal(256) // Value higher than our finalization - TODO: should come from config
171			.nonce(nonce);
172
173		if let Some(tip_amount) = tip {
174			params_builder = params_builder.tip(tip_amount);
175			log_verbose!("πŸ’° Using tip: {} to increase priority", tip_amount);
176		} else {
177			log_verbose!("πŸ’° No tip specified, using default priority");
178		}
179
180		// Try to get chain parameters from the client
181		let genesis_hash = quantus_client.get_genesis_hash().await?;
182		let (spec_version, transaction_version) = quantus_client.get_runtime_version().await?;
183
184		log_verbose!("πŸ” Chain parameters:");
185		log_verbose!("   Genesis hash: {:?}", genesis_hash);
186		log_verbose!("   Spec version: {}", spec_version);
187		log_verbose!("   Transaction version: {}", transaction_version);
188
189		// For now, just use the default params
190		let params = params_builder.build();
191
192		// Log transaction parameters for debugging
193		log_verbose!("πŸ” Transaction parameters:");
194		log_verbose!("   Nonce: {}", nonce);
195		log_verbose!("   Tip: {:?}", tip);
196		log_verbose!("   Latest block hash: {:?}", latest_block_hash);
197
198		// Get and log era information
199		log_verbose!("   Era: Using default era from SubXT");
200		log_verbose!("   Genesis hash: Using default from SubXT");
201		log_verbose!("   Spec version: Using default from SubXT");
202
203		// Log additional debugging info
204		log_verbose!("πŸ” Additional debugging:");
205		log_verbose!("   Call type: {:?}", std::any::type_name::<Call>());
206
207		// Submit the transaction with fresh nonce and optional tip
208		match quantus_client
209			.client()
210			.tx()
211			.sign_and_submit_then_watch(&call, &signer, params)
212			.await
213		{
214			Ok(mut tx_progress) => {
215				crate::log_verbose!("πŸ“‹ Transaction submitted: {:?}", tx_progress);
216
217				let tx_hash = tx_progress.extrinsic_hash();
218				wait_tx_inclusion(&mut tx_progress, finalized).await?;
219
220				return Ok(tx_hash);
221			},
222			Err(e) => {
223				let error_msg = format!("{e:?}");
224
225				// Check if it's a retryable error
226				let is_retryable = error_msg.contains("Priority is too low") ||
227					error_msg.contains("Transaction is outdated") ||
228					error_msg.contains("Transaction is temporarily banned") ||
229					error_msg.contains("Transaction has a bad signature") ||
230					error_msg.contains("Invalid Transaction");
231
232				if is_retryable && attempt < 5 {
233					log_verbose!(
234						"⚠️  Transaction error detected (attempt {}/5): {}",
235						attempt,
236						error_msg
237					);
238
239					// Exponential backoff: 2s, 4s, 8s, 16s
240					let delay = std::cmp::min(2u64.pow(attempt as u32), 16);
241					log_verbose!("⏳ Waiting {} seconds before retry...", delay);
242					tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await;
243					continue;
244				} else {
245					log_verbose!("❌ Final error after {} attempts: {}", attempt, error_msg);
246					return Err(crate::error::QuantusError::NetworkError(format!(
247						"Failed to submit transaction: {e:?}"
248					)));
249				}
250			},
251		}
252	}
253}
254
255/// Submit transaction with manual nonce (no retry logic - use exact nonce provided)
256pub async fn submit_transaction_with_nonce<Call>(
257	quantus_client: &crate::chain::client::QuantusClient,
258	from_keypair: &crate::wallet::QuantumKeyPair,
259	call: Call,
260	tip: Option<u128>,
261	nonce: u32,
262	finalized: bool,
263) -> crate::error::Result<subxt::utils::H256>
264where
265	Call: subxt::tx::Payload,
266{
267	let signer = from_keypair.to_subxt_signer().map_err(|e| {
268		crate::error::QuantusError::NetworkError(format!("Failed to convert keypair: {e:?}"))
269	})?;
270
271	// Get current block for logging using latest block hash
272	let latest_block_hash = quantus_client.get_latest_block().await.map_err(|e| {
273		crate::error::QuantusError::NetworkError(format!("Failed to get latest block: {e:?}"))
274	})?;
275
276	log_verbose!("πŸ”— Latest block hash: {:?}", latest_block_hash);
277
278	// Create custom params with manual nonce and optional tip
279	use subxt::config::DefaultExtrinsicParamsBuilder;
280	let mut params_builder = DefaultExtrinsicParamsBuilder::new()
281		.mortal(256) // Value higher than our finalization - TODO: should come from config
282		.nonce(nonce.into());
283
284	if let Some(tip_amount) = tip {
285		params_builder = params_builder.tip(tip_amount);
286		log_verbose!("πŸ’° Using tip: {}", tip_amount);
287	}
288
289	let params = params_builder.build();
290
291	log_verbose!("πŸ”’ Using manual nonce: {}", nonce);
292	log_verbose!("πŸ“€ Submitting transaction with manual nonce...");
293
294	// Submit the transaction with manual nonce
295	match quantus_client
296		.client()
297		.tx()
298		.sign_and_submit_then_watch(&call, &signer, params)
299		.await
300	{
301		Ok(mut tx_progress) => {
302			let tx_hash = tx_progress.extrinsic_hash();
303			log_verbose!("βœ… Transaction submitted successfully: {:?}", tx_hash);
304			wait_tx_inclusion(&mut tx_progress, finalized).await?;
305			Ok(tx_hash)
306		},
307		Err(e) => {
308			log_error!("❌ Failed to submit transaction with manual nonce {}: {e:?}", nonce);
309			Err(crate::error::QuantusError::NetworkError(format!(
310				"Failed to submit transaction with nonce {nonce}: {e:?}"
311			)))
312		},
313	}
314}
315
316/// Watch transaction until it is included in the best block or finalized
317///
318/// Since Quantus network is PoW, we can't use default subxt's way of waiting for finalized block as
319/// it may take a long time. We wait for the transaction to be included in the best block and leave
320/// it up to the user to check the status of the transaction.
321async fn wait_tx_inclusion(
322	tx_progress: &mut TxProgress<ChainConfig, OnlineClient<ChainConfig>>,
323	finalized: bool,
324) -> Result<()> {
325	use indicatif::{ProgressBar, ProgressStyle};
326
327	// Create spinner (only in non-verbose mode)
328	let spinner = if !crate::log::is_verbose() {
329		let pb = ProgressBar::new_spinner();
330		pb.set_style(
331			ProgressStyle::default_spinner()
332				.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
333				.template("{spinner:.cyan} {msg} {elapsed:.dim}")
334				.unwrap(),
335		);
336
337		if finalized {
338			pb.set_message("Waiting for finalized block...");
339		} else {
340			pb.set_message("Waiting for block inclusion...");
341		}
342
343		pb.enable_steady_tick(std::time::Duration::from_millis(100));
344		Some(pb)
345	} else {
346		None
347	};
348
349	while let Some(Ok(status)) = tx_progress.next().await {
350		crate::log_verbose!("   Transaction status: {:?}", status);
351
352		match status {
353			TxStatus::Validated =>
354				if let Some(ref pb) = spinner {
355					pb.set_message("Transaction validated βœ“");
356				},
357			TxStatus::InBestBlock(block_hash) => {
358				crate::log_verbose!("   Transaction included in block: {:?}", block_hash);
359				if finalized {
360					if let Some(ref pb) = spinner {
361						pb.set_message("In best block, waiting for finalization...");
362					}
363					continue;
364				} else {
365					if let Some(pb) = spinner {
366						pb.finish_with_message("βœ… Transaction included in block!");
367					}
368					break;
369				};
370			},
371			TxStatus::InFinalizedBlock(block_hash) => {
372				crate::log_verbose!("   Transaction finalized in block: {:?}", block_hash);
373				if let Some(pb) = spinner {
374					pb.finish_with_message("βœ… Transaction finalized!");
375				}
376				break;
377			},
378			TxStatus::Error { message } | TxStatus::Invalid { message } => {
379				crate::log_error!("   Transaction error: {}", message);
380				if let Some(pb) = spinner {
381					pb.finish_with_message("❌ Transaction error!");
382				}
383				break;
384			},
385			_ => continue,
386		}
387	}
388
389	Ok(())
390}