Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/scripts/codegen-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/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 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.
#
# Requires `tx3c` and `python` 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"

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' \
'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 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)
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"
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,27 @@ jobs:
working-directory: ./sdk
run: pytest tests -m "not e2e"

codegen:
name: codegen
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: Setup tx3 toolchain
uses: tx3-lang/actions/setup@v1

- name: Render and verify codegen plugin
run: bash .github/scripts/codegen-check.sh

e2e:
name: e2e
runs-on: ubuntu-latest
Expand Down
113 changes: 73 additions & 40 deletions .trix/client-lib/__init__.py.hbs
Original file line number Diff line number Diff line change
@@ -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)
Loading