quantus_cli/cli/
common.rs

1//! Common SubXT utilities and functions shared across CLI commands
2use crate::{error::Result, log_error, log_verbose};
3use colored::Colorize;
4use sp_core::crypto::{AccountId32, Ss58Codec};
5
6/// Resolve address - if it's a wallet name, return the wallet's address
7/// If it's already an SS58 address, return it as is
8pub fn resolve_address(address_or_wallet_name: &str) -> Result<String> {
9	// First, try to parse as SS58 address
10	if AccountId32::from_ss58check_with_version(address_or_wallet_name).is_ok() {
11		// It's a valid SS58 address, return as is
12		return Ok(address_or_wallet_name.to_string());
13	}
14
15	// If not a valid SS58 address, try to find it as a wallet name
16	let wallet_manager = crate::wallet::WalletManager::new()?;
17	if let Some(wallet_address) = wallet_manager.find_wallet_address(address_or_wallet_name)? {
18		log_verbose!(
19			"🔍 Found wallet '{}' with address: {}",
20			address_or_wallet_name.bright_cyan(),
21			wallet_address.bright_green()
22		);
23		return Ok(wallet_address);
24	}
25
26	// Neither a valid SS58 address nor a wallet name
27	Err(crate::error::QuantusError::Generic(format!(
28		"Invalid destination: '{address_or_wallet_name}' is neither a valid SS58 address nor a known wallet name"
29	)))
30}
31
32/// Get fresh nonce for account from the latest block using existing QuantusClient
33/// This function ensures we always get the most current nonce from the chain
34/// to avoid "Transaction is outdated" errors
35pub async fn get_fresh_nonce_with_client(
36	quantus_client: &crate::chain::client::QuantusClient,
37	from_keypair: &crate::wallet::QuantumKeyPair,
38) -> Result<u64> {
39	let (from_account_id, _version) =
40		AccountId32::from_ss58check_with_version(&from_keypair.to_account_id_ss58check()).map_err(
41			|e| crate::error::QuantusError::NetworkError(format!("Invalid from address: {e:?}")),
42		)?;
43
44	// Get nonce from the latest block (best block)
45	let latest_nonce = quantus_client
46		.get_account_nonce_from_best_block(&from_account_id)
47		.await
48		.map_err(|e| {
49			crate::error::QuantusError::NetworkError(format!(
50				"Failed to get account nonce from best block: {e:?}"
51			))
52		})?;
53
54	log_verbose!("🔢 Using fresh nonce from latest block: {}", latest_nonce);
55
56	// Compare with nonce from finalized block for debugging
57	let finalized_nonce = quantus_client
58		.client()
59		.tx()
60		.account_nonce(&from_account_id)
61		.await
62		.map_err(|e| {
63			crate::error::QuantusError::NetworkError(format!(
64				"Failed to get account nonce from finalized block: {e:?}"
65			))
66		})?;
67
68	if latest_nonce != finalized_nonce {
69		log_verbose!(
70			"⚠️  Nonce difference detected! Latest: {}, Finalized: {}",
71			latest_nonce,
72			finalized_nonce
73		);
74	}
75
76	Ok(latest_nonce)
77}
78
79/// Get incremented nonce for retry scenarios from the latest block using existing QuantusClient
80/// This is useful when a transaction fails but the chain doesn't update the nonce
81pub async fn get_incremented_nonce_with_client(
82	quantus_client: &crate::chain::client::QuantusClient,
83	from_keypair: &crate::wallet::QuantumKeyPair,
84	base_nonce: u64,
85) -> Result<u64> {
86	let (from_account_id, _version) =
87		AccountId32::from_ss58check_with_version(&from_keypair.to_account_id_ss58check()).map_err(
88			|e| crate::error::QuantusError::NetworkError(format!("Invalid from address: {e:?}")),
89		)?;
90
91	// Get current nonce from the latest block
92	let current_nonce = quantus_client
93		.get_account_nonce_from_best_block(&from_account_id)
94		.await
95		.map_err(|e| {
96			crate::error::QuantusError::NetworkError(format!(
97				"Failed to get account nonce from best block: {e:?}"
98			))
99		})?;
100
101	// Use the higher of current nonce or base_nonce + 1
102	let incremented_nonce = std::cmp::max(current_nonce, base_nonce + 1);
103	log_verbose!(
104		"🔢 Using incremented nonce: {} (base: {}, current from latest block: {})",
105		incremented_nonce,
106		base_nonce,
107		current_nonce
108	);
109	Ok(incremented_nonce)
110}
111
112/// Helper function to submit transaction with nonce management and retry logic
113/// Submit transaction with best block nonce for better performance on fast chains
114pub async fn submit_transaction<Call>(
115	quantus_client: &crate::chain::client::QuantusClient,
116	from_keypair: &crate::wallet::QuantumKeyPair,
117	call: Call,
118	tip: Option<u128>,
119) -> crate::error::Result<subxt::utils::H256>
120where
121	Call: subxt::tx::Payload,
122{
123	let signer = from_keypair.to_subxt_signer().map_err(|e| {
124		crate::error::QuantusError::NetworkError(format!("Failed to convert keypair: {e:?}"))
125	})?;
126
127	// Retry logic with automatic nonce management
128	let mut attempt = 0;
129	let mut current_nonce = None;
130
131	loop {
132		attempt += 1;
133
134		// Get fresh nonce for each attempt, or increment if we have a previous nonce
135		let nonce = if let Some(prev_nonce) = current_nonce {
136			// After first failure, try with incremented nonce
137			let incremented_nonce =
138				get_incremented_nonce_with_client(quantus_client, from_keypair, prev_nonce).await?;
139			log_verbose!(
140				"🔢 Using incremented nonce from best block: {} (previous: {})",
141				incremented_nonce,
142				prev_nonce
143			);
144			incremented_nonce
145		} else {
146			// First attempt - get fresh nonce from best block
147			let fresh_nonce = get_fresh_nonce_with_client(quantus_client, from_keypair).await?;
148			log_verbose!("🔢 Using fresh nonce from best block: {}", fresh_nonce);
149			fresh_nonce
150		};
151		current_nonce = Some(nonce);
152
153		// Get current block for logging using latest block hash
154		let latest_block_hash = quantus_client.get_latest_block().await.map_err(|e| {
155			crate::error::QuantusError::NetworkError(format!("Failed to get latest block: {e:?}"))
156		})?;
157
158		log_verbose!("🔗 Latest block hash: {:?}", latest_block_hash);
159
160		// Create custom params with fresh nonce and optional tip
161		use subxt::config::DefaultExtrinsicParamsBuilder;
162		let mut params_builder = DefaultExtrinsicParamsBuilder::new()
163			.mortal(256) // Value higher than our finalization - TODO: should come from config
164			.nonce(nonce);
165
166		if let Some(tip_amount) = tip {
167			params_builder = params_builder.tip(tip_amount);
168			log_verbose!("💰 Using tip: {} to increase priority", tip_amount);
169		} else {
170			log_verbose!("💰 No tip specified, using default priority");
171		}
172
173		// Try to get chain parameters from the client
174		let genesis_hash = quantus_client.get_genesis_hash().await?;
175		let (spec_version, transaction_version) = quantus_client.get_runtime_version().await?;
176
177		log_verbose!("🔍 Chain parameters:");
178		log_verbose!("   Genesis hash: {:?}", genesis_hash);
179		log_verbose!("   Spec version: {}", spec_version);
180		log_verbose!("   Transaction version: {}", transaction_version);
181
182		// For now, just use the default params
183		let params = params_builder.build();
184
185		// Log transaction parameters for debugging
186		log_verbose!("🔍 Transaction parameters:");
187		log_verbose!("   Nonce: {}", nonce);
188		log_verbose!("   Tip: {:?}", tip);
189		log_verbose!("   Latest block hash: {:?}", latest_block_hash);
190
191		// Get and log era information
192		log_verbose!("   Era: Using default era from SubXT");
193		log_verbose!("   Genesis hash: Using default from SubXT");
194		log_verbose!("   Spec version: Using default from SubXT");
195
196		// Log additional debugging info
197		log_verbose!("🔍 Additional debugging:");
198		log_verbose!("   Call type: {:?}", std::any::type_name::<Call>());
199
200		// Submit the transaction with fresh nonce and optional tip
201		match quantus_client.client().tx().sign_and_submit(&call, &signer, params).await {
202			Ok(tx_hash) => {
203				crate::log_verbose!("📋 Transaction submitted: {:?}", tx_hash);
204				return Ok(tx_hash);
205			},
206			Err(e) => {
207				let error_msg = format!("{e:?}");
208
209				// Check if it's a retryable error
210				let is_retryable = error_msg.contains("Priority is too low") ||
211					error_msg.contains("Transaction is outdated") ||
212					error_msg.contains("Transaction is temporarily banned") ||
213					error_msg.contains("Transaction has a bad signature") ||
214					error_msg.contains("Invalid Transaction");
215
216				if is_retryable && attempt < 5 {
217					log_verbose!(
218						"⚠️  Transaction error detected (attempt {}/5): {}",
219						attempt,
220						error_msg
221					);
222
223					// Exponential backoff: 2s, 4s, 8s, 16s
224					let delay = std::cmp::min(2u64.pow(attempt as u32), 16);
225					log_verbose!("⏳ Waiting {} seconds before retry...", delay);
226					tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await;
227					continue;
228				} else {
229					log_verbose!("❌ Final error after {} attempts: {}", attempt, error_msg);
230					return Err(crate::error::QuantusError::NetworkError(format!(
231						"Failed to submit transaction: {e:?}"
232					)));
233				}
234			},
235		}
236	}
237}
238
239/// Submit transaction with manual nonce (no retry logic - use exact nonce provided)
240pub async fn submit_transaction_with_nonce<Call>(
241	quantus_client: &crate::chain::client::QuantusClient,
242	from_keypair: &crate::wallet::QuantumKeyPair,
243	call: Call,
244	tip: Option<u128>,
245	nonce: u32,
246) -> crate::error::Result<subxt::utils::H256>
247where
248	Call: subxt::tx::Payload,
249{
250	let signer = from_keypair.to_subxt_signer().map_err(|e| {
251		crate::error::QuantusError::NetworkError(format!("Failed to convert keypair: {e:?}"))
252	})?;
253
254	// Get current block for logging using latest block hash
255	let latest_block_hash = quantus_client.get_latest_block().await.map_err(|e| {
256		crate::error::QuantusError::NetworkError(format!("Failed to get latest block: {e:?}"))
257	})?;
258
259	log_verbose!("🔗 Latest block hash: {:?}", latest_block_hash);
260
261	// Create custom params with manual nonce and optional tip
262	use subxt::config::DefaultExtrinsicParamsBuilder;
263	let mut params_builder = DefaultExtrinsicParamsBuilder::new()
264		.mortal(256) // Value higher than our finalization - TODO: should come from config
265		.nonce(nonce.into());
266
267	if let Some(tip_amount) = tip {
268		params_builder = params_builder.tip(tip_amount);
269		log_verbose!("💰 Using tip: {}", tip_amount);
270	}
271
272	let params = params_builder.build();
273
274	log_verbose!("🔢 Using manual nonce: {}", nonce);
275	log_verbose!("📤 Submitting transaction with manual nonce...");
276
277	// Submit the transaction with manual nonce
278	match quantus_client.client().tx().sign_and_submit(&call, &signer, params).await {
279		Ok(tx_hash) => {
280			log_verbose!("✅ Transaction submitted successfully: {:?}", tx_hash);
281			Ok(tx_hash)
282		},
283		Err(e) => {
284			log_error!("❌ Failed to submit transaction with manual nonce {}: {e:?}", nonce);
285			Err(crate::error::QuantusError::NetworkError(format!(
286				"Failed to submit transaction with nonce {nonce}: {e:?}"
287			)))
288		},
289	}
290}