tencoinlib 0.1.0__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.
- tencoinlib/__init__.py +68 -0
- tencoinlib/__version__.py +1 -0
- tencoinlib/constants.py +59 -0
- tencoinlib/keys/bip32.py +438 -0
- tencoinlib/keys/bip39.py +166 -0
- tencoinlib/keys/bip84.py +85 -0
- tencoinlib/keys/ec.py +148 -0
- tencoinlib/keys/wordlist.py +1 -0
- tencoinlib/rpc/__init__.py +15 -0
- tencoinlib/rpc/client.py +384 -0
- tencoinlib/rpc/exceptions.py +23 -0
- tencoinlib/transaction/__init__.py +54 -0
- tencoinlib/transaction/address.py +146 -0
- tencoinlib/transaction/builder.py +512 -0
- tencoinlib/transaction/core.py +270 -0
- tencoinlib/transaction/fee.py +105 -0
- tencoinlib/transaction/script.py +137 -0
- tencoinlib/transaction/signer.py +371 -0
- tencoinlib/utils.py +280 -0
- tencoinlib/wallet.py +569 -0
- tencoinlib-0.1.0.dist-info/METADATA +69 -0
- tencoinlib-0.1.0.dist-info/RECORD +24 -0
- tencoinlib-0.1.0.dist-info/WHEEL +5 -0
- tencoinlib-0.1.0.dist-info/top_level.txt +1 -0
tencoinlib/__init__.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# tencoin/tencoinlib/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
tencoinlib - Official Python library for Tencoin
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .constants import (
|
|
7
|
+
MAINNET_HRP,
|
|
8
|
+
TENOS_PER_TEC,
|
|
9
|
+
DUST_LIMIT,
|
|
10
|
+
DEFAULT_RPC_PORT,
|
|
11
|
+
COIN_TYPE,
|
|
12
|
+
DERIVATION_PATH,
|
|
13
|
+
DEFAULT_RPC_TOKEN
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .wallet import Wallet, WalletError
|
|
17
|
+
from .rpc import RPCClient, RPCError
|
|
18
|
+
from .transaction import (
|
|
19
|
+
Transaction, TxIn, TxOut, parse_transaction,
|
|
20
|
+
TransactionBuilder, TransactionBuilderError,
|
|
21
|
+
SegWitSigner, LegacySigner, TransactionSigner, SigningError,
|
|
22
|
+
FeeCalculator,
|
|
23
|
+
decode_address, address_to_script, is_valid_address,
|
|
24
|
+
get_address_type, AddressError
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Version
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
# Constants
|
|
32
|
+
"MAINNET_HRP",
|
|
33
|
+
"TENOS_PER_TEC",
|
|
34
|
+
"DUST_LIMIT",
|
|
35
|
+
"DEFAULT_RPC_PORT",
|
|
36
|
+
"DEFAULT_RPC_TOKEN",
|
|
37
|
+
"COIN_TYPE",
|
|
38
|
+
"DERIVATION_PATH",
|
|
39
|
+
|
|
40
|
+
# Wallet
|
|
41
|
+
"Wallet",
|
|
42
|
+
"WalletError",
|
|
43
|
+
|
|
44
|
+
# RPC
|
|
45
|
+
"RPCClient",
|
|
46
|
+
"RPCError",
|
|
47
|
+
|
|
48
|
+
# Transaction
|
|
49
|
+
"Transaction",
|
|
50
|
+
"TxIn",
|
|
51
|
+
"TxOut",
|
|
52
|
+
"parse_transaction",
|
|
53
|
+
"TransactionBuilder",
|
|
54
|
+
"TransactionBuilderError",
|
|
55
|
+
"SegWitSigner",
|
|
56
|
+
"LegacySigner",
|
|
57
|
+
"TransactionSigner",
|
|
58
|
+
"SigningError",
|
|
59
|
+
"FeeCalculator",
|
|
60
|
+
"decode_address",
|
|
61
|
+
"address_to_script",
|
|
62
|
+
"is_valid_address",
|
|
63
|
+
"get_address_type",
|
|
64
|
+
"AddressError",
|
|
65
|
+
|
|
66
|
+
# Version
|
|
67
|
+
"__version__",
|
|
68
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
tencoinlib/constants.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# tencoinlib/constants.py
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
MAINNET_HRP = "tc"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
COIN_TYPE = 5353
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# BIP purposes
|
|
11
|
+
BIP44_PURPOSE = 44 # Legacy P2PKH
|
|
12
|
+
BIP49_PURPOSE = 49 # Nested SegWit (reserved for future use)
|
|
13
|
+
BIP84_PURPOSE = 84 # Native SegWit (P2WPKH)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Default BIP84 derivation path for the primary SegWit address
|
|
17
|
+
DERIVATION_PATH = f"m/{BIP84_PURPOSE}'/{COIN_TYPE}'/0'/0/0"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
BIP39_ENTROPY_BITS = 128
|
|
21
|
+
BIP39_CHECKSUM_BITS = 4
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
P2PKH_VERSION = 0x41
|
|
25
|
+
P2SH_VERSION = 0x32
|
|
26
|
+
P2WPKH_VERSION = 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
TENOS_PER_TEC = 100_000_000
|
|
30
|
+
DUST_LIMIT = 546
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
DEFAULT_RPC_PORT = 10111
|
|
34
|
+
DEFAULT_P2P_PORT = 10110
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
DEFAULT_RPC_TOKEN = ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
DEFAULT_SEED_NODES = [
|
|
41
|
+
"seed.tencoin.org",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
DEFAULT_FEE_RATE = 10
|
|
46
|
+
PRIORITY_FEE_RATE = 20
|
|
47
|
+
ECONOMY_FEE_RATE = 5
|
|
48
|
+
MIN_RELAY_FEE = 1000
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
MAINNET_MAGIC = b'\x0a\x0d\x0e\x10'
|
|
52
|
+
TESTNET_MAGIC = b'\x0b\x11\x09\x07'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
OP_DUP = 0x76
|
|
56
|
+
OP_HASH160 = 0xa9
|
|
57
|
+
OP_EQUALVERIFY = 0x88
|
|
58
|
+
OP_CHECKSIG = 0xac
|
|
59
|
+
OP_EQUAL = 0x87
|
tencoinlib/keys/bip32.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import List, Tuple
|
|
4
|
+
|
|
5
|
+
from .ec import (
|
|
6
|
+
Gx,
|
|
7
|
+
Gy,
|
|
8
|
+
N,
|
|
9
|
+
point_add,
|
|
10
|
+
point_to_pubkey,
|
|
11
|
+
privkey_to_pubkey,
|
|
12
|
+
pubkey_to_point,
|
|
13
|
+
scalar_mult,
|
|
14
|
+
)
|
|
15
|
+
from ..utils import (
|
|
16
|
+
base58_decode,
|
|
17
|
+
base58_encode,
|
|
18
|
+
hash160,
|
|
19
|
+
hmac_sha512,
|
|
20
|
+
parse_256,
|
|
21
|
+
ser_32,
|
|
22
|
+
ser_256,
|
|
23
|
+
sha256d,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BIP32Error(Exception):
|
|
28
|
+
"""BIP-32 related errors"""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# BIP-32 constants
|
|
34
|
+
HARDENED_OFFSET = 0x80000000
|
|
35
|
+
MAINNET_VERSION = 0x0488B21E # xpub
|
|
36
|
+
MAINNET_PRIVATE_VERSION = 0x0488ADE4 # xprv
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _b58check_encode(payload: bytes) -> str:
|
|
40
|
+
"""Encode data with Base58Check (checksum over full payload)."""
|
|
41
|
+
checksum = sha256d(payload)[:4]
|
|
42
|
+
return base58_encode(payload + checksum)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _b58check_decode(s: str) -> bytes:
|
|
46
|
+
"""Decode Base58Check string and return raw payload (without checksum)."""
|
|
47
|
+
data = base58_decode(s)
|
|
48
|
+
if len(data) < 4:
|
|
49
|
+
raise BIP32Error("Invalid extended key: too short")
|
|
50
|
+
payload, checksum = data[:-4], data[-4:]
|
|
51
|
+
if sha256d(payload)[:4] != checksum:
|
|
52
|
+
raise BIP32Error("Invalid extended key checksum")
|
|
53
|
+
return payload
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def derive_child_key(
|
|
57
|
+
parent_key: bytes, parent_chain_code: bytes, index: int, is_private: bool = True
|
|
58
|
+
) -> Tuple[bytes, bytes]:
|
|
59
|
+
"""
|
|
60
|
+
Derive child key from parent key (CKDpriv / CKDpub).
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
parent_key: 33-byte public key or 32-byte private key
|
|
64
|
+
parent_chain_code: 32-byte chain code
|
|
65
|
+
index: Child index
|
|
66
|
+
is_private: Whether parent_key is private
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
(child_key, child_chain_code)
|
|
70
|
+
"""
|
|
71
|
+
if is_private:
|
|
72
|
+
if len(parent_key) != 32:
|
|
73
|
+
raise BIP32Error(f"Invalid private key length: {len(parent_key)}")
|
|
74
|
+
|
|
75
|
+
if index >= HARDENED_OFFSET:
|
|
76
|
+
# Hardened derivation
|
|
77
|
+
data = b"\x00" + parent_key + struct.pack(">I", index)
|
|
78
|
+
else:
|
|
79
|
+
# Normal derivation
|
|
80
|
+
parent_pubkey = privkey_to_pubkey(parent_key, compressed=True)
|
|
81
|
+
data = parent_pubkey + struct.pack(">I", index)
|
|
82
|
+
else:
|
|
83
|
+
if len(parent_key) != 33:
|
|
84
|
+
raise BIP32Error(f"Invalid public key length: {len(parent_key)}")
|
|
85
|
+
|
|
86
|
+
if index >= HARDENED_OFFSET:
|
|
87
|
+
raise BIP32Error("Cannot derive hardened child from public key")
|
|
88
|
+
|
|
89
|
+
data = parent_key + struct.pack(">I", index)
|
|
90
|
+
|
|
91
|
+
# HMAC-SHA512
|
|
92
|
+
hmac_result = hmac_sha512(parent_chain_code, data)
|
|
93
|
+
left = hmac_result[:32]
|
|
94
|
+
right = hmac_result[32:]
|
|
95
|
+
|
|
96
|
+
il_int = parse_256(left)
|
|
97
|
+
if il_int >= N or il_int == 0:
|
|
98
|
+
raise BIP32Error("Invalid derived key (IL out of range)")
|
|
99
|
+
|
|
100
|
+
if is_private:
|
|
101
|
+
# Private key derivation (CKDpriv)
|
|
102
|
+
parent_key_int = parse_256(parent_key)
|
|
103
|
+
child_key_int = (il_int + parent_key_int) % N
|
|
104
|
+
if child_key_int == 0:
|
|
105
|
+
raise BIP32Error("Derived invalid private key")
|
|
106
|
+
|
|
107
|
+
child_key = ser_256(child_key_int)
|
|
108
|
+
else:
|
|
109
|
+
# Public key derivation (CKDpub)
|
|
110
|
+
gx, gy = Gx, Gy
|
|
111
|
+
generator_point = (gx, gy)
|
|
112
|
+
parent_point = pubkey_to_point(parent_key)
|
|
113
|
+
derived_point = scalar_mult(il_int, generator_point)
|
|
114
|
+
if derived_point is None:
|
|
115
|
+
raise BIP32Error("Derived invalid public key (IL * G is infinity)")
|
|
116
|
+
child_point = point_add(derived_point, parent_point)
|
|
117
|
+
if child_point is None:
|
|
118
|
+
raise BIP32Error("Derived invalid public key (point at infinity)")
|
|
119
|
+
|
|
120
|
+
child_key = point_to_pubkey(child_point[0], child_point[1], compressed=True)
|
|
121
|
+
|
|
122
|
+
return child_key, right
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def create_master_key(seed: bytes) -> Tuple[bytes, bytes]:
|
|
126
|
+
"""
|
|
127
|
+
Create master key and chain code from seed.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
seed: 64-byte seed
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
(master_private_key, master_chain_code)
|
|
134
|
+
"""
|
|
135
|
+
# HMAC-SHA512 with key "Bitcoin seed"
|
|
136
|
+
hmac_result = hmac_sha512(b"Bitcoin seed", seed)
|
|
137
|
+
|
|
138
|
+
master_key = hmac_result[:32]
|
|
139
|
+
master_chain_code = hmac_result[32:]
|
|
140
|
+
|
|
141
|
+
# Validate key
|
|
142
|
+
key_int = parse_256(master_key)
|
|
143
|
+
if key_int == 0 or key_int >= N:
|
|
144
|
+
raise BIP32Error("Invalid master key")
|
|
145
|
+
|
|
146
|
+
return master_key, master_chain_code
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def derive_path_from_seed(seed: bytes, path: str) -> Tuple[bytes, bytes]:
|
|
150
|
+
"""
|
|
151
|
+
Derive key at specified path from seed.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
seed: 64-byte seed
|
|
155
|
+
path: Derivation path like "m/84'/5353'/0'/0/0"
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
(private_key, chain_code)
|
|
159
|
+
"""
|
|
160
|
+
indices = path_to_indices(path)
|
|
161
|
+
|
|
162
|
+
# Create master key
|
|
163
|
+
private_key, chain_code = create_master_key(seed)
|
|
164
|
+
|
|
165
|
+
# Derive through each level
|
|
166
|
+
for index in indices:
|
|
167
|
+
private_key, chain_code = derive_child_key(
|
|
168
|
+
private_key, chain_code, index, is_private=True
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return private_key, chain_code
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def path_to_indices(path: str) -> List[int]:
|
|
175
|
+
"""
|
|
176
|
+
Convert absolute derivation path string to list of indices.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
path: Like "m/84'/5353'/0'/0/0"
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of indices
|
|
183
|
+
"""
|
|
184
|
+
if not path.startswith("m/"):
|
|
185
|
+
raise BIP32Error(f"Invalid path: {path}")
|
|
186
|
+
|
|
187
|
+
parts = path[2:].split("/")
|
|
188
|
+
if not parts or any(p == "" for p in parts):
|
|
189
|
+
raise BIP32Error(f"Invalid path: {path}")
|
|
190
|
+
|
|
191
|
+
indices: List[int] = []
|
|
192
|
+
for part in parts:
|
|
193
|
+
if part.endswith("'"):
|
|
194
|
+
indices.append(int(part[:-1]) + HARDENED_OFFSET)
|
|
195
|
+
else:
|
|
196
|
+
indices.append(int(part))
|
|
197
|
+
|
|
198
|
+
return indices
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def relative_path_to_indices(path: str) -> List[int]:
|
|
202
|
+
"""
|
|
203
|
+
Convert relative path string (no leading 'm/') to list of indices.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
path: Like "0/0" or "1/5'"
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List of indices
|
|
210
|
+
"""
|
|
211
|
+
path = path.strip()
|
|
212
|
+
if not path:
|
|
213
|
+
return []
|
|
214
|
+
|
|
215
|
+
parts = path.split("/")
|
|
216
|
+
if any(p == "" for p in parts):
|
|
217
|
+
raise BIP32Error(f"Invalid relative path: {path}")
|
|
218
|
+
|
|
219
|
+
indices: List[int] = []
|
|
220
|
+
for part in parts:
|
|
221
|
+
if part.endswith("'"):
|
|
222
|
+
indices.append(int(part[:-1]) + HARDENED_OFFSET)
|
|
223
|
+
else:
|
|
224
|
+
indices.append(int(part))
|
|
225
|
+
|
|
226
|
+
return indices
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass
|
|
230
|
+
class ExtendedKey:
|
|
231
|
+
"""Base class for BIP32 extended keys (metadata only)."""
|
|
232
|
+
|
|
233
|
+
depth: int
|
|
234
|
+
parent_fingerprint: bytes
|
|
235
|
+
child_number: int
|
|
236
|
+
chain_code: bytes
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass
|
|
240
|
+
class ExtendedPrivateKey(ExtendedKey):
|
|
241
|
+
"""BIP32 extended private key (xprv)."""
|
|
242
|
+
|
|
243
|
+
key: bytes
|
|
244
|
+
|
|
245
|
+
@classmethod
|
|
246
|
+
def from_seed(cls, seed: bytes) -> "ExtendedPrivateKey":
|
|
247
|
+
priv, chain = create_master_key(seed)
|
|
248
|
+
return cls(
|
|
249
|
+
depth=0,
|
|
250
|
+
parent_fingerprint=b"\x00\x00\x00\x00",
|
|
251
|
+
child_number=0,
|
|
252
|
+
chain_code=chain,
|
|
253
|
+
key=priv,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def from_xprv(cls, xprv: str) -> "ExtendedPrivateKey":
|
|
258
|
+
payload = _b58check_decode(xprv)
|
|
259
|
+
if len(payload) != 78:
|
|
260
|
+
raise BIP32Error("Invalid extended private key length")
|
|
261
|
+
|
|
262
|
+
version = int.from_bytes(payload[0:4], "big")
|
|
263
|
+
if version != MAINNET_PRIVATE_VERSION:
|
|
264
|
+
raise BIP32Error("Invalid extended private key version")
|
|
265
|
+
|
|
266
|
+
depth = payload[4]
|
|
267
|
+
parent_fingerprint = payload[5:9]
|
|
268
|
+
child_number = int.from_bytes(payload[9:13], "big")
|
|
269
|
+
chain_code = payload[13:45]
|
|
270
|
+
key_data = payload[45:78]
|
|
271
|
+
|
|
272
|
+
if key_data[0] != 0:
|
|
273
|
+
raise BIP32Error("Invalid extended private key data")
|
|
274
|
+
|
|
275
|
+
key = key_data[1:33]
|
|
276
|
+
if len(key) != 32:
|
|
277
|
+
raise BIP32Error("Invalid private key length in xprv")
|
|
278
|
+
|
|
279
|
+
return cls(
|
|
280
|
+
depth=depth,
|
|
281
|
+
parent_fingerprint=parent_fingerprint,
|
|
282
|
+
child_number=child_number,
|
|
283
|
+
chain_code=chain_code,
|
|
284
|
+
key=key,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def to_xprv(self) -> str:
|
|
288
|
+
if len(self.chain_code) != 32:
|
|
289
|
+
raise BIP32Error("Invalid chain code length")
|
|
290
|
+
if len(self.key) != 32:
|
|
291
|
+
raise BIP32Error("Invalid private key length")
|
|
292
|
+
|
|
293
|
+
version = ser_32(MAINNET_PRIVATE_VERSION)
|
|
294
|
+
depth_byte = bytes([self.depth])
|
|
295
|
+
child_bytes = self.child_number.to_bytes(4, "big")
|
|
296
|
+
key_data = b"\x00" + self.key
|
|
297
|
+
payload = (
|
|
298
|
+
version
|
|
299
|
+
+ depth_byte
|
|
300
|
+
+ self.parent_fingerprint
|
|
301
|
+
+ child_bytes
|
|
302
|
+
+ self.chain_code
|
|
303
|
+
+ key_data
|
|
304
|
+
)
|
|
305
|
+
return _b58check_encode(payload)
|
|
306
|
+
|
|
307
|
+
def to_public(self) -> "ExtendedPublicKey":
|
|
308
|
+
pub = privkey_to_pubkey(self.key, compressed=True)
|
|
309
|
+
return ExtendedPublicKey(
|
|
310
|
+
depth=self.depth,
|
|
311
|
+
parent_fingerprint=self.parent_fingerprint,
|
|
312
|
+
child_number=self.child_number,
|
|
313
|
+
chain_code=self.chain_code,
|
|
314
|
+
key=pub,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def fingerprint(self) -> bytes:
|
|
318
|
+
"""Fingerprint of *this* node (first 4 bytes of HASH160(pubkey))."""
|
|
319
|
+
pub = privkey_to_pubkey(self.key, compressed=True)
|
|
320
|
+
return hash160(pub)[:4]
|
|
321
|
+
|
|
322
|
+
def child(self, index: int) -> "ExtendedPrivateKey":
|
|
323
|
+
child_key, child_chain = derive_child_key(
|
|
324
|
+
self.key, self.chain_code, index, is_private=True
|
|
325
|
+
)
|
|
326
|
+
return ExtendedPrivateKey(
|
|
327
|
+
depth=self.depth + 1,
|
|
328
|
+
parent_fingerprint=self.fingerprint(),
|
|
329
|
+
child_number=index,
|
|
330
|
+
chain_code=child_chain,
|
|
331
|
+
key=child_key,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def derive_path(self, path: str) -> "ExtendedPrivateKey":
|
|
335
|
+
"""
|
|
336
|
+
Derive along an absolute ("m/...") or relative ("0/0") path.
|
|
337
|
+
"""
|
|
338
|
+
if path.startswith("m/"):
|
|
339
|
+
indices = path_to_indices(path)
|
|
340
|
+
elif path == "m":
|
|
341
|
+
return self
|
|
342
|
+
else:
|
|
343
|
+
indices = relative_path_to_indices(path)
|
|
344
|
+
|
|
345
|
+
node: ExtendedPrivateKey = self
|
|
346
|
+
for index in indices:
|
|
347
|
+
node = node.child(index)
|
|
348
|
+
return node
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@dataclass
|
|
352
|
+
class ExtendedPublicKey(ExtendedKey):
|
|
353
|
+
"""BIP32 extended public key (xpub)."""
|
|
354
|
+
|
|
355
|
+
key: bytes
|
|
356
|
+
|
|
357
|
+
@classmethod
|
|
358
|
+
def from_xpub(cls, xpub: str) -> "ExtendedPublicKey":
|
|
359
|
+
payload = _b58check_decode(xpub)
|
|
360
|
+
if len(payload) != 78:
|
|
361
|
+
raise BIP32Error("Invalid extended public key length")
|
|
362
|
+
|
|
363
|
+
version = int.from_bytes(payload[0:4], "big")
|
|
364
|
+
if version != MAINNET_VERSION:
|
|
365
|
+
raise BIP32Error("Invalid extended public key version")
|
|
366
|
+
|
|
367
|
+
depth = payload[4]
|
|
368
|
+
parent_fingerprint = payload[5:9]
|
|
369
|
+
child_number = int.from_bytes(payload[9:13], "big")
|
|
370
|
+
chain_code = payload[13:45]
|
|
371
|
+
key = payload[45:78]
|
|
372
|
+
|
|
373
|
+
if len(key) != 33:
|
|
374
|
+
raise BIP32Error("Invalid public key length in xpub")
|
|
375
|
+
|
|
376
|
+
return cls(
|
|
377
|
+
depth=depth,
|
|
378
|
+
parent_fingerprint=parent_fingerprint,
|
|
379
|
+
child_number=child_number,
|
|
380
|
+
chain_code=chain_code,
|
|
381
|
+
key=key,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def to_xpub(self) -> str:
|
|
385
|
+
if len(self.chain_code) != 32:
|
|
386
|
+
raise BIP32Error("Invalid chain code length")
|
|
387
|
+
if len(self.key) != 33:
|
|
388
|
+
raise BIP32Error("Invalid public key length")
|
|
389
|
+
|
|
390
|
+
version = ser_32(MAINNET_VERSION)
|
|
391
|
+
depth_byte = bytes([self.depth])
|
|
392
|
+
child_bytes = self.child_number.to_bytes(4, "big")
|
|
393
|
+
payload = (
|
|
394
|
+
version
|
|
395
|
+
+ depth_byte
|
|
396
|
+
+ self.parent_fingerprint
|
|
397
|
+
+ child_bytes
|
|
398
|
+
+ self.chain_code
|
|
399
|
+
+ self.key
|
|
400
|
+
)
|
|
401
|
+
return _b58check_encode(payload)
|
|
402
|
+
|
|
403
|
+
def fingerprint(self) -> bytes:
|
|
404
|
+
"""Fingerprint of *this* node (first 4 bytes of HASH160(pubkey))."""
|
|
405
|
+
return hash160(self.key)[:4]
|
|
406
|
+
|
|
407
|
+
def child(self, index: int) -> "ExtendedPublicKey":
|
|
408
|
+
child_key, child_chain = derive_child_key(
|
|
409
|
+
self.key, self.chain_code, index, is_private=False
|
|
410
|
+
)
|
|
411
|
+
return ExtendedPublicKey(
|
|
412
|
+
depth=self.depth + 1,
|
|
413
|
+
parent_fingerprint=self.fingerprint(),
|
|
414
|
+
child_number=index,
|
|
415
|
+
chain_code=child_chain,
|
|
416
|
+
key=child_key,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def derive_path(self, path: str) -> "ExtendedPublicKey":
|
|
420
|
+
"""
|
|
421
|
+
Derive along a relative path ("0/0") from this xpub.
|
|
422
|
+
Absolute hardened paths are not supported from public keys.
|
|
423
|
+
"""
|
|
424
|
+
if path.startswith("m/"):
|
|
425
|
+
# From a pure xpub we cannot move through hardened steps,
|
|
426
|
+
# so only allow non-hardened absolute paths if they match depth.
|
|
427
|
+
indices = path_to_indices(path)
|
|
428
|
+
elif path == "m":
|
|
429
|
+
return self
|
|
430
|
+
else:
|
|
431
|
+
indices = relative_path_to_indices(path)
|
|
432
|
+
|
|
433
|
+
node: ExtendedPublicKey = self
|
|
434
|
+
for index in indices:
|
|
435
|
+
if index >= HARDENED_OFFSET:
|
|
436
|
+
raise BIP32Error("Cannot derive hardened child from public key")
|
|
437
|
+
node = node.child(index)
|
|
438
|
+
return node
|