Skip to content

Commit 201a7f1

Browse files
committed
Fix: BitcoinCash address implementation
1 parent 7803b4a commit 201a7f1

5 files changed

Lines changed: 195 additions & 69 deletions

File tree

examples/addresses/slip10_secp256k1_address.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
bytes_to_string, get_bytes
99
)
1010
from hdwallet.cryptocurrencies import (
11-
Bitcoin, Cosmos, Filecoin, Avalanche, Ergo, OKTChain, Harmony, Zilliqa, Injective
11+
Bitcoin, BitcoinCash, Cosmos, Filecoin, Avalanche, Ergo, OKTChain, Harmony, Zilliqa, Injective
1212
)
1313
from hdwallet.addresses import (
1414
P2PKHAddress,
@@ -18,6 +18,7 @@
1818
P2WPKHInP2SHAddress,
1919
P2WSHAddress,
2020
P2WSHInP2SHAddress,
21+
BitcoinCashAddress,
2122
EthereumAddress,
2223
CosmosAddress,
2324
XinFinAddress,
@@ -35,7 +36,7 @@
3536
)
3637

3738
private_key: IPrivateKey = SLIP10Secp256k1PrivateKey.from_bytes(get_bytes(
38-
"be3851aa7822b92deb2f34655e41a40fd510f6cf9aa2a4f0c4d7a4bc81f0ad74"
39+
"0000000000000000000000000000000000000000000000000000000000000001"
3940
))
4041
public_key: IPublicKey = private_key.public_key()
4142

@@ -116,6 +117,15 @@
116117
)
117118
print("P2WSH-In-P2SH Address:", p2wsh_in_p2sh_address, p2wsh_in_p2sh_address_hash)
118119

120+
bitcoincash_address: str = BitcoinCashAddress.encode(
121+
public_key=public_key,
122+
public_key_type=PUBLIC_KEY_TYPES.COMPRESSED
123+
)
124+
bitcoincash_address_hash: str = BitcoinCashAddress.decode(
125+
address=bitcoincash_address
126+
)
127+
print("BitcoinCash Address:", bitcoincash_address, bitcoincash_address_hash)
128+
119129
ethereum_address: str = EthereumAddress.encode(
120130
public_key=public_key, skip_checksum_encode=False
121131
)

hdwallet/addresses/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python3
22

3-
# Copyright © 2020-2024, Meheret Tesfaye Batu <meherett.batu@gmail.com>
3+
# Copyright © 2020-2026, Meheret Tesfaye Batu <meherett.batu@gmail.com>
44
# Distributed under the MIT software license, see the accompanying
55
# file COPYING or https://opensource.org/license/mit
66

@@ -12,6 +12,7 @@
1212
from .algorand import AlgorandAddress
1313
from .aptos import AptosAddress
1414
from .avalanche import AvalancheAddress
15+
from .bitcoincash import BitcoinCashAddress
1516
from .cardano import CardanoAddress
1617
from .cosmos import CosmosAddress
1718
from .eos import EOSAddress
@@ -63,6 +64,8 @@ class ADDRESSES:
6364
+-----------------+------------------------------------------------------------------+
6465
| Avalanche | :class:`hdwallet.addresses.avalanche.AvalancheAddress` |
6566
+-----------------+------------------------------------------------------------------+
67+
| BitcoinCash | :class:`hdwallet.addresses.bitcoincash.BitcoinCash` |
68+
+-----------------+------------------------------------------------------------------+
6669
| Cardano | :class:`hdwallet.addresses.cardano.CardanoAddress` |
6770
+-----------------+------------------------------------------------------------------+
6871
| Cosmos | :class:`hdwallet.addresses.cosmos.CosmosAddress` |
@@ -130,6 +133,7 @@ class ADDRESSES:
130133
AlgorandAddress.name(): AlgorandAddress,
131134
AptosAddress.name(): AptosAddress,
132135
AvalancheAddress.name(): AvalancheAddress,
136+
BitcoinCashAddress.name(): BitcoinCashAddress,
133137
CardanoAddress.name(): CardanoAddress,
134138
CosmosAddress.name(): CosmosAddress,
135139
EOSAddress.name(): EOSAddress,

