diff --git a/Cargo.toml b/Cargo.toml index 95c3905..7098228 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "py-bip39-bindings" description = "Python bindings for tiny-bip39 RUST crate" authors=["Stichting Polkascan (Polkascan Foundation)"] -version = "0.1.8" +version = "0.1.9" repository = "https://github.com/polkascan/py-bip39-bindings" homepage = "https://github.com/polkascan/py-bip39-bindings" license = "Apache-2.0" @@ -12,8 +12,8 @@ edition = "2018" [dependencies] hmac = "0.7.0" pbkdf2 = { version = "0.3.0", default-features = false } -sha2 = "0.8.0" -tiny-bip39 = { version = "0.6.2", default-features = false } +sha2 = "0.8.2" +tiny-bip39 = { version = "0.8.2", default-features = true } [lib] name = "bip39" @@ -34,5 +34,6 @@ classifier = [ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9" + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10" ] diff --git a/README.md b/README.md index a1c8319..ad585e8 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,14 @@ pip install py-bip39-bindings pip install -r requirements.txt maturin develop ``` -### Build wheelhouses +### Build wheels ```shell script pip install -r requirements.txt -# Build local OS wheelhouse +# Build local OS wheel maturin build -# Build manylinux1 wheelhouse +# Build manylinux1 wheel docker build . --tag polkasource/maturin docker run --rm -i -v $(pwd):/io polkasource/maturin build @@ -48,5 +48,22 @@ seed_hex = binascii.hexlify(bytearray(seed_array)).decode("ascii") ``` +## Multi-language support + +The following language codes are supported: 'en', 'zh-hans', 'zh-hant', 'fr', 'it', 'jap', 'ko', 'es'. Defaults to 'en' + +```python +mnemonic = bip39_generate(12, 'fr') +# 'moufle veinard tronc magasin merle amour toboggan admettre biotype décembre régalien billard' +bip39_validate(mnemonic, 'fr') + +seed_array = bip39_to_mini_secret(mnemonic, "", 'fr') + +mnemonic = bip39_generate(12, 'zh-hans') +# '观 敲 荣 硬 责 雪 专 宴 醇 飞 图 菌' +``` + + + ## License https://github.com/polkascan/py-bip39-bindings/blob/master/LICENSE diff --git a/src/lib.rs b/src/lib.rs index 608f588..2e33b1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,15 +35,22 @@ use sha2::Sha512; /// /// * `phrase` - Mnemonic phrase /// * `password` - Use empty string for no password +/// * `language_code` - The language to use, valid values are: 'en', 'zh-hans', 'zh-hant', 'fr', 'it', 'jap', 'ko', 'es'. Defaults to 'en' /// /// # Returns /// /// Returns the 32-bytes mini-secret via entropy #[pyfunction] -#[text_signature = "(phrase, password)"] -pub fn bip39_to_mini_secret(phrase: &str, password: &str) -> PyResult> { +#[text_signature = "(phrase, password, language_code, /)"] +pub fn bip39_to_mini_secret(phrase: &str, password: &str, language_code: Option<&str>) -> PyResult> { let salt = format!("mnemonic{}", password); - let mnemonic = match Mnemonic::from_phrase(phrase, Language::English) { + + let language = match Language::from_language_code(language_code.unwrap_or("en")) { + Some(language) => language, + None => return Err(exceptions::ValueError::py_err("Invalid language_code")) + }; + + let mnemonic = match Mnemonic::from_phrase(phrase, language) { Ok(some_mnemomic) => some_mnemomic, Err(err) => return Err(exceptions::ValueError::py_err(format!("Invalid mnemonic: {}", err.to_string()))) }; @@ -59,20 +66,26 @@ pub fn bip39_to_mini_secret(phrase: &str, password: &str) -> PyResult> { /// # Arguments /// /// * `words` - The amount of words to generate, valid values are 12, 15, 18, 21 and 24 + /// /// # Returns /// /// A string containing the mnemonic words. #[pyfunction] -#[text_signature = "(words)"] -pub fn bip39_generate(words: u32) -> PyResult { +#[text_signature = "(words, language_code, /)"] +pub fn bip39_generate(words: u32, language_code: Option<&str>) -> PyResult { + + let language = match Language::from_language_code(language_code.unwrap_or("en")) { + Some(language) => language, + None => return Err(exceptions::ValueError::py_err("Invalid language_code")) + }; let word_count_type = match MnemonicType::for_word_count(words as usize) { Ok(some_work_count) => some_work_count, Err(err) => return Err(exceptions::ValueError::py_err(err.to_string())) }; - let phrase = Mnemonic::new(word_count_type, Language::English).into_phrase(); + let phrase = Mnemonic::new(word_count_type, language).into_phrase(); assert_eq!(phrase.split(" ").count(), words as usize); @@ -85,14 +98,21 @@ pub fn bip39_generate(words: u32) -> PyResult { /// /// * `phrase` - Mnemonic phrase /// * `password` - Use empty string for no password +/// * `language_code` - The language to use, valid values are: 'en', 'zh-hans', 'zh-hant', 'fr', 'it', 'jap', 'ko', 'es'. Defaults to 'en' /// /// # Returns /// /// Returns a 32-bytes seed #[pyfunction] -#[text_signature = "(phrase, password)"] -pub fn bip39_to_seed(phrase: &str, password: &str) -> PyResult> { - let mnemonic = match Mnemonic::from_phrase(phrase, Language::English) { +#[text_signature = "(phrase, password, language_code, /)"] +pub fn bip39_to_seed(phrase: &str, password: &str, language_code: Option<&str>) -> PyResult> { + + let language = match Language::from_language_code(language_code.unwrap_or("en")) { + Some(language) => language, + None => return Err(exceptions::ValueError::py_err("Invalid language_code")) + }; + + let mnemonic = match Mnemonic::from_phrase(phrase, language) { Ok(some_mnemomic) => some_mnemomic, Err(err) => return Err(exceptions::ValueError::py_err(format!("Invalid mnemonic: {}", err.to_string()))) }; @@ -108,16 +128,22 @@ pub fn bip39_to_seed(phrase: &str, password: &str) -> PyResult> { /// # Arguments /// /// * `phrase` - Mnemonic phrase +/// * `language_code` - The language to use, valid values are: 'en', 'zh-hans', 'zh-hant', 'fr', 'it', 'jap', 'ko', 'es'. Defaults to 'en' /// /// # Returns /// /// Returns boolean with validation result #[pyfunction] -#[text_signature = "(phrase)"] -pub fn bip39_validate(phrase: &str) -> bool { - match Mnemonic::validate(phrase, Language::English) { - Err(_) => false, - _ => true +#[text_signature = "(phrase, language_code, /)"] +pub fn bip39_validate(phrase: &str, language_code: Option<&str>) -> PyResult { + let language = match Language::from_language_code(language_code.unwrap_or("en")) { + Some(language) => language, + None => return Err(exceptions::ValueError::py_err("Invalid language_code")) + }; + + match Mnemonic::validate(phrase, language) { + Err(_) => Ok(false), + _ => Ok(true) } } diff --git a/tests.py b/tests.py index 6b696c3..2cf4a88 100644 --- a/tests.py +++ b/tests.py @@ -30,27 +30,70 @@ def test_generate_mnemonic(self): mnemonic = bip39.bip39_generate(12) self.assertTrue(bip39.bip39_validate(mnemonic)) + def test_generate_mnemonic_french(self): + mnemonic = bip39.bip39_generate(12, 'fr') + self.assertTrue(bip39.bip39_validate(mnemonic, 'fr')) + def test_generate_invalid_mnemonic(self): self.assertRaises(ValueError, bip39.bip39_generate, 13) def test_validate_mnemonic(self): self.assertTrue(bip39.bip39_validate(self.mnemonic)) + def test_validate_mnemonic_zh_hans(self): + self.assertTrue(bip39.bip39_validate('观 敲 荣 硬 责 雪 专 宴 醇 飞 图 菌', 'zh-hans')) + + def test_validate_mnemonic_fr(self): + self.assertTrue(bip39.bip39_validate( + 'moufle veinard tronc magasin merle amour toboggan admettre biotype décembre régalien billard', 'fr' + )) + def test_invalidate_mnemonic(self): self.assertFalse(bip39.bip39_validate("invalid mnemonic")) def test_mini_seed(self): self.assertEqual(self.mini_secret, bip39.bip39_to_mini_secret(self.mnemonic, '')) + def test_mini_seed_zh_hans(self): + + mini_secret = bip39.bip39_to_mini_secret('观 敲 荣 硬 责 雪 专 宴 醇 飞 图 菌', '', 'zh-hans') + self.assertEqual( + [60, 215, 169, 79, 32, 218, 203, 59, 53, 155, 18, 234, 160, 215, 97, 30, 176, 243, 224, 103, 240, 114, 170, + 26, 4, 63, 250, 164, 88, 148, 41, 68], mini_secret) + def test_invalid_mini_seed(self): self.assertRaises(ValueError, bip39.bip39_to_mini_secret, 'invalid mnemonic', '') def test_seed(self): self.assertEqual(self.seed, bip39.bip39_to_seed(self.mnemonic, '')) + def test_seed_zh_hans(self): + mnemonic = '旅 滨 昂 园 扎 点 郎 能 指 死 爬 根' + seed = bip39.bip39_to_seed(mnemonic, '', 'zh-hans') + + self.assertEqual( + '3e349679fd7fb457810d578a8b63237c6ba1fd09b39d7f33650c0f879a2cdc46', + bytes(seed).hex() + ) + + def test_seed_fr(self): + mnemonic = 'moufle veinard tronc magasin merle amour toboggan admettre biotype décembre régalien billard' + seed = bip39.bip39_to_seed(mnemonic, '', 'fr') + + self.assertEqual( + 'fe7ca72e2de46c24f121cf649057202ffdd9a51e63fc9fd98f8614fc68c6bbff', + bytes(seed).hex() + ) + def test_invalid_seed(self): self.assertRaises(ValueError, bip39.bip39_to_seed, 'invalid mnemonic', '') + def test_invalid_language_code(self): + with self.assertRaises(ValueError) as e: + bip39.bip39_generate(12, "unknown") + + self.assertEqual('Invalid language_code', str(e.exception)) + if __name__ == '__main__': unittest.main()