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 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"
@@ -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
@@ -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