|
| 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) |
0 commit comments