neo3/neo_wallets/
bip39_account.rs

1use crate::{
2	crypto::KeyPair,
3	neo_protocol::{Account, AccountTrait},
4};
5use bip39::{Language, Mnemonic};
6use sha2::{Digest, Sha256};
7
8/// A BIP-39 compatible neo account that uses mnemonic phrases for key generation and recovery.
9///
10/// This implementation follows the BIP-39 standard for generating and recovering neo accounts using
11/// mnemonic phrases. The account can be created with a new random mnemonic or recovered from an
12/// existing mnemonic phrase.
13///
14/// # Examples
15///
16/// ## Creating a new account
17/// ```
18/// use neo3::neo_wallets::Bip39Account;
19///
20/// // Create a new account with a password
21/// let password = "your_secure_password";
22/// let account = Bip39Account::create(password).unwrap();
23///
24/// // The account will have a randomly generated 24-word mnemonic
25/// println!("Mnemonic: {}", account.mnemonic());
26/// ```
27///
28/// ## Recovering an existing account
29/// ```
30/// use neo3::neo_wallets::Bip39Account;
31///
32/// // Recover an account using an existing mnemonic and password
33/// let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"; // Your 24 word mnemonic
34/// let password = "your_secure_password";
35/// let recovered = Bip39Account::from_bip39_mnemonic(password, mnemonic).unwrap();
36/// ```
37#[derive(Debug)]
38pub struct Bip39Account {
39	/// The underlying neo account
40	account: Account,
41
42	/// Generated BIP-39 mnemonic for the account
43	mnemonic: String,
44}
45
46impl Bip39Account {
47	/// Returns the mnemonic phrase used for this account
48	pub fn mnemonic(&self) -> &str {
49		&self.mnemonic
50	}
51
52	/// Returns a reference to the underlying Neo account
53	pub fn account(&self) -> &Account {
54		&self.account
55	}
56	/// Creates a new BIP-39 compatible neo account with a randomly generated mnemonic.
57	///
58	/// The private key for the wallet is calculated using:
59	/// `Key = SHA-256(BIP_39_SEED(mnemonic, password))`
60	///
61	/// The password is used as a BIP-39 passphrase and is required to recover the account later.
62	/// The same password must be provided during recovery to generate the same keys.
63	///
64	/// # Arguments
65	/// * `password` - The passphrase used in BIP-39 seed generation. This must be saved to recover the account.
66	///
67	/// # Returns
68	/// A Result containing the new Bip39Account or an error if creation fails.
69	///
70	/// # Example
71	/// ```
72	/// use neo3::neo_wallets::Bip39Account;
73	///
74	/// let account = Bip39Account::create("my secure password").unwrap();
75	/// // Save the mnemonic securely
76	/// let mnemonic = account.mnemonic().to_string();
77	/// ```
78	pub fn create(password: &str) -> Result<Self, Box<dyn std::error::Error>> {
79		let mut rng = bip39::rand::thread_rng();
80		let mnemonic =
81			Mnemonic::generate_in_with(&mut rng, Language::English, 24).map_err(|e| {
82				Box::<dyn std::error::Error>::from(format!("Failed to generate mnemonic: {e}"))
83			})?;
84		let seed = mnemonic.to_seed(password);
85
86		let mut hasher = Sha256::new();
87		hasher.update(&seed);
88		let private_key = hasher.finalize();
89
90		let key_pair = KeyPair::from_private_key(private_key.as_ref()).map_err(|e| {
91			Box::<dyn std::error::Error>::from(format!("Failed to create key pair: {e}"))
92		})?;
93		let account = Account::from_key_pair(key_pair.clone(), None, None).map_err(|e| {
94			Box::<dyn std::error::Error>::from(format!(
95				"Failed to create account from key pair: {}",
96				e
97			))
98		})?;
99
100		Ok(Self { account, mnemonic: mnemonic.to_string() })
101	}
102
103	/// Recovers a neo account from an existing BIP-39 mnemonic phrase and password.
104	///
105	/// This method will reconstruct the exact same neo account if provided with the same
106	/// mnemonic and password combination that was used to create the original account.
107	///
108	/// # Arguments
109	/// * `password` - The same passphrase that was used when generating the original account
110	/// * `mnemonic` - The 24-word mnemonic phrase from the original account
111	///
112	/// # Returns
113	/// A Result containing the recovered Bip39Account or an error if recovery fails
114	///
115	/// # Example
116	/// ```
117	/// use neo3::neo_wallets::Bip39Account;
118	///
119	/// let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"; // Your saved 24-word mnemonic
120	/// let password = "your_secure_password";      // Original password used
121	/// let account = Bip39Account::from_bip39_mnemonic(password, mnemonic).unwrap();
122	/// ```
123	pub fn from_bip39_mnemonic(
124		password: &str,
125		mnemonic: &str,
126	) -> Result<Self, Box<dyn std::error::Error>> {
127		let mnemonic = Mnemonic::parse_in(Language::English, mnemonic)?;
128		let seed = mnemonic.to_seed(password);
129
130		let mut hasher = Sha256::new();
131		hasher.update(&seed);
132		let private_key = hasher.finalize();
133
134		let key_pair = KeyPair::from_private_key(private_key.as_ref()).map_err(|e| {
135			Box::<dyn std::error::Error>::from(format!("Failed to create key pair: {e}"))
136		})?;
137		let account = Account::from_key_pair(key_pair.clone(), None, None).map_err(|e| {
138			Box::<dyn std::error::Error>::from(format!(
139				"Failed to create account from key pair: {}",
140				e
141			))
142		})?;
143
144		Ok(Self { account, mnemonic: mnemonic.to_string() })
145	}
146}
147
148#[cfg(test)]
149mod tests {
150	use super::*;
151
152	#[test]
153	fn test_create_bip39_account() {
154		let password =
155			std::env::var("TEST_PASSWORD").unwrap_or_else(|_| "test_password".to_string());
156		let account =
157			Bip39Account::create(&password).expect("Should be able to create Bip39Account in test");
158
159		// Check that mnemonic is 24 words
160		assert_eq!(account.mnemonic.split_whitespace().count(), 24);
161
162		// Verify account was created with valid key pair
163		assert!(account.account.key_pair().is_some());
164	}
165
166	#[test]
167	fn test_recover_from_mnemonic() {
168		let password =
169			std::env::var("TEST_PASSWORD").unwrap_or_else(|_| "test_password".to_string());
170		let original =
171			Bip39Account::create(&password).expect("Should be able to create Bip39Account in test");
172		let mnemonic = original.mnemonic.clone();
173
174		// Recover account using mnemonic
175		let recovered = Bip39Account::from_bip39_mnemonic(&password, &mnemonic)
176			.expect("Should be able to recover Bip39Account from mnemonic in test");
177
178		// Verify recovered account matches original
179		assert_eq!(original.account.get_script_hash(), recovered.account.get_script_hash());
180		assert_eq!(original.mnemonic, recovered.mnemonic);
181	}
182
183	#[test]
184	fn test_invalid_mnemonic() {
185		let result = Bip39Account::from_bip39_mnemonic("password", "invalid mnemonic phrase");
186		assert!(result.is_err());
187	}
188
189	#[test]
190	fn test_different_passwords_different_accounts() {
191		let account1 = Bip39Account::create("password1")
192			.expect("Should be able to create Bip39Account in test");
193		let account2 = Bip39Account::create("password2")
194			.expect("Should be able to create Bip39Account in test");
195
196		assert_ne!(account1.account.get_script_hash(), account2.account.get_script_hash());
197	}
198
199	#[test]
200	fn test_generate_and_recover_bip39_account() {
201		let password =
202			std::env::var("TEST_PASSWORD").unwrap_or_else(|_| "test_password".to_string());
203		let account1 =
204			Bip39Account::create(&password).expect("Should be able to create Bip39Account in test");
205		let account2 = Bip39Account::from_bip39_mnemonic(&password, &account1.mnemonic)
206			.expect("Should be able to recover Bip39Account from mnemonic in test");
207
208		assert_eq!(account1.account.get_address(), account2.account.get_address());
209		assert!(account1.account.key_pair().is_some());
210		assert_eq!(account1.account.key_pair(), account2.account.key_pair());
211		assert_eq!(account1.mnemonic, account2.mnemonic);
212		assert!(!account1.mnemonic.is_empty());
213	}
214}