Source code for cashscript_py.helpers.argument_encoding

"""CashScript ABI argument encoding.

Key behavior:
- 'int' encodes to Script Number (little-endian, minimal-length).
- 'bool' encodes to 0x00/0x01.
- 'string' encodes to UTF-8 bytes.
- 'pubkey' accepts raw bytes or hex string (33/65 bytes).
- 'bytes'/'bytesN' accept raw bytes or hex string; bytesN enforces exact length.
- 'sig' accepts SignatureTemplate, or raw bytes with valid lengths {0,65,71,72,73}.
- 'datasig' accepts raw bytes with valid lengths {0,64,70,71,72}.
"""

from typing import cast

from cashscript_py.helpers.script import encode_int as encode_script_number
from cashscript_py.signature_template import SignatureTemplate


[docs] class ArgumentTypeError(Exception): """Raised when an ABI argument's Python type does not match the expected CashScript type.""" def __init__(self, actual_type: str, expected_type: str): """Initialize the type error with actual and expected type names.""" super().__init__(f"Expected type `{expected_type}` but got `{actual_type}`")
ConstructorArgument = bool | int | str | bytes FunctionArgument = ConstructorArgument | SignatureTemplate def _parse_type(type_str: str) -> tuple[str, int | None]: """Parse a CashScript type string. Args: type_str: Type string (e.g., "int", "bytes", "bytes20", "pubkey", "sig"). Returns: A tuple of (base_type, bound) where bound is the integer size for bytesN or None. """ import re m = re.match(r"bytes(\d+)$", type_str) if m: return ("bytes", int(m.group(1))) return (type_str, None) def _encode_bool(value: bool) -> bytes: return b"\x01" if value else b"\x00" def _encode_string(value: str) -> bytes: return value.encode("utf-8") def _encode_pubkey(value: bytes | str) -> bytes: return value if isinstance(value, bytes) else bytes.fromhex(value)
[docs] def encode_constructor_arguments( contract_params: list[ConstructorArgument], constructor_inputs: list[dict[str, str]] ) -> list[bytes]: """Encode constructor arguments according to the artifact's constructor types. Args: contract_params: Values provided to the contract constructor. constructor_inputs: Type descriptors (in declaration order) from the artifact. Returns: Encoded arguments as raw bytes (ABI format). Raises: ValueError: If a signature type is used in the constructor (disallowed) or types mismatch. """ if "sig" in [i["type"] for i in constructor_inputs]: raise ValueError("Cannot use signatures in constructor") return [ cast(bytes, encode_function_argument(value, arg["type"])) for value, arg in zip(contract_params, constructor_inputs, strict=False) ]
[docs] def encode_function_argument(argument: FunctionArgument, type_str: str) -> bytes | SignatureTemplate: """Encode a single function argument for the CashScript ABI. Args: argument: The argument value (bytes/hex/string/int/bool/SigTemplate). type_str: The ABI type string (e.g., "int", "bool", "string", "bytesN", "pubkey", "sig", "datasig"). Returns: Encoded bytes or a SignatureTemplate (for "sig"). Raises: ArgumentTypeError: If the value type does not match the expected ABI type. ValueError: If bounded bytesN lengths, signature lengths, or unknown types are invalid. """ # Normalize to bytes (convert hex string to bytes) def _normalize_bytes(value: FunctionArgument) -> bool | int | bytes | SignatureTemplate: if isinstance(value, str): value = value[2:] if value.startswith("0x") else value value = bytes.fromhex(value) return value t, bound = _parse_type(type_str) # Special handling for signature-like types if t == "sig": if isinstance(argument, SignatureTemplate): return argument argument = _normalize_bytes(argument) if not isinstance(argument, bytes): raise ArgumentTypeError(type(argument).__name__, "sig") # Allowed lengths: NULLFAIL (0), Schnorr+hashtype (65), ECDSA DER+hashtype (71/72/73) if len(argument) not in {0, 65, 71, 72, 73}: raise ValueError(f"Expected sig length in {{0,65,71,72,73}} but got {len(argument)}") return argument if t == "datasig": argument = _normalize_bytes(argument) if not isinstance(argument, bytes): raise ArgumentTypeError(type(argument).__name__, "datasig") # Allowed lengths: NULLFAIL (0), Schnorr (64), ECDSA DER (70/71/72) if len(argument) not in {0, 64, 70, 71, 72}: raise ValueError(f"Expected datasig length in {{0,64,70,71,72}} but got {len(argument)}") return argument if t == "int": if not isinstance(argument, int): raise ArgumentTypeError(type(argument).__name__, "int") return bytes(encode_script_number(argument)) if t == "bool": if not isinstance(argument, bool): raise ArgumentTypeError(type(argument).__name__, "bool") return _encode_bool(argument) if t == "string": if not isinstance(argument, str): raise ArgumentTypeError(type(argument).__name__, "string") return _encode_string(argument) if t == "pubkey": if not isinstance(argument, (bytes, str)): raise ArgumentTypeError(type(argument).__name__, "pubkey") return _encode_pubkey(argument) if t == "bytes": # Ensure correct type first – use the full type_str (e.g., 'bytes20') in error for clarity argument = _normalize_bytes(argument) if not isinstance(argument, bytes): raise ArgumentTypeError(type(argument).__name__, type_str) # Enforce bounded length for bytesN if bound is not None and len(argument) != bound: raise ValueError(f"Expected bytes of length {bound} for type `{type_str}` but got {len(argument)}") return argument raise ValueError(f"Unknown type: {type_str}")