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/// Default function for transaction submission, waits until transaction is in the best block
117pub async fn submit_transaction<Call>(
118	quantus_client: &crate::chain::client::QuantusClient,
119	from_keypair: &crate::wallet::QuantumKeyPair,
120	call: Call,
121	tip: Option<u128>,
122) -> crate::error::Result<subxt::utils::H256>
123where
124	Call: subxt::tx::Payload,
125{
126	submit_transaction_with_finalization(quantus_client, from_keypair, call, tip, false).await
127}
128
129/// Helper function to submit transaction with an optional finalization check
130pub async fn submit_transaction_with_finalization<Call>(
131	quantus_client: &crate::chain::client::QuantusClient,
132	from_keypair: &crate::wallet::QuantumKeyPair,
133	call: Call,
134	tip: Option<u128>,
135	finalized: bool,
136) -> crate::error::Result<subxt::utils::H256>
137where
138	Call: subxt::tx::Payload,
139{
140	let signer = from_keypair.to_subxt_signer().map_err(|e| {
141		crate::error::QuantusError::NetworkError(format!("Failed to convert keypair: {e:?}"))
142	})?;
143
144	// Retry logic with automatic nonce management
145	let mut attempt = 0;
146	let mut current_nonce = None;
147
148	loop {
149		attempt += 1;
150
151		// Get fresh nonce for each attempt, or increment if we have a previous nonce
152		let nonce = if let Some(prev_nonce) = current_nonce {
153			// After first failure, try with incremented nonce
154			let incremented_nonce =
155				get_incremented_nonce_with_client(quantus_client, from_keypair, prev_nonce).await?;
156			log_verbose!(
157				"🔢 Using incremented nonce from best block: {} (previous: {})",
158				incremented_nonce,
159				prev_nonce
160			);
161			incremented_nonce
162		} else {
163			// First attempt - get fresh nonce from best block
164			let fresh_nonce = get_fresh_nonce_with_client(quantus_client, from_keypair).await?;
165			log_verbose!("🔢 Using fresh nonce from best block: {}", fresh_nonce);
166			fresh_nonce
167		};
168		current_nonce = Some(nonce);
169
170		// Get current block for logging using latest block hash
171		let latest_block_hash = quantus_client.get_latest_block().await.map_err(|e| {
172			crate::error::QuantusError::NetworkError(format!("Failed to get latest block: {e:?}"))
173		})?;
174
175		log_verbose!("🔗 Latest block hash: {:?}", latest_block_hash);
176
177		// Create custom params with fresh nonce and optional tip
178		use subxt::config::DefaultExtrinsicParamsBuilder;
179		let mut params_builder = DefaultExtrinsicParamsBuilder::new()
180			.mortal(256) // Value higher than our finalization - TODO: should come from config
181			.nonce(nonce);
182
183		if let Some(tip_amount) = tip {
184			params_builder = params_builder.tip(tip_amount);
185			log_verbose!("💰 Using tip: {} to increase priority", tip_amount);
186		} else {
187			log_verbose!("💰 No tip specified, using default priority");
188		}
189
190		// Try to get chain parameters from the client
191		let genesis_hash = quantus_client.get_genesis_hash().await?;
192		let (spec_version, transaction_version) = quantus_client.get_runtime_version().await?;
193
194		log_verbose!("🔍 Chain parameters:");
195		log_verbose!("   Genesis hash: {:?}", genesis_hash);
196		log_verbose!("   Spec version: {}", spec_version);
197		log_verbose!("   Transaction version: {}", transaction_version);
198
199		// For now, just use the default params
200		let params = params_builder.build();
201
202		// Log transaction parameters for debugging
203		log_verbose!("🔍 Transaction parameters:");
204		log_verbose!("   Nonce: {}", nonce);
205		log_verbose!("   Tip: {:?}", tip);
206		log_verbose!("   Latest block hash: {:?}", latest_block_hash);
207
208		// Get and log era information
209		log_verbose!("   Era: Using default era from SubXT");
210		log_verbose!("   Genesis hash: Using default from SubXT");
211		log_verbose!("   Spec version: Using default from SubXT");
212
213		// Log additional debugging info
214		log_verbose!("🔍 Additional debugging:");
215		log_verbose!("   Call type: {:?}", std::any::type_name::<Call>());
216
217		// Submit the transaction with fresh nonce and optional tip
218		match quantus_client
219			.client()
220			.tx()
221			.sign_and_submit_then_watch(&call, &signer, params)
222			.await
223		{
224			Ok(mut tx_progress) => {
225				crate::log_verbose!("📋 Transaction submitted: {:?}", tx_progress);
226
227				let tx_hash = tx_progress.extrinsic_hash();
228				wait_tx_inclusion(&mut tx_progress, finalized).await?;
229
230				return Ok(tx_hash);
231			},
232			Err(e) => {
233				let error_msg = format!("{e:?}");
234
235				// Check if it's a retryable error
236				let is_retryable = error_msg.contains("Priority is too low") ||
237					error_msg.contains("Transaction is outdated") ||
238					error_msg.contains("Transaction is temporarily banned") ||
239					error_msg.contains("Transaction has a bad signature") ||
240					error_msg.contains("Invalid Transaction");
241
242				if is_retryable && attempt < 5 {
243					log_verbose!(
244						"⚠️  Transaction error detected (attempt {}/5): {}",
245						attempt,
246						error_msg
247					);
248
249					// Exponential backoff: 2s, 4s, 8s, 16s
250					let delay = std::cmp::min(2u64.pow(attempt as u32), 16);
251					log_verbose!("⏳ Waiting {} seconds before retry...", delay);
252					tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await;
253					continue;
254				} else {
255					log_verbose!("❌ Final error after {} attempts: {}", attempt, error_msg);
256					return Err(crate::error::QuantusError::NetworkError(format!(
257						"Failed to submit transaction: {e:?}"
258					)));
259				}
260			},
261		}
262	}
263}
264
265/// Submit transaction with manual nonce (no retry logic - use exact nonce provided)
266pub async fn submit_transaction_with_nonce<Call>(
267	quantus_client: &crate::chain::client::QuantusClient,
268	from_keypair: &crate::wallet::QuantumKeyPair,
269	call: Call,
270	tip: Option<u128>,
271	nonce: u32,
272	finalized: bool,
273) -> crate::error::Result<subxt::utils::H256>
274where
275	Call: subxt::tx::Payload,
276{
277	let signer = from_keypair.to_subxt_signer().map_err(|e| {
278		crate::error::QuantusError::NetworkError(format!("Failed to convert keypair: {e:?}"))
279	})?;
280
281	// Get current block for logging using latest block hash
282	let latest_block_hash = quantus_client.get_latest_block().await.map_err(|e| {
283		crate::error::QuantusError::NetworkError(format!("Failed to get latest block: {e:?}"))
284	})?;
285
286	log_verbose!("🔗 Latest block hash: {:?}", latest_block_hash);
287
288	// Create custom params with manual nonce and optional tip
289	use subxt::config::DefaultExtrinsicParamsBuilder;
290	let mut params_builder = DefaultExtrinsicParamsBuilder::new()
291		.mortal(256) // Value higher than our finalization - TODO: should come from config
292		.nonce(nonce.into());
293
294	if let Some(tip_amount) = tip {
295		params_builder = params_builder.tip(tip_amount);
296		log_verbose!("💰 Using tip: {}", tip_amount);
297	}
298
299	let params = params_builder.build();
300
301	log_verbose!("🔢 Using manual nonce: {}", nonce);
302	log_verbose!("📤 Submitting transaction with manual nonce...");
303
304	// Submit the transaction with manual nonce
305	match quantus_client
306		.client()
307		.tx()
308		.sign_and_submit_then_watch(&call, &signer, params)
309		.await
310	{
311		Ok(mut tx_progress) => {
312			let tx_hash = tx_progress.extrinsic_hash();
313			log_verbose!("✅ Transaction submitted successfully: {:?}", tx_hash);
314			wait_tx_inclusion(&mut tx_progress, finalized).await?;
315			Ok(tx_hash)
316		},
317		Err(e) => {
318			log_error!("❌ Failed to submit transaction with manual nonce {}: {e:?}", nonce);
319			Err(crate::error::QuantusError::NetworkError(format!(
320				"Failed to submit transaction with nonce {nonce}: {e:?}"
321			)))
322		},
323	}
324}
325
326/// Watch transaction until it is included in the best block or finalized
327///
328/// Since Quantus network is PoW, we can't use default subxt's way of waiting for finalized block as
329/// it may take a long time. We wait for the transaction to be included in the best block and leave
330/// it up to the user to check the status of the transaction.
331async fn wait_tx_inclusion(
332	tx_progress: &mut TxProgress<ChainConfig, OnlineClient<ChainConfig>>,
333	finalized: bool,
334) -> Result<()> {
335	while let Some(Ok(status)) = tx_progress.next().await {
336		crate::log_verbose!("   Transaction status: {:?}", status);
337		match status {
338			TxStatus::InBestBlock(block_hash) => {
339				crate::log_verbose!("   Transaction included in block: {:?}", block_hash);
340				if finalized {
341					continue;
342				} else {
343					break;
344				};
345			},
346			TxStatus::InFinalizedBlock(block_hash) => {
347				crate::log_verbose!("   Transaction finalized in block: {:?}", block_hash);
348				break;
349			},
350			TxStatus::Error { message } | TxStatus::Invalid { message } => {
351				crate::log_error!("   Transaction error: {}", message);
352				break;
353			},
354			_ => continue,
355		}
356	}
357
358	Ok(())
359}