# -*- coding: utf-8 -*-
"""Base58 encoding
Implementations of Base58 and Base58Check encodings that are compatible
with the Bitcoin network.
"""
from functools import lru_cache
from hashlib import sha256
from typing import Mapping, Union
# 58 character alphabet used
BITCOIN_ALPHABET = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
RIPPLE_ALPHABET = b"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"
XRP_ALPHABET = RIPPLE_ALPHABET
# Retro compatibility
alphabet = BITCOIN_ALPHABET
[docs]def b58encode_int(
i: int, default_one: bool = True, alphabet: bytes = BITCOIN_ALPHABET
) -> bytes:
"""
Encode an integer using Base58
"""
if not i and default_one:
return alphabet[0:1]
string = b""
base = len(alphabet)
while i:
i, idx = divmod(i, base)
string = alphabet[idx : idx + 1] + string
return string
[docs]def b58encode(v: Union[str, bytes], alphabet: bytes = BITCOIN_ALPHABET) -> bytes:
"""
Encode a string using Base58
"""
v = scrub_input(v)
origlen = len(v)
v = v.lstrip(b"\0")
newlen = len(v)
acc = int.from_bytes(v, byteorder="big") # first byte is most significant
result = b58encode_int(acc, default_one=False, alphabet=alphabet)
return alphabet[0:1] * (origlen - newlen) + result
@lru_cache()
def _get_base58_decode_map(alphabet: bytes, autofix: bool) -> Mapping[int, int]:
invmap = {char: index for index, char in enumerate(alphabet)}
if autofix:
groups = [b"0Oo", b"Il1"]
for group in groups:
pivots = [c for c in group if c in invmap]
if len(pivots) == 1:
for alternative in group:
invmap[alternative] = invmap[pivots[0]]
return invmap
[docs]def b58decode_int(
v: Union[str, bytes], alphabet: bytes = BITCOIN_ALPHABET, *, autofix: bool = False
) -> int:
"""
Decode a Base58 encoded string as an integer
"""
if b" " not in alphabet:
v = v.rstrip()
v = scrub_input(v)
decode_map = _get_base58_decode_map(alphabet, autofix=autofix)
decimal = 0
base = len(alphabet)
try:
for char in v:
decimal = decimal * base + decode_map[char]
except KeyError as e:
raise ValueError("Invalid character {!r}".format(chr(e.args[0]))) from None
return decimal
[docs]def b58decode(
v: Union[str, bytes], alphabet: bytes = BITCOIN_ALPHABET, *, autofix: bool = False
) -> bytes:
"""
Decode a Base58 encoded string
"""
v = v.rstrip()
v = scrub_input(v)
origlen = len(v)
v = v.lstrip(alphabet[0:1])
newlen = len(v)
acc = b58decode_int(v, alphabet=alphabet, autofix=autofix)
return acc.to_bytes(origlen - newlen + (acc.bit_length() + 7) // 8, "big")
[docs]def b58encode_check(v: Union[str, bytes], alphabet: bytes = BITCOIN_ALPHABET) -> bytes:
"""
Encode a string using Base58 with a 4 character checksum
"""
v = scrub_input(v)
digest = sha256(sha256(v).digest()).digest()
return b58encode(v + digest[:4], alphabet=alphabet)
[docs]def b58decode_check(
v: Union[str, bytes], alphabet: bytes = BITCOIN_ALPHABET, *, autofix: bool = False
) -> bytes:
"""Decode and verify the checksum of a Base58 encoded string"""
result = b58decode(v, alphabet=alphabet, autofix=autofix)
result, check = result[:-4], result[-4:]
digest = sha256(sha256(result).digest()).digest()
if check != digest[:4]:
raise ValueError("Invalid checksum")
return result
[docs]def is_b58check(
v: Union[str, bytes], alphabet: bytes = BITCOIN_ALPHABET, *, autofix: bool = False
) -> bool:
"""Check if a string is valid Base58Check"""
try:
_ = b58decode_check(v, alphabet, autofix=autofix)
return True
except ValueError:
return False