xchainpy2_crypto 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2023 Tirinox (aka TRX1 aka account1242 aka Old1)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: xchainpy2_crypto
3
+ Version: 0.1.1
4
+ Summary: XChainPy2 Crypto utils and keystore management
5
+ Author-email: Tirinox <tirinox@gmail.com>
6
+ License: MIT
7
+ Project-URL: source, https://github.com/tirinox/xchainpy
8
+ Keywords: Crypto,THORChain,Blockchain,XChain
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Software Development :: Build Tools
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.6
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Requires-Python: >=3.7
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: bip-utils<3.0.0,>=2.12.1.0
24
+ Requires-Dist: pycryptodome<4.0,>=3.23
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest; extra == "test"
27
+ Dynamic: license-file
28
+
29
+ # How it works
30
+ Typically keystore files encrypt a seed to a file, however this is not appropriate or UX friendly, since the phrase
31
+ cannot be recovered after the fact.
32
+
33
+ Crypto design:
34
+
35
+ `[entropy] -> [phrase] -> [seed] -> [privateKey] -> [publicKey] -> [address]`
36
+
37
+ Instead, XCHAIN-CRYPTO stores the phrase in a keystore file, then decrypts and passes this phrase to other clients:
38
+
39
+ `[keystore] -> XCHAIN-CRYPTO -> [phrase] -> ChainClient`
40
+
41
+ The ChainClients can then convert this into their respective key-pairs and addresses. Users can also export their
42
+ phrases after the fact, ensuring they have saved it securely. This could enhance UX onboarding since users aren't forced
43
+ to write their phrases down immediately for empty or test wallets.
44
+
45
+ ### Documentation
46
+
47
+ 👉 https://xchainpy2.readthedocs.io/en/latest/packages/xchainpy2_crypto.html
@@ -0,0 +1,19 @@
1
+ # How it works
2
+ Typically keystore files encrypt a seed to a file, however this is not appropriate or UX friendly, since the phrase
3
+ cannot be recovered after the fact.
4
+
5
+ Crypto design:
6
+
7
+ `[entropy] -> [phrase] -> [seed] -> [privateKey] -> [publicKey] -> [address]`
8
+
9
+ Instead, XCHAIN-CRYPTO stores the phrase in a keystore file, then decrypts and passes this phrase to other clients:
10
+
11
+ `[keystore] -> XCHAIN-CRYPTO -> [phrase] -> ChainClient`
12
+
13
+ The ChainClients can then convert this into their respective key-pairs and addresses. Users can also export their
14
+ phrases after the fact, ensuring they have saved it securely. This could enhance UX onboarding since users aren't forced
15
+ to write their phrases down immediately for empty or test wallets.
16
+
17
+ ### Documentation
18
+
19
+ 👉 https://xchainpy2.readthedocs.io/en/latest/packages/xchainpy2_crypto.html
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools", "setuptools-scm"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "xchainpy2_crypto"
7
+ version = "0.1.1"
8
+ authors = [
9
+ { name = "Tirinox", email = "tirinox@gmail.com" },
10
+ ]
11
+ description = "XChainPy2 Crypto utils and keystore management"
12
+ readme = "README.md"
13
+ requires-python = ">=3.7"
14
+ keywords = ["Crypto", "THORChain", "Blockchain", "XChain"]
15
+ license = { text = "MIT" }
16
+ urls = { source = "https://github.com/tirinox/xchainpy" }
17
+ classifiers = [
18
+ 'Development Status :: 3 - Alpha',
19
+ # Chose either "3 - Alpha", "4 - Beta" or "5 - Production/Stable" as the current state of your package
20
+ 'Intended Audience :: Developers', # Define that your audience are developers
21
+ 'Topic :: Software Development :: Build Tools',
22
+ 'License :: OSI Approved :: MIT License',
23
+ 'Programming Language :: Python :: 3',
24
+ 'Programming Language :: Python :: 3.6',
25
+ 'Programming Language :: Python :: 3.7',
26
+ 'Programming Language :: Python :: 3.8',
27
+ 'Programming Language :: Python :: 3.9',
28
+ 'Programming Language :: Python :: 3.10',
29
+ 'Programming Language :: Python :: 3.11',
30
+ ]
31
+ dependencies = [
32
+ "bip-utils>=2.12.1.0,<3.0.0",
33
+ "pycryptodome>=3.23,<4.0",
34
+ ]
35
+ [project.optional-dependencies]
36
+ test = ["pytest"]
37
+
38
+ [tool.setuptools]
39
+ packages = ["xchainpy2_crypto"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ from .keystore import *
2
+ from .utils import *
@@ -0,0 +1,174 @@
1
+ import hashlib
2
+ import json
3
+ import uuid
4
+ from os import urandom
5
+ from typing import NamedTuple
6
+
7
+ from Crypto.Cipher import AES
8
+ from Crypto.Hash import BLAKE2b
9
+ from Crypto.Util import Counter
10
+
11
+ from .utils import validate_mnemonic, generate_mnemonic
12
+
13
+ CIPHER = 'aes-128-ctr'
14
+ NBITS = 128
15
+ KDF = 'pbkdf2'
16
+ PRF = 'hmac-sha256'
17
+ DKLEN = 32
18
+ C = 262144
19
+ HASH_FUNCTION = 'sha256'
20
+ META = 'xchain-keystore'
21
+ VERSION = 1
22
+ ENCODING = 'utf-8'
23
+
24
+
25
+ class InvalidPasswordException(Exception):
26
+ pass
27
+
28
+
29
+ class KeyStore(NamedTuple):
30
+ cipher: str
31
+ ciphertext: str
32
+ cipherparams_iv: str
33
+ kdf: str
34
+ kdfparams_prf: str
35
+ kdfparams_dklen: int
36
+ kdfparams_salt: str
37
+ kdfparams_c: int
38
+ mac: str
39
+ id: str
40
+ version: int
41
+ meta: str
42
+
43
+ @property
44
+ def to_dict(self):
45
+ return {
46
+ 'crypto': {
47
+ 'cipher': self.cipher,
48
+ 'ciphertext': self.ciphertext,
49
+ 'cipherparams': {
50
+ 'iv': self.cipherparams_iv
51
+ },
52
+ 'kdf': self.kdf,
53
+ 'kdfparams': {
54
+ 'prf': self.kdfparams_prf,
55
+ 'dklen': self.kdfparams_dklen,
56
+ 'salt': self.kdfparams_salt,
57
+ 'c': self.kdfparams_c
58
+ },
59
+ 'mac': self.mac,
60
+ },
61
+ 'id': self.id,
62
+ 'version': self.version,
63
+ 'meta': self.meta
64
+ }
65
+
66
+ @property
67
+ def to_json(self):
68
+ return json.dumps(self.to_dict)
69
+
70
+ @classmethod
71
+ def from_dict(cls, j):
72
+ crypto = j.get('crypto')
73
+ return cls(
74
+ cipher=crypto['cipher'],
75
+ ciphertext=crypto['ciphertext'],
76
+ cipherparams_iv=crypto['cipherparams']['iv'],
77
+ kdf=crypto['kdf'],
78
+ kdfparams_prf=crypto['kdfparams']['prf'],
79
+ kdfparams_dklen=int(crypto['kdfparams']['dklen']),
80
+ kdfparams_salt=crypto['kdfparams']['salt'],
81
+ kdfparams_c=crypto['kdfparams']['c'],
82
+ mac=crypto['mac'],
83
+ id=j['id'],
84
+ version=j['version'],
85
+ meta=j['meta']
86
+ )
87
+
88
+ @classmethod
89
+ def from_file(cls, path):
90
+ with open(path) as f:
91
+ data = json.load(f)
92
+ return cls.from_dict(data)
93
+
94
+ def save(self, path: str, indent=4):
95
+ with open(path, 'w') as f:
96
+ json.dump(self.to_dict, f, indent=indent)
97
+
98
+ @classmethod
99
+ def encrypt_to_keystore(cls, mnemonic: str, password: str):
100
+ """
101
+ Encrypts a mnemonic to a keystore with a password
102
+ :param str mnemonic: BIP39 mnemonic
103
+ :param str password: Password
104
+ :return: KeyStore
105
+ """
106
+ if not validate_mnemonic(mnemonic):
107
+ raise Exception("Invalid BIP39 Phrase")
108
+
109
+ ID = str(uuid.uuid4())
110
+ salt = urandom(32)
111
+ iv = urandom(16).hex()
112
+
113
+ derived_key = hashlib.pbkdf2_hmac(
114
+ HASH_FUNCTION,
115
+ password.encode(ENCODING),
116
+ salt,
117
+ C,
118
+ DKLEN
119
+ )
120
+
121
+ ctr = Counter.new(NBITS, initial_value=int(iv, 16))
122
+ aes_cipher = AES.new(derived_key[0:16], AES.MODE_CTR, counter=ctr)
123
+ cipher_bytes = aes_cipher.encrypt(mnemonic.encode("utf8"))
124
+
125
+ blake256 = BLAKE2b.new(digest_bits=256)
126
+ blake256.update((derived_key[16:32] + cipher_bytes))
127
+ mac = blake256.hexdigest()
128
+
129
+ return cls(
130
+ cipher=CIPHER,
131
+ ciphertext=cipher_bytes.hex(),
132
+ cipherparams_iv=iv,
133
+ kdf=KDF,
134
+ kdfparams_prf=PRF,
135
+ kdfparams_dklen=DKLEN,
136
+ kdfparams_salt=salt.hex(),
137
+ kdfparams_c=C,
138
+ mac=mac,
139
+ id=ID,
140
+ version=VERSION,
141
+ meta=META
142
+ )
143
+
144
+ def decrypt_from_keystore(self, password: str):
145
+ """
146
+ Derives a mnemonic from a keystore with a password
147
+ :param str password: password
148
+ :return: str Mnemonic phrase
149
+ """
150
+ derived_key = hashlib.pbkdf2_hmac(
151
+ HASH_FUNCTION,
152
+ password.encode(ENCODING),
153
+ bytes.fromhex(self.kdfparams_salt),
154
+ self.kdfparams_c,
155
+ self.kdfparams_dklen
156
+ )
157
+
158
+ cipher_bytes = bytes.fromhex(self.ciphertext)
159
+
160
+ blake256 = BLAKE2b.new(digest_bits=256)
161
+ blake256.update((derived_key[16:32] + cipher_bytes))
162
+ mac = blake256.hexdigest()
163
+ if mac != self.mac:
164
+ raise InvalidPasswordException("Invalid password")
165
+
166
+ ctr = Counter.new(NBITS, initial_value=int(self.cipherparams_iv, 16))
167
+ aes_cipher = AES.new(derived_key[0:16], AES.MODE_CTR, counter=ctr)
168
+ phrase = aes_cipher.decrypt(cipher_bytes)
169
+ return phrase.decode("utf8")
170
+
171
+ @classmethod
172
+ def generate_and_encrypt(cls, password: str, *args, **kwargs):
173
+ mnemonic = generate_mnemonic(*args, **kwargs)
174
+ return cls.encrypt_to_keystore(mnemonic, password)
@@ -0,0 +1,50 @@
1
+ import os
2
+ import random
3
+ import string
4
+
5
+ import pytest
6
+ from bip_utils import Bech32ChecksumError
7
+
8
+ from xchainpy2_crypto import encode_address, decode_address
9
+
10
+
11
+ def randomize_characters(s, n):
12
+ # Create a string of valid characters to choose from
13
+ valid_characters = string.ascii_letters + string.digits
14
+
15
+ # Convert the string to a list of characters
16
+ chars = list(s)
17
+
18
+ # Randomize n characters in the list
19
+ for _ in range(n):
20
+ index = random.randint(0, len(chars) - 1)
21
+ random_char = random.choice(valid_characters)
22
+ chars[index] = random_char
23
+
24
+ # Convert the list back to a string
25
+ randomized_string = ''.join(chars)
26
+
27
+ return randomized_string
28
+
29
+
30
+ def test_address_encode_decode():
31
+ prefixes = ('thor', 'tthor', 'thorpub', 'tthorpub', 'maya', 'cacao', 'btc')
32
+ for i in range(100):
33
+ pub_key = os.urandom(32)
34
+ prefix = prefixes[i % len(prefixes)]
35
+ address = encode_address(pub_key, prefix)
36
+
37
+ assert address.startswith(prefix)
38
+ decoded = decode_address(address, prefix)
39
+ assert decoded == pub_key
40
+
41
+ other_pub_key = os.urandom(32)
42
+ assert decode_address(address, prefix) != other_pub_key
43
+
44
+ # noinspection PyTypeChecker
45
+ with pytest.raises((ValueError, Bech32ChecksumError)):
46
+ spoiled_address = randomize_characters(address, random.randint(2, 10))
47
+ decode_address(spoiled_address, prefix)
48
+
49
+ with pytest.raises(ValueError):
50
+ decode_address(address, randomize_characters(prefix, random.randint(5, 10)))
@@ -0,0 +1,54 @@
1
+ import os
2
+ import random
3
+ import tempfile
4
+
5
+ import pytest
6
+
7
+ from xchainpy2_crypto import generate_mnemonic, KeyStore, InvalidPasswordException
8
+
9
+
10
+ def test_keystore_encrypt_decrypt():
11
+ for i in range(10):
12
+ mnemonic = generate_mnemonic()
13
+ password = os.urandom(random.randint(1, 16)).hex()
14
+ ks = KeyStore.encrypt_to_keystore(mnemonic, password)
15
+
16
+ mnemonic_out = ks.decrypt_from_keystore(password)
17
+ assert mnemonic == mnemonic_out
18
+
19
+
20
+ def test_invalid_password():
21
+ mnemonic = generate_mnemonic()
22
+ password = 'good_password123'
23
+ ks = KeyStore.encrypt_to_keystore(mnemonic, password)
24
+
25
+ with pytest.raises(InvalidPasswordException):
26
+ ks.decrypt_from_keystore('wrong_password')
27
+
28
+
29
+
30
+ def test_save_load():
31
+ mnemonic = generate_mnemonic()
32
+ password = 'good_password123'
33
+
34
+ ks = KeyStore.encrypt_to_keystore(mnemonic, password)
35
+
36
+ # save to temp file
37
+ with tempfile.NamedTemporaryFile(delete=False) as temp_file:
38
+ ks.save(temp_file.name)
39
+ assert os.path.exists(temp_file.name)
40
+
41
+ ks2 = KeyStore.from_file(temp_file.name)
42
+ assert ks.to_dict == ks2.to_dict
43
+ assert ks.meta == ks2.meta and bool(ks.meta)
44
+ assert ks.id == ks2.id and bool(ks.id)
45
+ assert ks.ciphertext == ks2.ciphertext and bool(ks.ciphertext)
46
+ assert ks.cipher == ks2.cipher and bool(ks.cipher)
47
+ assert ks.cipherparams_iv == ks2.cipherparams_iv and bool(ks.cipherparams_iv)
48
+ assert ks.kdf == ks2.kdf and bool(ks.kdf)
49
+ assert ks.kdfparams_prf == ks2.kdfparams_prf and bool(ks.kdfparams_prf)
50
+
51
+ assert ks.decrypt_from_keystore(password) == ks2.decrypt_from_keystore(password) == mnemonic
52
+
53
+ with pytest.raises(FileNotFoundError):
54
+ KeyStore.from_file('_some_non_existent.txt')
@@ -0,0 +1,49 @@
1
+ import pytest
2
+
3
+ from xchainpy2_crypto import *
4
+
5
+
6
+ def test_create_mnemonic():
7
+ mnemonic = generate_mnemonic()
8
+ assert len(mnemonic.split()) == 12
9
+ assert validate_mnemonic(mnemonic)
10
+
11
+ mnemonic = generate_mnemonic(15)
12
+ assert len(mnemonic.split()) == 15
13
+ assert validate_mnemonic(mnemonic)
14
+
15
+ mnemonic = generate_mnemonic(24)
16
+ assert len(mnemonic.split()) == 24
17
+ assert validate_mnemonic(mnemonic)
18
+
19
+ assert not validate_mnemonic('')
20
+ assert not validate_mnemonic(None)
21
+
22
+
23
+ @pytest.mark.parametrize('wallet_index, key, address', [
24
+ (0, '437a3090352a646872f9ddc9c172e7d74d3e0472bc67ca5eccef0a64b94d791d',
25
+ 'thor14nhwuxr8e00m2qtfgpqtwafnw25x8vmtka8c5a'),
26
+ (1, 'aea64d24898d02b080b6231ee0778e3f4c0b31cf756b565d586b8e7f390feafe',
27
+ 'thor14p9fz9f8hw6f7jeh9msxg6v7xw65r52r7xnjka'),
28
+ (2, '55935cd344247aa0cb2dca2c13780f7ed9a571efd709cae9e82204ef57ec62ef',
29
+ 'thor1xs3tksyfhyxe2xlwr9rtlcjm0pzthdsuavx39c')
30
+ ])
31
+ def test_wallet_private_key_index(wallet_index, key, address):
32
+ # totally random, no worries
33
+ mnemonic = 'grain dizzy better fossil taste install tobacco bless source science category van'
34
+
35
+ derivation_path = f"44'/931'/0'/0/{wallet_index}"
36
+
37
+ seed = get_seed(mnemonic)
38
+ assert len(seed) == 64
39
+ gen_key = get_bip32(seed, derivation_path)
40
+
41
+ priv_key = get_private_key(gen_key)
42
+ pub_key = get_public_key(gen_key)
43
+
44
+ assert priv_key.hex() == key, 'Generated key does not match expected key'
45
+ assert create_address(pub_key, 'thor') == address, 'Generated address does not match expected address'
46
+
47
+ assert derive_private_key(mnemonic, derivation_path).hex() == key, 'Generated key does not match expected key'
48
+ assert derive_address(mnemonic, derivation_path, 'thor') == address, \
49
+ 'Generated address does not match expected address'
@@ -0,0 +1,129 @@
1
+ from typing import Union
2
+
3
+ from Crypto.Hash import RIPEMD160, SHA256
4
+ from bip_utils import Bip39MnemonicGenerator, Bip39WordsNum, Bip39Languages, Bip39MnemonicValidator, \
5
+ Bip39SeedGenerator, Bech32Encoder, Bip32Secp256k1, Bech32Decoder
6
+
7
+
8
+ def generate_mnemonic(words_number=Bip39WordsNum.WORDS_NUM_12, lang=Bip39Languages.ENGLISH):
9
+ """
10
+ Generate a mnemonic phrase
11
+ :param words_number: Words number 12/24, default: 12
12
+ :param lang: Language, default: English
13
+ :return: str: Mnemonic phrase
14
+ """
15
+ mnemonic = Bip39MnemonicGenerator(lang=lang).FromWordsNumber(words_number)
16
+ return str(mnemonic)
17
+
18
+
19
+ def _normalize_mnemonic_type(mnemonic: str) -> str:
20
+ if isinstance(mnemonic, (list, tuple)):
21
+ mnemonic = ' '.join(mnemonic)
22
+ return mnemonic
23
+
24
+
25
+ def validate_mnemonic(mnemonic: str, lang: str = None) -> bool:
26
+ """
27
+ :param mnemonic: Mnemonic phrase or list of words
28
+ :param lang: Bip39Languages or None
29
+ :return: bool: True if valid, False otherwise
30
+ """
31
+ if not mnemonic:
32
+ return False
33
+ mnemonic = _normalize_mnemonic_type(mnemonic)
34
+ return Bip39MnemonicValidator(lang).IsValid(mnemonic)
35
+
36
+
37
+ def get_seed(mnemonic, lang: str = None) -> bytes:
38
+ """
39
+ :param mnemonic: Mnemonic phrase or list of words
40
+ :param lang: Bip39Languages or None
41
+ :return: bytes: Seed
42
+ """
43
+ mnemonic = _normalize_mnemonic_type(mnemonic)
44
+ return Bip39SeedGenerator(mnemonic, lang).Generate()
45
+
46
+
47
+ def sha256ripemd160(hex_str: str) -> str:
48
+ """
49
+ Calculate `ripemd160(sha256(hex))` from the hex string
50
+ :param str hex_str: Input hex string
51
+ :return: str: Output hex string
52
+ """
53
+ data = bytes.fromhex(hex_str)
54
+ return RIPEMD160.RIPEMD160Hash(
55
+ SHA256.SHA256Hash(data).digest()
56
+ ).hexdigest()
57
+
58
+
59
+ def encode_address(value: Union[str, bytes], prefix='thor') -> str:
60
+ """
61
+ Encode address from the string or bytes
62
+ :param str value: Input
63
+ :param str prefix: Address prefix 'thor' by default
64
+ :return: str: Address
65
+ """
66
+ if isinstance(value, str):
67
+ value = bytes.fromhex(value)
68
+ return Bech32Encoder.Encode(prefix, value)
69
+
70
+
71
+ def decode_address(address: str, prefix: str) -> bytes:
72
+ """
73
+ Decode address to bytes
74
+ :param str address: Address
75
+ :param str prefix: Address prefix (e.g. 'thor')
76
+ :return: bytes: Decoded address
77
+ """
78
+ return Bech32Decoder.Decode(prefix, address)
79
+
80
+
81
+ def create_address(public_key: bytes, prefix='thor') -> str:
82
+ """
83
+ Create address from public key
84
+ :param bytes public_key: Public key
85
+ :param str prefix: Address prefix
86
+ :return: str: Address
87
+ """
88
+ hexed = public_key.hex()
89
+ hash_hex = sha256ripemd160(hexed)
90
+ return encode_address(hash_hex, prefix)
91
+
92
+
93
+ def get_bip32(seed: bytes, derivation_path: str) -> Bip32Secp256k1:
94
+ return Bip32Secp256k1.FromSeed(seed).DerivePath(derivation_path)
95
+
96
+
97
+ def get_private_key(key: Bip32Secp256k1) -> bytes:
98
+ return key.PrivateKey().Raw().ToBytes()
99
+
100
+
101
+ def get_public_key(key: Bip32Secp256k1) -> bytes:
102
+ return key.PublicKey().RawCompressed().ToBytes()
103
+
104
+
105
+ def derive_private_key(mnemonic: str, derivation_path: str) -> bytes:
106
+ """
107
+ Derive private key from mnemonic and derivation path
108
+ :param str mnemonic: Mnemonic phrase or list of words
109
+ :param str derivation_path: Derivation path
110
+ :return: bytes: Private key
111
+ """
112
+ seed = get_seed(mnemonic)
113
+ key = get_bip32(seed, derivation_path)
114
+ return get_private_key(key)
115
+
116
+
117
+ def derive_address(mnemonic: str, derivation_path: str, prefix='thor', lang: str = None) -> str:
118
+ """
119
+ Derive address from mnemonic and derivation path
120
+ :param str mnemonic: Mnemonic phrase or list of words
121
+ :param str derivation_path: Derivation path
122
+ :param str prefix: Address prefix
123
+ :param str lang: Language of mnemonic words
124
+ :return: str: Address
125
+ """
126
+ seed = get_seed(mnemonic, lang)
127
+ key = get_bip32(seed, derivation_path)
128
+ public_key = get_public_key(key)
129
+ return create_address(public_key, prefix)
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: xchainpy2_crypto
3
+ Version: 0.1.1
4
+ Summary: XChainPy2 Crypto utils and keystore management
5
+ Author-email: Tirinox <tirinox@gmail.com>
6
+ License: MIT
7
+ Project-URL: source, https://github.com/tirinox/xchainpy
8
+ Keywords: Crypto,THORChain,Blockchain,XChain
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Software Development :: Build Tools
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.6
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Requires-Python: >=3.7
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: bip-utils<3.0.0,>=2.12.1.0
24
+ Requires-Dist: pycryptodome<4.0,>=3.23
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest; extra == "test"
27
+ Dynamic: license-file
28
+
29
+ # How it works
30
+ Typically keystore files encrypt a seed to a file, however this is not appropriate or UX friendly, since the phrase
31
+ cannot be recovered after the fact.
32
+
33
+ Crypto design:
34
+
35
+ `[entropy] -> [phrase] -> [seed] -> [privateKey] -> [publicKey] -> [address]`
36
+
37
+ Instead, XCHAIN-CRYPTO stores the phrase in a keystore file, then decrypts and passes this phrase to other clients:
38
+
39
+ `[keystore] -> XCHAIN-CRYPTO -> [phrase] -> ChainClient`
40
+
41
+ The ChainClients can then convert this into their respective key-pairs and addresses. Users can also export their
42
+ phrases after the fact, ensuring they have saved it securely. This could enhance UX onboarding since users aren't forced
43
+ to write their phrases down immediately for empty or test wallets.
44
+
45
+ ### Documentation
46
+
47
+ 👉 https://xchainpy2.readthedocs.io/en/latest/packages/xchainpy2_crypto.html
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ xchainpy2_crypto/__init__.py
5
+ xchainpy2_crypto/keystore.py
6
+ xchainpy2_crypto/utils.py
7
+ xchainpy2_crypto.egg-info/PKG-INFO
8
+ xchainpy2_crypto.egg-info/SOURCES.txt
9
+ xchainpy2_crypto.egg-info/dependency_links.txt
10
+ xchainpy2_crypto.egg-info/requires.txt
11
+ xchainpy2_crypto.egg-info/top_level.txt
12
+ xchainpy2_crypto/tests/__init__.py
13
+ xchainpy2_crypto/tests/test_address.py
14
+ xchainpy2_crypto/tests/test_keystore.py
15
+ xchainpy2_crypto/tests/test_mnemonic.py
@@ -0,0 +1,5 @@
1
+ bip-utils<3.0.0,>=2.12.1.0
2
+ pycryptodome<4.0,>=3.23
3
+
4
+ [test]
5
+ pytest
@@ -0,0 +1 @@
1
+ xchainpy2_crypto