Skip to main content

md_codec/
phrase.rs

1//! BIP-39 phrase rendering per spec ยง8.4.
2
3use crate::error::Error;
4
5/// A 12-word BIP-39 phrase rendering of a 128-bit identity.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Phrase(
8    /// The 12 BIP-39 words (English wordlist) derived from the 128-bit input.
9    pub [String; 12],
10);
11
12impl Phrase {
13    /// Render a 16-byte (128-bit) identity as a 12-word BIP-39 phrase.
14    ///
15    /// 128 bits of entropy is always a valid BIP-39 input, so the underlying
16    /// `Mnemonic::from_entropy` cannot fail for this length.
17    pub fn from_id_bytes(id: &[u8; 16]) -> Result<Self, Error> {
18        let mnemonic = bip39::Mnemonic::from_entropy(id)
19            .expect("128-bit entropy is always a valid BIP-39 input");
20        let mut words: [String; 12] = Default::default();
21        for (slot, word) in words.iter_mut().zip(mnemonic.words()) {
22            *slot = word.to_string();
23        }
24        Ok(Phrase(words))
25    }
26}
27
28impl std::fmt::Display for Phrase {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        write!(f, "{}", self.0.join(" "))
31    }
32}
33
34#[cfg(test)]
35mod tests {
36    use super::*;
37
38    #[test]
39    fn phrase_deterministic() {
40        let id = [0xab; 16];
41        let p1 = Phrase::from_id_bytes(&id).unwrap();
42        let p2 = Phrase::from_id_bytes(&id).unwrap();
43        assert_eq!(p1, p2);
44    }
45
46    #[test]
47    fn phrase_has_12_words() {
48        let id = [0u8; 16];
49        let p = Phrase::from_id_bytes(&id).unwrap();
50        assert_eq!(p.0.len(), 12);
51        for word in &p.0 {
52            assert!(!word.is_empty());
53        }
54    }
55
56    #[test]
57    fn phrase_to_string_is_space_separated() {
58        let id = [0u8; 16];
59        let p = Phrase::from_id_bytes(&id).unwrap();
60        let s = p.to_string();
61        assert_eq!(s.split(' ').count(), 12);
62    }
63}