pyrxd 0.2.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.
Files changed (83) hide show
  1. pyrxd/__init__.py +82 -0
  2. pyrxd/aes_cbc.py +30 -0
  3. pyrxd/base58.py +78 -0
  4. pyrxd/btc_wallet/__init__.py +28 -0
  5. pyrxd/btc_wallet/keys.py +334 -0
  6. pyrxd/btc_wallet/payment.py +267 -0
  7. pyrxd/btc_wallet/validate.py +57 -0
  8. pyrxd/constants.py +341 -0
  9. pyrxd/crypto/__init__.py +0 -0
  10. pyrxd/curve.py +100 -0
  11. pyrxd/fee_model.py +16 -0
  12. pyrxd/fee_models/__init__.py +4 -0
  13. pyrxd/fee_models/satoshis_per_kilobyte.py +67 -0
  14. pyrxd/glyph/__init__.py +82 -0
  15. pyrxd/glyph/builder.py +908 -0
  16. pyrxd/glyph/creator.py +133 -0
  17. pyrxd/glyph/dmint.py +1093 -0
  18. pyrxd/glyph/ft.py +325 -0
  19. pyrxd/glyph/inspector.py +116 -0
  20. pyrxd/glyph/payload.py +282 -0
  21. pyrxd/glyph/scanner.py +179 -0
  22. pyrxd/glyph/script.py +215 -0
  23. pyrxd/glyph/types.py +437 -0
  24. pyrxd/gravity/__init__.py +58 -0
  25. pyrxd/gravity/artifacts/__init__.py +0 -0
  26. pyrxd/gravity/artifacts/maker_covenant_6x12_p2wpkh.artifact.json +100 -0
  27. pyrxd/gravity/artifacts/maker_covenant_flat_12x10_11_12_13_14_p2wpkh.artifact.json +119 -0
  28. pyrxd/gravity/artifacts/maker_covenant_flat_12x20_sentinel_all.artifact.json +123 -0
  29. pyrxd/gravity/artifacts/maker_covenant_flat_6x10_11_12_13_14_p2wpkh.artifact.json +95 -0
  30. pyrxd/gravity/artifacts/maker_covenant_flat_6x12_p2wpkh.artifact.json +91 -0
  31. pyrxd/gravity/artifacts/maker_covenant_flat_6x13_p2wpkh.artifact.json +95 -0
  32. pyrxd/gravity/artifacts/maker_covenant_trade.artifact.json +95 -0
  33. pyrxd/gravity/artifacts/maker_covenant_unified_p2wpkh.artifact.json +119 -0
  34. pyrxd/gravity/artifacts/maker_offer.artifact.json +52 -0
  35. pyrxd/gravity/codehash.py +67 -0
  36. pyrxd/gravity/covenant.py +476 -0
  37. pyrxd/gravity/maker.py +449 -0
  38. pyrxd/gravity/trade.py +521 -0
  39. pyrxd/gravity/transactions.py +820 -0
  40. pyrxd/gravity/types.py +151 -0
  41. pyrxd/hash.py +37 -0
  42. pyrxd/hd/__init__.py +47 -0
  43. pyrxd/hd/bip32.py +336 -0
  44. pyrxd/hd/bip39.py +110 -0
  45. pyrxd/hd/bip44.py +107 -0
  46. pyrxd/hd/wallet.py +475 -0
  47. pyrxd/hd/wordlist/chinese_simplified.txt +2048 -0
  48. pyrxd/hd/wordlist/english.txt +2048 -0
  49. pyrxd/keys.py +421 -0
  50. pyrxd/merkle_path.py +341 -0
  51. pyrxd/network/__init__.py +27 -0
  52. pyrxd/network/bitcoin.py +868 -0
  53. pyrxd/network/chaintracker.py +89 -0
  54. pyrxd/network/electrumx.py +662 -0
  55. pyrxd/py.typed +0 -0
  56. pyrxd/script/__init__.py +23 -0
  57. pyrxd/script/script.py +180 -0
  58. pyrxd/script/type.py +272 -0
  59. pyrxd/script/unlocking_template.py +15 -0
  60. pyrxd/security/__init__.py +56 -0
  61. pyrxd/security/errors.py +122 -0
  62. pyrxd/security/rng.py +64 -0
  63. pyrxd/security/secrets.py +266 -0
  64. pyrxd/security/types.py +297 -0
  65. pyrxd/spv/__init__.py +40 -0
  66. pyrxd/spv/chain.py +70 -0
  67. pyrxd/spv/merkle.py +153 -0
  68. pyrxd/spv/payment.py +131 -0
  69. pyrxd/spv/pow.py +83 -0
  70. pyrxd/spv/proof.py +214 -0
  71. pyrxd/spv/witness.py +127 -0
  72. pyrxd/transaction/__init__.py +13 -0
  73. pyrxd/transaction/transaction.py +432 -0
  74. pyrxd/transaction/transaction_input.py +87 -0
  75. pyrxd/transaction/transaction_output.py +50 -0
  76. pyrxd/transaction/transaction_preimage.py +238 -0
  77. pyrxd/utils.py +612 -0
  78. pyrxd/wallet.py +330 -0
  79. pyrxd-0.2.0.dist-info/METADATA +201 -0
  80. pyrxd-0.2.0.dist-info/RECORD +83 -0
  81. pyrxd-0.2.0.dist-info/WHEEL +4 -0
  82. pyrxd-0.2.0.dist-info/licenses/LICENSE +202 -0
  83. pyrxd-0.2.0.dist-info/licenses/NOTICE +11 -0
