artimonist 2.0.1

A tool for generating mnemonics and wallets.
Documentation
/*!
 * # Reference
 *
 * [1] - Unicode characters
 *      <https://www.unicodepedia.com/>
 *
 * # Descriptions
 *
 * [1] - Complex Diagram secret data construction
 *      (diagram version == 1)
 *      |-----n segments----|-n bytes-|-7 bytes-|-1 byte-|
 *      |String1|String2|...|N1|N2|...| Indices |CheckSum|
 *      |-------->>>--------|--->>>---|---------|--------|
 *      n = indices.count_ones() - 1
 *      N1,N2... is bytes count of String1,String2...
 *
 * [2] - Complex Diagram indices data construction
 *      0b1xxx_xxxx
 *      0b0xxx_xxxx
 *      0b0xxx_xxxx
 *      0b0xxx_xxxx
 *      0b0xxx_xxxx
 *      0b0xxx_xxxx
 *      0b0xxx_xxxx
 *      1 bit at top left corner is version of complex diagram.
 *      others x bits indices string position in diagram.
**/
use super::{Error, GenericDiagram, Result};
use crate::macros::ImpDeref;
use bitcoin::hashes::{Hash, sha256};

/// Complex Diagram
///
/// Complex diagram contains strings in 7 * 7 grid cells.
/// All UTF-8 strings with less than 50 characters are supported.
///
#[derive(Debug, Default, Clone, PartialEq)]
pub struct ComplexDiagram(pub [[Option<String>; 7]; 7]);
ImpDeref!(ComplexDiagram, [[Option<String>; 7]; 7]);

impl GenericDiagram for ComplexDiagram {
    type Item = String;

    /// Compatible with previous versions
    fn to_bytes(&self) -> Result<Vec<u8>> {
        let mut str_list: Vec<&str> = vec![];
        let mut str_lens: Vec<u8> = vec![];
        let mut indices: [u8; 7] = [0; 7];

        (0..7).rev().try_for_each(|col| {
            (0..7).rev().try_for_each(|row| {
                if let Some(s) = &self[row][col]
                    && !s.is_empty()
                {
                    if s.len() > u8::MAX as usize {
                        return Err(Error::StringTooLong(s.to_string()));
                    }
                    str_list.push(s);
                    str_lens.push(s.len() as u8);
                    indices[row] |= 1 << (6 - col);
                }
                Ok(())
            })
        })?;

        indices[0] |= 1 << 7; // version number of complex diagram
        let mut secret = [str_list.join("").as_bytes(), &str_lens[..], &indices[..]].concat();
        let check = sha256::Hash::hash(&secret).as_byte_array()[0];
        secret.push(check);
        Ok(secret)
    }
}

impl ComplexDiagram {
    /// cell chars count limit
    pub const CELL_CHARS_LIMIT: usize = 50;

    /// create complex diagram
    #[deprecated(since = "1.7.2", note = "Use `Diagram` instead")]
    pub fn new() -> Self {
        Self(core::array::from_fn(|_| core::array::from_fn(|_| None)))
    }

    /// create ComplexDiagram from items
    #[deprecated(since = "1.7.2", note = "Use `Diagram` instead")]
    pub fn from_values(items: &[&str], indices: &[(usize, usize)]) -> Self {
        let mut diagram = Self(core::array::from_fn(|_| core::array::from_fn(|_| None)));
        indices.iter().zip(items).for_each(|(&(r, c), &s)| {
            diagram[r][c] = match s.is_empty() {
                false => Some(s.to_owned()),
                true => None,
            }
        });
        diagram
    }
}

#[cfg(test)]
#[allow(deprecated)]
mod complex_diagram_test {
    use super::*;
    use bitcoin::hex::DisplayHex;

    #[test]
    fn test_complex_diagram() -> Result<()> {
        const STR_LIST: &[&str] = &["ABC", "123", "测试", "混A1", "A&*王😊"];
        const INDICES: &[(usize, usize)] = &[(0, 6), (1, 1), (1, 3), (4, 2), (6, 6)];
        const SECRET_HEX: &str =
            "41262ae78e8bf09f988a414243e6b58be8af95e6b7b741313132330a0306050381280000100001c8";

        let cdm = ComplexDiagram::from_values(STR_LIST, INDICES);
        assert_eq!(cdm.to_bytes()?.to_lower_hex_string(), SECRET_HEX);
        assert_eq!(cdm[6][6], Some(STR_LIST[4].to_owned()));

        #[cfg(not(feature = "testnet"))]
        {
            let master = "xprv9s21ZrQH143K2CKVamMeHC5bmLWCDx4dLohvk3cVABv8NCFpyBpzeRi3uiZT5gmGz9rgpHZTBWCDXqCD5PaG93yE69qoRotCNqX6F4qwU7P";
            assert_eq!(cdm.to_master(&[])?.to_string(), master);

            let master_v1 = "xprv9s21ZrQH143K3DUYRs4DRSY7JGsaL6FrXCAwuv1ZseQdHs6NJTX7wg99bUp7z3zgRUFwJTZfZp3Zhs9nrsfK6rFdkY78tbkAr1ovxALxUJu";
            assert_eq!(cdm.to_master_v1(&[])?.to_string(), master_v1);
        }
        Ok(())
    }

    #[test]
    fn test_complex_entropy() -> Result<()> {
        const STR_LIST: &[&str] = &["ABC", "混A1", "123", "测试", "A&*王😊"];
        const INDICES: &[(usize, usize)] = &[(0, 6), (1, 1), (1, 3), (4, 2), (6, 0)];
        const SECRET_HEX: &str =
            "414243313233e6b58be8af95e6b7b7413141262ae78e8bf09f988a030306050a8128000010004052";
        const RAW_ENTROPY: &str =
            "58affc63a2c628806721390ece8f446b0e7f133962e018a3a32e3d888b391791";
        const SALT_STR: &str = "123abc";
        const SALT_ENTROPY: &str =
            "4f76acb612ba64c5f7612778666ce4055a5c465544653affad99f206daa836a5";

        let cdm = ComplexDiagram::from_values(STR_LIST, INDICES);
        assert_eq!(cdm.to_bytes()?.to_lower_hex_string(), SECRET_HEX);

        let entropy = cdm.to_entropy(Default::default())?;
        assert_eq!(entropy.to_lower_hex_string(), RAW_ENTROPY);

        let salt_entropy = cdm.to_entropy(SALT_STR.as_bytes())?;
        assert_eq!(salt_entropy.to_lower_hex_string(), SALT_ENTROPY);

        #[cfg(not(feature = "testnet"))]
        {
            let master = "xprv9s21ZrQH143K2xfe53kNiHqjGC6Jv8zMfjAVhPQZw1nur4CnM79LqpSKZpURNEvyC4xjpPGj37efmviNjkpK6mv9LgRyJBSB8rSK18w6yaY";
            assert_eq!(cdm.to_master(SALT_STR.as_bytes())?.to_string(), master);

            let master_v1 = "xprv9s21ZrQH143K32BBNz2hduzSS7p8q18MtvDzyGvHFKvMfLRKaS7Bk27BhbMb47X5qeBpEmSiFtsbRv9Zw6QoMDbTEyNo1BU5Qka1PQvAZ4u";
            assert_eq!(
                cdm.to_master_v1(SALT_STR.as_bytes())?.to_string(),
                master_v1
            );
        }
        Ok(())
    }
}