"""Minimal Bech32 helpers used by CashAddr encoding/decoding."""
import re
# The list of 32 symbols used in Bech32 encoding.
bech32_character_set = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
# An object mapping each of the 32 symbols used in Bech32 encoding to their respective index in the character set.
bech32_character_set_index = {char: index for index, char in enumerate(bech32_character_set)}
[docs]
class BitRegroupingError(ValueError):
"""Errors returned by bit regrouping during base conversion."""
integer_out_of_range = (
"An integer provided in the source array is out of the range of the specified source word length."
)
has_disallowed_padding = "Encountered padding when padding was disallowed."
requires_disallowed_padding = "Encoding requires padding while padding is disallowed."
[docs]
def regroup_bits(
bin: list[int],
source_word_length: int,
result_word_length: int,
allow_padding: bool = True,
) -> list[int]:
"""Convert a list of integers from source_word_length to result_word_length.
Args:
bin: Source integers (each fits in source_word_length bits).
source_word_length: Bit-width of each input value.
result_word_length: Desired bit-width of each output value.
allow_padding: If False, fail when padding would be required.
Returns:
List of regrouped integers.
Raises:
BitRegroupingError: If input values are out of range or padding is not allowed.
"""
accumulator = 0
bits = 0
result: list[int] = []
max_result_int = (1 << result_word_length) - 1
for value in bin:
if value < 0 or value >> source_word_length != 0:
raise BitRegroupingError(BitRegroupingError.integer_out_of_range)
accumulator = (accumulator << source_word_length) | value
bits += source_word_length
while bits >= result_word_length:
bits -= result_word_length
result.append((accumulator >> bits) & max_result_int)
if allow_padding:
if bits > 0:
result.append((accumulator << (result_word_length - bits)) & max_result_int)
else:
if bits >= source_word_length or (((accumulator << (result_word_length - bits)) & max_result_int) > 0):
raise BitRegroupingError(
BitRegroupingError.has_disallowed_padding
if bits >= source_word_length
else BitRegroupingError.requires_disallowed_padding
)
return result
[docs]
def encode_bech32(base32_integer_array: list[int]) -> str:
"""Encode a list of 5-bit integers to a Bech32 string."""
return "".join([bech32_character_set[i] for i in base32_integer_array])
[docs]
def decode_bech32(valid_bech32: str) -> list[int]:
"""Decode a Bech32 string into a list of 5-bit integers.
Raises:
Bech32DecodingError: If the input contains characters outside of the Bech32 charset.
"""
if not is_bech32_character_set(valid_bech32):
raise Bech32DecodingError(Bech32DecodingError.not_bech32_character_set)
return [bech32_character_set_index[char] for char in valid_bech32]
non_bech32_characters = re.compile(f"[^{bech32_character_set}]")
[docs]
def is_bech32_character_set(maybe_bech32: str) -> bool:
"""Return True if the string contains only Bech32 characters."""
return not non_bech32_characters.search(maybe_bech32)
[docs]
class Bech32DecodingError(ValueError):
"""Bech32 decoding error messages."""
not_bech32_character_set = "Bech32 decoding error: input contains characters outside of the Bech32 character set."