hive-nectar 0.2.9__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.
- hive_nectar-0.2.9.dist-info/METADATA +194 -0
- hive_nectar-0.2.9.dist-info/RECORD +87 -0
- hive_nectar-0.2.9.dist-info/WHEEL +4 -0
- hive_nectar-0.2.9.dist-info/entry_points.txt +2 -0
- hive_nectar-0.2.9.dist-info/licenses/LICENSE.txt +23 -0
- nectar/__init__.py +37 -0
- nectar/account.py +5076 -0
- nectar/amount.py +553 -0
- nectar/asciichart.py +303 -0
- nectar/asset.py +122 -0
- nectar/block.py +574 -0
- nectar/blockchain.py +1242 -0
- nectar/blockchaininstance.py +2590 -0
- nectar/blockchainobject.py +263 -0
- nectar/cli.py +5937 -0
- nectar/comment.py +1552 -0
- nectar/community.py +854 -0
- nectar/constants.py +95 -0
- nectar/discussions.py +1437 -0
- nectar/exceptions.py +152 -0
- nectar/haf.py +381 -0
- nectar/hive.py +630 -0
- nectar/imageuploader.py +114 -0
- nectar/instance.py +113 -0
- nectar/market.py +876 -0
- nectar/memo.py +542 -0
- nectar/message.py +379 -0
- nectar/nodelist.py +309 -0
- nectar/price.py +603 -0
- nectar/profile.py +74 -0
- nectar/py.typed +0 -0
- nectar/rc.py +333 -0
- nectar/snapshot.py +1024 -0
- nectar/storage.py +62 -0
- nectar/transactionbuilder.py +659 -0
- nectar/utils.py +630 -0
- nectar/version.py +3 -0
- nectar/vote.py +722 -0
- nectar/wallet.py +472 -0
- nectar/witness.py +728 -0
- nectarapi/__init__.py +12 -0
- nectarapi/exceptions.py +126 -0
- nectarapi/graphenerpc.py +596 -0
- nectarapi/node.py +194 -0
- nectarapi/noderpc.py +79 -0
- nectarapi/openapi.py +107 -0
- nectarapi/py.typed +0 -0
- nectarapi/rpcutils.py +98 -0
- nectarapi/version.py +3 -0
- nectarbase/__init__.py +15 -0
- nectarbase/ledgertransactions.py +106 -0
- nectarbase/memo.py +242 -0
- nectarbase/objects.py +521 -0
- nectarbase/objecttypes.py +21 -0
- nectarbase/operationids.py +102 -0
- nectarbase/operations.py +1357 -0
- nectarbase/py.typed +0 -0
- nectarbase/signedtransactions.py +89 -0
- nectarbase/transactions.py +11 -0
- nectarbase/version.py +3 -0
- nectargraphenebase/__init__.py +27 -0
- nectargraphenebase/account.py +1121 -0
- nectargraphenebase/aes.py +49 -0
- nectargraphenebase/base58.py +197 -0
- nectargraphenebase/bip32.py +575 -0
- nectargraphenebase/bip38.py +110 -0
- nectargraphenebase/chains.py +15 -0
- nectargraphenebase/dictionary.py +2 -0
- nectargraphenebase/ecdsasig.py +309 -0
- nectargraphenebase/objects.py +130 -0
- nectargraphenebase/objecttypes.py +8 -0
- nectargraphenebase/operationids.py +5 -0
- nectargraphenebase/operations.py +25 -0
- nectargraphenebase/prefix.py +13 -0
- nectargraphenebase/py.typed +0 -0
- nectargraphenebase/signedtransactions.py +221 -0
- nectargraphenebase/types.py +557 -0
- nectargraphenebase/unsignedtransactions.py +288 -0
- nectargraphenebase/version.py +3 -0
- nectarstorage/__init__.py +57 -0
- nectarstorage/base.py +317 -0
- nectarstorage/exceptions.py +15 -0
- nectarstorage/interfaces.py +244 -0
- nectarstorage/masterpassword.py +237 -0
- nectarstorage/py.typed +0 -0
- nectarstorage/ram.py +27 -0
- nectarstorage/sqlite.py +343 -0
|
@@ -0,0 +1,1121 @@
|
|
|
1
|
+
import binascii
|
|
2
|
+
import bisect
|
|
3
|
+
import hashlib
|
|
4
|
+
import itertools
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import unicodedata
|
|
8
|
+
from binascii import hexlify, unhexlify
|
|
9
|
+
from typing import Any, List, Optional, Tuple, Union
|
|
10
|
+
|
|
11
|
+
import ecdsa
|
|
12
|
+
from ecdsa.numbertheory import square_root_mod_prime
|
|
13
|
+
from ecdsa.util import number_to_string
|
|
14
|
+
|
|
15
|
+
from .base58 import Base58, doublesha256, ripemd160
|
|
16
|
+
from .bip32 import BIP32Key, parse_path
|
|
17
|
+
from .dictionary import words as BrainKeyDictionary
|
|
18
|
+
from .dictionary import words_bip39 as MnemonicDictionary
|
|
19
|
+
from .prefix import Prefix
|
|
20
|
+
|
|
21
|
+
PBKDF2_ROUNDS = 2048
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# secp256k1 curve parameters for pure Python implementation
|
|
25
|
+
SECP256K1_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
|
|
26
|
+
SECP256K1_A = 0
|
|
27
|
+
SECP256K1_B = 7
|
|
28
|
+
SECP256K1_GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
|
|
29
|
+
SECP256K1_GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
|
|
30
|
+
SECP256K1_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _mod_inverse(a: int, m: int) -> int:
|
|
34
|
+
"""
|
|
35
|
+
Return the modular inverse of a modulo m using the extended Euclidean algorithm.
|
|
36
|
+
|
|
37
|
+
Given integers a and m, compute x such that (a * x) % m == 1. The result is normalized
|
|
38
|
+
to the range [0, m-1]. If m == 1 the function returns 0.
|
|
39
|
+
|
|
40
|
+
Parameters:
|
|
41
|
+
a (int): The value to invert modulo m.
|
|
42
|
+
m (int): The modulus.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
int: The modular inverse of a modulo m, or 0 when m == 1.
|
|
46
|
+
"""
|
|
47
|
+
m0, y, x = m, 0, 1
|
|
48
|
+
if m == 1:
|
|
49
|
+
return 0
|
|
50
|
+
while a > 1:
|
|
51
|
+
q = a // m
|
|
52
|
+
m, a = a % m, m
|
|
53
|
+
y, x = x - q * y, y
|
|
54
|
+
if x < 0:
|
|
55
|
+
x += m0
|
|
56
|
+
return x
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_on_curve(
|
|
60
|
+
x: int, y: int, p: int = SECP256K1_P, a: int = SECP256K1_A, b: int = SECP256K1_B
|
|
61
|
+
) -> bool:
|
|
62
|
+
"""Check if point (x, y) is on the secp256k1 curve: y² = x³ + a*x + b"""
|
|
63
|
+
left_side = (y * y) % p
|
|
64
|
+
right_side = (x * x * x + a * x + b) % p
|
|
65
|
+
return left_side == right_side
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _point_add(
|
|
69
|
+
p1: Optional[Tuple[int, int]], p2: Optional[Tuple[int, int]], p: int = SECP256K1_P
|
|
70
|
+
) -> Optional[Tuple[int, int]]:
|
|
71
|
+
"""
|
|
72
|
+
Add two points on an elliptic curve over a prime field.
|
|
73
|
+
|
|
74
|
+
Both p1 and p2 are either None (the point at infinity) or 2-tuples (x, y) of integers
|
|
75
|
+
interpreted modulo p. Returns a 2-tuple (x, y) representing the sum (mod p), or None
|
|
76
|
+
for the point at infinity. Uses the short Weierstrass group law (handles point
|
|
77
|
+
doubling and addition, including the inverse/vertical-tangent cases).
|
|
78
|
+
|
|
79
|
+
Parameters:
|
|
80
|
+
p1 (tuple|None): First point as (x, y) or None for infinity.
|
|
81
|
+
p2 (tuple|None): Second point as (x, y) or None for infinity.
|
|
82
|
+
p (int): Prime modulus of the field (defaults to SECP256K1_P).
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
tuple|None: Resulting point (x, y) modulo p, or None if the result is the
|
|
86
|
+
point at infinity.
|
|
87
|
+
"""
|
|
88
|
+
if p1 is None:
|
|
89
|
+
return p2
|
|
90
|
+
if p2 is None:
|
|
91
|
+
return p1
|
|
92
|
+
|
|
93
|
+
x1, y1 = p1
|
|
94
|
+
x2, y2 = p2
|
|
95
|
+
|
|
96
|
+
# P + (-P) = O
|
|
97
|
+
if x1 == x2 and (y1 + y2) % p == 0:
|
|
98
|
+
return None # Point at infinity
|
|
99
|
+
|
|
100
|
+
if x1 == x2:
|
|
101
|
+
# Point doubling - for secp256k1: s = (3*x1^2) / (2*y1)
|
|
102
|
+
if y1 % p == 0:
|
|
103
|
+
return None # vertical tangent => infinity
|
|
104
|
+
numerator = (3 * x1 * x1) % p
|
|
105
|
+
denominator = (2 * y1) % p
|
|
106
|
+
s = (numerator * _mod_inverse(denominator, p)) % p
|
|
107
|
+
else:
|
|
108
|
+
# Point addition
|
|
109
|
+
denom = (x2 - x1) % p
|
|
110
|
+
s = ((y2 - y1) % p) * _mod_inverse(denom, p) % p
|
|
111
|
+
|
|
112
|
+
x3 = (s * s - x1 - x2) % p
|
|
113
|
+
y3 = (s * (x1 - x3) - y1) % p
|
|
114
|
+
|
|
115
|
+
return (x3, y3)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _scalar_mult(
|
|
119
|
+
k: int, point: Optional[Tuple[int, int]], p: int = SECP256K1_P
|
|
120
|
+
) -> Optional[Tuple[int, int]]:
|
|
121
|
+
"""
|
|
122
|
+
Compute k * point on the secp256k1 curve using the binary (double-and-add) method.
|
|
123
|
+
|
|
124
|
+
Parameters:
|
|
125
|
+
k (int): Non-negative integer scalar.
|
|
126
|
+
point (tuple|None): Elliptic-curve point as (x, y) or None to represent the point at infinity.
|
|
127
|
+
p (int, optional): Prime modulus of the field (defaults to SECP256K1_P).
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
tuple|None: The resulting point (x, y) after scalar multiplication, or None for the point at infinity.
|
|
131
|
+
"""
|
|
132
|
+
if k == 0:
|
|
133
|
+
return None # Point at infinity
|
|
134
|
+
if k == 1:
|
|
135
|
+
return point
|
|
136
|
+
|
|
137
|
+
result = None
|
|
138
|
+
current = point
|
|
139
|
+
|
|
140
|
+
while k > 0:
|
|
141
|
+
if k & 1:
|
|
142
|
+
result = _point_add(result, current, p)
|
|
143
|
+
current = _point_add(current, current, p)
|
|
144
|
+
k >>= 1
|
|
145
|
+
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _point_to_compressed(point: Tuple[int, int]) -> bytes:
|
|
150
|
+
"""
|
|
151
|
+
Return the 33-byte SEC compressed encoding of an EC point on secp256k1.
|
|
152
|
+
|
|
153
|
+
The input `point` must be a tuple (x, y) of integers representing coordinates on the curve.
|
|
154
|
+
The result is a 33-byte bytes object: a 1-byte prefix 0x02 (even y) or 0x03 (odd y)
|
|
155
|
+
followed by the 32-byte big-endian x coordinate.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
ValueError: If `point` is None (the point at infinity cannot be compressed).
|
|
159
|
+
"""
|
|
160
|
+
if point is None:
|
|
161
|
+
raise ValueError("Cannot compress point at infinity")
|
|
162
|
+
|
|
163
|
+
x, y = point
|
|
164
|
+
prefix = 0x02 if y % 2 == 0 else 0x03
|
|
165
|
+
return prefix.to_bytes(1, "big") + x.to_bytes(32, "big")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _compressed_to_point(compressed: bytes) -> Tuple[int, int]:
|
|
169
|
+
"""
|
|
170
|
+
Convert a 33-byte SEC compressed public key to an (x, y) point on the secp256k1 curve.
|
|
171
|
+
|
|
172
|
+
Parameters:
|
|
173
|
+
compressed (bytes): 33-byte compressed point (prefix 0x02 or 0x03 followed by 32-byte big-endian x).
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
tuple[int, int]: The affine coordinates (x, y) of the corresponding point.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
ValueError: If the input length is not 33 bytes, the prefix is not 0x02/0x03, or the recovered point is not on the secp256k1 curve.
|
|
180
|
+
"""
|
|
181
|
+
if len(compressed) != 33:
|
|
182
|
+
raise ValueError("Invalid compressed point length")
|
|
183
|
+
|
|
184
|
+
prefix = compressed[0]
|
|
185
|
+
x = int.from_bytes(compressed[1:], "big")
|
|
186
|
+
|
|
187
|
+
if prefix not in (0x02, 0x03):
|
|
188
|
+
raise ValueError("Invalid compressed point prefix")
|
|
189
|
+
|
|
190
|
+
# Calculate y from x using curve equation: y^2 = x^3 + a*x + b
|
|
191
|
+
y_squared = (x * x * x + SECP256K1_A * x + SECP256K1_B) % SECP256K1_P
|
|
192
|
+
|
|
193
|
+
# Find square root mod p
|
|
194
|
+
y = pow(y_squared, (SECP256K1_P + 1) // 4, SECP256K1_P)
|
|
195
|
+
|
|
196
|
+
# Choose the correct y based on parity
|
|
197
|
+
if (prefix == 0x02 and y % 2 != 0) or (prefix == 0x03 and y % 2 == 0):
|
|
198
|
+
y = SECP256K1_P - y
|
|
199
|
+
|
|
200
|
+
if not _is_on_curve(x, y):
|
|
201
|
+
raise ValueError("Point not on curve")
|
|
202
|
+
|
|
203
|
+
return (x, y)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# From <https://stackoverflow.com/questions/212358/binary-search-bisection-in-python/2233940#2233940>
|
|
207
|
+
def binary_search(a: List[Any], x: Any, lo: int = 0, hi: Optional[int] = None) -> int:
|
|
208
|
+
"""
|
|
209
|
+
Locate the index of x in sorted sequence a using binary search.
|
|
210
|
+
|
|
211
|
+
Performs a binary search on the sorted sequence `a` and returns the lowest index i in [lo, hi)
|
|
212
|
+
such that a[i] == x. If x is not present in that slice, returns -1.
|
|
213
|
+
|
|
214
|
+
Parameters:
|
|
215
|
+
a (Sequence): Sorted sequence (ascending) to search.
|
|
216
|
+
x: Value to locate.
|
|
217
|
+
lo (int, optional): Lower bound (inclusive) index to search from. Defaults to 0.
|
|
218
|
+
hi (int, optional): Upper bound (exclusive) index to search to. Defaults to len(a).
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
int: Index of the first matching element in [lo, hi), or -1 if not found.
|
|
222
|
+
"""
|
|
223
|
+
hi = hi if hi is not None else len(a) # hi defaults to len(a)
|
|
224
|
+
pos = bisect.bisect_left(a, x, lo, hi) # find insertion position
|
|
225
|
+
return pos if pos != hi and a[pos] == x else -1 # don't walk off the end
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class PasswordKey(Prefix):
|
|
229
|
+
"""This class derives a private key given the account name, the
|
|
230
|
+
role and a password. It leverages the technology of Brainkeys
|
|
231
|
+
and allows people to have a secure private key by providing a
|
|
232
|
+
passphrase only.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
def __init__(
|
|
236
|
+
self,
|
|
237
|
+
account: Optional[str],
|
|
238
|
+
password: str,
|
|
239
|
+
role: str = "active",
|
|
240
|
+
prefix: Optional[str] = None,
|
|
241
|
+
) -> None:
|
|
242
|
+
self.set_prefix(prefix)
|
|
243
|
+
self.account = account
|
|
244
|
+
self.role = role
|
|
245
|
+
self.password = password
|
|
246
|
+
|
|
247
|
+
def normalize(self, seed: str) -> str:
|
|
248
|
+
"""Correct formating with single whitespace syntax and no trailing space"""
|
|
249
|
+
return " ".join(re.compile("[\t\n\v\f\r ]+").split(seed))
|
|
250
|
+
|
|
251
|
+
def get_private(self) -> "PrivateKey":
|
|
252
|
+
"""Derive private key from the account, the role and the password"""
|
|
253
|
+
if self.account is None and self.role is None:
|
|
254
|
+
seed = self.password
|
|
255
|
+
elif self.account == "" and self.role == "":
|
|
256
|
+
seed = self.password
|
|
257
|
+
else:
|
|
258
|
+
seed = self.normalize(f"{self.account or ''}{self.role or ''}{self.password}")
|
|
259
|
+
return PrivateKey(
|
|
260
|
+
hexlify(hashlib.sha256(seed.encode()).digest()).decode("ascii"), prefix=self.prefix
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def get_public(self) -> "PublicKey":
|
|
264
|
+
return self.get_private().pubkey
|
|
265
|
+
|
|
266
|
+
def get_private_key(self) -> "PrivateKey":
|
|
267
|
+
return self.get_private()
|
|
268
|
+
|
|
269
|
+
def get_public_key(self) -> "PublicKey":
|
|
270
|
+
return self.get_public()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class BrainKey(Prefix):
|
|
274
|
+
"""Brainkey implementation similar to the graphene-ui web-wallet.
|
|
275
|
+
|
|
276
|
+
:param str brainkey: Brain Key
|
|
277
|
+
:param int sequence: Sequence number for consecutive keys
|
|
278
|
+
|
|
279
|
+
Keys in Graphene are derived from a seed brain key which is a string of
|
|
280
|
+
16 words out of a predefined dictionary with 49744 words. It is a
|
|
281
|
+
simple single-chain key derivation scheme that is not compatible with
|
|
282
|
+
BIP44 but easy to use.
|
|
283
|
+
|
|
284
|
+
Given the brain key, a private key is derived as::
|
|
285
|
+
|
|
286
|
+
privkey = SHA256(SHA512(brainkey + " " + sequence))
|
|
287
|
+
|
|
288
|
+
Incrementing the sequence number yields a new key that can be
|
|
289
|
+
regenerated given the brain key.
|
|
290
|
+
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
def __init__(
|
|
294
|
+
self, brainkey: Optional[str] = None, sequence: int = 0, prefix: Optional[str] = None
|
|
295
|
+
) -> None:
|
|
296
|
+
self.set_prefix(prefix)
|
|
297
|
+
if not brainkey:
|
|
298
|
+
self.brainkey = self.suggest()
|
|
299
|
+
else:
|
|
300
|
+
self.brainkey = self.normalize(brainkey).strip()
|
|
301
|
+
self.sequence = sequence
|
|
302
|
+
|
|
303
|
+
def __next__(self) -> "BrainKey":
|
|
304
|
+
"""Get the next private key (sequence number increment) for
|
|
305
|
+
iterators
|
|
306
|
+
"""
|
|
307
|
+
return self.next_sequence()
|
|
308
|
+
|
|
309
|
+
def next_sequence(self) -> "BrainKey":
|
|
310
|
+
"""Increment the sequence number by 1"""
|
|
311
|
+
self.sequence += 1
|
|
312
|
+
return self
|
|
313
|
+
|
|
314
|
+
def normalize(self, brainkey: str) -> str:
|
|
315
|
+
"""Correct formating with single whitespace syntax and no trailing space"""
|
|
316
|
+
return " ".join(re.compile("[\t\n\v\f\r ]+").split(brainkey))
|
|
317
|
+
|
|
318
|
+
def get_brainkey(self) -> str:
|
|
319
|
+
"""Return brain key of this instance"""
|
|
320
|
+
return self.normalize(self.brainkey)
|
|
321
|
+
|
|
322
|
+
def get_private(self) -> "PrivateKey":
|
|
323
|
+
"""Derive private key from the brain key and the current sequence
|
|
324
|
+
number
|
|
325
|
+
"""
|
|
326
|
+
encoded = "%s %d" % (self.brainkey, self.sequence)
|
|
327
|
+
a = bytes(encoded, "ascii")
|
|
328
|
+
s = hashlib.sha256(hashlib.sha512(a).digest()).digest()
|
|
329
|
+
return PrivateKey(hexlify(s).decode("ascii"), prefix=self.prefix)
|
|
330
|
+
|
|
331
|
+
def get_blind_private(self) -> "PrivateKey":
|
|
332
|
+
"""Derive private key from the brain key (and no sequence number)"""
|
|
333
|
+
a = bytes(self.brainkey, "ascii")
|
|
334
|
+
return PrivateKey(hashlib.sha256(a).hexdigest(), prefix=self.prefix)
|
|
335
|
+
|
|
336
|
+
def get_public(self) -> "PublicKey":
|
|
337
|
+
return self.get_private().pubkey
|
|
338
|
+
|
|
339
|
+
def get_private_key(self) -> "PrivateKey":
|
|
340
|
+
return self.get_private()
|
|
341
|
+
|
|
342
|
+
def get_public_key(self) -> "PublicKey":
|
|
343
|
+
return self.get_public()
|
|
344
|
+
|
|
345
|
+
def suggest(self, word_count: int = 16) -> str:
|
|
346
|
+
"""Suggest a new random brain key. Randomness is provided by the
|
|
347
|
+
operating system using ``os.urandom()``.
|
|
348
|
+
"""
|
|
349
|
+
brainkey: List[str] = [""] * word_count
|
|
350
|
+
dict_lines = BrainKeyDictionary.split(",")
|
|
351
|
+
if not len(dict_lines) == 49744:
|
|
352
|
+
raise AssertionError()
|
|
353
|
+
for j in range(0, word_count):
|
|
354
|
+
urand = os.urandom(2)
|
|
355
|
+
num = int.from_bytes(urand, byteorder="little")
|
|
356
|
+
rndMult = num / 2**16 # returns float between 0..1 (inclusive)
|
|
357
|
+
wIdx = int(round(len(dict_lines) * rndMult))
|
|
358
|
+
brainkey[j] = dict_lines[wIdx]
|
|
359
|
+
return " ".join(brainkey).upper()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# From https://github.com/trezor/python-mnemonic/blob/master/mnemonic/mnemonic.py
|
|
363
|
+
#
|
|
364
|
+
# Copyright (c) 2013 Pavol Rusnak
|
|
365
|
+
# Copyright (c) 2017 mruddy
|
|
366
|
+
class Mnemonic:
|
|
367
|
+
"""BIP39 mnemoric implementation"""
|
|
368
|
+
|
|
369
|
+
def __init__(self) -> None:
|
|
370
|
+
self.wordlist = MnemonicDictionary.split(",")
|
|
371
|
+
self.radix = 2048
|
|
372
|
+
|
|
373
|
+
def generate(self, strength: int = 128) -> str:
|
|
374
|
+
"""Generates a word list based on the given strength
|
|
375
|
+
|
|
376
|
+
:param int strength: initial entropy strength, must be one of [128, 160, 192, 224, 256]
|
|
377
|
+
|
|
378
|
+
"""
|
|
379
|
+
if strength not in [128, 160, 192, 224, 256]:
|
|
380
|
+
raise ValueError(
|
|
381
|
+
"Strength should be one of the following: [128, 160, 192, 224, 256], but it is not (%d)."
|
|
382
|
+
% strength
|
|
383
|
+
)
|
|
384
|
+
return self.to_mnemonic(os.urandom(strength // 8))
|
|
385
|
+
|
|
386
|
+
# Adapted from <http://tinyurl.com/oxmn476>
|
|
387
|
+
def to_entropy(self, words: Union[str, List[str]]) -> bytes:
|
|
388
|
+
if not isinstance(words, list):
|
|
389
|
+
words = words.split(" ")
|
|
390
|
+
if len(words) not in [12, 15, 18, 21, 24]:
|
|
391
|
+
raise ValueError(
|
|
392
|
+
"Number of words must be one of the following: [12, 15, 18, 21, 24], but it is not (%d)."
|
|
393
|
+
% len(words)
|
|
394
|
+
)
|
|
395
|
+
# Look up all the words in the list and construct the
|
|
396
|
+
# concatenation of the original entropy and the checksum.
|
|
397
|
+
concatLenBits = len(words) * 11
|
|
398
|
+
concatBits = [False] * concatLenBits
|
|
399
|
+
wordindex = 0
|
|
400
|
+
use_binary_search = True
|
|
401
|
+
for word in words:
|
|
402
|
+
# Find the words index in the wordlist
|
|
403
|
+
ndx = (
|
|
404
|
+
binary_search(self.wordlist, word)
|
|
405
|
+
if use_binary_search
|
|
406
|
+
else self.wordlist.index(word)
|
|
407
|
+
)
|
|
408
|
+
if ndx < 0:
|
|
409
|
+
raise LookupError('Unable to find "%s" in word list.' % word)
|
|
410
|
+
# Set the next 11 bits to the value of the index.
|
|
411
|
+
for ii in range(11):
|
|
412
|
+
concatBits[(wordindex * 11) + ii] = (ndx & (1 << (10 - ii))) != 0
|
|
413
|
+
wordindex += 1
|
|
414
|
+
checksumLengthBits = concatLenBits // 33
|
|
415
|
+
entropyLengthBits = concatLenBits - checksumLengthBits
|
|
416
|
+
# Extract original entropy as bytes.
|
|
417
|
+
entropy = bytearray(entropyLengthBits // 8)
|
|
418
|
+
for ii in range(len(entropy)):
|
|
419
|
+
for jj in range(8):
|
|
420
|
+
if concatBits[(ii * 8) + jj]:
|
|
421
|
+
entropy[ii] |= 1 << (7 - jj)
|
|
422
|
+
# Take the digest of the entropy.
|
|
423
|
+
hashBytes = hashlib.sha256(entropy).digest()
|
|
424
|
+
hashBits = list(
|
|
425
|
+
itertools.chain.from_iterable(
|
|
426
|
+
[c & (1 << (7 - i)) != 0 for i in range(8)] for c in hashBytes
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
# Check all the checksum bits.
|
|
430
|
+
for i in range(checksumLengthBits):
|
|
431
|
+
if concatBits[entropyLengthBits + i] != hashBits[i]:
|
|
432
|
+
raise ValueError("Failed checksum.")
|
|
433
|
+
return bytes(entropy)
|
|
434
|
+
|
|
435
|
+
def to_mnemonic(self, data: bytes) -> str:
|
|
436
|
+
if len(data) not in [16, 20, 24, 28, 32]:
|
|
437
|
+
raise ValueError(
|
|
438
|
+
"Data length should be one of the following: [16, 20, 24, 28, 32], but it is not (%d)."
|
|
439
|
+
% len(data)
|
|
440
|
+
)
|
|
441
|
+
h = hashlib.sha256(data).hexdigest()
|
|
442
|
+
b = (
|
|
443
|
+
bin(int(binascii.hexlify(data), 16))[2:].zfill(len(data) * 8)
|
|
444
|
+
+ bin(int(h, 16))[2:].zfill(256)[: len(data) * 8 // 32]
|
|
445
|
+
)
|
|
446
|
+
result = []
|
|
447
|
+
for i in range(len(b) // 11):
|
|
448
|
+
idx = int(b[i * 11 : (i + 1) * 11], 2)
|
|
449
|
+
result.append(self.wordlist[idx])
|
|
450
|
+
|
|
451
|
+
result_phrase = " ".join(result)
|
|
452
|
+
return result_phrase
|
|
453
|
+
|
|
454
|
+
def check(self, mnemonic: Union[str, List[str]]) -> bool:
|
|
455
|
+
"""Checks the mnemonic word list is valid
|
|
456
|
+
:param list mnemonic: mnemonic word list with lenght of 12, 15, 18, 21, 24
|
|
457
|
+
:returns: True, when valid
|
|
458
|
+
"""
|
|
459
|
+
mnemonic_str = " ".join(mnemonic) if isinstance(mnemonic, list) else mnemonic
|
|
460
|
+
mnemonic = self.normalize_string(mnemonic_str).split(" ")
|
|
461
|
+
# list of valid mnemonic lengths
|
|
462
|
+
if len(mnemonic) not in [12, 15, 18, 21, 24]:
|
|
463
|
+
return False
|
|
464
|
+
try:
|
|
465
|
+
idx = map(lambda x: bin(self.wordlist.index(x))[2:].zfill(11), mnemonic)
|
|
466
|
+
b = "".join(idx)
|
|
467
|
+
except ValueError:
|
|
468
|
+
return False
|
|
469
|
+
l = len(b) # noqa: E741
|
|
470
|
+
d = b[: l // 33 * 32]
|
|
471
|
+
h = b[-l // 33 :]
|
|
472
|
+
nd = binascii.unhexlify(hex(int(d, 2))[2:].rstrip("L").zfill(l // 33 * 8))
|
|
473
|
+
nh = bin(int(hashlib.sha256(nd).hexdigest(), 16))[2:].zfill(256)[: l // 33]
|
|
474
|
+
return h == nh
|
|
475
|
+
|
|
476
|
+
def check_word(self, word: str) -> bool:
|
|
477
|
+
return word in self.wordlist
|
|
478
|
+
|
|
479
|
+
def expand_word(self, prefix: str) -> str:
|
|
480
|
+
"""Expands a word when sufficient chars are given
|
|
481
|
+
|
|
482
|
+
:param str prefix: first chars of a valid dict word
|
|
483
|
+
|
|
484
|
+
"""
|
|
485
|
+
if prefix in self.wordlist:
|
|
486
|
+
return prefix
|
|
487
|
+
else:
|
|
488
|
+
matches = [word for word in self.wordlist if word.startswith(prefix)]
|
|
489
|
+
if len(matches) == 1: # matched exactly one word in the wordlist
|
|
490
|
+
return matches[0]
|
|
491
|
+
else:
|
|
492
|
+
# exact match not found.
|
|
493
|
+
# this is not a validation routine, just return the input
|
|
494
|
+
return prefix
|
|
495
|
+
|
|
496
|
+
def expand(self, mnemonic: str) -> str:
|
|
497
|
+
"""Expands all words given in a list"""
|
|
498
|
+
return " ".join(map(self.expand_word, mnemonic.split(" ")))
|
|
499
|
+
|
|
500
|
+
@classmethod
|
|
501
|
+
def normalize_string(cls, txt: str) -> str:
|
|
502
|
+
"""Normalizes strings"""
|
|
503
|
+
return unicodedata.normalize("NFKD", txt)
|
|
504
|
+
|
|
505
|
+
@classmethod
|
|
506
|
+
def to_seed(cls, mnemonic: Union[str, List[str]], passphrase: str = "") -> bytes:
|
|
507
|
+
"""Returns a seed based on bip39
|
|
508
|
+
|
|
509
|
+
:param str mnemonic: string containing a valid mnemonic word list
|
|
510
|
+
:param str passphrase: optional, passphrase can be set to modify the returned seed.
|
|
511
|
+
|
|
512
|
+
"""
|
|
513
|
+
mnemonic = cls.normalize_string(mnemonic)
|
|
514
|
+
passphrase = cls.normalize_string(passphrase)
|
|
515
|
+
passphrase = "mnemonic" + passphrase
|
|
516
|
+
mnemonic = mnemonic.encode("utf-8")
|
|
517
|
+
passphrase = passphrase.encode("utf-8")
|
|
518
|
+
stretched = hashlib.pbkdf2_hmac("sha512", mnemonic, passphrase, PBKDF2_ROUNDS)
|
|
519
|
+
return stretched[:64]
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class MnemonicKey(Prefix):
|
|
523
|
+
"""This class derives a private key from a BIP39 mnemoric implementation"""
|
|
524
|
+
|
|
525
|
+
def __init__(
|
|
526
|
+
self,
|
|
527
|
+
word_list: Optional[Union[str, List[str]]] = None,
|
|
528
|
+
passphrase: str = "",
|
|
529
|
+
account_sequence: int = 0,
|
|
530
|
+
key_sequence: int = 0,
|
|
531
|
+
prefix: Optional[str] = None,
|
|
532
|
+
) -> None:
|
|
533
|
+
self.set_prefix(prefix)
|
|
534
|
+
if word_list is not None:
|
|
535
|
+
self.set_mnemonic(word_list, passphrase=passphrase)
|
|
536
|
+
else:
|
|
537
|
+
self.seed = None
|
|
538
|
+
self.account_sequence = account_sequence
|
|
539
|
+
self.key_sequence = key_sequence
|
|
540
|
+
self.prefix = prefix or ""
|
|
541
|
+
self.path = "m/48'/13'/0'/%d'/%d'" % (self.account_sequence, self.key_sequence)
|
|
542
|
+
|
|
543
|
+
def set_mnemonic(self, word_list: Union[str, List[str]], passphrase: str = "") -> None:
|
|
544
|
+
mnemonic = Mnemonic()
|
|
545
|
+
if not mnemonic.check(word_list):
|
|
546
|
+
raise ValueError("Word list is not valid!")
|
|
547
|
+
self.seed = mnemonic.to_seed(word_list, passphrase=passphrase)
|
|
548
|
+
|
|
549
|
+
def generate_mnemonic(self, passphrase: str = "", strength: int = 256) -> str:
|
|
550
|
+
mnemonic = Mnemonic()
|
|
551
|
+
word_list = mnemonic.generate(strength=strength)
|
|
552
|
+
self.seed = mnemonic.to_seed(word_list, passphrase=passphrase)
|
|
553
|
+
return word_list
|
|
554
|
+
|
|
555
|
+
def set_path_BIP32(self, path: str) -> None:
|
|
556
|
+
self.path = path
|
|
557
|
+
|
|
558
|
+
def set_path_BIP44(
|
|
559
|
+
self,
|
|
560
|
+
account_sequence: int = 0,
|
|
561
|
+
chain_sequence: int = 0,
|
|
562
|
+
key_sequence: int = 0,
|
|
563
|
+
hardened_address: bool = True,
|
|
564
|
+
) -> None:
|
|
565
|
+
if account_sequence < 0:
|
|
566
|
+
raise ValueError("account_sequence must be >= 0")
|
|
567
|
+
if key_sequence < 0:
|
|
568
|
+
raise ValueError("key_sequence must be >= 0")
|
|
569
|
+
if chain_sequence < 0:
|
|
570
|
+
raise ValueError("chain_sequence must be >= 0")
|
|
571
|
+
self.account_sequence = account_sequence
|
|
572
|
+
self.key_sequence = key_sequence
|
|
573
|
+
if hardened_address:
|
|
574
|
+
self.path = "m/44'/0'/%d'/%d/%d'" % (
|
|
575
|
+
self.account_sequence,
|
|
576
|
+
chain_sequence,
|
|
577
|
+
self.key_sequence,
|
|
578
|
+
)
|
|
579
|
+
else:
|
|
580
|
+
self.path = "m/44'/0'/%d'/%d/%d" % (
|
|
581
|
+
self.account_sequence,
|
|
582
|
+
chain_sequence,
|
|
583
|
+
self.key_sequence,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
def set_path_BIP48(
|
|
587
|
+
self,
|
|
588
|
+
network_index: int = 13,
|
|
589
|
+
role: Union[str, int] = "owner",
|
|
590
|
+
account_sequence: int = 0,
|
|
591
|
+
key_sequence: int = 0,
|
|
592
|
+
) -> None:
|
|
593
|
+
if account_sequence < 0:
|
|
594
|
+
raise ValueError("account_sequence must be >= 0")
|
|
595
|
+
if key_sequence < 0:
|
|
596
|
+
raise ValueError("key_sequence must be >= 0")
|
|
597
|
+
if network_index < 0:
|
|
598
|
+
raise ValueError("network_index must be >= 0")
|
|
599
|
+
if isinstance(role, str) and role not in ["owner", "active", "posting", "memo"]:
|
|
600
|
+
raise ValueError("Wrong role!")
|
|
601
|
+
elif isinstance(role, int) and role < 0:
|
|
602
|
+
raise ValueError("role must be >= 0")
|
|
603
|
+
if role == "owner":
|
|
604
|
+
role = 0
|
|
605
|
+
elif role == "active":
|
|
606
|
+
role = 1
|
|
607
|
+
elif role == "posting":
|
|
608
|
+
role = 4
|
|
609
|
+
elif role == "memo":
|
|
610
|
+
role = 3
|
|
611
|
+
|
|
612
|
+
self.account_sequence = account_sequence
|
|
613
|
+
self.key_sequence = key_sequence
|
|
614
|
+
self.path = "m/48'/%d'/%d'/%d'/%d'" % (
|
|
615
|
+
network_index,
|
|
616
|
+
role,
|
|
617
|
+
self.account_sequence,
|
|
618
|
+
self.key_sequence,
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
def next_account_sequence(self) -> "MnemonicKey":
|
|
622
|
+
"""Increment the account sequence number by 1"""
|
|
623
|
+
self.account_sequence += 1
|
|
624
|
+
return self
|
|
625
|
+
|
|
626
|
+
def next_sequence(self) -> "MnemonicKey":
|
|
627
|
+
"""Increment the key sequence number by 1"""
|
|
628
|
+
self.key_sequence += 1
|
|
629
|
+
return self
|
|
630
|
+
|
|
631
|
+
def set_path(self, path: str) -> None:
|
|
632
|
+
self.path = path
|
|
633
|
+
|
|
634
|
+
def get_path(self) -> str:
|
|
635
|
+
return self.path
|
|
636
|
+
|
|
637
|
+
def get_private(self) -> "PrivateKey":
|
|
638
|
+
"""Derive private key from the account_sequence, the role and the key_sequence"""
|
|
639
|
+
if self.seed is None:
|
|
640
|
+
raise ValueError("seed is None, set or generate a mnemonic first")
|
|
641
|
+
key = BIP32Key.fromEntropy(self.seed)
|
|
642
|
+
path_result = parse_path(self.get_path(), as_bytes=False)
|
|
643
|
+
for n in path_result:
|
|
644
|
+
key = key.ChildKey(n)
|
|
645
|
+
if key is None:
|
|
646
|
+
raise ValueError(f"Failed to derive child key for path index: {n}")
|
|
647
|
+
|
|
648
|
+
return PrivateKey(key.WalletImportFormat(), prefix=self.prefix)
|
|
649
|
+
|
|
650
|
+
def get_public(self) -> "PublicKey":
|
|
651
|
+
return self.get_private().pubkey
|
|
652
|
+
|
|
653
|
+
def get_private_key(self) -> "PrivateKey":
|
|
654
|
+
return self.get_private()
|
|
655
|
+
|
|
656
|
+
def get_public_key(self) -> "PublicKey":
|
|
657
|
+
return self.get_public()
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
class Address(Prefix):
|
|
661
|
+
"""Address class
|
|
662
|
+
|
|
663
|
+
This class serves as an address representation for Public Keys.
|
|
664
|
+
|
|
665
|
+
:param str address: Base58 encoded address (defaults to ``None``)
|
|
666
|
+
:param str prefix: Network prefix (defaults to ``STM``)
|
|
667
|
+
|
|
668
|
+
Example::
|
|
669
|
+
|
|
670
|
+
Address("STMFN9r6VYzBK8EKtMewfNbfiGCr56pHDBFi")
|
|
671
|
+
|
|
672
|
+
"""
|
|
673
|
+
|
|
674
|
+
def __init__(self, address: str, prefix: Optional[str] = None) -> None:
|
|
675
|
+
self.set_prefix(prefix)
|
|
676
|
+
self._address = Base58(address, prefix=self.prefix)
|
|
677
|
+
|
|
678
|
+
@classmethod
|
|
679
|
+
def from_pubkey(
|
|
680
|
+
cls,
|
|
681
|
+
pubkey: Union[str, "PublicKey"],
|
|
682
|
+
compressed: bool = True,
|
|
683
|
+
version: int = 56,
|
|
684
|
+
prefix: Optional[str] = None,
|
|
685
|
+
) -> "Address":
|
|
686
|
+
"""Load an address provided by the public key.
|
|
687
|
+
Version: 56 => PTS
|
|
688
|
+
"""
|
|
689
|
+
# Ensure this is a public key
|
|
690
|
+
pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix)
|
|
691
|
+
if compressed:
|
|
692
|
+
pubkey_plain = pubkey.compressed()
|
|
693
|
+
else:
|
|
694
|
+
pubkey_plain = pubkey.uncompressed()
|
|
695
|
+
sha = hashlib.sha256(unhexlify(pubkey_plain)).hexdigest()
|
|
696
|
+
rep = hexlify(ripemd160(sha)).decode("ascii")
|
|
697
|
+
s = ("%.2x" % version) + rep
|
|
698
|
+
result = s + hexlify(doublesha256(s)[:4]).decode("ascii")
|
|
699
|
+
result = hexlify(ripemd160(result)).decode("ascii")
|
|
700
|
+
return cls(result, prefix=pubkey.prefix)
|
|
701
|
+
|
|
702
|
+
@classmethod
|
|
703
|
+
def derivesha256address(
|
|
704
|
+
cls, pubkey: Union[str, "PublicKey"], compressed: bool = True, prefix: Optional[str] = None
|
|
705
|
+
) -> "Address":
|
|
706
|
+
"""Derive address using ``RIPEMD160(SHA256(x))``"""
|
|
707
|
+
pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix)
|
|
708
|
+
if compressed:
|
|
709
|
+
pubkey_plain = pubkey.compressed()
|
|
710
|
+
else:
|
|
711
|
+
pubkey_plain = pubkey.uncompressed()
|
|
712
|
+
pkbin = unhexlify(repr(pubkey_plain))
|
|
713
|
+
result = hexlify(hashlib.sha256(pkbin).digest())
|
|
714
|
+
result = hexlify(ripemd160(result)).decode("ascii")
|
|
715
|
+
return cls(result, prefix=pubkey.prefix)
|
|
716
|
+
|
|
717
|
+
@classmethod
|
|
718
|
+
def derivesha512address(
|
|
719
|
+
cls, pubkey: Union[str, "PublicKey"], compressed: bool = True, prefix: Optional[str] = None
|
|
720
|
+
) -> "Address":
|
|
721
|
+
"""Derive address using ``RIPEMD160(SHA512(x))``"""
|
|
722
|
+
pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix)
|
|
723
|
+
if compressed:
|
|
724
|
+
pubkey_plain = pubkey.compressed()
|
|
725
|
+
else:
|
|
726
|
+
pubkey_plain = pubkey.uncompressed()
|
|
727
|
+
pkbin = unhexlify(repr(pubkey_plain))
|
|
728
|
+
result = hexlify(hashlib.sha512(pkbin).digest())
|
|
729
|
+
result = hexlify(ripemd160(result)).decode("ascii")
|
|
730
|
+
return cls(result, prefix=pubkey.prefix)
|
|
731
|
+
|
|
732
|
+
def __repr__(self) -> str:
|
|
733
|
+
"""Gives the hex representation of the ``GrapheneBase58CheckEncoded``
|
|
734
|
+
Graphene address.
|
|
735
|
+
"""
|
|
736
|
+
return repr(self._address)
|
|
737
|
+
|
|
738
|
+
def __str__(self) -> str:
|
|
739
|
+
"""Returns the readable Graphene address. This call is equivalent to
|
|
740
|
+
``format(Address, "STM")``
|
|
741
|
+
"""
|
|
742
|
+
return format(self._address, self.prefix)
|
|
743
|
+
|
|
744
|
+
def __format__(self, _format: str) -> str:
|
|
745
|
+
"""May be issued to get valid "MUSE", "PLAY" or any other Graphene compatible
|
|
746
|
+
address with corresponding prefix.
|
|
747
|
+
"""
|
|
748
|
+
return format(self._address, _format)
|
|
749
|
+
|
|
750
|
+
def __bytes__(self) -> bytes:
|
|
751
|
+
"""Returns the raw content of the ``Base58CheckEncoded`` address"""
|
|
752
|
+
return bytes(self._address)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
class GrapheneAddress(Address):
|
|
756
|
+
"""Graphene Addresses are different. Hence we have a different class"""
|
|
757
|
+
|
|
758
|
+
@classmethod
|
|
759
|
+
def from_pubkey(
|
|
760
|
+
cls,
|
|
761
|
+
pubkey: Union[str, "PublicKey"],
|
|
762
|
+
compressed: bool = True,
|
|
763
|
+
prefix: Optional[str] = None,
|
|
764
|
+
) -> "GrapheneAddress":
|
|
765
|
+
# Ensure this is a public key
|
|
766
|
+
pubkey = PublicKey(pubkey, prefix=prefix or Prefix.prefix)
|
|
767
|
+
if compressed:
|
|
768
|
+
pubkey_plain = pubkey.compressed()
|
|
769
|
+
else:
|
|
770
|
+
pubkey_plain = pubkey.uncompressed()
|
|
771
|
+
|
|
772
|
+
""" Derive address using ``RIPEMD160(SHA512(x))`` """
|
|
773
|
+
addressbin = ripemd160(hashlib.sha512(unhexlify(pubkey_plain)).hexdigest())
|
|
774
|
+
result = Base58(hexlify(addressbin).decode("ascii"))
|
|
775
|
+
return cls(result, prefix=pubkey.prefix)
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
class PublicKey(Prefix):
|
|
779
|
+
"""This class deals with Public Keys and inherits ``Address``.
|
|
780
|
+
|
|
781
|
+
:param str pk: Base58 encoded public key
|
|
782
|
+
:param str prefix: Network prefix (defaults to ``STM``)
|
|
783
|
+
|
|
784
|
+
Example::
|
|
785
|
+
|
|
786
|
+
PublicKey("STM6UtYWWs3rkZGV8JA86qrgkG6tyFksgECefKE1MiH4HkLD8PFGL")
|
|
787
|
+
|
|
788
|
+
.. note:: By default, graphene-based networks deal with **compressed**
|
|
789
|
+
public keys. If an **uncompressed** key is required, the
|
|
790
|
+
method :func:`unCompressed` can be used::
|
|
791
|
+
|
|
792
|
+
PublicKey("xxxxx").unCompressed()
|
|
793
|
+
|
|
794
|
+
"""
|
|
795
|
+
|
|
796
|
+
def __init__(self, pk: Union[str, "PublicKey"], prefix: Optional[str] = None) -> None:
|
|
797
|
+
"""Init PublicKey
|
|
798
|
+
:param str pk: Base58 encoded public key
|
|
799
|
+
:param str prefix: Network prefix (defaults to ``STM``)
|
|
800
|
+
"""
|
|
801
|
+
self.set_prefix(prefix)
|
|
802
|
+
if isinstance(pk, PublicKey):
|
|
803
|
+
pk = format(pk, self.prefix)
|
|
804
|
+
|
|
805
|
+
if str(pk).startswith("04"):
|
|
806
|
+
# We only ever deal with compressed keys, so let's make it
|
|
807
|
+
# compressed
|
|
808
|
+
order = ecdsa.SECP256k1.order
|
|
809
|
+
p = ecdsa.VerifyingKey.from_string(
|
|
810
|
+
unhexlify(pk[2:]), curve=ecdsa.SECP256k1
|
|
811
|
+
).pubkey.point
|
|
812
|
+
x_str = number_to_string(int(p.x()), order)
|
|
813
|
+
pk = hexlify(chr(2 + (int(p.y()) & 1)).encode("ascii") + x_str).decode("ascii")
|
|
814
|
+
|
|
815
|
+
self._pk = Base58(pk, prefix=self.prefix)
|
|
816
|
+
|
|
817
|
+
@property
|
|
818
|
+
def pubkey(self) -> str:
|
|
819
|
+
return repr(self._pk)
|
|
820
|
+
|
|
821
|
+
def get_public_key(self) -> str:
|
|
822
|
+
"""Returns the pubkey"""
|
|
823
|
+
return self.pubkey
|
|
824
|
+
|
|
825
|
+
@property
|
|
826
|
+
def compressed_key(self) -> "PublicKey":
|
|
827
|
+
return PublicKey(self.compressed())
|
|
828
|
+
|
|
829
|
+
def __str__(self) -> str:
|
|
830
|
+
"""Return the string representation of the public key"""
|
|
831
|
+
return self.prefix + str(self._pk)
|
|
832
|
+
|
|
833
|
+
def __repr__(self) -> str:
|
|
834
|
+
"""Return the string representation of the public key"""
|
|
835
|
+
return str(self)
|
|
836
|
+
|
|
837
|
+
def __format__(self, _format: str) -> str:
|
|
838
|
+
"""Format the public key with the given prefix"""
|
|
839
|
+
return _format + str(self._pk)
|
|
840
|
+
|
|
841
|
+
def __bytes__(self) -> bytes:
|
|
842
|
+
"""Return the bytes representation of the public key"""
|
|
843
|
+
return bytes(self._pk)
|
|
844
|
+
|
|
845
|
+
def _derive_y_from_x(self, x: int, is_even: bool) -> int:
|
|
846
|
+
"""Derive y point from x point"""
|
|
847
|
+
curve = ecdsa.SECP256k1.curve
|
|
848
|
+
# The curve equation over F_p is:
|
|
849
|
+
# y^2 = x^3 + ax + b
|
|
850
|
+
a, b, p = curve.a(), curve.b(), curve.p()
|
|
851
|
+
alpha = (pow(x, 3, p) + a * x + b) % p
|
|
852
|
+
beta = square_root_mod_prime(alpha, p)
|
|
853
|
+
if (beta % 2) == is_even:
|
|
854
|
+
beta = p - beta
|
|
855
|
+
return beta
|
|
856
|
+
|
|
857
|
+
def compressed(self) -> str:
|
|
858
|
+
"""Derive compressed public key"""
|
|
859
|
+
return repr(self._pk)
|
|
860
|
+
|
|
861
|
+
def uncompressed(self) -> str:
|
|
862
|
+
"""Derive uncompressed key"""
|
|
863
|
+
public_key = repr(self._pk)
|
|
864
|
+
prefix = public_key[0:2]
|
|
865
|
+
if prefix == "04":
|
|
866
|
+
return public_key
|
|
867
|
+
if not (prefix == "02" or prefix == "03"):
|
|
868
|
+
raise AssertionError()
|
|
869
|
+
x = int(public_key[2:], 16)
|
|
870
|
+
y = self._derive_y_from_x(x, (prefix == "02"))
|
|
871
|
+
key = "04" + "%064x" % x + "%064x" % y
|
|
872
|
+
return key
|
|
873
|
+
|
|
874
|
+
def point(self) -> Any:
|
|
875
|
+
"""Return the point for the public key"""
|
|
876
|
+
string = unhexlify(self.uncompressed())
|
|
877
|
+
return ecdsa.VerifyingKey.from_string(string[1:], curve=ecdsa.SECP256k1).pubkey.point
|
|
878
|
+
|
|
879
|
+
def child(self, offset256: bytes) -> "PublicKey":
|
|
880
|
+
"""Derive new public key from this key and a sha256 "offset" """
|
|
881
|
+
a = bytes(self) + offset256
|
|
882
|
+
s = hashlib.sha256(a).digest()
|
|
883
|
+
return self.add(s)
|
|
884
|
+
|
|
885
|
+
def add(self, digest256: bytes) -> "PublicKey":
|
|
886
|
+
"""
|
|
887
|
+
Return a new PublicKey obtained by adding a 32-byte tweak (interpreted as a big-endian scalar) times the curve generator to this public key.
|
|
888
|
+
|
|
889
|
+
Parameters:
|
|
890
|
+
digest256 (bytes): A 32-byte SHA-256 digest used as the tweak scalar (big-endian). Must be length 32, non-zero, and less than the curve order.
|
|
891
|
+
|
|
892
|
+
Returns:
|
|
893
|
+
PublicKey: A new PublicKey instance representing (tweak * G) + current_public_key, preserving this key's prefix.
|
|
894
|
+
|
|
895
|
+
Raises:
|
|
896
|
+
ValueError: If `digest256` is not bytes, not 32 bytes long, is zero, is >= curve order, or if intermediate point multiplication/addition results in the point at infinity.
|
|
897
|
+
"""
|
|
898
|
+
# Validate tweak
|
|
899
|
+
if not isinstance(digest256, (bytes, bytearray)):
|
|
900
|
+
raise ValueError("Tweak must be bytes")
|
|
901
|
+
if len(digest256) != 32:
|
|
902
|
+
raise ValueError("Tweak must be exactly 32 bytes")
|
|
903
|
+
|
|
904
|
+
tweak = int.from_bytes(digest256, "big")
|
|
905
|
+
if tweak == 0:
|
|
906
|
+
raise ValueError("Tweak cannot be zero")
|
|
907
|
+
if tweak >= SECP256K1_N:
|
|
908
|
+
raise ValueError("Tweak must be less than curve order")
|
|
909
|
+
|
|
910
|
+
# Convert current public key to point
|
|
911
|
+
current_compressed = bytes(self)
|
|
912
|
+
current_point = _compressed_to_point(current_compressed)
|
|
913
|
+
|
|
914
|
+
# Compute G*tweak (scalar multiplication of generator)
|
|
915
|
+
generator_point = (SECP256K1_GX, SECP256K1_GY)
|
|
916
|
+
tweak_point = _scalar_mult(tweak, generator_point)
|
|
917
|
+
|
|
918
|
+
if tweak_point is None:
|
|
919
|
+
raise ValueError("Tweak multiplication resulted in point at infinity")
|
|
920
|
+
|
|
921
|
+
# Add points: result = tweak_point + current_point
|
|
922
|
+
result_point = _point_add(tweak_point, current_point)
|
|
923
|
+
|
|
924
|
+
if result_point is None:
|
|
925
|
+
raise ValueError("Point addition resulted in point at infinity")
|
|
926
|
+
|
|
927
|
+
# Convert back to compressed format
|
|
928
|
+
result_compressed = _point_to_compressed(result_point)
|
|
929
|
+
|
|
930
|
+
# Create new PublicKey with same prefix
|
|
931
|
+
return PublicKey(hexlify(result_compressed).decode("ascii"), prefix=self.prefix)
|
|
932
|
+
|
|
933
|
+
@classmethod
|
|
934
|
+
def from_privkey(
|
|
935
|
+
cls, privkey: Union[str, "PrivateKey"], prefix: Optional[str] = None
|
|
936
|
+
) -> "PublicKey":
|
|
937
|
+
"""
|
|
938
|
+
Derive a compressed public key from a private key and return a PublicKey instance.
|
|
939
|
+
|
|
940
|
+
Parameters:
|
|
941
|
+
privkey: The private key material to derive from — accepts a WIF/hex string or a PrivateKey instance.
|
|
942
|
+
prefix (optional): Network/key prefix to use for the resulting PublicKey; if omitted the module default prefix is used.
|
|
943
|
+
|
|
944
|
+
Returns:
|
|
945
|
+
PublicKey: A PublicKey (compressed form) constructed from the derived public key bytes.
|
|
946
|
+
|
|
947
|
+
Raises:
|
|
948
|
+
ImportError: If the `ecdsa` library is not available.
|
|
949
|
+
"""
|
|
950
|
+
privkey = PrivateKey(privkey, prefix=prefix or Prefix.prefix)
|
|
951
|
+
secret = unhexlify(repr(privkey))
|
|
952
|
+
|
|
953
|
+
order = ecdsa.SigningKey.from_string(secret, curve=ecdsa.SECP256k1).curve.generator.order()
|
|
954
|
+
p = ecdsa.SigningKey.from_string(secret, curve=ecdsa.SECP256k1).verifying_key.pubkey.point
|
|
955
|
+
x_str = number_to_string(int(p.x()), order)
|
|
956
|
+
compressed = hexlify(chr(2 + (int(p.y()) & 1)).encode("ascii") + x_str).decode("ascii")
|
|
957
|
+
return cls(compressed, prefix=prefix or Prefix.prefix)
|
|
958
|
+
|
|
959
|
+
def unCompressed(self) -> str:
|
|
960
|
+
"""Alias for self.uncompressed() - LEGACY"""
|
|
961
|
+
return self.uncompressed()
|
|
962
|
+
|
|
963
|
+
@property
|
|
964
|
+
def address(self) -> GrapheneAddress:
|
|
965
|
+
"""Obtain a GrapheneAddress from a public key"""
|
|
966
|
+
return GrapheneAddress.from_pubkey(str(self), prefix=self.prefix)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
class PrivateKey(Prefix):
|
|
970
|
+
"""Derives the compressed and uncompressed public keys and
|
|
971
|
+
constructs two instances of :class:`PublicKey`:
|
|
972
|
+
|
|
973
|
+
:param str wif: Base58check-encoded wif key
|
|
974
|
+
:param str prefix: Network prefix (defaults to ``STM``)
|
|
975
|
+
|
|
976
|
+
Example::
|
|
977
|
+
|
|
978
|
+
PrivateKey("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd")
|
|
979
|
+
|
|
980
|
+
Compressed vs. Uncompressed:
|
|
981
|
+
|
|
982
|
+
* ``PrivateKey("w-i-f").pubkey``:
|
|
983
|
+
Instance of :class:`PublicKey` using compressed key.
|
|
984
|
+
* ``PrivateKey("w-i-f").pubkey.address``:
|
|
985
|
+
Instance of :class:`Address` using compressed key.
|
|
986
|
+
* ``PrivateKey("w-i-f").uncompressed``:
|
|
987
|
+
Instance of :class:`PublicKey` using uncompressed key.
|
|
988
|
+
* ``PrivateKey("w-i-f").uncompressed.address``:
|
|
989
|
+
Instance of :class:`Address` using uncompressed key.
|
|
990
|
+
|
|
991
|
+
"""
|
|
992
|
+
|
|
993
|
+
def __init__(
|
|
994
|
+
self, wif: Optional[Union[str, "PrivateKey", Base58]] = None, prefix: Optional[str] = None
|
|
995
|
+
) -> None:
|
|
996
|
+
self.set_prefix(prefix)
|
|
997
|
+
if wif is None:
|
|
998
|
+
import os
|
|
999
|
+
|
|
1000
|
+
self._wif = Base58(hexlify(os.urandom(32)).decode("ascii"))
|
|
1001
|
+
elif isinstance(wif, PrivateKey):
|
|
1002
|
+
self._wif = wif._wif
|
|
1003
|
+
elif isinstance(wif, Base58):
|
|
1004
|
+
self._wif = wif
|
|
1005
|
+
else:
|
|
1006
|
+
self._wif = Base58(wif)
|
|
1007
|
+
|
|
1008
|
+
assert len(repr(self._wif)) == 64
|
|
1009
|
+
|
|
1010
|
+
@property
|
|
1011
|
+
def bitcoin(self) -> PublicKey:
|
|
1012
|
+
return BitcoinPublicKey.from_privkey(self)
|
|
1013
|
+
|
|
1014
|
+
@property
|
|
1015
|
+
def address(self) -> Address:
|
|
1016
|
+
return Address.from_pubkey(self.pubkey, prefix=self.prefix)
|
|
1017
|
+
|
|
1018
|
+
@property
|
|
1019
|
+
def pubkey(self) -> PublicKey:
|
|
1020
|
+
return self.compressed
|
|
1021
|
+
|
|
1022
|
+
def get_public_key(self) -> PublicKey:
|
|
1023
|
+
"""Legacy: Returns the pubkey"""
|
|
1024
|
+
return self.pubkey
|
|
1025
|
+
|
|
1026
|
+
@property
|
|
1027
|
+
def compressed(self) -> PublicKey:
|
|
1028
|
+
return PublicKey.from_privkey(self, prefix=self.prefix)
|
|
1029
|
+
|
|
1030
|
+
@property
|
|
1031
|
+
def uncompressed(self) -> PublicKey:
|
|
1032
|
+
return PublicKey(self.pubkey.uncompressed(), prefix=self.prefix)
|
|
1033
|
+
|
|
1034
|
+
def get_secret(self) -> bytes:
|
|
1035
|
+
"""Get sha256 digest of the wif key."""
|
|
1036
|
+
return hashlib.sha256(bytes(self)).digest()
|
|
1037
|
+
|
|
1038
|
+
def derive_private_key(self, sequence: int) -> "PrivateKey":
|
|
1039
|
+
"""Derive new private key from this private key and an arbitrary
|
|
1040
|
+
sequence number
|
|
1041
|
+
"""
|
|
1042
|
+
encoded = "%s %d" % (str(self), sequence)
|
|
1043
|
+
a = bytes(encoded, "ascii")
|
|
1044
|
+
s = hashlib.sha256(hashlib.sha512(a).digest()).digest()
|
|
1045
|
+
return PrivateKey(hexlify(s).decode("ascii"), prefix=self.pubkey.prefix)
|
|
1046
|
+
|
|
1047
|
+
def child(self, offset256: bytes) -> "PrivateKey":
|
|
1048
|
+
"""Derive new private key from this key and a sha256 "offset" """
|
|
1049
|
+
a = bytes(self.pubkey) + offset256
|
|
1050
|
+
s = hashlib.sha256(a).digest()
|
|
1051
|
+
return self.derive_from_seed(s)
|
|
1052
|
+
|
|
1053
|
+
def derive_from_seed(self, offset: bytes) -> "PrivateKey":
|
|
1054
|
+
"""
|
|
1055
|
+
Derive a new PrivateKey by adding a 32-byte integer offset to this key's seed modulo the secp256k1 order.
|
|
1056
|
+
|
|
1057
|
+
Parameters:
|
|
1058
|
+
offset (bytes): A 32-byte SHA-256 digest interpreted as a big-endian integer offset to add to this key's secret.
|
|
1059
|
+
|
|
1060
|
+
Returns:
|
|
1061
|
+
PrivateKey: A new PrivateKey created from (seed + offset) mod SECP256K1_N, preserving this key's prefix.
|
|
1062
|
+
"""
|
|
1063
|
+
seed = int(hexlify(bytes(self)).decode("ascii"), 16)
|
|
1064
|
+
z = int(hexlify(offset).decode("ascii"), 16)
|
|
1065
|
+
order = SECP256K1_N
|
|
1066
|
+
secexp = (seed + z) % order
|
|
1067
|
+
secret = "%0x" % secexp
|
|
1068
|
+
if len(secret) < 64: # left-pad with zeroes
|
|
1069
|
+
secret = ("0" * (64 - len(secret))) + secret
|
|
1070
|
+
return PrivateKey(secret, prefix=self.pubkey.prefix)
|
|
1071
|
+
|
|
1072
|
+
def __format__(self, _format: str) -> str:
|
|
1073
|
+
"""Formats the instance of:doc:`Base58 <base58>` according to
|
|
1074
|
+
``_format``
|
|
1075
|
+
"""
|
|
1076
|
+
return format(self._wif, _format)
|
|
1077
|
+
|
|
1078
|
+
def __repr__(self) -> str:
|
|
1079
|
+
"""Gives the hex representation of the Graphene private key."""
|
|
1080
|
+
return repr(self._wif)
|
|
1081
|
+
|
|
1082
|
+
def __str__(self) -> str:
|
|
1083
|
+
"""Returns the readable (uncompressed wif format) Graphene private key. This
|
|
1084
|
+
call is equivalent to ``format(PrivateKey, "WIF")``
|
|
1085
|
+
"""
|
|
1086
|
+
return format(self._wif, "WIF")
|
|
1087
|
+
|
|
1088
|
+
def __bytes__(self) -> bytes:
|
|
1089
|
+
"""Returns the raw private key"""
|
|
1090
|
+
return bytes(self._wif)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
class BitcoinAddress(Address):
|
|
1094
|
+
@classmethod
|
|
1095
|
+
def from_pubkey(
|
|
1096
|
+
cls,
|
|
1097
|
+
pubkey: Union[str, PublicKey],
|
|
1098
|
+
compressed: bool = False,
|
|
1099
|
+
) -> "BitcoinAddress":
|
|
1100
|
+
# Ensure this is a public key
|
|
1101
|
+
pubkey = PublicKey(pubkey)
|
|
1102
|
+
if compressed:
|
|
1103
|
+
pubkey = pubkey.compressed()
|
|
1104
|
+
else:
|
|
1105
|
+
pubkey = pubkey.uncompressed()
|
|
1106
|
+
|
|
1107
|
+
""" Derive address using ``RIPEMD160(SHA256(x))`` """
|
|
1108
|
+
addressbin = ripemd160(hexlify(hashlib.sha256(unhexlify(pubkey)).digest()))
|
|
1109
|
+
return cls(hexlify(addressbin).decode("ascii"))
|
|
1110
|
+
|
|
1111
|
+
def __str__(self) -> str:
|
|
1112
|
+
"""Returns the readable Graphene address. This call is equivalent to
|
|
1113
|
+
``format(Address, "GPH")``
|
|
1114
|
+
"""
|
|
1115
|
+
return format(self._address, "BTC")
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
class BitcoinPublicKey(PublicKey):
|
|
1119
|
+
@property
|
|
1120
|
+
def address(self) -> BitcoinAddress:
|
|
1121
|
+
return BitcoinAddress.from_pubkey(repr(self))
|