From 8cc7be00d13ce006649f1b508e6eb740ed11ae4e Mon Sep 17 00:00:00 2001 From: Santiago Date: Thu, 30 Jul 2026 09:57:24 -0300 Subject: [PATCH] fix(tii): accept native byte arrays in the bytes arg encoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native `bytes`/`bytearray` values (and integer lists — the JSON shape other SDKs' native byte arrays serialize to) were rejected by the encoder, so byte params could only be supplied as hex strings. Canonicalize them to 0x-prefixed hex, per SDK spec §3.9 value marshalling — mirroring the rust-sdk/web-sdk change. Covers the Hydra `init` participants/parties/head_id and Asteria create_ship pilot/ship-name shapes with regression tests (TRP `(-32005) value is not bytes`). Co-Authored-By: Claude Fable 5 --- sdk/src/tx3_sdk/tii/encode.py | 22 ++++++++++++++++- sdk/tests/test_encode.py | 46 +++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/sdk/src/tx3_sdk/tii/encode.py b/sdk/src/tx3_sdk/tii/encode.py index 67a38b5..ed352bf 100644 --- a/sdk/src/tx3_sdk/tii/encode.py +++ b/sdk/src/tx3_sdk/tii/encode.py @@ -46,6 +46,19 @@ def _wrong_shape(kind: str, expected: str, value: Any) -> EncodeArgError: ) +def _as_byte_array(value: Any) -> bytes | None: + """Interprets a value as a raw byte array: ``bytes``/``bytearray``, or a + list whose every element is an integer in ``0..=255``. ``None`` if it is + neither.""" + if isinstance(value, (bytes, bytearray)): + return bytes(value) + if isinstance(value, list) and all( + isinstance(b, int) and not isinstance(b, bool) and 0 <= b <= 255 for b in value + ): + return bytes(value) + return None + + def _leaf(tag: str, value: Any, nested: bool) -> Any: """Renders a scalar leaf: bare at the top level (the resolver knows the param's flat type), tagged when nested inside an aggregate (it doesn't).""" @@ -88,7 +101,14 @@ def _marshal(param: ParamType, value: Any, nested: bool) -> Any: # Hex string or a BytesEnvelope object. if isinstance(value, (str, dict)): return _leaf("bytes", value, nested) - raise _wrong_shape("bytes", "hex string or bytes envelope", value) + # A native byte array (`bytes`/`bytearray`, or an integer list — the + # JSON shape other SDKs' native byte arrays serialize to) canonicalizes + # to 0x-prefixed hex, the wire form the resolver coerces (SDK spec + # §3.9). + raw = _as_byte_array(value) + if raw is not None: + return _leaf("bytes", f"0x{raw.hex()}", nested) + raise _wrong_shape("bytes", "hex string, bytes envelope, or byte array", value) if kind is ParamKind.ADDRESS: if isinstance(value, str): diff --git a/sdk/tests/test_encode.py b/sdk/tests/test_encode.py index de904a3..964bc48 100644 --- a/sdk/tests/test_encode.py +++ b/sdk/tests/test_encode.py @@ -97,3 +97,49 @@ def test_unit_lowers_to_nullary_struct() -> None: assert encode(param_type_from_schema({"type": "null"}), None) == { "struct": {"constructor": 0, "fields": []} } + + +BYTES_SCHEMA = {"$ref": "https://tx3.land/specs/v1beta0/tii#/$defs/Bytes"} +LIST_OF_BYTES_SCHEMA = {"type": "array", "items": BYTES_SCHEMA} + + +def test_native_byte_arrays_canonicalize_to_hex() -> None: + # A native byte array (`bytes`, or an integer list — the JSON shape other + # SDKs' native byte arrays serialize to) canonicalizes to 0x-prefixed hex + # (regression: TRP `(-32005) value is not bytes: [1,1]`). + bytes_param = param_type_from_schema(BYTES_SCHEMA) + assert encode(bytes_param, b"\x01\x01") == "0x0101" + assert encode(bytes_param, bytearray(b"\x01\x01")) == "0x0101" + assert encode(bytes_param, [1, 1]) == "0x0101" + + list_param = param_type_from_schema(LIST_OF_BYTES_SCHEMA) + assert encode(list_param, [b"\x01\x02"]) == {"list": [{"bytes": "0x0102"}]} + + +def test_rejects_non_byte_arrays_for_bytes() -> None: + bytes_param = param_type_from_schema(BYTES_SCHEMA) + for bad in ([1, 256], [1, -1], ["aa", 1], [True], 42): + with pytest.raises(EncodeArgError): + encode(bytes_param, bad) + + +def test_hydra_init_arg_shapes() -> None: + # Hydra `init`: `participants` / `parties` are `List`, `head_id` is + # `Bytes` (regression: `(-32005) target type not supported: List` / + # `value is not bytes: [1,2]`). + list_param = param_type_from_schema(LIST_OF_BYTES_SCHEMA) + assert encode(list_param, ["0102", "0304"]) == { + "list": [{"bytes": "0102"}, {"bytes": "0304"}] + } + assert encode(list_param, [b"\x01\x02"]) == {"list": [{"bytes": "0x0102"}]} + + bytes_param = param_type_from_schema(BYTES_SCHEMA) + assert encode(bytes_param, "abcd0123") == "abcd0123" + + +def test_asteria_name_arg_shapes() -> None: + # Asteria `create_ship`: `ship_name` / `pilot_name` are `Bytes` params + # (regression: `(-32005) value is not bytes: [1,1]`). + bytes_param = param_type_from_schema(BYTES_SCHEMA) + assert encode(bytes_param, "53484950313233") == "53484950313233" + assert encode(bytes_param, b"SHIP") == "0x53484950"