86 lines
2.8 KiB
Python
86 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from typing import Optional
|
|
|
|
from ..constants import ADDRESS_SAFE_RE, BASE58_INDEX, BECH32_CHARSET_MAP
|
|
|
|
|
|
def is_address_characters_safe(address: str) -> bool:
|
|
return ADDRESS_SAFE_RE.fullmatch(address) is not None
|
|
|
|
|
|
def base58_decode(value: str) -> Optional[bytes]:
|
|
number = 0
|
|
for char in value:
|
|
digit = BASE58_INDEX.get(char)
|
|
if digit is None:
|
|
return None
|
|
number = number * 58 + digit
|
|
|
|
raw = b"" if number == 0 else number.to_bytes((number.bit_length() + 7) // 8, "big")
|
|
pad = len(value) - len(value.lstrip("1"))
|
|
return b"\x00" * pad + raw
|
|
|
|
|
|
def base58check_verify(address: str) -> tuple[bool, str, Optional[int], Optional[int]]:
|
|
decoded = base58_decode(address)
|
|
if decoded is None:
|
|
return False, "Invalid Base58 characters", None, None
|
|
if len(decoded) < 4:
|
|
return False, "Too short for Base58Check", None, None
|
|
|
|
payload, checksum = decoded[:-4], decoded[-4:]
|
|
digest = hashlib.sha256(hashlib.sha256(payload).digest()).digest()
|
|
if checksum != digest[:4]:
|
|
return False, "Base58Check checksum mismatch", None, None
|
|
if not payload:
|
|
return False, "Missing version byte", None, None
|
|
|
|
return True, "Valid Base58Check", payload[0], len(payload)
|
|
|
|
|
|
def bech32_polymod(values: list[int]) -> int:
|
|
generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
|
|
checksum = 1
|
|
for value in values:
|
|
top = checksum >> 25
|
|
checksum = ((checksum & 0x1FFFFFF) << 5) ^ value
|
|
for index in range(5):
|
|
if (top >> index) & 1:
|
|
checksum ^= generator[index]
|
|
return checksum
|
|
|
|
|
|
def bech32_hrp_expand(hrp: str) -> list[int]:
|
|
return [ord(char) >> 5 for char in hrp] + [0] + [ord(char) & 31 for char in hrp]
|
|
|
|
|
|
def bech32_verify_checksum(hrp: str, data: list[int], spec: str) -> bool:
|
|
expected = 1 if spec == "bech32" else 0x2BC830A3
|
|
return bech32_polymod(bech32_hrp_expand(hrp) + data) == expected
|
|
|
|
|
|
def bech32_decode(address: str) -> tuple[Optional[str], Optional[list[int]], Optional[str]]:
|
|
if address.lower() != address and address.upper() != address:
|
|
return None, None, None
|
|
|
|
normalized = address.lower()
|
|
separator_index = normalized.rfind("1")
|
|
if separator_index < 1 or separator_index + 7 > len(normalized):
|
|
return None, None, None
|
|
|
|
hrp = normalized[:separator_index]
|
|
data: list[int] = []
|
|
for char in normalized[separator_index + 1 :]:
|
|
mapped = BECH32_CHARSET_MAP.get(char)
|
|
if mapped is None:
|
|
return None, None, None
|
|
data.append(mapped)
|
|
|
|
if bech32_verify_checksum(hrp, data, "bech32"):
|
|
return hrp, data[:-6], "bech32"
|
|
if bech32_verify_checksum(hrp, data, "bech32m"):
|
|
return hrp, data[:-6], "bech32m"
|
|
return None, None, None
|