Skip to content

Commit

Permalink
Added multi-language support (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
arjanz authored Feb 22, 2022
1 parent b248e20 commit 3f3115f
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 21 deletions.
9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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"
]
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
54 changes: 40 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>> {
#[text_signature = "(phrase, password, language_code, /)"]
pub fn bip39_to_mini_secret(phrase: &str, password: &str, language_code: Option<&str>) -> PyResult<Vec<u8>> {
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())))
};
Expand All @@ -59,20 +66,26 @@ pub fn bip39_to_mini_secret(phrase: &str, password: &str) -> PyResult<Vec<u8>> {
/// # 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<String> {
#[text_signature = "(words, language_code, /)"]
pub fn bip39_generate(words: u32, language_code: Option<&str>) -> PyResult<String> {

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);

Expand All @@ -85,14 +98,21 @@ pub fn bip39_generate(words: u32) -> PyResult<String> {
///
/// * `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<Vec<u8>> {
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<Vec<u8>> {

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())))
};
Expand All @@ -108,16 +128,22 @@ pub fn bip39_to_seed(phrase: &str, password: &str) -> PyResult<Vec<u8>> {
/// # 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<bool> {
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)
}
}

Expand Down
43 changes: 43 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

0 comments on commit 3f3115f

Please sign in to comment.