hdwallet/addresses/bitcoincash.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright © 2020-2026, Meheret Tesfaye Batu <meherett.batu@gmail.com>
4+
# Distributed under the MIT software license, see the accompanying
5+
# file COPYING or https://opensource.org/license/mit
6+
7+
from typing import (
8+
Any, Union
9+
)
10+
11+
from ..libs.base58 import ensure_string
12+
from ..libs.bech32 import (
13+
CHARSET, convertbits
14+
)
15+
from ..consts import PUBLIC_KEY_TYPES
16+
from ..eccs import (
17+
IPublicKey, SLIP10Secp256k1PublicKey, validate_and_get_public_key
18+
)
19+
from ..cryptocurrencies import BitcoinCash
20+
from ..crypto import hash160
21+
from ..utils import bytes_to_string
22+
from .iaddress import IAddress
23+
24+
25+
class BitcoinCashAddress(IAddress):
26+
27+
hrp: str = BitcoinCash.NETWORKS.MAINNET.HRP
28+
public_key_address_prefix: int = BitcoinCash.NETWORKS.MAINNET.STD_PUBLIC_KEY_ADDRESS_PREFIX
29+
script_address_prefix: int = BitcoinCash.NETWORKS.MAINNET.STD_SCRIPT_ADDRESS_PREFIX
30+
31+
@staticmethod
32+
def name() -> str:
33+
"""
34+
Returns the name of the cryptocurrency.
35+
36+
:return: The name of the address type.
37+
:rtype: str
38+
"""
39+
return "BitcoinCash"
40+
41+
@classmethod
42+
def encode(cls, public_key: Union[bytes, str, IPublicKey], **kwargs: Any) -> str:
43+
"""
44+
Encode a public key into a Bitcoin Cash CashAddr address.
45+
46+
:param public_key: The public key to encode.
47+
:type public_key: Union[bytes, str, IPublicKey]
48+
:param kwargs: Additional keyword arguments.
49+
- hrp: Human-readable part (optional).
50+
- public_key_type: Type of the public key (optional).
51+
- public_key_address_prefix: Address prefix for P2PKH (optional).
52+
- script_address_prefix: Address prefix for P2SH (optional).
53+
:type kwargs: Any
54+
55+
:return: The encoded CashAddr address.
56+
:rtype: str
57+
"""
58+
hrp = kwargs.get("hrp", cls.hrp)
59+
public_key_address_prefix = kwargs.get("public_key_address_prefix", cls.public_key_address_prefix)
60+
61+
public_key: IPublicKey = validate_and_get_public_key(
62+
public_key=public_key, public_key_cls=SLIP10Secp256k1PublicKey
63+
)
64+
public_key_hash: bytes = hash160(
65+
public_key.raw_compressed()
66+
if kwargs.get("public_key_type", PUBLIC_KEY_TYPES.COMPRESSED) == PUBLIC_KEY_TYPES.COMPRESSED else
67+
public_key.raw_uncompressed()
68+
)
69+
70+
# CashAddr version byte: 0 for P2PKH, 1 for P2SH
71+
version_byte = 0x00 # P2PKH with 160-bit hash
72+
73+
# Pack version and hash
74+
payload = bytes([version_byte]) + public_key_hash
75+
76+
# Convert to 5-bit groups
77+
data = convertbits(payload, 8, 5)
78+
79+
# CashAddr polymod for checksum
80+
generator = [0x98f2bc8e61, 0x79b76d99e2, 0xf33e5fb3c4, 0xae2eabe2a8, 0x1e4f43e470]
81+
hrp_expand = [ord(x) & 0x1f for x in hrp] + [0]
82+
values = hrp_expand + data + [0, 0, 0, 0, 0, 0, 0, 0]
83+
84+
chk = 1
85+
for value in values:
86+
top = chk >> 35
87+
chk = ((chk & 0x07ffffffff) << 5) ^ value
88+
for i in range(5):
89+
chk ^= generator[i] if ((top >> i) & 1) else 0
90+
polymod = chk ^ 1
91+
92+
# Create checksum
93+
checksum = [(polymod >> (5 * (7 - i))) & 0x1f for i in range(8)]
94+
95+
# Encode as CashAddr
96+
combined = data + checksum
97+
return ensure_string(hrp + ':' + ''.join([CHARSET[d] for d in combined]))
98+
99+
@classmethod
100+
def decode(cls, address: str, **kwargs: Any) -> str:
101+
"""
102+
Decode a Bitcoin Cash CashAddr address.
103+
104+
:param address: The CashAddr address to decode.
105+
:type address: str
106+
:param kwargs: Additional keyword arguments.
107+
- hrp: Human-readable part (optional).
108+
:type kwargs: Any
109+
110+
:return: The decoded address as a string.
111+
:rtype: str
112+
"""
113+
hrp_expected = kwargs.get("hrp", cls.hrp)
114+
115+
# Parse address
116+
if ':' in address:
117+
hrp, addr = address.split(':', 1)
118+
else:
119+
hrp = None
120+
addr = address
121+
122+
if not all(x in CHARSET for x in addr.lower()):
123+
raise ValueError("Invalid CashAddr characters")
124+
125+
addr = addr.lower()
126+
data = [CHARSET.find(x) for x in addr]
127+
128+
# Verify checksum
129+
if hrp:
130+
generator = [0x98f2bc8e61, 0x79b76d99e2, 0xf33e5fb3c4, 0xae2eabe2a8, 0x1e4f43e470]
131+
hrp_expand = [ord(x) & 0x1f for x in hrp.lower()] + [0]
132+
values = hrp_expand + data
133+
134+
chk = 1
135+
for value in values:
136+
top = chk >> 35
137+
chk = ((chk & 0x07ffffffff) << 5) ^ value
138+
for i in range(5):
139+
chk ^= generator[i] if ((top >> i) & 1) else 0
140+
polymod = chk ^ 1
141+
142+
if polymod != 0:
143+
raise ValueError("Invalid CashAddr checksum")
144+
145+
if hrp and hrp != hrp_expected:
146+
raise ValueError(f"Invalid HRP (expected: {hrp_expected}, got: {hrp})")
147+
148+
# Remove 8-byte checksum
149+
data = data[:-8]
150+
151+
# Convert from 5-bit to 8-bit
152+
decoded = convertbits(data, 5, 8, False)
153+
if decoded is None or len(decoded) < 21:
154+
raise ValueError("Invalid CashAddr data")
155+
156+
# First byte is version, rest is hash
157+
version = decoded[0]
158+
address_hash = bytes(decoded[1:])
159+
160+
return bytes_to_string(address_hash)

