1#[cfg(feature = "alloc")]
4use alloc::string::{String, ToString};
5
6use bip39::Mnemonic;
7use zeroize::Zeroizing;
8
9use crate::Error;
10
11#[derive(Debug)]
23pub struct Wallet {
24 mnemonic: Zeroizing<String>,
26 seed: Zeroizing<[u8; 64]>,
28 has_passphrase: bool,
30}
31
32impl Wallet {
33 #[cfg(feature = "rand")]
48 pub fn generate(word_count: usize, passphrase: Option<&str>) -> Result<Self, Error> {
49 if !matches!(word_count, 12 | 15 | 18 | 21 | 24) {
50 return Err(Error::InvalidWordCount(word_count));
51 }
52
53 let mnemonic = Mnemonic::generate(word_count)?;
54 Self::from_mnemonic(mnemonic.to_string().as_str(), passphrase)
55 }
56
57 pub fn from_entropy(entropy: &[u8], passphrase: Option<&str>) -> Result<Self, Error> {
71 let mnemonic = Mnemonic::from_entropy(entropy)?;
72 Self::from_mnemonic(mnemonic.to_string().as_str(), passphrase)
73 }
74
75 pub fn from_mnemonic(phrase: &str, passphrase: Option<&str>) -> Result<Self, Error> {
86 let mnemonic: Mnemonic = phrase.parse()?;
87 let passphrase_str = passphrase.unwrap_or("");
88 let seed_bytes = mnemonic.to_seed(passphrase_str);
89
90 Ok(Self {
91 mnemonic: Zeroizing::new(mnemonic.to_string()),
92 seed: Zeroizing::new(seed_bytes),
93 has_passphrase: passphrase.is_some() && !passphrase_str.is_empty(),
94 })
95 }
96
97 #[must_use]
102 pub fn mnemonic(&self) -> &str {
103 &self.mnemonic
104 }
105
106 #[must_use]
111 pub fn seed(&self) -> &[u8; 64] {
112 &self.seed
113 }
114
115 #[must_use]
117 pub const fn has_passphrase(&self) -> bool {
118 self.has_passphrase
119 }
120
121 #[must_use]
123 pub fn word_count(&self) -> usize {
124 self.mnemonic.split_whitespace().count()
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
133
134 #[cfg(feature = "rand")]
135 #[test]
136 fn test_generate_12_words() {
137 let wallet = Wallet::generate(12, None).unwrap();
138 assert_eq!(wallet.word_count(), 12);
139 assert!(!wallet.has_passphrase());
140 }
141
142 #[cfg(feature = "rand")]
143 #[test]
144 fn test_generate_24_words() {
145 let wallet = Wallet::generate(24, None).unwrap();
146 assert_eq!(wallet.word_count(), 24);
147 }
148
149 #[cfg(feature = "rand")]
150 #[test]
151 fn test_generate_with_passphrase() {
152 let wallet = Wallet::generate(12, Some("secret")).unwrap();
153 assert!(wallet.has_passphrase());
154 }
155
156 #[test]
157 fn test_invalid_entropy_length() {
158 let result = Wallet::from_entropy(&[0u8; 15], None);
160 assert!(result.is_err());
161 }
162
163 #[test]
164 fn test_from_entropy() {
165 let entropy = [0u8; 16];
167 let wallet = Wallet::from_entropy(&entropy, None).unwrap();
168 assert_eq!(wallet.word_count(), 12);
169 }
170
171 #[test]
172 fn test_from_mnemonic() {
173 let wallet = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
174 assert_eq!(wallet.mnemonic(), TEST_MNEMONIC);
175 }
176
177 #[test]
178 fn test_passphrase_changes_seed() {
179 let wallet1 = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
180 let wallet2 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("password")).unwrap();
181
182 assert_ne!(wallet1.seed(), wallet2.seed());
184 }
185
186 #[test]
187 fn test_deterministic_seed() {
188 let wallet1 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("test")).unwrap();
189 let wallet2 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("test")).unwrap();
190
191 assert_eq!(wallet1.seed(), wallet2.seed());
193 }
194}