from typing import (
Any,
Optional,
Type,
Union,
cast,
ClassVar,
TYPE_CHECKING
)
import warnings
from typing_extensions import (
Literal,
get_args,
)
import sys
from hexbytes import (
HexBytes
)
from eth_typing.evm import (
ChecksumAddress,
HexAddress,
)
from eth_utils.crypto import (
keccak
)
from eth_utils.address import (
to_checksum_address,
is_hex_address
)
from cfx_address import (
base32
)
from cfx_address._utils import (
validate_hex_address,
validate_network_id
)
from cfx_address.types import (
AddressType,
Base32AddressParts,
NetworkPrefix,
)
from cfx_utils.exceptions import (
InvalidAddress,
InvalidBase32Address,
InvalidConfluxHexAddress,
)
from cfx_address.consts import (
MAINNET_NETWORK_ID,
TESTNET_NETWORK_ID,
TYPE,
MAINNET_PREFIX ,
TESTNET_PREFIX,
COMMON_NET_PREFIX,
TYPE_NULL,
TYPE_BUILTIN,
TYPE_USER,
TYPE_CONTRACT,
TYPE_INVALID,
HEX_PREFIX,
DELIMITER,
VERSION_BYTE,
CHECKSUM_TEMPLATE,
)
from cfx_address._utils import (
public_key_to_cfx_hex
)
if TYPE_CHECKING:
from pydantic import GetCoreSchemaHandler
from pydantic_core import CoreSchema
default = object()
if sys.version_info >= (3, 8):
from functools import cached_property
else:
from cached_property import cached_property
class Base32AddressMeta(type):
"""
This is the metaclass of Base32Address to add a setter to class variable :attr:`Base32Address.default_network_id`
"""
@property
def default_network_id(self) -> Union[int, None]:
"""
Refer to :attr:`Base32Address.default_network_id` for the document
"""
return self._default_network_id
@default_network_id.setter
def default_network_id(cls, new_default: Union[None, int]) -> None:
if new_default is not None:
validate_network_id(new_default)
cls._default_network_id = new_default
[docs]
class Base32Address(str, metaclass=Base32AddressMeta):
"""
This class can be used to create Base32Address instances and provides useful class methods to deal with base32 format addresses.
:class:`~Base32Address` inherits from str, so the Base32Address can be trivially used as strings
:examples:
>>> address = Base32Address("0x1ecde7223747601823f7535d7968ba98b4881e09", network_id=1)
>>> address
'cfxtest:aatp533cg7d0agbd87kz48nj1mpnkca8be1rz695j4'
>>> [
... address.address_type,
... address.network_id,
... address.hex_address,
... address.verbose_address,
... address.abbr,
... address.mapped_evm_space_address,
... ]
['user', 1, '0x1ECdE7223747601823f7535d7968Ba98b4881E09', 'CFXTEST:TYPE.USER:AATP533CG7D0AGBD87KZ48NJ1MPNKCA8BE1RZ695J4', 'cfxtest:aat...95j4', '0x349f086998cF4a0C5a00b853a0E93239D81A97f6']
"""
_default_network_id: ClassVar[Optional[int]] = None
default_network_id: ClassVar[Optional[int]]
"""
| Default network id of Base32Address. :meth:`~Base32Address`, :meth:`~Base32Address.encode`,
and :meth:`~Base32Address.zero_address` will use this variable if `network_id` parameter is not specified.
| For most cases, it is recommended not to directly set this variable but use
:meth:`~cfx_address.address.get_base32_address_factory` to set :attr:`Base32Address.default_network_id`
| The setter is implemented in the metaclass :class:`cfx_address.address.Base32AddressMeta`
:param Union[None,int] new_default: new default network id, could be None or positive int
:raises InvalidNetworkId: the new_default value is not a positive integer
:examples:
>>> Base32Address.zero_address()
Traceback (most recent call last):
...
cfx_utils.exceptions.InvalidNetworkId: Expected network_id to be a positive integer. Receives None of type <class 'NoneType'>
>>> from cfx_address.address import get_base32_address_factory
>>> base32_address_factory = get_base32_address_factory(default_network_id=1)
>>> base32_address_factory.zero_address()
'cfxtest:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa6f0vrcsw'
>>> base32_address_factory.default_network_id = 1029
>>> base32_address_factory.zero_address()
'cfx:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0sfbnjm2'
>>> base32_address_factory.default_network_id = 2.5
Traceback (most recent call last):
...
cfx_utils.exceptions.InvalidNetworkId: Expected network_id to be a positive integer. Receives 2.5 of type <class 'float'>
"""
def __new__(cls, address: Union["Base32Address", HexAddress, str], network_id: Optional[int]=cast(int, default), verbose: Optional[bool] = None, *, _from_trust: bool = False, _ignore_invalid_type: bool = False) -> "Base32Address":
"""
:param Union[Base32Address,HexAddress,str] address: a base32-or-hex format address
:param Optional[int] network_id: target network_id of the address, defaults to :attr:`~default_network_id`. Can be None if first argument is a base32 address, which means don't change network id
:param Optional[bool] verbose: whether the return value will be encoded in verbose mode, defaults to None (will be viewed as None)
:param bool _from_trust: whether the value is a verified Base32Address, if true, network_id and verbose option should be None, and the verification and encoding process will be skipped. Not recommended to set unless preformance is critical. Defaults to False
:param bool _ignore_invalid_type: whether the address type is validated, defaults to False
:raises InvalidAddress: address is neither base32 address nor hex address
:raises InvalidNetworkId: network_id argument is not a positive integer or is None when address argument is a hex address
:return Base32Address: an encoded base32 object, which can be trivially used as python str, specially, if from_trusted_source is true, the input value will be directly used as the encoded value
:examples:
>>> address = Base32Address("0x1ecde7223747601823f7535d7968ba98b4881e09", network_id=1)
>>> address
'cfxtest:aatp533cg7d0agbd87kz48nj1mpnkca8be1rz695j4'
>>> address_ = Base32Address(address, network_id=1029, verbose=True)
>>> address_
'CFX:TYPE.USER:AATP533CG7D0AGBD87KZ48NJ1MPNKCA8BE7GGP3VPU'
>>> isinstance(address_, str)
True
"""
if network_id is default:
# if network_id is not specified (default),
# # network_id will be cls.default_network_id, which defaults to None
network_id = cls.default_network_id
# if the address is an instance of Base32Address, and network_id and verbose are not specified,
# return the address as a str ignoring validity check because it is already validated or allowed by _ignore_invalid_type
if isinstance(address, cls) and network_id is None and verbose is None:
return str.__new__(cls, str(address))
# if _from_trust is True,
# it requires network_id is None and verbose is None
# the new Base32Address will be initialized without validating, which means you can do
# Base32Address("hello world", network_id=None, verbose=None, _from_trust=True)
# so this API should be used carefully
if _from_trust:
if network_id is None and verbose is None:
return str.__new__(cls, address)
else:
raise ValueError("Invalid argument: `network_id` and `verbose` should be None if from_trusted_source is True")
try:
parts = cls.decode(address) # this line might raise exception if the address is not valid
if verbose is None:
# if verbose is None, the new address verbose is determined by the original address
verbose = (address[0] == "N" or address[0] == "C")
if network_id is None:
# if network_id is None, the new address verbose is determined by the original address
network_id = parts["network_id"]
validate_network_id(network_id)
val = cls._encode(parts["hex_address"], network_id, verbose, _ignore_invalid_type=_ignore_invalid_type)
except InvalidBase32Address:
if verbose is None:
verbose = False
if is_hex_address(address):
validate_network_id(network_id)
val = cls._encode(address, network_id, verbose, _ignore_invalid_type=_ignore_invalid_type) # type: ignore
else:
raise InvalidAddress("Address should be either base32 or hex, "
f"receives {address}")
return str.__new__(cls, val)
[docs]
def __eq__(self, _address: object) -> bool:
"""
invoked when a base32 address is compared to another object (or Base32Address typed object),
:param str _address: value compared to, which is supposed to be encoded in Base32 format (else return false)
:return bool: True if self and _address are of same hex_address and network_id
:examples:
>>> address = Base32Address("cfxtest:aatp533cg7d0agbd87kz48nj1mpnkca8be1rz695j4")
>>> assert address == "CFXTEST:TYPE.USER:AATP533CG7D0AGBD87KZ48NJ1MPNKCA8BE1RZ695J4"
>>> assert "CFXTEST:TYPE.USER:AATP533CG7D0AGBD87KZ48NJ1MPNKCA8BE1RZ695J4" == address
>>> assert "cfxtest:aatp533cg7d0agbd87kz48nj1mpnkca8be1rz695j4" == address
"""
try:
parts = self.__class__.decode(_address) # type: ignore
return self.hex_address == parts["hex_address"] and self.network_id == parts["network_id"]
except:
return False
def __ne__(self, _address: object) -> bool:
return not (self == _address)
def __hash__(self) -> int:
return super().__hash__()
[docs]
@classmethod
def from_public_key(
cls,
public_key: Union[str, bytes],
network_id: int = cast(int, default),
verbose: bool = False
) -> "Base32Address":
"""
create a Base32Address from public key
:param Union[str,bytes] public_key: str or bytes representation of public key
:param int network_id: network id of the return Base32Address, defaults to class variable :attr:`~default_network_id`
:param bool verbose: whether the address will be represented in verbose mode, defaults to False
:return Base32Address: Base32 representation of the address
>>> Base32Address.from_public_key("0xdacdaeba8e391e7649d3ac4b5329ca0e202d38facd928d88b5f729b89a497e43cc4ad3816fcfdb241497b3b43862afb4c899bc284bf60feca4ee66ff868d1feb", 1)
'cfxtest:aamw4kj6g41pgedw1efjnsm59fbz0b9r1awbp8k2p2'
"""
hex_address = public_key_to_cfx_hex(public_key)
return cls(hex_address, network_id, verbose)
@cached_property
def network_id(self) -> int:
"""
:return int: network_id of the address
:examples:
>>> address = Base32Address("cfxtest:aatp533cg7d0agbd87kz48nj1mpnkca8be1rz695j4")
>>> address.network_id
1
"""
return self.__class__._decode_network_id(self)
@cached_property
def hex_address(self) -> ChecksumAddress:
"""
:return ChecksumAddress: hex address of the address, will be encoded in ethereum checksum format
>>> address = Base32Address("cfxtest:aatp533cg7d0agbd87kz48nj1mpnkca8be1rz695j4")
>>> address.hex_address
'0x1ECdE7223747601823f7535d7968Ba98b4881E09'
"""
return self.__class__._decode_hex_address(self)
@cached_property
def address_type(self) -> AddressType:
"""
:return Literal["null", "builtin", "user", "contract", "invalid"]: address type of an address.
:examples:
>>> address = Base32Address("cfx:aatp533cg7d0agbd87kz48nj1mpnkca8be7ggp3vpu")
>>> address.address_type
'user'
"""
return self.__class__._detect_address_type(HexBytes(self.hex_address))
@cached_property
def eth_checksum_address(self) -> ChecksumAddress:
"""
:return ChecksumAddress: alias for :attr:`~hex_address`. This API will be deprecated in a future version
>>> address = Base32Address("cfxtest:aatp533cg7d0agbd87kz48nj1mpnkca8be1rz695j4")
>>> address.eth_checksum_address
'0x1ECdE7223747601823f7535d7968Ba98b4881E09'
"""
warnings.warn("eth_checksum_address will be deprecated in a future version. use hex_address instead", DeprecationWarning)
return self.hex_address
@cached_property
def verbose_address(self) -> "Base32Address":
"""
:return Base32Address: self presented in verbose mode
>>> address = Base32Address("cfxtest:aatp533cg7d0agbd87kz48nj1mpnkca8be1rz695j4")
>>> address.verbose_address
'CFXTEST:TYPE.USER:AATP533CG7D0AGBD87KZ48NJ1MPNKCA8BE1RZ695J4'
"""
return self.encode_base32(self.hex_address, self.network_id, True)
@cached_property
def abbr(self) -> str:
"""
:return str: abbreviation of the address, as mentioned in https://forum.conflux.fun/t/voting-results-for-new-address-abbreviation-standard/7131
:examples:
>>> Base32Address("cfxtest:aatp533cg7d0agbd87kz48nj1mpnkca8be1rz695j4").abbr
'cfxtest:aat...95j4'
>>> Base32Address("cfx:aatp533cg7d0agbd87kz48nj1mpnkca8be7ggp3vpu").abbr
'cfx:aat...7ggp3vpu'
"""
return self.__class__._shorten_base32_address(self)
@cached_property
def compressed_abbr(self) -> str:
"""
:return str: compressed abbreviation of the address, as mentioned in https://forum.conflux.fun/t/voting-results-for-new-address-abbreviation-standard/7131
:examples:
>>> address = Base32Address("cfx:aatp533cg7d0agbd87kz48nj1mpnkca8be7ggp3vpu")
>>> address.abbr
'cfx:aat...7ggp3vpu'
>>> address.compressed_abbr
'cfx:aat...3vpu'
"""
return self.__class__._shorten_base32_address(self, True)
@cached_property
def mapped_evm_space_address(self) -> ChecksumAddress:
"""
:return ChecksumAddress: the address of mapped account for EVM space as defined in https://github.com/Conflux-Chain/CIPs/blob/master/CIPs/cip-90.md#mapped-account
>>> address = Base32Address("cfx:aatp533cg7d0agbd87kz48nj1mpnkca8be7ggp3vpu")
>>> address.mapped_evm_space_address
'0x349f086998cF4a0C5a00b853a0E93239D81A97f6'
"""
return self.__class__._mapped_evm_address_from_hex(self.hex_address)
[docs]
@classmethod
def encode_base32(
cls, hex_address: str, network_id: int=cast(int, default), verbose: bool=False, *, _ignore_invalid_type: bool=False
) -> "Base32Address":
"""
Encode hex address to base32 address
:param str hex_address: hex address begins with 0x
:param int network_id: address network id, e.g., 1 for testnet and 1029 for mainnet, defaults to class variable :attr:`~default_network_id`
:param bool verbose: whether the address will be presented in verbose mode, defaults to False
:param bool _ignore_invalid_type: whether the address type is validated, defaults to False
:return Base32Address: an encoded base32 object, which can be trivially used as python str
:examples:
>>> Base32Address.encode_base32("0x1ecde7223747601823f7535d7968ba98b4881e09", 1)
'cfxtest:aatp533cg7d0agbd87kz48nj1mpnkca8be1rz695j4'
>>> Base32Address.encode_base32("0x1ecde7223747601823f7535d7968ba98b4881e09", 1029, verbose=True)
'CFX:TYPE.USER:AATP533CG7D0AGBD87KZ48NJ1MPNKCA8BE7GGP3VPU'
"""
if network_id is default:
network_id = cast(int, cls.default_network_id)
validate_hex_address(hex_address)
validate_network_id(network_id)
return cls(hex_address, network_id, verbose, _ignore_invalid_type=_ignore_invalid_type)
@classmethod
def _encode(
cls, hex_address: str, network_id: int, verbose: bool, *, _ignore_invalid_type: bool=False
) -> str:
network_prefix = cls._encode_network_prefix(network_id)
address_bytes = HexBytes(hex_address)
payload = base32.encode(VERSION_BYTE + address_bytes)
checksum = cls._create_checksum(network_prefix, payload)
parts = [network_prefix]
address_type = cls._detect_address_type(address_bytes)
if address_type == TYPE_INVALID and not _ignore_invalid_type:
raise InvalidConfluxHexAddress(f"The hex address should start with 0x0, 0x1 or 0x8, received {hex_address}."
"Check your code logic or set _ignore_invalid_type=True")
if verbose:
parts.append(f"{TYPE}.{address_type}")
parts.append(payload + checksum)
address = DELIMITER.join(parts)
if verbose:
return address.upper()
return address
@classmethod
def _decode_network_id(cls, base32_address: str) -> int:
base32_address = base32_address.lower()
parts = base32_address.split(DELIMITER)
return cls._network_prefix_to_id(parts[0])
@classmethod
def _decode_hex_address(cls, base32_address: str) -> ChecksumAddress:
base32_address = base32_address.lower()
parts = base32_address.split(DELIMITER)
return cls._payload_to_hex(parts[-1])
@classmethod
def _payload_to_hex(cls, payload: str) -> ChecksumAddress:
address_buf = base32.decode(payload)
hex_buf = address_buf[1:21]
return to_checksum_address(HEX_PREFIX + hex_buf.hex()) # type: ignore
[docs]
@classmethod
def decode(cls, base32_address: str) -> Base32AddressParts:
"""
Decode a base32 address string and get its hex_address, address_type, and network_id
:param str base32_address: address encoded in base32 format
:raises InvalidBase32Address: the parameter is not a valid CIP-37 address
:return Base32AddressParts: a dict object with field "hex_address", "address_type" and "network_id"
:examples:
>>> Base32Address.decode("cfxtest:aatp533cg7d0agbd87kz48nj1mpnkca8be1rz695j4")
{'network_id': 1, 'hex_address': '0x1ECdE7223747601823f7535d7968Ba98b4881E09', 'address_type': 'user'}
"""
try:
if not isinstance(base32_address, str):
raise InvalidBase32Address(f"Receives an argument of type {type(base32_address)}, expected a string")
if not any([
str(base32_address) == base32_address.upper(),
str(base32_address) == base32_address.lower(),
]):
raise InvalidBase32Address("Base32 address is supposed to be composed of all uppercase or lower case, "
f"Receives {base32_address}")
base32_address = base32_address.lower()
splits = base32_address.split(DELIMITER)
if len(splits) != 2 and len(splits) != 3:
raise InvalidBase32Address(
"Address needs to be encode in Base32 format, such as cfx:aaejuaaaaaaaaaaaaaaaaaaaaaaaaaaaajrwuc9jnb. "
"Received: {}".format(base32_address))
# if exception occurs, outer try-except will handle
address_parts = cls._decode(base32_address)
# check address type
address_type = address_parts["address_type"]
# it is ok to decode an address whose type is invalid
# if address_type == TYPE_INVALID:
# raise InvalidBase32Address(f"Invalid address type: the hex address of the provided address is {address_parts['hex_address']}, "
# "while valid conflux address is supposed to start with 0x0, 0x1 or 0x8")
"""
cip-37 #Decoding
6.Verify optional fields:
If the optional fields contain type.*: Verify the address-type according to the specification above.
Unknown options (options other than type.*) should be ignored.
which means
if address field is not expected, reject if the address field is a known one, else ignore
e.g.
valid CFX:TYPE.USER:AATP533CG7D0AGBD87KZ48NJ1MPNKCA8BE7GGP3VPU
invalid CFX:TYPE.NULL:AATP533CG7D0AGBD87KZ48NJ1MPNKCA8BE7GGP3VPU
ignore(but valid) CFX:GOD:AATP533CG7D0AGBD87KZ48NJ1MPNKCA8BE7GGP3VPU
"""
if len(splits) == 3 and splits[1] != f"{TYPE}.{address_type}":
address_field = splits[1]
if address_field.startswith(f"{TYPE}.") and address_field[5:] in get_args(AddressType):
raise InvalidBase32Address(f"Invalid address type field: the address type field does not match expected, "
f"expected {TYPE}.{address_type} but receives {address_field}, which is a known address type")
# check checksum
hex_address = address_parts["hex_address"]
address_bytes = HexBytes(hex_address)
payload = base32.encode(VERSION_BYTE + address_bytes)
checksum = cls._create_checksum(splits[0], payload)
if checksum != base32_address[-8:]:
raise InvalidBase32Address(f"Invalid Base32 address: checksum verification failed")
return address_parts
except Exception as e:
if isinstance(e, InvalidBase32Address):
raise e
else:
# unexpected error
raise InvalidBase32Address(
"Address needs to be a Base32 format string, such as cfx:aaejuaaaaaaaaaaaaaaaaaaaaaaaaaaaajrwuc9jnb\n"
f"Received argument {base32_address} of type {type(base32_address)}")
@classmethod
def _decode(cls, base32_address: str) -> Base32AddressParts:
"""
do not validate unless necessary, used if validity is known
"""
parts = base32_address.split(DELIMITER)
network_id = cls._network_prefix_to_id(parts[0])
hex_address = cls._payload_to_hex(parts[-1])
address_type = cls._detect_address_type(HexBytes(hex_address))
return {
"network_id": network_id,
"hex_address": hex_address,
"address_type": address_type
}
[docs]
@classmethod
def is_valid_base32(cls, value: Any) -> bool:
"""
Whether a value is a valid string-typed base32 address
:return bool: True if valid, else False
"""
try:
cls.decode(value)
return True
except:
return False
[docs]
@classmethod
def validate(cls, value: Any) -> Literal[True]:
"""
Validate if a value is a valid string-typed base32_address, raises an exception if not
:param str value: value to validate
:raises InvalidBase32Address: raises an exception if the address is not a valid base32 address
:return Literal[True]: returns True only if address is valid
"""
# an exception will be raised if decode failure
cls.decode(value)
return True
[docs]
@classmethod
def equals(cls, address1: str, address2: str) -> bool:
"""
| Check if two addresses share same hex_address and network_id
| It will throw an error if any param is not in CIP-37 format while :meth:`~__eq__` doesn't
:param str address1: base32 address to compare
:param str address2: base32 address to compare
:raises InvalidBase32Address: either address is not a valid base32 address
:return bool: whether two addresses share same hex_address and network_id
"""
return cls(address1) == cls(address2)
[docs]
@classmethod
def zero_address(cls, network_id: int=cast(int, default), verbose: bool = False) -> "Base32Address":
"""
Get zero address of the target network.
:param int network_id: target network_id, defaults to `Base32Address.default_network_id`
:param bool verbose: whether the zero address is presented in verbose mode, defaults to False
:return Base32Address: base32 format zero address of the target network
:examples:
>>> Base32Address.zero_address(network_id=1)
'cfxtest:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa6f0vrcsw'
"""
if network_id is default:
network_id = cast(int, cls.default_network_id)
return cls.encode_base32("0x0000000000000000000000000000000000000000", network_id, verbose)
[docs]
@classmethod
def shorten_base32_address(cls, base32_address: str, compressed: bool=False) -> str:
"""
Returns the abbreviation of the address
:param str base32_address: address to shorten
:raises InvalidBase32Address: raised if address is invalid
:param bool compressed: whether the abbreviation will be presented in compressed form,\
which only affects mainnet addresses, defaults to False
:return str: the abbreviation string
>>> Base32Address.shorten_base32_address("cfx:aatp533cg7d0agbd87kz48nj1mpnkca8be7ggp3vpu")
'cfx:aat...7ggp3vpu'
>>> Base32Address.shorten_base32_address("cfx:aatp533cg7d0agbd87kz48nj1mpnkca8be7ggp3vpu", compressed=True)
'cfx:aat...3vpu'
"""
cls.validate(base32_address)
return cls._shorten_base32_address(base32_address, compressed)
@classmethod
def _shorten_base32_address(cls, base32_address: str, compressed: bool=False) -> str:
lowcase = base32_address.lower()
splits = lowcase.split(DELIMITER)
prefix = splits[0]
payload = splits[-1]
if splits[0] != MAINNET_PREFIX or compressed:
return f"{prefix}{DELIMITER}{payload[:3]}...{payload[-4:]}"
else:
return f"{prefix}{DELIMITER}{payload[:3]}...{payload[-8:]}"
[docs]
@classmethod
def calculate_mapped_evm_space_address(cls, base32_address: str) -> ChecksumAddress:
"""
Calculate the address of mapped account for EVM space as defined in https://github.com/Conflux-Chain/CIPs/blob/master/CIPs/cip-90.md#mapped-account
:raises InvalidBase32Address: raised when address is not a valid base32 address
:examples:
>>> Base32Address.calculate_mapped_evm_space_address("CFXTEST:TYPE.USER:AATP533CG7D0AGBD87KZ48NJ1MPNKCA8BE1RZ695J4")
'0x349f086998cF4a0C5a00b853a0E93239D81A97f6'
"""
hex_address = cls.decode(base32_address)["hex_address"]
return cls._mapped_evm_address_from_hex(hex_address)
@classmethod
def _mapped_evm_address_from_hex(cls, hex_address: str) -> ChecksumAddress:
# do not check hex_address validity here
mapped_hash = keccak(HexBytes(hex_address)).hex()
mapped_evm_space_address = to_checksum_address(HEX_PREFIX + mapped_hash[-40:])
return mapped_evm_space_address
@classmethod
def _encode_network_prefix(cls, network_id: int) -> NetworkPrefix:
if network_id == MAINNET_NETWORK_ID:
return MAINNET_PREFIX
elif network_id == TESTNET_NETWORK_ID:
return TESTNET_PREFIX
else:
return COMMON_NET_PREFIX + str(network_id)
@classmethod
def _network_prefix_to_id(cls, network_prefix: NetworkPrefix) -> int:
network_prefix = network_prefix.lower()
if network_prefix == MAINNET_PREFIX:
return MAINNET_NETWORK_ID
elif network_prefix == TESTNET_PREFIX:
return TESTNET_NETWORK_ID
else:
if not network_prefix.startswith(COMMON_NET_PREFIX):
raise InvalidBase32Address(f"The network prefix {network_prefix} is invalid")
try:
return int(network_prefix[3:])
except:
raise InvalidBase32Address(f"The network prefix {network_prefix} is invalid")
@classmethod
def _create_checksum(cls, prefix: NetworkPrefix, payload: str) -> str:
"""
create checksum from prefix and payload
:param prefix: network prefix (string)
:param payload: bytes
:return: string
"""
delimiter = VERSION_BYTE
template = CHECKSUM_TEMPLATE
mod = cls._poly_mod(cls._prefix_to_words(prefix) + delimiter + base32.decode_to_words(payload) + template)
return base32.encode(cls._checksum_to_bytes(mod))
@classmethod
def _detect_address_type(cls, hex_address_buf: bytes) -> AddressType:
if hex_address_buf == bytes(20):
return TYPE_NULL
first_byte = hex_address_buf[0] & 0xf0
if first_byte == 0x00:
return TYPE_BUILTIN
elif first_byte == 0x10:
return TYPE_USER
elif first_byte == 0x80:
return TYPE_CONTRACT
else:
return TYPE_INVALID
@classmethod
def _prefix_to_words(cls, prefix: NetworkPrefix) -> bytearray:
words = bytearray()
for v in bytes(prefix, 'ascii'):
words.append(v & 0x1f)
return words
@classmethod
def _checksum_to_bytes(cls, data: int) -> bytearray:
result = bytearray(0)
result.append((data >> 32) & 0xff)
result.append((data >> 24) & 0xff)
result.append((data >> 16) & 0xff)
result.append((data >> 8) & 0xff)
result.append((data) & 0xff)
return result
@classmethod
def _poly_mod(cls, v: Union[bytes, bytearray]) -> int:
"""
:param v: bytes
:return: int64
"""
assert type(v) == bytes or type(v) == bytearray
c = 1
for d in v:
c0 = c >> 35
c = ((c & 0x07ffffffff) << 5) ^ d
if c0 & 0x01:
c ^= 0x98f2bc8e61
if c0 & 0x02:
c ^= 0x79b76d99e2
if c0 & 0x04:
c ^= 0xf33e5fb3c4
if c0 & 0x08:
c ^= 0xae2eabe2a8
if c0 & 0x10:
c ^= 0x1e4f43e470
return c ^ 1
@classmethod
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: "GetCoreSchemaHandler"
) -> "CoreSchema":
from pydantic_core.core_schema import (
no_info_plain_validator_function
)
def custom_handler(value: Any):
if isinstance(value, Base32Address):
return value
address_type = cls.decode(value)["address_type"]
if address_type == TYPE_INVALID:
raise ValueError(f"Invalid address type: {address_type}, please convert responding address to Base32Address using Base32Address(address, _ignore_invalid_type=True)")
return cls(value)
return no_info_plain_validator_function(custom_handler)
def get_base32_address_factory(default_network_id: int) -> Type[Base32Address]:
"""
Generate a new :class:`~Base32Address` class object with `default_network_id`
so the class variable will not influence the global :attr:`~Base32Address.default_network_id` setting
>>> base32_address_factory = get_base32_address_factory(default_network_id=1)
>>> base32_address_factory.zero_address()
'cfxtest:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa6f0vrcsw'
:param int default_network_id: default network
:return Type[Base32Address]: a Class object of Base32Address with default_network_id
"""
return type(
"Base32Address",
(Base32Address,),
{
"_default_network_id": default_network_id
}
)