"""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}")