pyrxd/__init__.py ADDED
@@ -0,0 +1,82 @@
1
+ """pyrxd — Python SDK for the Radiant (RXD) blockchain.
2
+
3
+ Provides transaction building, HD wallet, Glyph token protocol (NFT/FT/dMint),
4
+ Gravity cross-chain atomic swaps, SPV verification, and ElectrumX networking.
5
+
6
+ Quickstart::
7
+
8
+ from pyrxd import GlyphBuilder, GlyphMetadata, GlyphProtocol
9
+ from pyrxd import RxdSdkError, ValidationError
10
+
11
+ Subpackages:
12
+ pyrxd.glyph — Glyph token protocol (NFT, FT, dMint, mutable, V2)
13
+ pyrxd.gravity — BTC↔RXD atomic swaps (Gravity protocol)
14
+ pyrxd.security — Typed secrets, error hierarchy, secure RNG
15
+ pyrxd.hd — BIP-32/39/44 HD wallet
16
+ pyrxd.network — ElectrumX client, BTC data sources
17
+ pyrxd.spv — SPV chain/payment verification
18
+ pyrxd.transaction — Transaction building and serialization
19
+ pyrxd.script — Script types and evaluation
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from pyrxd.glyph import (
24
+ GlyphBuilder,
25
+ GlyphInspector,
26
+ GlyphItem,
27
+ GlyphMetadata,
28
+ GlyphProtocol,
29
+ GlyphRef,
30
+ GlyphScanner,
31
+ )
32
+ from pyrxd.gravity import ActiveOffer, GravityMakerSession, GravityOfferParams, GravityTrade
33
+ from pyrxd.hd.bip32 import Xprv, Xpub, ckd, bip32_derive_xprv_from_mnemonic, bip32_derive_xkeys_from_xkey
34
+ from pyrxd.hd.bip39 import mnemonic_from_entropy, seed_from_mnemonic
35
+ from pyrxd.hd.bip44 import bip44_derive_xprv_from_mnemonic
36
+ from pyrxd.hd.wallet import AddressRecord, HdWallet
37
+ from pyrxd.keys import PrivateKey
38
+ from pyrxd.network.electrumx import UtxoRecord, script_hash_for_address
39
+ from pyrxd.security import (
40
+ RxdSdkError,
41
+ ValidationError,
42
+ )
43
+
44
+ __version__ = "0.2.0"
45
+
46
+ __all__ = [
47
+ "__version__",
48
+ # Glyph
49
+ "GlyphBuilder",
50
+ "GlyphInspector",
51
+ "GlyphItem",
52
+ "GlyphMetadata",
53
+ "GlyphProtocol",
54
+ "GlyphRef",
55
+ "GlyphScanner",
56
+ # Gravity
57
+ "GravityTrade",
58
+ "GravityMakerSession",
59
+ "GravityOfferParams",
60
+ "ActiveOffer",
61
+ # HD wallet — BIP-32
62
+ "Xprv",
63
+ "Xpub",
64
+ "ckd",
65
+ "bip32_derive_xprv_from_mnemonic",
66
+ "bip32_derive_xkeys_from_xkey",
67
+ # HD wallet — BIP-39
68
+ "mnemonic_from_entropy",
69
+ "seed_from_mnemonic",
70
+ # HD wallet — BIP-44
71
+ "AddressRecord",
72
+ "bip44_derive_xprv_from_mnemonic",
73
+ "HdWallet",
74
+ # Keys
75
+ "PrivateKey",
76
+ # Network utilities
77
+ "UtxoRecord",
78
+ "script_hash_for_address",
79
+ # Errors
80
+ "RxdSdkError",
81
+ "ValidationError",
82
+ ]
pyrxd/aes_cbc.py ADDED
@@ -0,0 +1,30 @@
1
+ from Cryptodome.Cipher import AES
2
+
3
+
4
+ class InvalidPadding(Exception):
5
+ pass
6
+
7
+
8
+ def append_pkcs7_padding(message: bytes) -> bytes:
9
+ pad = 16 - (len(message) % 16)
10
+ return message + bytes([pad]) * pad
11
+
12
+
13
+ def strip_pkcs7_padding(message: bytes) -> bytes:
14
+ if len(message) % 16 != 0 or len(message) == 0:
15
+ raise InvalidPadding("invalid length")
16
+ pad = message[-1]
17
+ if not 1 <= pad <= 16:
18
+ raise InvalidPadding("invalid padding byte (out of range)")
19
+ for i in message[-pad:]:
20
+ if i != pad:
21
+ raise InvalidPadding("invalid padding byte (inconsistent)")
22
+ return message[0:-pad]
23
+
24
+
25
+ def aes_encrypt_with_iv(key: bytes, iv: bytes, message: bytes) -> bytes:
26
+ return AES.new(key, AES.MODE_CBC, iv).encrypt(append_pkcs7_padding(message))
27
+
28
+
29
+ def aes_decrypt_with_iv(key: bytes, iv: bytes, message: bytes) -> bytes:
30
+ return strip_pkcs7_padding(AES.new(key, AES.MODE_CBC, iv).decrypt(message))
pyrxd/base58.py ADDED
@@ -0,0 +1,78 @@
1
+ from .hash import hash256
2
+
3
+ BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
4
+
5
+
6
+ def _checksum(payload: bytes) -> bytes:
7
+ return hash256(payload)[:4]
8
+
9
+
10
+ def b58_encode(payload: bytes) -> str:
11
+ pad = 0
12
+ for byte in payload:
13
+ if byte == 0:
14
+ pad += 1
15
+ else:
16
+ break
17
+ prefix = "1" * pad
18
+ num = int.from_bytes(payload, "big")
19
+ result = ""
20
+ while num > 0:
21
+ num, remaining = divmod(num, 58)
22
+ result = BASE58_ALPHABET[remaining] + result
23
+ return prefix + result
24
+
25
+
26
+ def base58check_encode(payload: bytes) -> str:
27
+ return b58_encode(payload + _checksum(payload))
28
+
29
+
30
+ def to_base58check(payload: bytes, prefix: bytes) -> str:
31
+ """
32
+ Converts a binary array into a base58check string with a checksum
33
+ :param payload: The binary array to convert to base58check
34
+ :param prefix: The prefix to add to the binary
35
+ :return: The base58check string representation
36
+ """
37
+ return base58check_encode(prefix + payload)
38
+
39
+
40
+ def from_base58check(encoded: str, prefix_len: int = 1) -> (bytes, bytes):
41
+ """
42
+ Converts a base58check string into payload and prefix
43
+ :param encoded: The base58check string to convert
44
+ :param prefix_len: The byte length of the prefix
45
+ :return: A tuple containing the prefix and the payload
46
+ """
47
+ payload = base58check_decode(encoded)
48
+ return payload[:prefix_len], payload[prefix_len:]
49
+
50
+
51
+ def b58_decode(encoded: str) -> bytes:
52
+ pad = 0
53
+ for char in encoded:
54
+ if char == "1":
55
+ pad += 1
56
+ else:
57
+ break
58
+ prefix = b"\x00" * pad
59
+ num = 0
60
+ try:
61
+ for char in encoded:
62
+ num *= 58
63
+ num += BASE58_ALPHABET.index(char)
64
+ except Exception:
65
+ raise ValueError(f"invalid base58 encoded {encoded}")
66
+ # if num is 0 then (0).to_bytes will return b''
67
+ return prefix + num.to_bytes((num.bit_length() + 7) // 8, "big")
68
+
69
+
70
+ def base58check_decode(encoded: str) -> bytes:
71
+ decoded = b58_decode(encoded)
72
+ payload = decoded[:-4]
73
+ decoded_checksum = decoded[-4:]
74
+ hash_checksum = _checksum(payload)
75
+ if decoded_checksum != hash_checksum:
76
+ _msg = f"unmatched base58 checksum, expect {decoded_checksum.hex()} but actually {hash_checksum.hex()}"
77
+ raise ValueError(_msg)
78
+ return payload
@@ -0,0 +1,28 @@
1
+ """Bitcoin wallet tooling for the Gravity Taker.
2
+
3
+ Public API
4
+ ----------
5
+ BtcKeypair — keypair with all 4 address formats
6
+ BtcUtxo — UTXO descriptor
7
+ BtcPaymentTx — signed transaction result
8
+ generate_keypair — generate a fresh keypair from CSPRNG
9
+ keypair_from_wif — load keypair from WIF (testing/recovery)
10
+ build_payment_tx — build+sign a 1-input segwit-v0 payment tx
11
+ validate_btc_address — validate a mainnet Bitcoin address string
12
+ validate_satoshis — validate a satoshi amount
13
+ """
14
+
15
+ from .keys import BtcKeypair, generate_keypair, keypair_from_wif
16
+ from .payment import BtcPaymentTx, BtcUtxo, build_payment_tx
17
+ from .validate import validate_btc_address, validate_satoshis
18
+
19
+ __all__ = [
20
+ "BtcKeypair",
21
+ "BtcPaymentTx",
22
+ "BtcUtxo",
23
+ "build_payment_tx",
24
+ "generate_keypair",
25
+ "keypair_from_wif",
26
+ "validate_btc_address",
27
+ "validate_satoshis",
28
+ ]
@@ -0,0 +1,334 @@
1
+ """Bitcoin keypair generation for the Gravity Taker.
2
+
3
+ Supports all 4 address formats required by the Gravity multi-type covenant:
4
+ P2PKH, P2WPKH, P2SH-P2WPKH, P2TR.
5
+
6
+ Networks
7
+ --------
8
+ The ``network`` parameter selects the bech32 HRP and the base58 version bytes
9
+ used for address and WIF serialization:
10
+
11
+ * ``"bc"`` — Bitcoin mainnet (default). bech32 HRP ``bc``, P2PKH version 0x00,
12
+ P2SH version 0x05, WIF version 0x80.
13
+ * ``"tb"`` — Bitcoin testnet3 / signet. bech32 HRP ``tb``, P2PKH version 0x6F,
14
+ P2SH version 0xC4, WIF version 0xEF.
15
+ * ``"bcrt"`` — Bitcoin regtest. bech32 HRP ``bcrt``, base58 versions + WIF
16
+ match testnet.
17
+
18
+ Any other HRP string is accepted (treated as mainnet-equivalent base58
19
+ versions) so that custom / local networks can be used without a code change.
20
+
21
+ Design rules
22
+ ------------
23
+ * Private key is stored in PrivateKeyMaterial — never leaks in repr/str.
24
+ * Uses secure_scalar_mod_n() for key generation (CSPRNG + rejection sampling).
25
+ * No assert in src/ — all invariants use explicit raises.
26
+ * unsafe_wif() is named 'unsafe' for grep-in-code-review visibility.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import hashlib
32
+ from dataclasses import dataclass
33
+
34
+ from pyrxd.security.errors import KeyMaterialError, ValidationError
35
+ from pyrxd.security.rng import secure_scalar_mod_n
36
+ from pyrxd.security.secrets import PrivateKeyMaterial
37
+
38
+ __all__ = ["BtcKeypair", "generate_keypair", "keypair_from_wif"]
39
+
40
+ # secp256k1 curve order
41
+ _N = int("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16)
42
+
43
+ # bech32 / bech32m character set
44
+ _BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
45
+
46
+ # bech32 GF polynomial constants (BIP173)
47
+ _BECH32_GENERATOR = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
48
+
49
+ # Checksum constants: bech32 uses 1, bech32m uses 0x2BC830A3 (BIP350)
50
+ _BECH32_CONST = 1
51
+ _BECH32M_CONST = 0x2BC830A3
52
+
53
+ # Base58 version bytes + WIF prefix per network.
54
+ # Testnet values per https://en.bitcoin.it/wiki/List_of_address_prefixes.
55
+ # Mainnet: P2PKH 0x00, P2SH 0x05, WIF 0x80
56
+ # Testnet/signet/regtest: P2PKH 0x6F, P2SH 0xC4, WIF 0xEF
57
+ _MAINNET_P2PKH = 0x00
58
+ _MAINNET_P2SH = 0x05
59
+ _MAINNET_WIF = 0x80
60
+ _TESTNET_P2PKH = 0x6F
61
+ _TESTNET_P2SH = 0xC4
62
+ _TESTNET_WIF = 0xEF
63
+
64
+ # Known HRPs for which we use testnet-style base58 version bytes.
65
+ _TESTNET_HRPS = frozenset({"tb", "bcrt"})
66
+
67
+
68
+ def _version_bytes_for(network: str) -> tuple[int, int, int]:
69
+ """Return (p2pkh_version, p2sh_version, wif_version) for ``network``.
70
+
71
+ Unknown HRPs fall back to mainnet versions; bech32 output is still produced
72
+ with the supplied HRP. This keeps custom/local HRPs usable without a code
73
+ change while giving correct values for the three well-known networks.
74
+ """
75
+ if network in _TESTNET_HRPS:
76
+ return _TESTNET_P2PKH, _TESTNET_P2SH, _TESTNET_WIF
77
+ return _MAINNET_P2PKH, _MAINNET_P2SH, _MAINNET_WIF
78
+
79
+
80
+ @dataclass
81
+ class BtcKeypair:
82
+ """A Bitcoin keypair with addresses in all 4 Gravity-supported formats.
83
+
84
+ Private key is stored as PrivateKeyMaterial (never logs/repr leaks).
85
+
86
+ Attributes:
87
+ network: bech32 HRP (``"bc"`` mainnet, ``"tb"`` testnet/signet,
88
+ ``"bcrt"`` regtest, or any custom HRP). Defaults to ``"bc"``.
89
+ """
90
+
91
+ _privkey: PrivateKeyMaterial # private — use with care
92
+ pubkey_bytes: bytes # 33-byte compressed pubkey
93
+
94
+ # Per-format address info
95
+ p2pkh_address: str
96
+ p2wpkh_address: str
97
+ p2sh_p2wpkh_address: str
98
+ p2tr_address: str
99
+
100
+ # 20-byte hashes (used for P2PKH, P2WPKH, P2SH-P2WPKH covenant params)
101
+ pkh: bytes # RIPEMD160(SHA256(pubkey)) — 20 bytes
102
+ p2sh_hash: bytes # RIPEMD160(SHA256(P2WPKH_redeem)) — 20 bytes
103
+
104
+ # 32-byte x-only tweaked output key (used for P2TR covenant param)
105
+ p2tr_output_key: bytes # 32 bytes
106
+
107
+ # Network / HRP used for all address + WIF serialization.
108
+ network: str = "bc"
109
+
110
+ def __repr__(self) -> str:
111
+ return f"BtcKeypair(p2wpkh={self.p2wpkh_address!r})"
112
+
113
+ def unsafe_wif(self) -> str:
114
+ """Export WIF. Named 'unsafe' to be visible in code review.
115
+
116
+ Uses the WIF version byte for ``self.network`` (0x80 for mainnet,
117
+ 0xEF for testnet/signet/regtest).
118
+ """
119
+ raw = self._privkey.unsafe_raw_bytes()
120
+ _, _, wif_version = _version_bytes_for(self.network)
121
+ # WIF: version + privkey(32) + 0x01 (compressed) + checksum(4)
122
+ payload = bytes([wif_version]) + raw + b"\x01"
123
+ checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
124
+ return _base58check_encode(payload + checksum)
125
+
126
+
127
+ def generate_keypair(network: str = "bc") -> BtcKeypair:
128
+ """Generate a fresh Bitcoin keypair using CSPRNG.
129
+
130
+ Uses secure_scalar_mod_n() for the private key — explicit range check,
131
+ rejection sampling, never Math.random().
132
+ Matches JS btc_wallet.js::generateKeypair() audit-hardened version.
133
+
134
+ Args:
135
+ network: bech32 HRP for address serialization. ``"bc"`` (default) for
136
+ mainnet, ``"tb"`` for testnet/signet, ``"bcrt"`` for regtest, or
137
+ any custom HRP.
138
+ """
139
+ import coincurve # noqa: PLC0415
140
+
141
+ privkey_material = secure_scalar_mod_n()
142
+ raw = privkey_material.unsafe_raw_bytes()
143
+ privkey_obj = coincurve.PrivateKey(raw)
144
+ pubkey_bytes = privkey_obj.public_key.format(compressed=True) # 33 bytes
145
+ return _build_keypair(privkey_material, pubkey_bytes, network=network)
146
+
147
+
148
+ def keypair_from_wif(wif: str, network: str = "bc") -> BtcKeypair:
149
+ """Load keypair from WIF string (for testing/recovery).
150
+
151
+ Args:
152
+ wif: WIF-encoded private key.
153
+ network: bech32 HRP for address serialization (see ``generate_keypair``).
154
+ Note: this controls OUTPUT address/WIF encoding only; the input WIF
155
+ is decoded regardless of its version byte.
156
+ """
157
+ import coincurve # noqa: PLC0415
158
+
159
+ privkey_material = PrivateKeyMaterial.from_wif(wif)
160
+ raw = privkey_material.unsafe_raw_bytes()
161
+ privkey_obj = coincurve.PrivateKey(raw)
162
+ pubkey_bytes = privkey_obj.public_key.format(compressed=True)
163
+ return _build_keypair(privkey_material, pubkey_bytes, network=network)
164
+
165
+
166
+ def _build_keypair(
167
+ privkey_material: PrivateKeyMaterial,
168
+ pubkey_bytes: bytes,
169
+ network: str = "bc",
170
+ ) -> BtcKeypair:
171
+ """Internal: build BtcKeypair from validated privkey + pubkey bytes."""
172
+ if len(pubkey_bytes) != 33:
173
+ raise KeyMaterialError("pubkey_bytes must be 33 bytes (compressed)")
174
+ if not isinstance(network, str) or not network:
175
+ raise ValidationError("network must be a non-empty string")
176
+
177
+ p2pkh_version, p2sh_version, _wif_version = _version_bytes_for(network)
178
+
179
+ # PKH = RIPEMD160(SHA256(pubkey))
180
+ pkh = _hash160(pubkey_bytes)
181
+
182
+ # P2SH-P2WPKH redeem script: OP_0 <20-byte pkh>
183
+ p2wpkh_redeem = b"\x00\x14" + pkh
184
+ p2sh_hash = _hash160(p2wpkh_redeem)
185
+
186
+ # P2TR: BIP341 key-path-only tweak on x-only pubkey
187
+ x_only = pubkey_bytes[1:] # drop parity byte
188
+ p2tr_output_key = _taproot_tweak(x_only)
189
+
190
+ return BtcKeypair(
191
+ _privkey=privkey_material,
192
+ pubkey_bytes=pubkey_bytes,
193
+ pkh=pkh,
194
+ p2sh_hash=p2sh_hash,
195
+ p2tr_output_key=p2tr_output_key,
196
+ p2pkh_address=_p2pkh_address(pkh, p2pkh_version),
197
+ p2wpkh_address=_p2wpkh_address(pkh, network),
198
+ p2sh_p2wpkh_address=_p2sh_address(p2sh_hash, p2sh_version),
199
+ p2tr_address=_p2tr_address(p2tr_output_key, network),
200
+ network=network,
201
+ )
202
+
203
+
204
+ def _hash160(data: bytes) -> bytes:
205
+ """RIPEMD160(SHA256(data)) — Bitcoin's hash160."""
206
+ return hashlib.new("ripemd160", hashlib.sha256(data).digest()).digest()
207
+
208
+
209
+ def _taproot_tweak(x_only_pubkey: bytes) -> bytes:
210
+ """BIP341 key-path tweak: tagged hash then add tweak*G to pubkey.
211
+
212
+ tagged hash = SHA256(SHA256('TapTweak') || SHA256('TapTweak') || x_only_pubkey)
213
+ """
214
+ import coincurve # noqa: PLC0415
215
+
216
+ tag = b"TapTweak"
217
+ tag_hash = hashlib.sha256(tag).digest()
218
+ tweak = hashlib.sha256(tag_hash + tag_hash + x_only_pubkey).digest()
219
+
220
+ # Reconstruct compressed pubkey (assume even parity = 0x02 prefix)
221
+ compressed = b"\x02" + x_only_pubkey
222
+ pub = coincurve.PublicKey(compressed)
223
+ # tweaked = pubkey + tweak*G; raises on scalar overflow (prob ~2^-128)
224
+ tweaked = pub.add(tweak)
225
+ tweaked_bytes = tweaked.format(compressed=True)
226
+ return tweaked_bytes[1:] # x-only (32 bytes)
227
+
228
+
229
+ # ---------------------------------------------------------------------------
230
+ # Address encoding helpers
231
+ # ---------------------------------------------------------------------------
232
+
233
+
234
+ def _base58check_encode(payload_with_checksum: bytes) -> str:
235
+ """Base58 encode a payload that already has its 4-byte checksum appended."""
236
+ _B58 = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
237
+ data = payload_with_checksum
238
+ # Count leading zero bytes
239
+ pad = 0
240
+ for b in data:
241
+ if b == 0:
242
+ pad += 1
243
+ else:
244
+ break
245
+ num = int.from_bytes(data, "big")
246
+ result = ""
247
+ while num > 0:
248
+ num, rem = divmod(num, 58)
249
+ result = chr(_B58[rem]) + result
250
+ return "1" * pad + result
251
+
252
+
253
+ def _p2pkh_address(pkh: bytes, version: int = _MAINNET_P2PKH) -> str:
254
+ """Base58Check P2PKH for the given version byte (mainnet 0x00, testnet 0x6F)."""
255
+ payload = bytes([version]) + pkh
256
+ checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
257
+ return _base58check_encode(payload + checksum)
258
+
259
+
260
+ def _p2wpkh_address(pkh: bytes, hrp: str = "bc") -> str:
261
+ """P2WPKH bech32 for the given HRP (bc=mainnet, tb=testnet, bcrt=regtest)."""
262
+ return _bech32_encode(hrp, 0, pkh)
263
+
264
+
265
+ def _p2sh_address(script_hash: bytes, version: int = _MAINNET_P2SH) -> str:
266
+ """Base58Check P2SH for the given version byte (mainnet 0x05, testnet 0xC4)."""
267
+ payload = bytes([version]) + script_hash
268
+ checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
269
+ return _base58check_encode(payload + checksum)
270
+
271
+
272
+ def _p2tr_address(output_key_32: bytes, hrp: str = "bc") -> str:
273
+ """P2TR bech32m for the given HRP (bc=mainnet, tb=testnet, bcrt=regtest)."""
274
+ return _bech32_encode(hrp, 1, output_key_32) # witness version 1 = bech32m
275
+
276
+
277
+ # ---------------------------------------------------------------------------
278
+ # BIP173/BIP350 bech32/bech32m encode
279
+ # ---------------------------------------------------------------------------
280
+
281
+
282
+ def _bech32_polymod(values: list[int]) -> int:
283
+ """Compute the bech32 checksum polynomial."""
284
+ c = 1
285
+ for v in values:
286
+ c0 = c >> 25
287
+ c = ((c & 0x1FFFFFF) << 5) ^ v
288
+ for i, gen in enumerate(_BECH32_GENERATOR):
289
+ if (c0 >> i) & 1:
290
+ c ^= gen
291
+ return c
292
+
293
+
294
+ def _bech32_hrp_expand(hrp: str) -> list[int]:
295
+ """Expand the HRP into values for checksum computation."""
296
+ return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
297
+
298
+
299
+ def _bech32_create_checksum(hrp: str, data: list[int], spec: int) -> list[int]:
300
+ """Compute the 6-character checksum for a bech32/bech32m string."""
301
+ values = _bech32_hrp_expand(hrp) + data
302
+ polymod = _bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ spec
303
+ return [(polymod >> (5 * (5 - i))) & 31 for i in range(6)]
304
+
305
+
306
+ def _convertbits(data: bytes, frombits: int, tobits: int) -> list[int]:
307
+ """Convert between bit groupings (e.g. 8-bit bytes to 5-bit groups)."""
308
+ acc = 0
309
+ bits = 0
310
+ ret = []
311
+ maxv = (1 << tobits) - 1
312
+ for value in data:
313
+ acc = ((acc << frombits) | value) & 0x3FFFFFFF
314
+ bits += frombits
315
+ while bits >= tobits:
316
+ bits -= tobits
317
+ ret.append((acc >> bits) & maxv)
318
+ if bits:
319
+ ret.append((acc << (tobits - bits)) & maxv)
320
+ return ret
321
+
322
+
323
+ def _bech32_encode(hrp: str, witness_version: int, witness_program: bytes) -> str:
324
+ """Encode a Bitcoin native SegWit address.
325
+
326
+ Uses bech32 for witness version 0 (BIP173) and bech32m for version 1+
327
+ (BIP350). On mainnet the encoded address starts with ``bc1q`` (v0) or
328
+ ``bc1p`` (v1); on testnet ``tb1q`` / ``tb1p``.
329
+ """
330
+ spec = _BECH32M_CONST if witness_version > 0 else _BECH32_CONST
331
+ data = _convertbits(witness_program, 8, 5)
332
+ combined = [witness_version] + data
333
+ checksum = _bech32_create_checksum(hrp, combined, spec)
334
+ return hrp + "1" + "".join(_BECH32_CHARSET[d] for d in combined + checksum)