1use bip39::Mnemonic;
4use zeroize::Zeroizing;
5
6use crate::Error;
7
8#[derive(Debug)]
20pub struct Wallet {
21 mnemonic: Zeroizing<String>,
23 seed: Zeroizing<[u8; 64]>,
25 has_passphrase: bool,
27}
28
29impl Wallet {
30 pub fn generate(word_count: usize, passphrase: Option<&str>) -> Result<Self, Error> {
41 if !matches!(word_count, 12 | 15 | 18 | 21 | 24) {
42 return Err(Error::InvalidWordCount(word_count));
43 }
44
45 let mnemonic = Mnemonic::generate(word_count)?;
46 Self::from_mnemonic(mnemonic.to_string().as_str(), passphrase)
47 }
48
49 pub fn from_mnemonic(phrase: &str, passphrase: Option<&str>) -> Result<Self, Error> {
60 let mnemonic: Mnemonic = phrase.parse()?;
61 let passphrase_str = passphrase.unwrap_or("");
62 let seed_bytes = mnemonic.to_seed(passphrase_str);
63
64 Ok(Self {
65 mnemonic: Zeroizing::new(mnemonic.to_string()),
66 seed: Zeroizing::new(seed_bytes),
67 has_passphrase: passphrase.is_some() && !passphrase_str.is_empty(),
68 })
69 }
70
71 #[must_use]
76 pub fn mnemonic(&self) -> &str {
77 &self.mnemonic
78 }
79
80 #[must_use]
85 pub fn seed(&self) -> &[u8; 64] {
86 &self.seed
87 }
88
89 #[must_use]
91 pub const fn has_passphrase(&self) -> bool {
92 self.has_passphrase
93 }
94
95 #[must_use]
97 pub fn word_count(&self) -> usize {
98 self.mnemonic.split_whitespace().count()
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
107
108 #[test]
109 fn test_generate_12_words() {
110 let wallet = Wallet::generate(12, None).unwrap();
111 assert_eq!(wallet.word_count(), 12);
112 assert!(!wallet.has_passphrase());
113 }
114
115 #[test]
116 fn test_generate_24_words() {
117 let wallet = Wallet::generate(24, None).unwrap();
118 assert_eq!(wallet.word_count(), 24);
119 }
120
121 #[test]
122 fn test_generate_with_passphrase() {
123 let wallet = Wallet::generate(12, Some("secret")).unwrap();
124 assert!(wallet.has_passphrase());
125 }
126
127 #[test]
128 fn test_invalid_word_count() {
129 let result = Wallet::generate(13, None);
130 assert!(result.is_err());
131 }
132
133 #[test]
134 fn test_from_mnemonic() {
135 let wallet = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
136 assert_eq!(wallet.mnemonic(), TEST_MNEMONIC);
137 }
138
139 #[test]
140 fn test_passphrase_changes_seed() {
141 let wallet1 = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
142 let wallet2 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("password")).unwrap();
143
144 assert_ne!(wallet1.seed(), wallet2.seed());
146 }
147
148 #[test]
149 fn test_deterministic_seed() {
150 let wallet1 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("test")).unwrap();
151 let wallet2 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("test")).unwrap();
152
153 assert_eq!(wallet1.seed(), wallet2.seed());
155 }
156}