From 8e33ac605bf147d0260c70160a6aee399a071056 Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 20 May 2026 06:34:33 -0300 Subject: [PATCH 1/7] feat(codegen): port client-lib plugin to TII v1beta0 The .trix/client-lib plugin still rendered against the legacy bindgen-v1alpha2 data shape, so every transaction was silently dropped against a v1beta0 TII. Rewrites the template to consume tii.transactions / tii.profiles / tii.environment, emits protocol identity constants and an embedded profile surface wired into the Client, and adds a render-fixture test that imports the generated module. Co-Authored-By: Claude Opus 4.7 (1M context) --- .trix/client-lib/__init__.py.hbs | 113 ++++++++++++++++++++----------- sdk/tests/test_codegen.py | 73 ++++++++++++++++++++ 2 files changed, 146 insertions(+), 40 deletions(-) create mode 100644 sdk/tests/test_codegen.py diff --git a/.trix/client-lib/__init__.py.hbs b/.trix/client-lib/__init__.py.hbs index 1097127..8ea89a6 100644 --- a/.trix/client-lib/__init__.py.hbs +++ b/.trix/client-lib/__init__.py.hbs @@ -1,55 +1,88 @@ -# This file is auto-generated. +# This file is auto-generated by trix codegen. +# Target TII version: {{tii.tii.version}} +from __future__ import annotations + +import json from dataclasses import dataclass -from typing import Dict, Any -from tx3_sdk.trp import Client as TRPClient, TxEnvelope, SubmitResponse, SubmitParams +from typing import Any -DEFAULT_TRP_ENDPOINT = "{{trpEndpoint}}" +from tx3_sdk.core.bytes import TirEnvelope +from tx3_sdk.trp.client import TrpClient +from tx3_sdk.trp.spec import ResolveParams, SubmitParams, SubmitResponse, TxEnvelope -DEFAULT_HEADERS = { -{{#each headers}} - "{{@key}}": "{{this}}", -{{/each}} -} +PROTOCOL_NAME = "{{tii.protocol.name}}" +PROTOCOL_VERSION = "{{tii.protocol.version}}" +TARGET_TII_VERSION = "{{tii.tii.version}}" -DEFAULT_ENV_ARGS = { -{{#each envArgs}} - "{{@key}}": "{{this}}", -{{/each}} -} +{{#if tii.environment}} +# JSON Schema of the protocol environment, embedded verbatim from the TII. +ENVIRONMENT_SCHEMA: dict[str, Any] = json.loads( + """{{{json tii.environment}}}""" +) -{{#each transactions}} +{{/if}} +# Profiles embedded from the TII, keyed by name. Each value carries that +# profile's `environment` values and `parties` addresses. +PROFILES: dict[str, dict[str, Any]] = json.loads( + """{{{json tii.profiles}}}""" +) + +{{#each tii.transactions}} @dataclass -class {{pascalCase params_name}}: -{{#each parameters}} - {{snakeCase name}}: {{typeFor type_name "python"}} # {{type_name}} +class {{pascalCase @key}}Params: + """Arguments for the {{@key}} transaction.""" + +{{#each params.properties}} + {{snakeCase @key}}: {{schemaTypeFor this "python"}} {{/each}} -{{constantCase constant_name}} = { - "content": "{{ir_bytes}}", - "encoding": "hex", - "version": "{{ir_version}}", -} + +{{constantCase @key}}_TIR = TirEnvelope( + content="{{tir.content}}", + encoding="{{tir.encoding}}", + version="{{tir.version}}", +) {{/each}} class Client: - def __init__(self, options: Dict[str, Any]): - self._client = TRPClient(options) -{{#each transactions}} - - async def {{snakeCase function_name}}(self, args: {{pascalCase params_name}}) -> TxEnvelope: - return await self._client.resolve({ - "tir": {{constantCase constant_name}}, - "args": vars(args), - }) + """Thin protocol facade over the TRP client.""" + + def __init__( + self, + endpoint: str, + headers: dict[str, str] | None = None, + profile: str | None = None, + ) -> None: + self._trp = TrpClient(endpoint=endpoint, headers=headers) + self._environment: dict[str, Any] | None = None + self._parties: dict[str, str] = {} + if profile is not None: + if profile not in PROFILES: + raise ValueError(f"unknown profile {profile!r}") + selected = PROFILES[profile] + self._environment = selected.get("environment") or None + self._parties = selected.get("parties") or {} + + async def _resolve(self, tir: TirEnvelope, args: dict[str, Any]) -> TxEnvelope: + merged: dict[str, Any] = {**self._parties, **args} + return await self._trp.resolve( + ResolveParams(tir=tir, args=merged, env=self._environment) + ) +{{#each tii.transactions}} + + async def {{snakeCase @key}}(self, args: {{pascalCase @key}}Params) -> TxEnvelope: + """Resolves the {{@key}} transaction.""" + return await self._resolve( + {{constantCase @key}}_TIR, + { +{{#each params.properties}} + "{{@key}}": args.{{snakeCase @key}}, +{{/each}} + }, + ) {{/each}} async def submit(self, params: SubmitParams) -> SubmitResponse: - return await self._client.submit(params) - -# Create a default client instance -protocol = Client({ - "endpoint": DEFAULT_TRP_ENDPOINT, - "headers": DEFAULT_HEADERS, - "env_args": DEFAULT_ENV_ARGS, -}) + """Submits a signed transaction to the network.""" + return await self._trp.submit(params) diff --git a/sdk/tests/test_codegen.py b/sdk/tests/test_codegen.py new file mode 100644 index 0000000..a7587e1 --- /dev/null +++ b/sdk/tests/test_codegen.py @@ -0,0 +1,73 @@ +"""Render-fixture test for the .trix/client-lib codegen plugin.""" + +from __future__ import annotations + +import importlib.util +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + + +def _resolve_tx3c() -> str: + """Locates the tx3c binary: TX3_TX3C_PATH first, then $PATH.""" + from_env = os.environ.get("TX3_TX3C_PATH") + if from_env and Path(from_env).is_file(): + return from_env + found = shutil.which("tx3c") + if found: + return found + raise RuntimeError("tx3c not found; set TX3_TX3C_PATH or install tx3c") + + +def test_codegen_client_lib_renders_and_imports() -> None: + """Renders the plugin against the shared fixture and imports the result. + + A successful render that produces an unloadable module is a failure. + """ + sdk_dir = Path(__file__).resolve().parent.parent + repo_root = sdk_dir.parent + template_dir = repo_root / ".trix" / "client-lib" + tii_path = sdk_dir / "tests" / "fixtures" / "transfer.tii" + + assert tii_path.is_file(), f"missing TII fixture: {tii_path}" + assert template_dir.is_dir(), f"missing template directory: {template_dir}" + + with tempfile.TemporaryDirectory() as out_dir: + subprocess.run( + [ + _resolve_tx3c(), + "codegen", + "--tii", + str(tii_path), + "--template", + str(template_dir), + "--output", + out_dir, + ], + check=True, + capture_output=True, + text=True, + ) + + generated = Path(out_dir) / "__init__.py" + assert generated.is_file(), "expected generated __init__.py" + + spec = importlib.util.spec_from_file_location("tx3_generated_protocol", generated) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + try: + spec.loader.exec_module(module) + finally: + sys.modules.pop(spec.name, None) + + assert module.TARGET_TII_VERSION == "v1beta0" + assert set(module.PROFILES) == {"local", "preprod"} + assert module.TRANSFER_TIR.version == "v1beta0" + params = module.TransferParams(quantity=1_000_000) + assert params.quantity == 1_000_000 + assert hasattr(module.Client, "transfer") + assert hasattr(module.Client, "submit") From abc964d34f4840b52bf6527f5c6681e7d8ca895f Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 20 May 2026 06:43:29 -0300 Subject: [PATCH 2/7] ci: install tx3 toolchain in the unit job for the codegen test The codegen render-fixture test shells out to `tx3c`; the unit job did not have it on PATH. Adds the tx3-lang/actions/setup step. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcb1475..faf8068 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,9 @@ jobs: python -m pip install --upgrade pip python -m pip install -e .[dev] + - name: Setup tx3 toolchain + uses: tx3-lang/actions/setup@v1 + - name: Run unit tests working-directory: ./sdk run: pytest tests -m "not e2e" From 8372c417ab3fb37ca313ebd8b37efd66004866fc Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 20 May 2026 06:51:55 -0300 Subject: [PATCH 3/7] ci: pin tx3 toolchain to the beta channel The codegen render-fixture test needs the `json` renderer helper, which ships in the beta channel ahead of stable. Tracks beta until the helper reaches the stable toolchain. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index faf8068..f706104 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,8 @@ jobs: - name: Setup tx3 toolchain uses: tx3-lang/actions/setup@v1 + with: + channel: beta - name: Run unit tests working-directory: ./sdk From 6fcc56d8c180f6a2b7961f68eb293db3334d502e Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 20 May 2026 07:13:30 -0300 Subject: [PATCH 4/7] test(codegen): move render-fixture test to a dedicated CI job The render-fixture test exercises the .trix/client-lib templates and tx3c, not the SDK runtime, so it does not belong in the unit job. Marks it with the `codegen` pytest marker, excludes it from the unit job, and runs it in a new `codegen` CI job. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 29 +++++++++++++++++++++++++++-- sdk/pyproject.toml | 1 + sdk/tests/test_codegen.py | 3 +++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f706104..22a12c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,31 @@ jobs: name: unit runs-on: ubuntu-latest timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + cache-dependency-path: sdk/pyproject.toml + + - name: Install dependencies + working-directory: ./sdk + run: | + python -m pip install --upgrade pip + python -m pip install -e .[dev] + + - name: Run unit tests + working-directory: ./sdk + run: pytest tests -m "not e2e and not codegen" + + codegen: + name: codegen + runs-on: ubuntu-latest + timeout-minutes: 15 steps: - name: Checkout repository uses: actions/checkout@v4 @@ -37,9 +62,9 @@ jobs: with: channel: beta - - name: Run unit tests + - name: Run codegen render-fixture test working-directory: ./sdk - run: pytest tests -m "not e2e" + run: pytest tests -m codegen e2e: name: e2e diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index 44b343d..dbce47b 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -42,6 +42,7 @@ tx3_sdk = ["py.typed"] testpaths = ["tests"] markers = [ "e2e: marks tests that require a live TRP endpoint", + "codegen: marks the codegen render-fixture test (exercises the templates + tx3c)", ] [tool.ruff] diff --git a/sdk/tests/test_codegen.py b/sdk/tests/test_codegen.py index a7587e1..86afbe0 100644 --- a/sdk/tests/test_codegen.py +++ b/sdk/tests/test_codegen.py @@ -10,6 +10,8 @@ import tempfile from pathlib import Path +import pytest + def _resolve_tx3c() -> str: """Locates the tx3c binary: TX3_TX3C_PATH first, then $PATH.""" @@ -22,6 +24,7 @@ def _resolve_tx3c() -> str: raise RuntimeError("tx3c not found; set TX3_TX3C_PATH or install tx3c") +@pytest.mark.codegen def test_codegen_client_lib_renders_and_imports() -> None: """Renders the plugin against the shared fixture and imports the result. From 6d370746bbdc8d36881cd310b6c11a19fb1d3c19 Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 20 May 2026 07:27:04 -0300 Subject: [PATCH 5/7] test(codegen): drive codegen check from CI, not the SDK test suite The render-fixture check exercises the .trix/client-lib templates and tx3c, not the SDK runtime, so it should not be SDK test code. Removes the pytest test and marker and replaces them with a CI-owned shell script that the `codegen` job runs: it invokes `tx3c codegen` directly, smoke-checks the generated surface, and imports the output against this repo's SDK. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/scripts/codegen-check.sh | 49 ++++++++++++++++++++ .github/workflows/ci.yml | 11 +++-- sdk/pyproject.toml | 1 - sdk/tests/test_codegen.py | 76 -------------------------------- 4 files changed, 54 insertions(+), 83 deletions(-) create mode 100644 .github/scripts/codegen-check.sh delete mode 100644 sdk/tests/test_codegen.py diff --git a/.github/scripts/codegen-check.sh b/.github/scripts/codegen-check.sh new file mode 100644 index 0000000..2b9ef19 --- /dev/null +++ b/.github/scripts/codegen-check.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# +# CI artifact — not part of the SDK. +# +# Renders the .trix/client-lib codegen plugin against the shared transfer +# fixture and verifies the result. The subject under test is the Handlebars +# templates + tx3c integration, not the SDK runtime. +# +# Steps: invoke `tx3c codegen`, assert the expected file exists, smoke-check +# the generated surface, and import the output against this repo's SDK. +# +# Requires `tx3c` and a `python` with this repo's SDK installed on PATH. +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +gen="$(mktemp -d)" +trap 'rm -rf "$gen"' EXIT + +tx3c codegen \ + --tii "$repo_root/sdk/tests/fixtures/transfer.tii" \ + --template "$repo_root/.trix/client-lib" \ + --output "$gen" + +test -f "$gen/__init__.py" || { echo "missing generated file: __init__.py"; exit 1; } + +for sym in \ + 'TARGET_TII_VERSION' \ + 'PROFILES' \ + 'TRANSFER_TIR' \ + 'class TransferParams' \ + 'class Client'; do + grep -qF "$sym" "$gen/__init__.py" || { echo "generated __init__.py missing: $sym"; exit 1; } +done + +# Import the rendered module to confirm it loads against this repo's SDK. +python -c " +import importlib.util, sys +spec = importlib.util.spec_from_file_location('tx3_generated_protocol', '$gen/__init__.py') +module = importlib.util.module_from_spec(spec) +sys.modules[spec.name] = module +spec.loader.exec_module(module) +assert module.TARGET_TII_VERSION == 'v1beta0', module.TARGET_TII_VERSION +assert set(module.PROFILES) == {'local', 'preprod'}, module.PROFILES +assert module.TRANSFER_TIR.version == 'v1beta0' +assert module.TransferParams(quantity=1).quantity == 1 +print('generated module imported OK') +" + +echo "codegen check passed" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22a12c0..54cefe6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - name: Run unit tests working-directory: ./sdk - run: pytest tests -m "not e2e and not codegen" + run: pytest tests -m "not e2e" codegen: name: codegen @@ -51,20 +51,19 @@ jobs: cache: pip cache-dependency-path: sdk/pyproject.toml - - name: Install dependencies + - name: Install SDK working-directory: ./sdk run: | python -m pip install --upgrade pip - python -m pip install -e .[dev] + python -m pip install -e . - name: Setup tx3 toolchain uses: tx3-lang/actions/setup@v1 with: channel: beta - - name: Run codegen render-fixture test - working-directory: ./sdk - run: pytest tests -m codegen + - name: Render and verify codegen plugin + run: bash .github/scripts/codegen-check.sh e2e: name: e2e diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index dbce47b..44b343d 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -42,7 +42,6 @@ tx3_sdk = ["py.typed"] testpaths = ["tests"] markers = [ "e2e: marks tests that require a live TRP endpoint", - "codegen: marks the codegen render-fixture test (exercises the templates + tx3c)", ] [tool.ruff] diff --git a/sdk/tests/test_codegen.py b/sdk/tests/test_codegen.py deleted file mode 100644 index 86afbe0..0000000 --- a/sdk/tests/test_codegen.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Render-fixture test for the .trix/client-lib codegen plugin.""" - -from __future__ import annotations - -import importlib.util -import os -import shutil -import subprocess -import sys -import tempfile -from pathlib import Path - -import pytest - - -def _resolve_tx3c() -> str: - """Locates the tx3c binary: TX3_TX3C_PATH first, then $PATH.""" - from_env = os.environ.get("TX3_TX3C_PATH") - if from_env and Path(from_env).is_file(): - return from_env - found = shutil.which("tx3c") - if found: - return found - raise RuntimeError("tx3c not found; set TX3_TX3C_PATH or install tx3c") - - -@pytest.mark.codegen -def test_codegen_client_lib_renders_and_imports() -> None: - """Renders the plugin against the shared fixture and imports the result. - - A successful render that produces an unloadable module is a failure. - """ - sdk_dir = Path(__file__).resolve().parent.parent - repo_root = sdk_dir.parent - template_dir = repo_root / ".trix" / "client-lib" - tii_path = sdk_dir / "tests" / "fixtures" / "transfer.tii" - - assert tii_path.is_file(), f"missing TII fixture: {tii_path}" - assert template_dir.is_dir(), f"missing template directory: {template_dir}" - - with tempfile.TemporaryDirectory() as out_dir: - subprocess.run( - [ - _resolve_tx3c(), - "codegen", - "--tii", - str(tii_path), - "--template", - str(template_dir), - "--output", - out_dir, - ], - check=True, - capture_output=True, - text=True, - ) - - generated = Path(out_dir) / "__init__.py" - assert generated.is_file(), "expected generated __init__.py" - - spec = importlib.util.spec_from_file_location("tx3_generated_protocol", generated) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = module - try: - spec.loader.exec_module(module) - finally: - sys.modules.pop(spec.name, None) - - assert module.TARGET_TII_VERSION == "v1beta0" - assert set(module.PROFILES) == {"local", "preprod"} - assert module.TRANSFER_TIR.version == "v1beta0" - params = module.TransferParams(quantity=1_000_000) - assert params.quantity == 1_000_000 - assert hasattr(module.Client, "transfer") - assert hasattr(module.Client, "submit") From 465468764edf2ac6eb67370436a87600d002b119 Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 20 May 2026 07:46:16 -0300 Subject: [PATCH 6/7] test(codegen): verify against the published SDK, not in-repo source The codegen check must replicate what a consumer gets: the rendered output resolves the published runtime SDK at the version its generated manifest pins. Drops the patch / replace / paths / editable-install overrides that bound the check to in-repo SDK source. This also exercises the version pin in the generated manifest itself. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/scripts/codegen-check.sh | 22 +++++++++++++--------- .github/workflows/ci.yml | 6 ------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/scripts/codegen-check.sh b/.github/scripts/codegen-check.sh index 2b9ef19..3f27ae3 100644 --- a/.github/scripts/codegen-check.sh +++ b/.github/scripts/codegen-check.sh @@ -3,13 +3,11 @@ # CI artifact — not part of the SDK. # # Renders the .trix/client-lib codegen plugin against the shared transfer -# fixture and verifies the result. The subject under test is the Handlebars -# templates + tx3c integration, not the SDK runtime. +# fixture and verifies the result the way a consumer would: the rendered module +# is imported in a fresh venv with the dependencies its generated +# requirements.txt pins installed — no editable install of the SDK source tree. # -# Steps: invoke `tx3c codegen`, assert the expected file exists, smoke-check -# the generated surface, and import the output against this repo's SDK. -# -# Requires `tx3c` and a `python` with this repo's SDK installed on PATH. +# Requires `tx3c` and `python` on PATH. set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" @@ -21,7 +19,9 @@ tx3c codegen \ --template "$repo_root/.trix/client-lib" \ --output "$gen" -test -f "$gen/__init__.py" || { echo "missing generated file: __init__.py"; exit 1; } +for f in __init__.py requirements.txt; do + test -f "$gen/$f" || { echo "missing generated file: $f"; exit 1; } +done for sym in \ 'TARGET_TII_VERSION' \ @@ -32,8 +32,12 @@ for sym in \ grep -qF "$sym" "$gen/__init__.py" || { echo "generated __init__.py missing: $sym"; exit 1; } done -# Import the rendered module to confirm it loads against this repo's SDK. -python -c " +# Import the rendered module in a fresh venv with the dependencies its generated +# requirements.txt pins, exactly as an end user would consume it. +python -m venv "$gen/venv" +"$gen/venv/bin/pip" install --quiet --upgrade pip +"$gen/venv/bin/pip" install --quiet -r "$gen/requirements.txt" +"$gen/venv/bin/python" -c " import importlib.util, sys spec = importlib.util.spec_from_file_location('tx3_generated_protocol', '$gen/__init__.py') module = importlib.util.module_from_spec(spec) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54cefe6..0dd7f20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,12 +51,6 @@ jobs: cache: pip cache-dependency-path: sdk/pyproject.toml - - name: Install SDK - working-directory: ./sdk - run: | - python -m pip install --upgrade pip - python -m pip install -e . - - name: Setup tx3 toolchain uses: tx3-lang/actions/setup@v1 with: From 2cda12939e54bf8dc5a4783190a19246f7fba7ae Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 20 May 2026 08:00:17 -0300 Subject: [PATCH 7/7] ci: track the stable tx3 channel for the codegen check Reverts the codegen job's toolchain setup to the default stable channel. The check stays red until a tx3c release carrying the codegen `json` helper reaches stable. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dd7f20..83e2165 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,8 +53,6 @@ jobs: - name: Setup tx3 toolchain uses: tx3-lang/actions/setup@v1 - with: - channel: beta - name: Render and verify codegen plugin run: bash .github/scripts/codegen-check.sh