neo3/neo_wallets/wallet/
wallet.rs

1use std::{collections::HashMap, fs::File, io::Write, path::PathBuf};
2
3use primitive_types::H160;
4use rayon::prelude::*;
5use serde_derive::{Deserialize, Serialize};
6
7use crate::{
8	neo_builder::{AccountSigner, Transaction, TransactionBuilder, VerificationScript, Witness},
9	neo_clients::{APITrait, JsonRpcProvider, ProviderError, RpcClient},
10	neo_config::NeoConstants,
11	neo_contract::ContractError,
12	neo_crypto::{CryptoError, HashableForVec, KeyPair, Secp256r1Signature},
13	neo_protocol::{Account, AccountTrait, UnclaimedGas},
14	neo_types::{
15		contract::ContractMethodToken,
16		script_hash::ScriptHashExtension,
17		serde_with_utils::{
18			deserialize_hash_map_h160_account, deserialize_script_hash,
19			serialize_hash_map_h160_account, serialize_script_hash,
20		},
21		AddressExtension, ScryptParamsDef,
22	},
23	neo_wallets::{NEP6Account, NEP6Contract, NEP6Parameter, Nep6Wallet, WalletError, WalletTrait},
24};
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Wallet {
28	pub name: String,
29	pub version: String,
30	pub scrypt_params: ScryptParamsDef,
31	#[serde(deserialize_with = "deserialize_hash_map_h160_account")]
32	#[serde(serialize_with = "serialize_hash_map_h160_account")]
33	pub accounts: HashMap<H160, Account>,
34	#[serde(deserialize_with = "deserialize_script_hash")]
35	#[serde(serialize_with = "serialize_script_hash")]
36	pub(crate) default_account: H160,
37	/// Additional wallet metadata stored as key-value pairs
38	#[serde(skip_serializing_if = "Option::is_none")]
39	pub extra: Option<HashMap<String, String>>,
40}
41
42impl WalletTrait for Wallet {
43	type Account = Account;
44
45	fn name(&self) -> &String {
46		&self.name
47	}
48
49	fn version(&self) -> &String {
50		&self.version
51	}
52
53	fn scrypt_params(&self) -> &ScryptParamsDef {
54		&self.scrypt_params
55	}
56
57	fn accounts(&self) -> Vec<Self::Account> {
58		self.accounts
59			.clone()
60			.into_iter()
61			.map(|(_k, v)| v.clone())
62			.collect::<Vec<Self::Account>>()
63	}
64
65	fn default_account(&self) -> &Account {
66		&self.accounts[&self.default_account]
67	}
68
69	fn set_name(&mut self, name: String) {
70		self.name = name;
71	}
72
73	fn set_version(&mut self, version: String) {
74		self.version = version;
75	}
76
77	fn set_scrypt_params(&mut self, params: ScryptParamsDef) {
78		self.scrypt_params = params;
79	}
80
81	fn set_default_account(&mut self, default_account: H160) {
82		self.default_account = default_account.clone();
83		if let Some(account) = self.accounts.get_mut(&self.default_account) {
84			account.is_default = true;
85		}
86	}
87
88	fn add_account(&mut self, account: Self::Account) {
89		// let weak_self = Arc::new(&self);
90		// account.set_wallet(Some(Arc::downgrade(weak_self)));
91		self.accounts.insert(account.get_script_hash().clone(), account);
92	}
93
94	fn remove_account(&mut self, hash: &H160) -> Option<Self::Account> {
95		self.accounts.remove(hash)
96	}
97}
98
99impl Wallet {
100	/// The default wallet name.
101	pub const DEFAULT_WALLET_NAME: &'static str = "NeoWallet";
102	/// The current wallet version.
103	pub const CURRENT_VERSION: &'static str = "1.0";
104
105	/// Creates a new wallet instance with a default account.
106	pub fn new() -> Self {
107		let account = match Account::create() {
108			Ok(mut acc) => {
109				acc.is_default = true;
110				acc
111			},
112			Err(e) => {
113				eprintln!("Failed to create account: {}", e);
114				return Self::default();
115			},
116		};
117
118		let mut accounts = HashMap::new();
119		accounts.insert(account.address_or_scripthash.script_hash(), account.clone());
120		Self {
121			name: "NeoWallet".to_string(),
122			version: "1.0".to_string(),
123			scrypt_params: ScryptParamsDef::default(),
124			accounts,
125			default_account: account.clone().address_or_scripthash.script_hash(),
126			extra: None,
127		}
128	}
129
130	/// Creates a new wallet instance without any accounts.
131	pub fn default() -> Self {
132		Self {
133			name: "NeoWallet".to_string(),
134			version: "1.0".to_string(),
135			scrypt_params: ScryptParamsDef::default(),
136			accounts: HashMap::new(),
137			default_account: H160::default(),
138			extra: None,
139		}
140	}
141
142	/// Converts the wallet to a NEP6Wallet format.
143	pub fn to_nep6(&self) -> Result<Nep6Wallet, WalletError> {
144		let accounts = self
145			.accounts
146			.clone()
147			.into_iter()
148			.filter_map(|(_, account)| match NEP6Account::from_account(&account) {
149				Ok(nep6_account) => Some(nep6_account),
150				Err(e) => {
151					eprintln!("Failed to convert account to NEP6Account: {}", e);
152					None
153				},
154			})
155			.collect::<Vec<NEP6Account>>();
156
157		Ok(Nep6Wallet {
158			name: self.name.clone(),
159			version: self.version.clone(),
160			scrypt: self.scrypt_params.clone(),
161			accounts,
162			extra: None,
163		})
164	}
165
166	/// Creates a wallet from a NEP6Wallet format.
167	pub fn from_nep6(nep6: Nep6Wallet) -> Result<Self, WalletError> {
168		let accounts = nep6
169			.accounts()
170			.into_iter()
171			.filter_map(|v| v.to_account().ok())
172			.collect::<Vec<_>>();
173
174		// Find default account or use first account
175		let default_account_address =
176			if let Some(account) = nep6.accounts().iter().find(|a| a.is_default) {
177				account.address().clone()
178			} else if let Some(account) = nep6.accounts().first() {
179				eprintln!("No default account found, using first account");
180				account.address().clone()
181			} else {
182				eprintln!("No accounts found, using empty address");
183				String::new()
184			};
185
186		Ok(Self {
187			name: nep6.name().clone(),
188			version: nep6.version().clone(),
189			scrypt_params: nep6.scrypt().clone(),
190			accounts: accounts.into_iter().map(|a| (a.get_script_hash().clone(), a)).collect(),
191			default_account: default_account_address.address_to_script_hash().map_err(|e| {
192				WalletError::AccountState(format!(
193					"Failed to convert address to script hash: {}",
194					e
195				))
196			})?,
197			extra: nep6.extra.clone(),
198		})
199	}
200
201	// pub async fn get_nep17_balances(&self) -> Result<HashMap<H160, u32>, WalletError> {
202	// 	let balances = HTTP_PROVIDER
203	// 		.get_nep17_balances(self.get_script_hash().clone())
204	// 		.await
205	// 		.map_err(|e| WalletError::RpcError(format!("Failed to get NEP17 balances: {}", e)))?;
206	// 	let mut nep17_balances = HashMap::new();
207	// 	for balance in balances.balances {
208	// 		nep17_balances.insert(balance.asset_hash, u32::from_str(&balance.amount).unwrap());
209	// 	}
210	// 	Ok(nep17_balances)
211	// }
212
213	pub fn from_account(account: &Account) -> Result<Wallet, WalletError> {
214		let mut wallet: Wallet = Wallet::new();
215		wallet.add_account(account.clone());
216		wallet.set_default_account(account.get_script_hash());
217		Ok(wallet)
218	}
219
220	/// Adds the given accounts to this wallet, if it doesn't contain an account with the same script hash (address).
221	///
222	/// # Parameters
223	///
224	/// * `accounts` - The accounts to add
225	///
226	/// # Returns
227	///
228	/// Returns the mutable wallet reference if the accounts were successfully added, or a `WalletError` if an account is already contained in another wallet.
229	///
230	/// # Errors
231	///
232	/// Returns a `WalletError::IllegalArgument` error if an account is already contained in another wallet.
233	///
234	/// # Example
235	///
236	/// ```
237	///
238	/// use neo3::prelude::*;
239	/// use neo3::neo_protocol::AccountTrait;
240	/// let account1 = protocol::Account::create().unwrap();
241	/// let account2 = protocol::Account::create().unwrap();
242	///
243	/// let mut wallet = wallets::Wallet::from_accounts(vec![account1, account2]).unwrap();
244	/// ```
245	pub fn from_accounts(accounts: Vec<Account>) -> Result<Wallet, WalletError> {
246		// for account in &accounts {
247		// 	if account.wallet().is_some() {
248		// 		return Err(WalletError::AccountState(format!(
249		// 			"The account {} is already contained in a wallet. Please remove this account from its containing wallet before adding it to another wallet.",
250		// 			account.address_or_scripthash.address()
251		// 		)));
252		// 	}
253		// }
254
255		let mut wallet: Wallet = Wallet::default();
256		for account in &accounts {
257			wallet.add_account(account.clone());
258			// account.wallet = Some(self);
259		}
260		if let Some(first_account) = accounts.first() {
261			wallet.set_default_account(first_account.get_script_hash());
262		} else {
263			return Err(WalletError::NoAccounts);
264		}
265		Ok(wallet)
266	}
267
268	pub fn save_to_file(&self, path: PathBuf) -> Result<(), WalletError> {
269		// Convert wallet to NEP6
270		let nep6 = self.to_nep6()?;
271
272		// Encode as JSON
273		let json = serde_json::to_string(&nep6).map_err(|e| {
274			WalletError::AccountState(format!("Failed to serialize wallet to JSON: {e}"))
275		})?;
276
277		// Write to file at path
278		let mut file = File::create(path)
279			.map_err(|e| WalletError::FileError(format!("Failed to create wallet file: {e}")))?;
280		file.write_all(json.as_bytes())
281			.map_err(|e| WalletError::FileError(format!("Failed to write wallet file: {e}")))?;
282
283		Ok(())
284	}
285
286	pub fn get_account(&self, script_hash: &H160) -> Option<&Account> {
287		self.accounts.get(script_hash)
288	}
289
290	pub fn remove_account(&mut self, script_hash: &H160) -> bool {
291		self.accounts.remove(script_hash).is_some()
292	}
293
294	pub fn encrypt_accounts(&mut self, password: &str) {
295		for account in self.accounts.values_mut() {
296			// Only encrypt accounts that have a key pair
297			if account.key_pair().is_some() {
298				if let Err(e) = account.encrypt_private_key(password) {
299					eprintln!(
300						"Warning: Failed to encrypt private key for account {}: {}",
301						account.get_address(),
302						e
303					);
304				}
305			}
306		}
307	}
308
309	/// Encrypts all accounts in the wallet using parallel processing.
310	///
311	/// This method provides significant performance improvements when dealing with
312	/// wallets containing many accounts by leveraging Rayon's parallel iteration.
313	/// The encryption of each account is independent and CPU-intensive (due to
314	/// scrypt key derivation), making it ideal for parallelization.
315	///
316	/// # Arguments
317	///
318	/// * `password` - The password to use for encrypting all accounts
319	///
320	/// # Performance Notes
321	///
322	/// - Uses Rayon's work-stealing thread pool for optimal CPU utilization
323	/// - Each account encryption is processed in parallel
324	/// - Thread pool size automatically adjusts to available CPU cores
325	/// - Performance gains scale with the number of accounts and CPU cores
326	///
327	/// # Example
328	///
329	/// ```no_run
330	/// # use neo3::prelude::*;
331	/// # let mut wallet = wallets::Wallet::new();
332	/// // For wallets with many accounts, use parallel encryption
333	/// wallet.encrypt_accounts_parallel("strong_password");
334	/// ```
335	pub fn encrypt_accounts_parallel(&mut self, password: &str) {
336		// Collect errors in a thread-safe manner
337		let errors: Vec<(String, String)> = self
338			.accounts
339			.par_iter_mut()
340			.filter_map(|(_, account)| {
341				// Only encrypt accounts that have a key pair
342				if account.key_pair().is_some() {
343					match account.encrypt_private_key(password) {
344						Err(e) => Some((account.get_address(), e.to_string())),
345						Ok(_) => None,
346					}
347				} else {
348					None
349				}
350			})
351			.collect();
352
353		// Log any errors that occurred
354		for (address, error) in errors {
355			eprintln!("Warning: Failed to encrypt private key for account {}: {}", address, error);
356		}
357	}
358
359	/// Encrypts accounts in parallel with custom thread pool configuration.
360	///
361	/// This method allows fine-tuning of the parallel encryption process by
362	/// configuring the number of threads used. This can be useful in scenarios
363	/// where you want to limit CPU usage or optimize for specific hardware.
364	///
365	/// # Arguments
366	///
367	/// * `password` - The password to use for encrypting all accounts
368	/// * `num_threads` - The number of threads to use for parallel processing
369	///
370	/// # Example
371	///
372	/// ```no_run
373	/// # use neo3::prelude::*;
374	/// # let mut wallet = wallets::Wallet::new();
375	/// // Use 4 threads for encryption
376	/// wallet.encrypt_accounts_parallel_with_threads("strong_password", 4);
377	/// ```
378	pub fn encrypt_accounts_parallel_with_threads(&mut self, password: &str, num_threads: usize) {
379		// Create a custom thread pool with the specified number of threads
380		let pool = rayon::ThreadPoolBuilder::new().num_threads(num_threads).build().unwrap();
381
382		pool.install(|| {
383			self.encrypt_accounts_parallel(password);
384		});
385	}
386
387	/// Encrypts accounts in parallel using batch processing.
388	///
389	/// This method processes accounts in batches, which can be more efficient
390	/// for very large wallets by reducing overhead and improving cache locality.
391	/// It uses a different approach than the standard parallel method by collecting
392	/// account data first to avoid mutable borrow conflicts.
393	///
394	/// # Arguments
395	///
396	/// * `password` - The password to use for encrypting all accounts
397	/// * `batch_size` - The number of accounts to process in each batch
398	///
399	/// # Example
400	///
401	/// ```no_run
402	/// # use neo3::prelude::*;
403	/// # let mut wallet = wallets::Wallet::new();
404	/// // Process accounts in batches of 50
405	/// wallet.encrypt_accounts_batch_parallel("strong_password", 50);
406	/// ```
407	pub fn encrypt_accounts_batch_parallel(&mut self, password: &str, batch_size: usize) {
408		use std::sync::{Arc, Mutex};
409
410		// Collect accounts that need encryption along with their script hashes
411		let accounts_to_encrypt: Vec<(H160, Account)> = self
412			.accounts
413			.iter()
414			.filter(|(_, account)| account.key_pair().is_some())
415			.map(|(hash, account)| (*hash, account.clone()))
416			.collect();
417
418		// Process in parallel batches and collect results
419		let results: Arc<Mutex<Vec<(H160, Result<Account, String>)>>> =
420			Arc::new(Mutex::new(Vec::new()));
421
422		accounts_to_encrypt.par_chunks(batch_size).for_each(|batch| {
423			let batch_results: Vec<(H160, Result<Account, String>)> = batch
424				.iter()
425				.map(|(hash, account)| {
426					let mut account_clone = account.clone();
427					match account_clone.encrypt_private_key(password) {
428						Ok(_) => (*hash, Ok(account_clone)),
429						Err(e) => (*hash, Err(format!("{}: {}", account.get_address(), e))),
430					}
431				})
432				.collect();
433
434			results.lock().unwrap().extend(batch_results);
435		});
436
437		// Apply successful encryptions and collect errors
438		let results = Arc::try_unwrap(results).unwrap().into_inner().unwrap();
439		for (hash, result) in results {
440			match result {
441				Ok(encrypted_account) => {
442					self.accounts.insert(hash, encrypted_account);
443				},
444				Err(error_msg) => {
445					eprintln!("Warning: Failed to encrypt private key for account {}", error_msg);
446				},
447			}
448		}
449	}
450
451	/// Creates a new wallet and saves it to the specified path
452	///
453	/// This method has been renamed to `create_wallet` for clarity.
454	/// Please use `create_wallet` instead.
455	///
456	/// # Arguments
457	///
458	/// * `path` - The file path where the wallet will be saved
459	/// * `password` - The password to encrypt the wallet
460	///
461	/// # Returns
462	///
463	/// A `Result` containing the new wallet or a `WalletError`
464	#[deprecated(since = "0.1.0", note = "Please use `create_wallet` instead")]
465	pub fn create(path: &PathBuf, password: &str) -> Result<Self, WalletError> {
466		Self::create_wallet(path, password)
467	}
468
469	/// Opens a wallet from the specified path
470	///
471	/// This method has been renamed to `open_wallet` for clarity.
472	/// Please use `open_wallet` instead.
473	///
474	/// # Arguments
475	///
476	/// * `path` - The file path of the wallet to open
477	/// * `password` - The password to decrypt the wallet
478	///
479	/// # Returns
480	///
481	/// A `Result` containing the opened wallet or a `WalletError`
482	#[deprecated(since = "0.1.0", note = "Please use `open_wallet` instead")]
483	pub fn open(path: &PathBuf, password: &str) -> Result<Self, WalletError> {
484		Self::open_wallet(path, password)
485	}
486
487	/// Returns all accounts in the wallet
488	pub fn get_accounts(&self) -> Vec<&Account> {
489		self.accounts.values().collect()
490	}
491
492	/// Creates a new account in the wallet
493	pub fn create_account(&mut self) -> Result<&Account, WalletError> {
494		let account = Account::create()?;
495		self.add_account(account.clone());
496		Ok(self.get_account(&account.get_script_hash()).unwrap())
497	}
498
499	/// Imports a private key in WIF format
500	pub fn import_private_key(&mut self, wif: &str) -> Result<&Account, WalletError> {
501		let key_pair = KeyPair::from_wif(wif)
502			.map_err(|e| WalletError::AccountState(format!("Failed to import private key: {e}")))?;
503
504		let account = Account::from_key_pair(key_pair, None, None)
505			.map_err(|e| WalletError::ProviderError(e))?;
506		self.add_account(account.clone());
507		Ok(self.get_account(&account.get_script_hash()).unwrap())
508	}
509
510	/// Verifies if the provided password is correct by attempting to decrypt any encrypted account
511	///
512	/// This function checks if the provided password can successfully decrypt at least one
513	/// of the encrypted private keys in the wallet. If at least one account can be decrypted,
514	/// the password is considered valid.
515	///
516	/// Returns true if the password is correct, false otherwise.
517	pub fn verify_password(&self, password: &str) -> bool {
518		// If there are no accounts, we can't verify the password
519		if self.accounts.is_empty() {
520			return false;
521		}
522
523		// Try to decrypt any account with the provided password
524		for account in self.accounts.values() {
525			// Skip accounts that don't have an encrypted private key
526			if account.encrypted_private_key().is_none() {
527				continue;
528			}
529
530			// Skip accounts that already have a key pair (already decrypted)
531			if account.key_pair().is_some() {
532				continue;
533			}
534
535			// Try to decrypt the account's private key
536			let mut account_clone = account.clone();
537			match account_clone.decrypt_private_key(password) {
538				Ok(_) => return true, // Password decrypted successfully
539				Err(_) => continue,   // Try the next account
540			}
541		}
542
543		// If we get here, none of the accounts could be decrypted with the provided password
544		false
545	}
546
547	/// Changes the wallet password
548	pub fn change_password(
549		&mut self,
550		current_password: &str,
551		new_password: &str,
552	) -> Result<(), WalletError> {
553		if !self.verify_password(current_password) {
554			return Err(WalletError::AccountState("Invalid password".to_string()));
555		}
556
557		// First decrypt all accounts with the current password
558		for account in self.accounts.values_mut() {
559			if account.encrypted_private_key().is_some() && account.key_pair().is_none() {
560				if let Err(e) = account.decrypt_private_key(current_password) {
561					return Err(WalletError::DecryptionError(format!(
562						"Failed to decrypt account {}: {}",
563						account.get_address(),
564						e
565					)));
566				}
567			}
568		}
569
570		// Re-encrypt all accounts with the new password
571		self.encrypt_accounts(new_password);
572
573		Ok(())
574	}
575
576	/// Changes the wallet password using parallel processing.
577	///
578	/// This method provides better performance for wallets with many accounts
579	/// by parallelizing both the decryption and re-encryption processes.
580	///
581	/// # Arguments
582	///
583	/// * `current_password` - The current password of the wallet
584	/// * `new_password` - The new password to set
585	///
586	/// # Returns
587	///
588	/// A `Result` indicating success or containing a `WalletError` on failure
589	///
590	/// # Example
591	///
592	/// ```no_run
593	/// # use neo3::prelude::*;
594	/// # let mut wallet = wallets::Wallet::new();
595	/// wallet.change_password_parallel("old_password", "new_password").unwrap();
596	/// ```
597	pub fn change_password_parallel(
598		&mut self,
599		current_password: &str,
600		new_password: &str,
601	) -> Result<(), WalletError> {
602		if !self.verify_password(current_password) {
603			return Err(WalletError::AccountState("Invalid password".to_string()));
604		}
605
606		// Collect accounts that need decryption
607		let accounts_to_decrypt: Vec<(H160, Account)> = self
608			.accounts
609			.iter()
610			.filter(|(_, account)| {
611				account.encrypted_private_key().is_some() && account.key_pair().is_none()
612			})
613			.map(|(hash, account)| (*hash, account.clone()))
614			.collect();
615
616		// Decrypt accounts in parallel
617		let decrypted_results: Vec<(H160, Result<Account, String>)> = accounts_to_decrypt
618			.into_par_iter()
619			.map(|(hash, account)| {
620				let mut account_clone = account.clone();
621				match account_clone.decrypt_private_key(current_password) {
622					Ok(_) => (hash, Ok(account_clone)),
623					Err(e) => (hash, Err(format!("{}: {}", account.get_address(), e))),
624				}
625			})
626			.collect();
627
628		// Check for decryption errors
629		for (_, result) in &decrypted_results {
630			if let Err(error_msg) = result {
631				return Err(WalletError::DecryptionError(format!(
632					"Failed to decrypt account {}",
633					error_msg
634				)));
635			}
636		}
637
638		// Apply successful decryptions
639		for (hash, result) in decrypted_results {
640			if let Ok(decrypted_account) = result {
641				self.accounts.insert(hash, decrypted_account);
642			}
643		}
644
645		// Re-encrypt all accounts with the new password using parallel processing
646		self.encrypt_accounts_parallel(new_password);
647
648		Ok(())
649	}
650
651	/// Gets the unclaimed GAS for all accounts in the wallet
652	pub async fn get_unclaimed_gas<P>(&self, rpc_client: &P) -> Result<UnclaimedGas, WalletError>
653	where
654		P: JsonRpcProvider + APITrait + 'static,
655		<P as APITrait>::Error: Into<ProviderError>,
656	{
657		let mut total_unclaimed = UnclaimedGas::default();
658
659		for account in self.get_accounts() {
660			let script_hash = account.get_script_hash();
661			let unclaimed = rpc_client
662				.get_unclaimed_gas(script_hash)
663				.await
664				.map_err(|e| WalletError::ProviderError(e.into()))?;
665
666			total_unclaimed += unclaimed;
667		}
668
669		Ok(total_unclaimed)
670	}
671}
672
673impl Wallet {
674	/// Signs a given message using the default account's private key.
675	///
676	/// This method computes the SHA-256 hash of the input message and then signs it
677	/// using the ECDSA Secp256r1 algorithm. It's primarily used for generating signatures
678	/// that can prove ownership of an address or for other cryptographic verifications.
679	///
680	/// # Parameters
681	///
682	/// - `message`: The message to be signed. This can be any data that implements `AsRef<[u8]>`,
683	/// allowing for flexibility in the type of data that can be signed.
684	///
685	/// # Returns
686	///
687	/// A `Result` that, on success, contains the `Secp256r1Signature` of the message. On failure,
688	/// it returns a `WalletError`, which could indicate issues like a missing key pair.
689	///
690	/// # Example
691	///
692	/// ```no_run
693	/// # use neo3::prelude::*;
694	///  async fn example() -> Result<(), Box<dyn std::error::Error>> {
695	/// # let wallet = wallets::Wallet::new();
696	/// let message = "Hello, world!";
697	/// let signature = wallet.sign_message(message).await?;
698	/// println!("Signed message: {:?}", signature);
699	/// # Ok(())
700	/// # }
701	/// ```
702	pub async fn sign_message<S: Send + Sync + AsRef<[u8]>>(
703		&self,
704		message: S,
705	) -> Result<Secp256r1Signature, WalletError> {
706		let message = message.as_ref();
707		let binding = message.hash256();
708		let message_hash = binding.as_slice();
709		self.default_account()
710			.clone()
711			.key_pair()
712			.clone()
713			.ok_or_else(|| WalletError::NoKeyPair)?
714			.private_key()
715			.sign_tx(message_hash)
716			.map_err(|_e| WalletError::NoKeyPair)
717	}
718
719	/// Generates a witness for a transaction using the default account's key pair.
720	///
721	/// This method is used to attach a signature to a transaction, proving that the
722	/// transaction was authorized by the owner of the default account. It's an essential
723	/// step in transaction validation for blockchain systems.
724	///
725	/// # Parameters
726	///
727	/// - `tx`: A reference to the transaction that needs a witness.
728	///
729	/// # Returns
730	///
731	/// A `Result` that, on success, contains the `Witness` for the given transaction.
732	/// On failure, it returns a `WalletError`, which could be due to issues like a missing
733	/// key pair.
734	///
735	/// # Example
736	///
737	/// ```no_run
738	/// # use neo3::prelude::*;
739	///  async fn example() -> Result<(), Box<dyn std::error::Error>> {
740	/// # let wallet = wallets::Wallet::new();
741	/// # let provider = providers::HttpProvider::new("http://localhost:10332")?;
742	/// # let client = providers::RpcClient::new(provider);
743	/// # let mut tx_builder = builder::TransactionBuilder::with_client(&client);
744	/// # let tx = tx_builder.get_unsigned_tx().await?;
745	/// let witness = wallet.get_witness(&tx).await?;
746	/// println!("Witness: {:?}", witness);
747	/// # Ok(())
748	/// # }
749	/// ```
750	pub async fn get_witness<'a, P: JsonRpcProvider + 'static>(
751		&self,
752		tx: &Transaction<'a, P>,
753	) -> Result<Witness, WalletError> {
754		let mut tx_with_chain = tx.clone();
755		if tx_with_chain.network().is_none() {
756			// in the case we don't have a network, let's use the signer network magic instead
757			// tx_with_chain.set_network(Some(self.network()));
758		}
759
760		Witness::create(
761			tx.get_hash_data().await?,
762			&self.default_account().key_pair.clone().ok_or_else(|| WalletError::NoKeyPair)?,
763		)
764		.map_err(|_e| WalletError::NoKeyPair)
765	}
766
767	/// Signs a transaction using the specified account.
768	///
769	/// # Arguments
770	///
771	/// * `tx_builder` - The transaction builder containing the transaction to sign
772	/// * `account_address` - The address of the account to use for signing
773	/// * `password` - The password to decrypt the account's private key if needed
774	///
775	/// # Returns
776	///
777	/// A `Result` containing the signed transaction or a `WalletError`
778	pub async fn sign_transaction<'a, P>(
779		&self,
780		tx_builder: &'a mut TransactionBuilder<'a, P>,
781		account_address: &str,
782		password: &str,
783	) -> Result<Transaction<'a, P>, WalletError>
784	where
785		P: JsonRpcProvider + 'static,
786	{
787		// Get the account from the wallet
788		let script_hash = H160::from_address(account_address)
789			.map_err(|e| WalletError::AccountState(format!("Invalid address: {e}")))?;
790
791		let account = self.get_account(&script_hash).ok_or_else(|| {
792			WalletError::AccountState(format!("Account not found: {account_address}"))
793		})?;
794
795		// Ensure the account has a key pair or can be decrypted
796		let key_pair = match account.key_pair() {
797			Some(kp) => kp.clone(),
798			None => {
799				// Try to decrypt the account with the provided password
800				let mut account_clone = account.clone();
801				account_clone.decrypt_private_key(password).map_err(|e| {
802					WalletError::DecryptionError(format!("Failed to decrypt account: {e}"))
803				})?;
804
805				match account_clone.key_pair() {
806					Some(kp) => kp.clone(),
807					None => return Err(WalletError::NoKeyPair),
808				}
809			},
810		};
811
812		// Build the transaction
813		let mut tx = tx_builder.get_unsigned_tx().await?;
814
815		// Create a witness for the transaction
816		let witness = Witness::create(tx.get_hash_data().await?, &key_pair)
817			.map_err(|e| WalletError::SigningError(format!("Failed to create witness: {e}")))?;
818
819		// Add the witness to the transaction
820		tx.add_witness(witness);
821
822		Ok(tx)
823	}
824
825	/// Returns the address of the wallet's default account.
826	///
827	/// This method provides access to the blockchain address associated with the
828	/// wallet's default account, which is typically used as the sender address in
829	/// transactions.
830	///
831	/// # Returns
832	///
833	/// The `Address` of the wallet's default account.
834	fn address(&self) -> String {
835		// Get the default account's address
836		if let Some(account) = self.get_account(&self.default_account) {
837			account.address_or_scripthash.address().clone()
838		} else {
839			// Return a default address if no default account exists
840			H160::default().to_address()
841		}
842	}
843
844	/// Creates a new wallet with the specified path and password.
845	///
846	/// # Arguments
847	///
848	/// * `path` - The file path where the wallet will be saved
849	/// * `password` - The password to encrypt the wallet
850	///
851	/// # Returns
852	///
853	/// A `Result` containing the new wallet or a `WalletError`
854	pub fn create_wallet(path: &PathBuf, password: &str) -> Result<Self, WalletError> {
855		let mut wallet = Wallet::new();
856
857		// Create a new account for the wallet
858		let account = Account::create().map_err(|e| WalletError::ProviderError(e))?;
859		wallet.add_account(account);
860
861		// Encrypt the wallet with the provided password
862		wallet.encrypt_accounts(password);
863
864		// Save the wallet to the specified path
865		wallet.save_to_file(path.clone())?;
866
867		Ok(wallet)
868	}
869
870	/// Opens an existing wallet from the specified path with the given password.
871	///
872	/// # Arguments
873	///
874	/// * `path` - The file path of the wallet to open
875	/// * `password` - The password to decrypt the wallet
876	///
877	/// # Returns
878	///
879	/// A `Result` containing the opened wallet or a `WalletError`
880	pub fn open_wallet(path: &PathBuf, password: &str) -> Result<Self, WalletError> {
881		// Read the wallet file
882		let wallet_json = std::fs::read_to_string(path)
883			.map_err(|e| WalletError::FileError(format!("Failed to read wallet file: {e}")))?;
884
885		// Parse the wallet JSON
886		let nep6_wallet: Nep6Wallet = serde_json::from_str(&wallet_json).map_err(|e| {
887			WalletError::DeserializationError(format!("Failed to parse wallet JSON: {e}"))
888		})?;
889
890		// Convert to Wallet
891		let mut wallet = Wallet::from_nep6(nep6_wallet)?;
892
893		// Verify the password by checking if we can decrypt any account
894		let can_decrypt = wallet.verify_password(password);
895
896		if !can_decrypt {
897			return Err(WalletError::CryptoError(CryptoError::InvalidPassphrase(
898				"Invalid password".to_string(),
899			)));
900		}
901
902		Ok(wallet)
903	}
904
905	/// Gets all accounts in the wallet.
906	///
907	/// # Returns
908	///
909	/// A vector of references to all accounts in the wallet
910	pub fn get_all_accounts(&self) -> Vec<&Account> {
911		self.accounts.values().collect()
912	}
913
914	/// Creates a new account in the wallet.
915	///
916	/// # Returns
917	///
918	/// A `Result` containing the new account or a `WalletError`
919	pub fn create_new_account(&mut self) -> Result<&Account, WalletError> {
920		let account = Account::create().map_err(|e| WalletError::ProviderError(e))?;
921		let script_hash = account.address_or_scripthash.script_hash();
922		self.add_account(account);
923
924		Ok(self.get_account(&script_hash).unwrap())
925	}
926
927	/// Imports a private key into the wallet.
928	///
929	/// # Arguments
930	///
931	/// * `private_key` - The private key to import
932	///
933	/// # Returns
934	///
935	/// A `Result` containing the imported account or a `WalletError`
936	pub fn import_from_wif(&mut self, private_key: &str) -> Result<&Account, WalletError> {
937		// Create a key pair from the private key
938		let key_pair = KeyPair::from_wif(private_key).map_err(|e| WalletError::CryptoError(e))?;
939
940		// Create an account from the key pair
941		let account = Account::from_key_pair(key_pair, None, None)
942			.map_err(|e| WalletError::AccountState(format!("Failed to create account: {e}")))?;
943		let script_hash = account.address_or_scripthash.script_hash();
944
945		// Add the account to the wallet
946		self.add_account(account);
947
948		Ok(self.get_account(&script_hash).unwrap())
949	}
950
951	/// Gets the unclaimed GAS for the wallet as a float value.
952	///
953	/// # Arguments
954	///
955	/// * `rpc_client` - The RPC client to use for the query
956	///
957	/// # Returns
958	///
959	/// A `Result` containing the unclaimed GAS amount as a float or a `WalletError`
960	pub async fn get_unclaimed_gas_as_float<P>(
961		&self,
962		rpc_client: &RpcClient<P>,
963	) -> Result<f64, WalletError>
964	where
965		P: JsonRpcProvider + 'static,
966	{
967		let mut total_unclaimed = 0.0;
968
969		// Get unclaimed GAS for each account
970		for account in self.accounts.values() {
971			let script_hash = account.address_or_scripthash.script_hash();
972
973			// Query the RPC client for unclaimed GAS
974			let unclaimed = rpc_client
975				.get_unclaimed_gas(script_hash)
976				.await
977				.map_err(|e| WalletError::ProviderError(e))?;
978
979			// Add to the total
980			total_unclaimed += unclaimed.unclaimed.parse::<f64>().unwrap_or(0.0);
981		}
982
983		Ok(total_unclaimed)
984	}
985
986	/// Retrieves the network ID associated with the wallet.
987	///
988	/// This network ID is used for network-specific operations, such as signing
989	/// transactions with EIP-155 to prevent replay attacks across chains.
990	///
991	/// # Returns
992	///
993	/// The network ID as a `u32`.
994	fn network(&self) -> u32 {
995		// Default to MainNet if not specified
996		self.extra
997			.as_ref()
998			.and_then(|extra| {
999				extra
1000					.get("network")
1001					.map(|n| n.parse::<u32>().unwrap_or(NeoConstants::MAGIC_NUMBER_MAINNET))
1002			})
1003			.unwrap_or(NeoConstants::MAGIC_NUMBER_MAINNET)
1004	}
1005
1006	//// Sets the network magic (ID) for the wallet.
1007	///
1008	/// This method configures the wallet to operate within a specific blockchain
1009	/// network by setting the network magic (ID), which is essential for correctly
1010	/// signing transactions.
1011	///
1012	/// # Parameters
1013	///
1014	/// - `network`: The network ID to set for the wallet.
1015	///
1016	/// # Returns
1017	///
1018	/// The modified `Wallet` instance with the new network ID set.
1019	///
1020	/// # Example
1021	///
1022	/// ```no_run
1023	/// # use neo3::prelude::*;
1024	/// let mut wallet = wallets::Wallet::new();
1025	/// wallet = wallet.with_network(0x334F454E); // MainNet magic number
1026	/// ```
1027	pub fn with_network(mut self, network: u32) -> Self {
1028		let mut extra = self.extra.unwrap_or_default();
1029		extra.insert("network".to_string(), network.to_string());
1030		self.extra = Some(extra);
1031		self
1032	}
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037	use crate::{
1038		neo_config::TestConstants,
1039		neo_protocol::{Account, AccountTrait},
1040		neo_wallets::{Wallet, WalletTrait},
1041	};
1042
1043	#[test]
1044	fn test_is_default() {
1045		let account = Account::from_address(TestConstants::DEFAULT_ACCOUNT_ADDRESS)
1046			.expect("Should be able to create account from valid address in test");
1047		let mut wallet: Wallet = Wallet::new();
1048		wallet.add_account(account.clone());
1049
1050		assert!(!account.is_default);
1051
1052		let hash = account.address_or_scripthash.script_hash();
1053		wallet.set_default_account(hash.clone());
1054		assert!(wallet.get_account(&hash).expect("Account should exist in wallet").is_default);
1055	}
1056
1057	// #[test]
1058	// fn test_wallet_link() {
1059	// 	let account = Account::from_address(TestConstants::DEFAULT_ACCOUNT_ADDRESS)
1060	// 		.expect("Should be able to create account from valid address in test");
1061	// 	let wallet = Wallet::create().unwrap();
1062	//
1063	// 	assert!(account.wallet.is_none());
1064	//
1065	// 	wallet.add_account(account).unwrap();
1066	// 	assert_eq!(account.wallet.as_ref().unwrap().as_ptr(), wallet.as_ptr());
1067	// }
1068
1069	#[test]
1070	fn test_create_default_wallet() {
1071		let wallet: Wallet = Wallet::default();
1072
1073		assert_eq!(&wallet.name, "NeoWallet");
1074		assert_eq!(&wallet.version, Wallet::CURRENT_VERSION);
1075		assert_eq!(wallet.accounts.len(), 0usize);
1076	}
1077
1078	#[test]
1079	fn test_create_wallet_with_accounts() {
1080		let account1 = Account::create().expect("Should be able to create account in test");
1081		let account2 = Account::create().expect("Should be able to create account in test");
1082
1083		let wallet = Wallet::from_accounts(vec![account1.clone(), account2.clone()])
1084			.expect("Should be able to create wallet from accounts in test");
1085
1086		assert_eq!(wallet.default_account(), &account1);
1087		assert_eq!(wallet.accounts.len(), 2);
1088		assert!(wallet
1089			.accounts
1090			.clone()
1091			.into_iter()
1092			.any(|(s, _)| s == account1.address_or_scripthash.script_hash()));
1093		assert!(wallet
1094			.accounts
1095			.clone()
1096			.into_iter()
1097			.any(|(s, _)| s == account2.address_or_scripthash.script_hash()));
1098	}
1099
1100	#[test]
1101	fn test_is_default_account() {
1102		let account = Account::create().expect("Should be able to create account in test");
1103		let mut wallet = Wallet::from_accounts(vec![account.clone()])
1104			.expect("Should be able to create wallet from accounts in test");
1105
1106		assert_eq!(wallet.default_account, account.get_script_hash());
1107	}
1108
1109	#[test]
1110	fn test_add_account() {
1111		let account = Account::create().expect("Should be able to create account in test");
1112		let mut wallet: Wallet = Wallet::new();
1113
1114		wallet.add_account(account.clone());
1115
1116		assert_eq!(wallet.accounts.len(), 2);
1117		assert_eq!(
1118			wallet.get_account(&account.address_or_scripthash.script_hash()),
1119			Some(&account)
1120		);
1121	}
1122
1123	#[test]
1124	fn test_encrypt_wallet() {
1125		let mut wallet: Wallet = Wallet::new();
1126		wallet.add_account(Account::create().expect("Should be able to create account in test"));
1127
1128		assert!(wallet.accounts()[0].key_pair().is_some());
1129		assert!(wallet.accounts()[1].key_pair().is_some());
1130
1131		wallet.encrypt_accounts("pw");
1132
1133		assert!(wallet.accounts()[0].key_pair().is_none());
1134		assert!(wallet.accounts()[1].key_pair().is_none());
1135	}
1136
1137	#[test]
1138	fn test_encrypt_wallet_parallel() {
1139		let mut wallet: Wallet = Wallet::new();
1140		// Add multiple accounts to test parallel processing
1141		for _ in 0..5 {
1142			wallet
1143				.add_account(Account::create().expect("Should be able to create account in test"));
1144		}
1145
1146		// Verify all accounts have key pairs
1147		for account in wallet.accounts() {
1148			assert!(account.key_pair().is_some());
1149		}
1150
1151		// Encrypt using parallel method
1152		wallet.encrypt_accounts_parallel("parallel_password");
1153
1154		// Verify all accounts are now encrypted
1155		for account in wallet.accounts() {
1156			assert!(account.key_pair().is_none());
1157			assert!(account.encrypted_private_key().is_some());
1158		}
1159	}
1160
1161	#[test]
1162	fn test_encrypt_wallet_batch_parallel() {
1163		let mut wallet: Wallet = Wallet::new();
1164		// Add many accounts to test batch processing
1165		for _ in 0..10 {
1166			wallet
1167				.add_account(Account::create().expect("Should be able to create account in test"));
1168		}
1169
1170		// Verify all accounts have key pairs
1171		for account in wallet.accounts() {
1172			assert!(account.key_pair().is_some());
1173		}
1174
1175		// Encrypt using batch parallel method with batch size of 3
1176		wallet.encrypt_accounts_batch_parallel("batch_password", 3);
1177
1178		// Verify all accounts are now encrypted
1179		for account in wallet.accounts() {
1180			assert!(account.key_pair().is_none());
1181			assert!(account.encrypted_private_key().is_some());
1182		}
1183	}
1184
1185	#[test]
1186	fn test_change_password_parallel() {
1187		let mut wallet = Wallet::new();
1188		// Add multiple accounts
1189		for _ in 0..5 {
1190			wallet
1191				.add_account(Account::create().expect("Should be able to create account in test"));
1192		}
1193
1194		let old_password = "old_password";
1195		let new_password = "new_password";
1196
1197		// Initially encrypt the wallet
1198		wallet.encrypt_accounts(old_password);
1199
1200		// Verify initial encryption
1201		assert!(wallet.verify_password(old_password));
1202		assert!(!wallet.verify_password(new_password));
1203
1204		// Change password using parallel method
1205		wallet
1206			.change_password_parallel(old_password, new_password)
1207			.expect("Password change should succeed");
1208
1209		// Verify new password works
1210		assert!(!wallet.verify_password(old_password));
1211		assert!(wallet.verify_password(new_password));
1212	}
1213
1214	#[test]
1215	fn test_verify_password() {
1216		let mut wallet = Wallet::new();
1217		let account = Account::create().unwrap();
1218		wallet.add_account(account.clone());
1219
1220		// Initially, the account is not encrypted so verification should fail
1221		assert!(!wallet.verify_password("password123"));
1222
1223		// Encrypt the account
1224		wallet.encrypt_accounts("password123");
1225
1226		// Now verification should succeed with the correct password
1227		assert!(wallet.verify_password("password123"));
1228
1229		// And fail with an incorrect password
1230		assert!(!wallet.verify_password("wrong_password"));
1231	}
1232}