hdwallet/cryptocurrencies/bitcoincash.py

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#!/usr/bin/env python3
22

3-
# Copyright © 2020-2025, Meheret Tesfaye Batu <meherett.batu@gmail.com>
3+
# Copyright © 2020-2026, Meheret Tesfaye Batu <meherett.batu@gmail.com>
44
# Distributed under the MIT software license, see the accompanying
55
# file COPYING or https://opensource.org/license/mit
66

77
from ..slip44 import CoinTypes
88
from ..eccs import SLIP10Secp256k1ECC
99
from ..consts import (
10-
Info, WitnessVersions, Entropies, Mnemonics, Seeds, HDs, Addresses, AddressTypes, Networks, XPrivateKeyVersions, XPublicKeyVersions
10+
Info, Entropies, Mnemonics, Seeds, HDs, Addresses, AddressTypes, Networks, XPrivateKeyVersions, XPublicKeyVersions
1111
)
1212
from .icryptocurrency import (
1313
ICryptocurrency, INetwork
@@ -22,25 +22,13 @@ class Mainnet(INetwork):
2222
STD_PUBLIC_KEY_ADDRESS_PREFIX = 0x00
2323
STD_SCRIPT_ADDRESS_PREFIX = 0x08
2424
HRP = "bitcoincash"
25-
WITNESS_VERSIONS = WitnessVersions({
26-
"P2WPKH": 0x00,
27-
"P2WSH": 0x00
28-
})
2925
XPRIVATE_KEY_VERSIONS = XPrivateKeyVersions({
3026
"P2PKH": 0x0488ade4,
31-
"P2SH": 0x0488ade4,
32-
"P2WPKH": 0x04b2430c,
33-
"P2WPKH_IN_P2SH": 0x049d7878,
34-
"P2WSH": 0x02aa7a99,
35-
"P2WSH_IN_P2SH": 0x0295b005
27+
"P2SH": 0x0488ade4
3628
})
3729
XPUBLIC_KEY_VERSIONS = XPublicKeyVersions({
3830
"P2PKH": 0x0488b21e,
39-
"P2SH": 0x0488b21e,
40-
"P2WPKH": 0x04b24746,
41-
"P2WPKH_IN_P2SH": 0x049d7cb2,
42-
"P2WSH": 0x02aa7ed3,
43-
"P2WSH_IN_P2SH": 0x0295b43f
31+
"P2SH": 0x0488b21e
4432
})
4533
WIF_PREFIX = 0x80
4634

@@ -53,25 +41,13 @@ class Testnet(INetwork):
5341
STD_PUBLIC_KEY_ADDRESS_PREFIX = 0x00
5442
STD_SCRIPT_ADDRESS_PREFIX = 0x08
5543
HRP = "bchtest"
56-
WITNESS_VERSIONS = WitnessVersions({
57-
"P2WPKH": 0x00,
58-
"P2WSH": 0x00
59-
})
6044
XPRIVATE_KEY_VERSIONS = XPrivateKeyVersions({
6145
"P2PKH": 0x04358394,
62-
"P2SH": 0x04358394,
63-
"P2WPKH": 0x045f18bc,
64-
"P2WPKH_IN_P2SH": 0x044a4e28,
65-
"P2WSH": 0x02575048,
66-
"P2WSH_IN_P2SH": 0x024285b5
46+
"P2SH": 0x04358394
6747
})
6848
XPUBLIC_KEY_VERSIONS = XPublicKeyVersions({
6949
"P2PKH": 0x043587cf,
70-
"P2SH": 0x043587cf,
71-
"P2WPKH": 0x045f1cf6,
72-
"P2WPKH_IN_P2SH": 0x044a5262,
73-
"P2WSH": 0x02575483,
74-
"P2WSH_IN_P2SH": 0x024289ef
50+
"P2SH": 0x043587cf
7551
})
7652
WIF_PREFIX = 0xef
7753

@@ -114,11 +90,11 @@ class BitcoinCash(ICryptocurrency):
11490
DEFAULT_HD = HDS.BIP44
11591
DEFAULT_PATH = f"m/44'/{COIN_TYPE}'/0'/0/0"
11692
ADDRESSES = Addresses((
117-
"P2PKH", "P2SH", "P2WPKH", {"P2WPKH_IN_P2SH": "P2WPKH-In-P2SH"}, "P2WSH", {"P2WSH_IN_P2SH": "P2WSH-In-P2SH"}
93+
{"BITCOINCASH": "BitcoinCash"}, "P2PKH", "P2SH"
11894
))
119-
DEFAULT_ADDRESS = ADDRESSES.P2PKH
95+
DEFAULT_ADDRESS = ADDRESSES.BITCOINCASH
12096
SEMANTICS = [
121-
"p2pkh", "p2sh", "p2wpkh", "p2wpkh-in-p2sh", "p2wsh", "p2wsh-in-p2sh"
97+
"p2pkh", "p2sh"
12298
]
12399
DEFAULT_SEMANTIC = "p2pkh"
124100
ADDRESS_TYPES = AddressTypes({

0 commit comments

Comments
 (0)