libcrypto 1.0.1__py3-none-any.whl
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.
- libcrypto/__init__.py +72 -0
- libcrypto/_version.py +19 -0
- libcrypto/addresses.py +191 -0
- libcrypto/bip32.py +234 -0
- libcrypto/cli.py +159 -0
- libcrypto/constants.py +288 -0
- libcrypto/formats.py +164 -0
- libcrypto/hash.py +88 -0
- libcrypto/keys.py +149 -0
- libcrypto/mnemonic.py +156 -0
- libcrypto/secp256k1.py +117 -0
- libcrypto/wallet.py +135 -0
- libcrypto-1.0.1.dist-info/METADATA +318 -0
- libcrypto-1.0.1.dist-info/RECORD +18 -0
- libcrypto-1.0.1.dist-info/WHEEL +5 -0
- libcrypto-1.0.1.dist-info/entry_points.txt +2 -0
- libcrypto-1.0.1.dist-info/licenses/LICENSE +21 -0
- libcrypto-1.0.1.dist-info/top_level.txt +1 -0
libcrypto/keys.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Private and Public Key Classes
|
|
3
|
+
|
|
4
|
+
This module provides classes for handling private and public keys with
|
|
5
|
+
format conversions, WIF support, and cryptographic operations.
|
|
6
|
+
"""
|
|
7
|
+
from typing import Union
|
|
8
|
+
from .secp256k1 import (
|
|
9
|
+
private_key_to_public_key, compress_public_key, decompress_public_key
|
|
10
|
+
)
|
|
11
|
+
from .hash import secure_random_bytes
|
|
12
|
+
from .constants import MAX_PRIVATE_KEY
|
|
13
|
+
from .formats import (
|
|
14
|
+
private_key_to_wif, wif_to_private_key, bytes_to_hex, hex_to_bytes,
|
|
15
|
+
int_to_bytes, bytes_to_int, InvalidFormatError
|
|
16
|
+
)
|
|
17
|
+
from .addresses import AddressGenerator
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class KeyError(ValueError):
|
|
21
|
+
"""Raised when key operations fail."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PrivateKey:
|
|
26
|
+
"""
|
|
27
|
+
Represents a secp256k1 private key.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, key: Union[bytes, int, str, None] = None, network: str = 'bitcoin'):
|
|
31
|
+
self.network = network
|
|
32
|
+
if key is None:
|
|
33
|
+
self._key_int = self._generate_random_key()
|
|
34
|
+
else:
|
|
35
|
+
self._key_int = self._normalize_key(key)
|
|
36
|
+
|
|
37
|
+
if not (1 <= self._key_int <= MAX_PRIVATE_KEY):
|
|
38
|
+
raise KeyError(f"Private key is out of valid range (1 to N-1).")
|
|
39
|
+
|
|
40
|
+
self._key_bytes = int_to_bytes(self._key_int, 32)
|
|
41
|
+
self._public_key_cache = {}
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _generate_random_key() -> int:
|
|
45
|
+
"""Generate a cryptographically secure random private key."""
|
|
46
|
+
while True:
|
|
47
|
+
key_bytes = secure_random_bytes(32)
|
|
48
|
+
key_int = bytes_to_int(key_bytes)
|
|
49
|
+
if 1 <= key_int <= MAX_PRIVATE_KEY:
|
|
50
|
+
return key_int
|
|
51
|
+
|
|
52
|
+
def _normalize_key(self, key: Union[bytes, int, str]) -> int:
|
|
53
|
+
"""Normalize various key formats to an integer."""
|
|
54
|
+
if isinstance(key, int):
|
|
55
|
+
return key
|
|
56
|
+
if isinstance(key, bytes):
|
|
57
|
+
if len(key) != 32:
|
|
58
|
+
raise KeyError(f"Private key bytes must be 32 bytes, got {len(key)}")
|
|
59
|
+
return bytes_to_int(key)
|
|
60
|
+
if isinstance(key, str):
|
|
61
|
+
try:
|
|
62
|
+
# First, try to decode as WIF
|
|
63
|
+
key_bytes, _, _ = wif_to_private_key(key)
|
|
64
|
+
self.network = _[2] # Update network from WIF
|
|
65
|
+
return bytes_to_int(key_bytes)
|
|
66
|
+
except InvalidFormatError:
|
|
67
|
+
# If WIF fails, try to decode as hex
|
|
68
|
+
try:
|
|
69
|
+
if len(key) != 64:
|
|
70
|
+
raise InvalidFormatError("Hex key must be 64 characters.")
|
|
71
|
+
key_bytes = hex_to_bytes(key)
|
|
72
|
+
return bytes_to_int(key_bytes)
|
|
73
|
+
except InvalidFormatError as e:
|
|
74
|
+
raise KeyError(f"Invalid private key format. Not valid WIF or Hex: {e}") from e
|
|
75
|
+
raise KeyError(f"Unsupported key type: {type(key)}")
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def hex(self) -> str:
|
|
79
|
+
return bytes_to_hex(self._key_bytes)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def bytes(self) -> bytes:
|
|
83
|
+
return self._key_bytes
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def int(self) -> int:
|
|
87
|
+
return self._key_int
|
|
88
|
+
|
|
89
|
+
def to_wif(self, compressed: bool = True) -> str:
|
|
90
|
+
return private_key_to_wif(self._key_bytes, compressed, self.network)
|
|
91
|
+
|
|
92
|
+
def get_public_key(self, compressed: bool = True) -> 'PublicKey':
|
|
93
|
+
"""Get the corresponding public key, using a cache for efficiency."""
|
|
94
|
+
if compressed not in self._public_key_cache:
|
|
95
|
+
public_key_bytes = private_key_to_public_key(self._key_int, compressed)
|
|
96
|
+
self._public_key_cache[compressed] = PublicKey(public_key_bytes)
|
|
97
|
+
return self._public_key_cache[compressed]
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def generate(cls, network: str = 'bitcoin') -> 'PrivateKey':
|
|
101
|
+
"""Generate a new random private key."""
|
|
102
|
+
return cls(None, network)
|
|
103
|
+
|
|
104
|
+
def __repr__(self) -> str:
|
|
105
|
+
return f"PrivateKey(network='{self.network}')"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PublicKey:
|
|
109
|
+
"""
|
|
110
|
+
Represents a secp256k1 public key.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(self, key: Union[bytes, str]):
|
|
114
|
+
if isinstance(key, str):
|
|
115
|
+
key = hex_to_bytes(key)
|
|
116
|
+
|
|
117
|
+
if len(key) not in [33, 65]:
|
|
118
|
+
raise KeyError(f"Invalid public key length: {len(key)}. Must be 33 or 65 bytes.")
|
|
119
|
+
|
|
120
|
+
self._key_bytes = key
|
|
121
|
+
self.compressed = (len(key) == 33)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def hex(self) -> str:
|
|
125
|
+
return bytes_to_hex(self._key_bytes)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def bytes(self) -> bytes:
|
|
129
|
+
return self._key_bytes
|
|
130
|
+
|
|
131
|
+
def get_address(self, address_type: str = 'p2pkh', network: str = 'bitcoin') -> str:
|
|
132
|
+
"""
|
|
133
|
+
Generate an address from this public key.
|
|
134
|
+
This is a method, not a property, as it requires arguments.
|
|
135
|
+
"""
|
|
136
|
+
return AddressGenerator.from_public_key(self.bytes, address_type, network)
|
|
137
|
+
|
|
138
|
+
def to_compressed(self) -> 'PublicKey':
|
|
139
|
+
if self.compressed:
|
|
140
|
+
return self
|
|
141
|
+
return PublicKey(compress_public_key(self.bytes))
|
|
142
|
+
|
|
143
|
+
def to_uncompressed(self) -> 'PublicKey':
|
|
144
|
+
if not self.compressed:
|
|
145
|
+
return self
|
|
146
|
+
return PublicKey(decompress_public_key(self.bytes))
|
|
147
|
+
|
|
148
|
+
def __repr__(self) -> str:
|
|
149
|
+
return f"PublicKey('{self.hex}', compressed={self.compressed})"
|
libcrypto/mnemonic.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BIP39 Mnemonic Phrase Implementation
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive BIP39 mnemonic phrase functionality including:
|
|
5
|
+
- Secure mnemonic generation with proper entropy
|
|
6
|
+
- Mnemonic validation with checksum verification
|
|
7
|
+
- Mnemonic to seed conversion with PBKDF2
|
|
8
|
+
- Conversion between entropy and mnemonic phrases
|
|
9
|
+
"""
|
|
10
|
+
from typing import Union
|
|
11
|
+
from .hash import sha256, bip39_pbkdf2, secure_random_bytes
|
|
12
|
+
from .constants import (
|
|
13
|
+
BIP39_WORD_LIST,
|
|
14
|
+
BIP39_ENTROPY_BITS,
|
|
15
|
+
BIP39_CHECKSUM_BITS,
|
|
16
|
+
VALID_MNEMONIC_LENGTHS,
|
|
17
|
+
ERROR_MESSAGES
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InvalidMnemonicError(ValueError):
|
|
22
|
+
"""Raised when a mnemonic phrase is invalid."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InvalidEntropyError(ValueError):
|
|
27
|
+
"""Raised when entropy is invalid."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _entropy_to_mnemonic_bits(entropy: bytes) -> str:
|
|
32
|
+
"""Internal helper to convert entropy to its bit representation with checksum."""
|
|
33
|
+
entropy_bits_len = len(entropy) * 8
|
|
34
|
+
if entropy_bits_len not in BIP39_ENTROPY_BITS.values():
|
|
35
|
+
raise InvalidEntropyError(f"Invalid entropy length: {len(entropy)} bytes")
|
|
36
|
+
|
|
37
|
+
checksum_len = BIP39_CHECKSUM_BITS[entropy_bits_len // 32 * 3]
|
|
38
|
+
|
|
39
|
+
# Calculate checksum
|
|
40
|
+
checksum_hash = sha256(entropy)
|
|
41
|
+
checksum_bits = bin(checksum_hash[0])[2:].zfill(8)[:checksum_len]
|
|
42
|
+
|
|
43
|
+
# Combine entropy and checksum
|
|
44
|
+
entropy_bits = bin(int.from_bytes(entropy, 'big'))[2:].zfill(entropy_bits_len)
|
|
45
|
+
|
|
46
|
+
return entropy_bits + checksum_bits
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def entropy_to_mnemonic(entropy: Union[bytes, str]) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Convert entropy to a BIP39 mnemonic phrase.
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(entropy, str):
|
|
54
|
+
try:
|
|
55
|
+
entropy = bytes.fromhex(entropy)
|
|
56
|
+
except ValueError as e:
|
|
57
|
+
raise InvalidEntropyError(f"Invalid hex string for entropy: {e}") from e
|
|
58
|
+
|
|
59
|
+
total_bits = _entropy_to_mnemonic_bits(entropy)
|
|
60
|
+
|
|
61
|
+
# Split into 11-bit chunks and map to words
|
|
62
|
+
words = []
|
|
63
|
+
for i in range(0, len(total_bits), 11):
|
|
64
|
+
chunk = total_bits[i:i + 11]
|
|
65
|
+
word_index = int(chunk, 2)
|
|
66
|
+
words.append(BIP39_WORD_LIST[word_index])
|
|
67
|
+
|
|
68
|
+
return ' '.join(words)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def mnemonic_to_entropy(mnemonic: str) -> bytes:
|
|
72
|
+
"""
|
|
73
|
+
Convert a BIP39 mnemonic phrase back to its original entropy.
|
|
74
|
+
"""
|
|
75
|
+
words = mnemonic.strip().split()
|
|
76
|
+
word_count = len(words)
|
|
77
|
+
|
|
78
|
+
if word_count not in VALID_MNEMONIC_LENGTHS:
|
|
79
|
+
raise InvalidMnemonicError(ERROR_MESSAGES['invalid_mnemonic_length'])
|
|
80
|
+
|
|
81
|
+
# Convert words to a bit string
|
|
82
|
+
bit_string = ""
|
|
83
|
+
for word in words:
|
|
84
|
+
try:
|
|
85
|
+
index = BIP39_WORD_LIST.index(word)
|
|
86
|
+
bit_string += bin(index)[2:].zfill(11)
|
|
87
|
+
except ValueError:
|
|
88
|
+
raise InvalidMnemonicError(f"{ERROR_MESSAGES['invalid_mnemonic_word']}: '{word}'")
|
|
89
|
+
|
|
90
|
+
# Split data and checksum
|
|
91
|
+
entropy_len = BIP39_ENTROPY_BITS[word_count]
|
|
92
|
+
checksum_len = BIP39_CHECKSUM_BITS[word_count]
|
|
93
|
+
|
|
94
|
+
entropy_bits = bit_string[:entropy_len]
|
|
95
|
+
checksum_bits = bit_string[entropy_len:]
|
|
96
|
+
|
|
97
|
+
# Convert entropy bits to bytes
|
|
98
|
+
entropy_bytes = int(entropy_bits, 2).to_bytes(entropy_len // 8, 'big')
|
|
99
|
+
|
|
100
|
+
# Verify checksum
|
|
101
|
+
expected_checksum_hash = sha256(entropy_bytes)
|
|
102
|
+
expected_checksum = bin(expected_checksum_hash[0])[2:].zfill(8)[:checksum_len]
|
|
103
|
+
|
|
104
|
+
if checksum_bits != expected_checksum:
|
|
105
|
+
raise InvalidMnemonicError(ERROR_MESSAGES['invalid_mnemonic_checksum'])
|
|
106
|
+
|
|
107
|
+
return entropy_bytes
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def generate_mnemonic(word_count: int = 12) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Generates a cryptographically secure mnemonic phrase.
|
|
113
|
+
"""
|
|
114
|
+
if word_count not in VALID_MNEMONIC_LENGTHS:
|
|
115
|
+
raise ValueError(ERROR_MESSAGES['invalid_mnemonic_length'])
|
|
116
|
+
|
|
117
|
+
entropy_bits = BIP39_ENTROPY_BITS[word_count]
|
|
118
|
+
entropy = secure_random_bytes(entropy_bits // 8)
|
|
119
|
+
|
|
120
|
+
return entropy_to_mnemonic(entropy)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_mnemonic(mnemonic: str) -> bool:
|
|
124
|
+
"""
|
|
125
|
+
Validate a BIP39 mnemonic phrase by trying to convert it to entropy.
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
mnemonic_to_entropy(mnemonic)
|
|
129
|
+
return True
|
|
130
|
+
except InvalidMnemonicError:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def mnemonic_to_seed(mnemonic: str, passphrase: str = "") -> bytes:
|
|
135
|
+
"""
|
|
136
|
+
Convert a BIP39 mnemonic phrase to a seed using PBKDF2.
|
|
137
|
+
"""
|
|
138
|
+
# NFKD normalization is recommended by BIP39 spec
|
|
139
|
+
import unicodedata
|
|
140
|
+
normalized_mnemonic = unicodedata.normalize('NFKD', mnemonic.strip())
|
|
141
|
+
|
|
142
|
+
if not validate_mnemonic(normalized_mnemonic):
|
|
143
|
+
raise InvalidMnemonicError("Invalid mnemonic phrase provided.")
|
|
144
|
+
|
|
145
|
+
return bip39_pbkdf2(normalized_mnemonic, passphrase)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
__all__ = [
|
|
149
|
+
'generate_mnemonic',
|
|
150
|
+
'validate_mnemonic',
|
|
151
|
+
'mnemonic_to_seed',
|
|
152
|
+
'mnemonic_to_entropy',
|
|
153
|
+
'entropy_to_mnemonic',
|
|
154
|
+
'InvalidMnemonicError',
|
|
155
|
+
'InvalidEntropyError'
|
|
156
|
+
]
|
libcrypto/secp256k1.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secp256k1 Elliptic Curve Operations (using the 'ecdsa' library)
|
|
3
|
+
|
|
4
|
+
This module provides a robust interface for secp256k1 operations by
|
|
5
|
+
wrapping the 'ecdsa' library, which is a highly stable and focused package
|
|
6
|
+
for Elliptic Curve Digital Signature Algorithm.
|
|
7
|
+
|
|
8
|
+
This implementation replaces the pycryptodome backend to avoid environment
|
|
9
|
+
and installation issues.
|
|
10
|
+
"""
|
|
11
|
+
from typing import Tuple
|
|
12
|
+
from ecdsa import SigningKey, VerifyingKey, SECP256k1
|
|
13
|
+
from ecdsa.util import sigencode_string, sigdecode_string
|
|
14
|
+
from .constants import MAX_PRIVATE_KEY
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Secp256k1Error(ValueError):
|
|
18
|
+
"""Custom exception for secp256k1 related errors."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def private_key_to_public_key(private_key: int, compressed: bool = True) -> bytes:
|
|
23
|
+
"""
|
|
24
|
+
Derives a public key from a private key integer using the 'ecdsa' library.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
private_key: The private key as an integer.
|
|
28
|
+
compressed: If True, returns a 33-byte compressed public key.
|
|
29
|
+
If False, returns a 65-byte uncompressed public key.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The public key as a byte string.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
Secp256k1Error: If the private key is out of the valid range.
|
|
36
|
+
"""
|
|
37
|
+
if not (1 <= private_key <= MAX_PRIVATE_KEY):
|
|
38
|
+
raise Secp256k1Error("Private key is out of the valid range (1 to N-1).")
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
# Create a SigningKey object from the private key bytes.
|
|
42
|
+
private_key_bytes = private_key.to_bytes(32, 'big')
|
|
43
|
+
sk = SigningKey.from_string(private_key_bytes, curve=SECP256k1)
|
|
44
|
+
|
|
45
|
+
# Get the corresponding VerifyingKey (public key).
|
|
46
|
+
vk = sk.verifying_key
|
|
47
|
+
|
|
48
|
+
# Return the public key in the requested format.
|
|
49
|
+
if compressed:
|
|
50
|
+
return vk.to_string("compressed")
|
|
51
|
+
else:
|
|
52
|
+
return vk.to_string("uncompressed")
|
|
53
|
+
except Exception as e:
|
|
54
|
+
raise Secp256k1Error(f"Failed to generate public key with ecdsa: {e}") from e
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def public_key_to_point_coords(public_key: bytes) -> Tuple[int, int]:
|
|
58
|
+
"""
|
|
59
|
+
Converts a public key byte string into its (x, y) integer coordinates.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
public_key: The public key as bytes (compressed or uncompressed).
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A tuple containing the (x, y) coordinates as integers.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
vk = VerifyingKey.from_string(public_key, curve=SECP256k1)
|
|
69
|
+
# The public_point attribute holds the x and y coordinates.
|
|
70
|
+
return (vk.pubkey.point.x(), vk.pubkey.point.y())
|
|
71
|
+
except Exception as e:
|
|
72
|
+
raise Secp256k1Error(f"Failed to extract point from public key: {e}") from e
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def decompress_public_key(public_key: bytes) -> bytes:
|
|
76
|
+
"""
|
|
77
|
+
Converts a public key to its uncompressed format (65 bytes) using 'ecdsa'.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
public_key: The public key in either compressed or uncompressed format.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The 65-byte uncompressed public key.
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
# Create a VerifyingKey from the input bytes. It handles both formats.
|
|
87
|
+
vk = VerifyingKey.from_string(public_key, curve=SECP256k1)
|
|
88
|
+
return vk.to_string("uncompressed")
|
|
89
|
+
except Exception as e:
|
|
90
|
+
raise Secp256k1Error(f"Failed to decompress public key: {e}") from e
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def compress_public_key(public_key: bytes) -> bytes:
|
|
94
|
+
"""
|
|
95
|
+
Converts a public key to its compressed format (33 bytes) using 'ecdsa'.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
public_key: The public key in either compressed or uncompressed format.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The 33-byte compressed public key.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
# Create a VerifyingKey from the input bytes.
|
|
105
|
+
vk = VerifyingKey.from_string(public_key, curve=SECP256k1)
|
|
106
|
+
return vk.to_string("compressed")
|
|
107
|
+
except Exception as e:
|
|
108
|
+
raise Secp256k1Error(f"Failed to compress public key: {e}") from e
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
__all__ = [
|
|
112
|
+
'private_key_to_public_key',
|
|
113
|
+
'public_key_to_point_coords',
|
|
114
|
+
'decompress_public_key',
|
|
115
|
+
'compress_public_key',
|
|
116
|
+
'Secp256k1Error',
|
|
117
|
+
]
|
libcrypto/wallet.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-Level Wallet Interface
|
|
3
|
+
|
|
4
|
+
This module provides a simple, high-level interface for creating cryptocurrency
|
|
5
|
+
wallets from a single private key and generating addresses for various coins.
|
|
6
|
+
"""
|
|
7
|
+
from typing import Union, Dict, List
|
|
8
|
+
from .keys import PrivateKey, PublicKey
|
|
9
|
+
from .addresses import AddressGenerator, AddressError
|
|
10
|
+
from .constants import BIP44_COIN_TYPES
|
|
11
|
+
|
|
12
|
+
# Configuration for coins, especially for properties like required public key format.
|
|
13
|
+
COIN_CONFIG = {
|
|
14
|
+
'ethereum': {'uncompressed_required': True, 'address_types': ['default']},
|
|
15
|
+
'tron': {'uncompressed_required': True, 'address_types': ['default']},
|
|
16
|
+
'ripple': {'uncompressed_required': False, 'address_types': ['default']}, # Prefers compressed
|
|
17
|
+
'bitcoin': {'uncompressed_required': False, 'address_types': ['p2pkh', 'p2sh-p2wpkh', 'p2wpkh']},
|
|
18
|
+
'litecoin': {'uncompressed_required': False, 'address_types': ['p2pkh', 'p2sh-p2wpkh', 'p2wpkh']},
|
|
19
|
+
'dogecoin': {'uncompressed_required': False, 'address_types': ['p2pkh']},
|
|
20
|
+
'dash': {'uncompressed_required': False, 'address_types': ['p2pkh']},
|
|
21
|
+
'bitcoin_cash': {'uncompressed_required': False, 'address_types': ['p2pkh']},
|
|
22
|
+
'testnet': {'uncompressed_required': False, 'address_types': ['p2pkh', 'p2sh-p2wpkh', 'p2wpkh']},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Wallet:
|
|
27
|
+
"""
|
|
28
|
+
A simple wallet class to manage a single private key and generate addresses.
|
|
29
|
+
|
|
30
|
+
This class serves as a high-level, easy-to-use wrapper around the library's
|
|
31
|
+
core functionalities. It is initialized with a single private key and can
|
|
32
|
+
generate corresponding addresses for multiple supported cryptocurrencies.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, private_key: Union[str, bytes, int, PrivateKey]):
|
|
36
|
+
"""
|
|
37
|
+
Initializes the wallet with a private key.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
private_key: The private key in WIF, hex, bytes, integer format,
|
|
41
|
+
or as a PrivateKey object.
|
|
42
|
+
"""
|
|
43
|
+
if isinstance(private_key, PrivateKey):
|
|
44
|
+
self.private_key = private_key
|
|
45
|
+
else:
|
|
46
|
+
self.private_key = PrivateKey(private_key)
|
|
47
|
+
|
|
48
|
+
self._public_key_compressed = self.private_key.get_public_key(compressed=True)
|
|
49
|
+
self._public_key_uncompressed = self.private_key.get_public_key(compressed=False)
|
|
50
|
+
|
|
51
|
+
def get_address(self, coin: str, address_type: str = 'p2pkh') -> str:
|
|
52
|
+
"""
|
|
53
|
+
Generates a single address for a specific coin and address type.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
coin (str): The name of the coin (e.g., 'bitcoin', 'ethereum').
|
|
57
|
+
address_type (str, optional): The type of address to generate.
|
|
58
|
+
For Bitcoin coins: 'p2pkh' (default), 'p2sh-p2wpkh', 'p2wpkh'.
|
|
59
|
+
For Ethereum coins, this is ignored.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
str: The generated address string.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If the coin or address type is not supported.
|
|
66
|
+
NotImplementedError: For coins requiring a different cryptographic curve (e.g., Ed25519).
|
|
67
|
+
"""
|
|
68
|
+
coin = coin.lower()
|
|
69
|
+
if coin not in COIN_CONFIG:
|
|
70
|
+
# Check if it's a known coin but just not in the simple config
|
|
71
|
+
if coin in BIP44_COIN_TYPES:
|
|
72
|
+
raise ValueError(f"Coin '{coin}' is recognized but not configured for simple address generation.")
|
|
73
|
+
raise ValueError(f"Unsupported coin: '{coin}'")
|
|
74
|
+
|
|
75
|
+
config = COIN_CONFIG[coin]
|
|
76
|
+
|
|
77
|
+
# Select the correct public key format
|
|
78
|
+
public_key = self._public_key_uncompressed if config['uncompressed_required'] else self._public_key_compressed
|
|
79
|
+
|
|
80
|
+
# For coins with a single address type, override the user's choice
|
|
81
|
+
if len(config['address_types']) == 1 and config['address_types'][0] == 'default':
|
|
82
|
+
addr_type = 'default' # A generic type for coins like ETH, TRX
|
|
83
|
+
else:
|
|
84
|
+
if address_type not in config['address_types']:
|
|
85
|
+
raise ValueError(f"Unsupported address type '{address_type}' for {coin}. "
|
|
86
|
+
f"Available types: {config['address_types']}")
|
|
87
|
+
addr_type = address_type
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# In AddressGenerator, 'default' type is handled internally for relevant networks
|
|
91
|
+
# and `addr_type` is used for Bitcoin-style networks.
|
|
92
|
+
effective_type_for_generator = 'default' if addr_type == 'default' else addr_type
|
|
93
|
+
return AddressGenerator.from_public_key(public_key.bytes, effective_type_for_generator, coin)
|
|
94
|
+
except AddressError as e:
|
|
95
|
+
raise ValueError(f"Could not generate address for {coin}: {e}")
|
|
96
|
+
|
|
97
|
+
def get_all_addresses(self, coin: str) -> Dict[str, str]:
|
|
98
|
+
"""
|
|
99
|
+
Generates all supported address formats for a given coin.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
coin (str): The name of the coin (e.g., 'bitcoin', 'ethereum').
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dict[str, str]: A dictionary where keys are address types and
|
|
106
|
+
values are the corresponding addresses.
|
|
107
|
+
"""
|
|
108
|
+
coin = coin.lower()
|
|
109
|
+
if coin not in COIN_CONFIG:
|
|
110
|
+
raise ValueError(f"Unsupported coin: '{coin}'")
|
|
111
|
+
|
|
112
|
+
addresses = {}
|
|
113
|
+
config = COIN_CONFIG[coin]
|
|
114
|
+
|
|
115
|
+
for addr_type in config['address_types']:
|
|
116
|
+
try:
|
|
117
|
+
# Use the main get_address method to ensure consistent logic
|
|
118
|
+
addresses[addr_type] = self.get_address(coin, addr_type)
|
|
119
|
+
except (ValueError, NotImplementedError, AddressError) as e:
|
|
120
|
+
addresses[addr_type] = f"Error: {e}"
|
|
121
|
+
|
|
122
|
+
return addresses
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def generate(cls) -> 'Wallet':
|
|
126
|
+
"""
|
|
127
|
+
Creates a new Wallet instance with a newly generated private key.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
A new Wallet instance.
|
|
131
|
+
"""
|
|
132
|
+
return cls(PrivateKey.generate())
|
|
133
|
+
|
|
134
|
+
def __repr__(self) -> str:
|
|
135
|
+
return f"Wallet(private_key='{self.private_key.hex}')"
|