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 #[inline]
102 #[must_use]
103 pub fn mnemonic(&self) -> &str {
104 &self.mnemonic
105 }
106
107 #[inline]
112 #[must_use]
113 pub fn seed(&self) -> &[u8; 64] {
114 &self.seed
115 }
116
117 #[must_use]
119 pub const fn has_passphrase(&self) -> bool {
120 self.has_passphrase
121 }
122
123 #[inline]
125 #[must_use]
126 pub fn word_count(&self) -> usize {
127 self.mnemonic.split_whitespace().count()
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
136
137 #[cfg(feature = "rand")]
138 #[test]
139 fn test_generate_12_words() {
140 let wallet = Wallet::generate(12, None).unwrap();
141 assert_eq!(wallet.word_count(), 12);
142 assert!(!wallet.has_passphrase());
143 }
144
145 #[cfg(feature = "rand")]
146 #[test]
147 fn test_generate_24_words() {
148 let wallet = Wallet::generate(24, None).unwrap();
149 assert_eq!(wallet.word_count(), 24);
150 }
151
152 #[cfg(feature = "rand")]
153 #[test]
154 fn test_generate_with_passphrase() {
155 let wallet = Wallet::generate(12, Some("secret")).unwrap();
156 assert!(wallet.has_passphrase());
157 }
158
159 #[test]
160 fn test_invalid_entropy_length() {
161 let result = Wallet::from_entropy(&[0u8; 15], None);
163 assert!(result.is_err());
164 }
165
166 #[test]
167 fn test_from_entropy() {
168 let entropy = [0u8; 16];
170 let wallet = Wallet::from_entropy(&entropy, None).unwrap();
171 assert_eq!(wallet.word_count(), 12);
172 }
173
174 #[test]
175 fn test_from_mnemonic() {
176 let wallet = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
177 assert_eq!(wallet.mnemonic(), TEST_MNEMONIC);
178 }
179
180 #[test]
181 fn test_passphrase_changes_seed() {
182 let wallet1 = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
183 let wallet2 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("password")).unwrap();
184
185 assert_ne!(wallet1.seed(), wallet2.seed());
187 }
188
189 #[test]
190 fn test_deterministic_seed() {
191 let wallet1 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("test")).unwrap();
192 let wallet2 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("test")).unwrap();
193
194 assert_eq!(wallet1.seed(), wallet2.seed());
196 }
197}