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
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,7 @@
[submodule "protocols/githoney-protocols"]
path = protocols/githoney
url = git@github.com:open-tx3/githoney-protocols.git
[submodule "tooling/cshell"]
path = tooling/cshell
url = git@github.com:txpipe/cshell.git
branch = main
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ working inside that group.

- `core/` → wire-format specs: `tii`, `tir`, `trp`. See [`core/AGENTS.md`](./core/AGENTS.md).
- `lang/` → the Tx3 language: `tx3`. See [`lang/AGENTS.md`](./lang/AGENTS.md).
- `tooling/` → toolchain binaries and developer tools: `trix`, `tx3up`, `tx3-lsp`, `tx3-mcp`, `tx3-lift`. See [`tooling/AGENTS.md`](./tooling/AGENTS.md).
- `tooling/` → toolchain binaries and developer tools: `trix`, `tx3up`, `tx3-lsp`, `tx3-mcp`, `tx3-lift`, `cshell`. See [`tooling/AGENTS.md`](./tooling/AGENTS.md).
- `plugins/` → editor, CI, and agent integrations: `vscode-tx3`, `tx3-skills`, `actions`. See [`plugins/AGENTS.md`](./plugins/AGENTS.md).
- `backends/` → transaction execution backends and gateways: `tx3-hydra`, `protocol-gateway`. See [`backends/AGENTS.md`](./backends/AGENTS.md).
- `protocols/` → third-party Tx3 protocol definitions from the `open-tx3` org: `indigo`, `strike`, `bodega`, `fluid`, `vyfi`, `snek-fun`, `acme`, `githoney`. See [`protocols/AGENTS.md`](./protocols/AGENTS.md).
- `backends/` → transaction execution backends and gateways: `tx3-hydra`, `protocol-gateway`, `dolos`. See [`backends/AGENTS.md`](./backends/AGENTS.md).
- `protocols/` → third-party Tx3 protocol definitions from the `open-tx3` org: `indigo`, `strike`, `bodega`, `fluid`, `vyfi`, `snek-fun`, `acme`, `githoney`, `txpipe`. See [`protocols/AGENTS.md`](./protocols/AGENTS.md).

Two submodules and one subtree sit outside the groupings:

Expand Down Expand Up @@ -69,6 +69,7 @@ Available skills:
- `skills/publish-docs-site/` — publish the latest Tx3 docs to the company-wide docs site (`docs.txpipe.io`) by triggering the `txpipe/docs` `update-submodules` workflow.
- `skills/commit-umbrella/` — commit the umbrella repo after submodule pointers move, pre-checking that submodules are pushed, track latest `main`, and that grouping `AGENTS.md` routing is up to date.
- `skills/add-language-feature/` — roll out a new Tx3 language feature (operator, expression form, builtin) across every toolchain layer: spec, grammar/AST, analysis/lowering, TIR/reduction, downstream consumers, docs, and agent skills.
- `skills/release-toolchain/` — interactively orchestrate a cross-cutting toolchain release: sequence the crates.io publish waves (tir → tx3-lang & siblings → lsp/mcp/registry), bump dependency pins in lockstep, raise the `trix` version floor, and hand off to `commit-umbrella` for the pointer bump. Pauses at each publish gate for the developer.
- `sdks/skills/` — SDK-fleet skills (`add-sdk-feature`, `audit-parity`, `propagate-change`, `release-synced`, `release-sdk-patch`, `run-e2e-tests`, `scaffold-new-sdk`).

## Scope of this repo
Expand Down
127 changes: 127 additions & 0 deletions plans/sdk-complex-type-followups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Plan: SDK complex-type model — deferred follow-ups

Status: **open / not started** — depends on the complex-type model PRs landing first
Scope: cross-cutting — the four SDK submodules
(`sdks/{rust,go,python,web}-sdk`), `lang/tx3` codegen
(`bin/tx3c/src/codegen.rs` + each SDK's `.trix/client-lib/` templates), and the
shared spec/fixtures (`sdks/sdk-spec/`).
Origin: explicitly-scoped-out items from the complex param-type model rework
(2026-06-12). Shipped PRs: toolchain#11 (spec + parity + fixture),
rust-sdk#41, go-sdk#14, python-sdk#18, web-sdk#34.
Related: [`sdk-codegen-v1beta0-migration.md`](./sdk-codegen-v1beta0-migration.md)
and [`codegen-client-lifecycle-facade.md`](./codegen-client-lifecycle-facade.md)
— same `.trix/client-lib/` templates and `schemaTypeFor` helper; sequence the
codegen workstream (§3) with those.

## Context

The merged work brought every SDK's **runtime `ParamType` interpretation** to
parity with the canonical TII schema model (`sdks/sdk-spec/api-surface/args.md`):
all four now read list/tuple/map/record/variant/unit/utxo/anyAsset, match core
`$ref`s by trailing name across both URL forms, resolve
`#/components/schemas/<Name>`, and never throw (unrecognized → `unknown`).

That work deliberately stopped at **interpretation**. Three capabilities were
left out, plus one verification-hardening item. They are tracked in
`sdks/parity-matrix.md` (the "Type-directed value validation / encoding" row is
❌×4 with a note) and collected here so they aren't lost.

None of this is required for correct resolution today: argument **values** are a
generic-recursive JSON pass-through (byte arrays → `0x`-hex, big integers → wire
encoding, applied recursively into nested lists/maps/records), and the TRP
resolver performs authoritative type checking. These follow-ups improve
client-side ergonomics, type-safety, and feedback — they do not fix a
correctness bug.

## Workstreams

### 1. Type-directed value validation / encoding

Today the resolved `ParamType` is built and exposed (`Invocation::params`,
`inv.Params()`, etc.) but **not** used to validate or encode the args a caller
supplies. A user can pass a structurally-wrong value and only learn of it from a
TRP error.

The goal: drive arg coercion/validation from the resolved `ParamType` so the SDK
can (a) reject a value whose shape can't match the declared param before sending,
and (b) apply kind-specific encoding (e.g. coerce a tuple's positional elements
by their declared types rather than relying on the runtime value's JS/Go/Python
type).

- **web-sdk** is the clearest gap: `core/args.ts` has an `ArgValue` tagged union
and a `fromJson(value, ParamTypeTag)` dispatcher whose `ParamTypeTag` enum has
**no** list/tuple/map/record/variant tags — complex values are currently passed
raw. Extend the tag set and the `fromJson`/`toJson` dispatch to recurse over the
full `ParamType`.
- **rust/go/python**: thread the resolved `ParamType` into the arg setters
(`with_arg`/`SetArg`/`arg`) or a dedicated `validate()` pass; coerce per-kind.
- Keep the generic-recursive path as the fallback for `unknown` params.

Spec: promote the "type-directed validation/encoding" note in `args.md` from
"not yet required" to a defined MUST/SHOULD once the shape is agreed. Update the
parity-matrix row.

### 2. Variant argument *construction* encoder

Interpretation models a `variant` (its cases and field types), but no SDK can yet
help a user **construct** a tagged-union value to pass as an arg. `tx3c` codegen
emits a placeholder for this (`TODO: tagged-union codegen pending the variant arg
encoder`).

The goal: an idiomatic constructor per SDK for externally-tagged variant values
(e.g. `Side.sell({ price })` → `{ "Sell": { "price": … } }`), validated against
the resolved `variant` `ParamType`. This is the value-side counterpart to §1 and
should land with it. It also unblocks the codegen TODO in §3.

### 3. Codegen typed bindings for tuple / map / variant

`lang/tx3` `bin/tx3c/src/codegen.rs` `schemaTypeFor` (and the per-SDK
`.trix/client-lib/*.hbs` templates that call it) currently map only:
- `array` + `items` → `List<T>` / `list[T]` / `[]T`
- `object` + `additionalProperties` → `Record<string,V>` / `dict[str,V]` / `map[string]V`

It does **not** handle `array` + `prefixItems` (tuple → emits a loose list/`Any`)
or `oneOf` (variant → emits `unknown`/`Any` + the TODO). So generated typed
clients lose tuple positional types and can't express variants.

The goal: extend `schemaTypeFor` to emit:
- tuples as the language's tuple/fixed-arity type (TS `[A, B]`, Python
`tuple[A, B]`, Go a generated positional struct or `[]any` with a doc note,
Rust a tuple),
- variants as the generated tagged-union type (pairs with §2's constructor),
- records as the already-generated component types (verify nested refs resolve).

This lives in the **lang/tx3 repo**, not the SDKs (codegen is delegated there),
but the `.hbs` templates that consume the output live in each SDK repo — change
them in lockstep. Sequence with `sdk-codegen-v1beta0-migration.md`.

### 4. Adopt the shared fixture in each SDK's unit suite

`sdks/sdk-spec/test-vectors/complex-types/complex.tii` declares one param of every
kind and was cross-checked through the python and go loaders during the rework,
but only as ad-hoc verification. Each SDK should copy/symlink the fixture into its
local fixtures and add a permanent "loads the shared complex-types vector and
resolves the expected `ParamType` kinds" test, so cross-SDK parity on one
canonical input is enforced by CI rather than asserted once. (The per-SDK unit
suites already cover the canonical *table*; this adds the composed end-to-end
load.)

## Sequencing

1. Land the five open PRs (toolchain#11 + the four SDK PRs); bump submodule
pointers via `commit-umbrella`; cut the coordinated SDK version bump
(`release-synced`) for the breaking `ParamType` reshape.
2. §4 (fixture adoption) — cheap, independent, do anytime after merge.
3. §1 + §2 together (value validation + variant constructor) — spec the shape in
`args.md` first, then fan out across SDKs via `add-sdk-feature` / `propagate-change`.
4. §3 (codegen) — after §1/§2 define the runtime types the templates render against;
coordinate with the other codegen plans.

## Verification

- §1/§2: per-SDK unit tests for accept/reject of well- and ill-shaped values
against each kind; round-trip a variant value through construct → wire → TRP.
- §3: extend `codegen-check.sh` to render a protocol with tuple/map/variant params
and compile the generated client in each language.
- §4: the shared-fixture load test green in all four CIs.
- `parity-matrix.md` updated as each row flips; only mark ✅ where covered by tests.
4 changes: 3 additions & 1 deletion sdks/parity-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
| ❌ | Not implemented. |
| — | Not applicable to this SDK (must be justified in notes). |

**Snapshot date:** 2026-05-24 (post-merge of the unified-builder ports across rust/web/python/go; see [rust-sdk#38](https://github.com/tx3-lang/rust-sdk/pull/38), [web-sdk#29](https://github.com/tx3-lang/web-sdk/pull/29), [python-sdk#13](https://github.com/tx3-lang/python-sdk/pull/13), [go-sdk#10](https://github.com/tx3-lang/go-sdk/pull/10)).
**Snapshot date:** 2026-06-12 (complex-type model parity: all four SDKs reworked their `ParamType` interpretation to the canonical model in `api-surface/args.md` — trailing-name core `$ref` matching across `tii#/$defs/` + legacy `core#` forms, distinct list/tuple/map/record/variant kinds carrying inner types, never-throw `unknown` fallback, component-ref resolution. Shared fixture: `sdk-spec/test-vectors/complex-types/complex.tii`). Prior snapshot 2026-05-24 (post-merge of the unified-builder ports across rust/web/python/go; see [rust-sdk#38](https://github.com/tx3-lang/rust-sdk/pull/38), [web-sdk#29](https://github.com/tx3-lang/web-sdk/pull/29), [python-sdk#13](https://github.com/tx3-lang/python-sdk/pull/13), [go-sdk#10](https://github.com/tx3-lang/go-sdk/pull/10)).

---

Expand Down Expand Up @@ -42,6 +42,8 @@ Capability references are to `sdk-spec/api-surface/`.
| 3.7 | `waitForConfirmed` / `waitForFinalized` + `PollConfig` | ✅ | ✅ [`web-sdk/sdk/src/facade/submitted.ts`](../web-sdk/sdk/src/facade/submitted.ts) | ✅ [`go-sdk/sdk/facade/submitted.go`](../go-sdk/sdk/facade/submitted.go) | ✅ [`python-sdk/sdk/src/tx3_sdk/facade/submitted.py`](../python-sdk/sdk/src/tx3_sdk/facade/submitted.py) | Defaults: 20 attempts, 5 s. Go respects `context.Context` cancellation. |
| 3.8 | Discriminated error model | ✅ (`facade::Error` enum) | ✅ Full hierarchy via `instanceof` | ✅ Per-domain marker interfaces + `errors.As()` | ✅ Rooted at `Tx3Error` with category subclasses | Go: `TiiError`, `TrpError`, `SignerError`, `FacadeError` marker interfaces. Note: `MissingTrpEndpoint`/`UnknownProfile`/`UnknownParty` builder-error variants only exist where the builder exists (rust-sdk today). |
| 3.9 | Argument marshalling | ✅ (`core::ArgMap`) | ✅ [`web-sdk/sdk/src/core/args.ts`](../web-sdk/sdk/src/core/args.ts) | ✅ [`go-sdk/sdk/core/args.go`](../go-sdk/sdk/core/args.go) | ✅ [`python-sdk/sdk/src/tx3_sdk/core/args.py`](../python-sdk/sdk/src/tx3_sdk/core/args.py) | Go: `ArgValue` tagged union + `CoerceArg()` for native types. |
| 3.9 | Complex param-type interpretation (`list`/`tuple`/`map`/`record`/`variant`) | ✅ [`rust-sdk/sdk/src/tii/mod.rs`](../rust-sdk/sdk/src/tii/mod.rs) | ✅ [`web-sdk/sdk/src/tii/paramType.ts`](../web-sdk/sdk/src/tii/paramType.ts) | ✅ [`go-sdk/sdk/tii/param_type.go`](../go-sdk/sdk/tii/param_type.go) | ✅ [`python-sdk/sdk/src/tx3_sdk/tii/param_type.py`](../python-sdk/sdk/src/tx3_sdk/tii/param_type.py) | All four implement the canonical model in [`api-surface/args.md`](sdk-spec/api-surface/args.md): scalar core `$ref`s matched by **trailing name** across both `tii#/$defs/<Name>` and legacy `core#<Name>` forms (incl. `Utxo`/`AnyAsset`); distinct `list`(inner)/`tuple`(elements)/`map`(value)/`record`(fields)/`variant`(cases) kinds carrying their element types; `#/components/schemas/<Name>` resolved + recursed; **never throws** — unrecognized shapes (bare `string`, unresolved object, unknown `$ref`) become `unknown`/`Unknown`/`UNKNOWN` carrying the raw schema. Each SDK has a unit suite over the full table; Go added a custom `Schema` unmarshaler for the `items:false` / `additionalProperties:false` forms `tx3c` emits, and Rust dropped `schemars` for `serde_json::Value`. **Breaking** `ParamType` reshape → minor/major bumps. Shared fixture: [`sdk-spec/test-vectors/complex-types/complex.tii`](sdk-spec/test-vectors/complex-types/complex.tii). |
| 3.9 | Type-directed value validation / encoding | ❌ | ❌ | ❌ | ❌ | Out of scope for the model rework. Today arg values are generic-recursive JSON pass-through (bytes→`0x`hex, bigint encoding applied to nested elements); the resolved `ParamType` is **not** used to validate/encode each arg, and there is no variant-construction encoder (`tx3c` codegen still emits a `TODO: tagged-union codegen pending the variant arg encoder` placeholder). Codegen typed bindings for tuple/map/variant also pending in `lang/tx3` `schemaTypeFor`. |
| §4 | Top-level re-exports | ✅ [`lib.rs`](../rust-sdk/sdk/src/lib.rs) | ✅ [`web-sdk/sdk/src/index.ts`](../web-sdk/sdk/src/index.ts) | ✅ [`go-sdk/sdk/tx3sdk.go`](../go-sdk/sdk/tx3sdk.go) | ✅ [`python-sdk/sdk/src/tx3_sdk/__init__.py`](../python-sdk/sdk/src/tx3_sdk/__init__.py) | All four export `Tx3Client`, `Tx3ClientBuilder`, `Party`, `CardanoSigner`/`Ed25519Signer`, `PollConfig`, plus `Profile` and `MissingTrpEndpointError`. |

---
Expand Down
52 changes: 51 additions & 1 deletion sdks/sdk-spec/api-surface/args.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,54 @@ An SDK MUST accept native host-language values for transaction args (integers, s

The SDK SHOULD expose an `ArgValue`-equivalent type for users who need to construct tagged values explicitly (useful for `UtxoRef`, `UtxoSet`, etc.).

*Rust reference:* `tx3_sdk::core::ArgMap`. *Web reference:* `web-sdk/sdks/src/core/args.ts`.
## The parameter-type model

Each SDK builds a **parameter-type model** (`ParamType`) by interpreting each property of a transaction's `params` JSON schema (and the protocol `environment` schema). This model drives introspection (`unspecifiedParams`) and — where implemented — value coercion. `tx3c` lowers every Tx3 type into one of a fixed, closed set of schema shapes; an SDK MUST interpret all of them.

### Scalar (`$ref`) types

Built-in scalar types are emitted as a JSON `$ref`. The **canonical** form is `https://tx3.land/specs/v1beta0/tii#/$defs/<Name>`; the **legacy** form `https://tx3.land/specs/v1beta0/core#<Name>` MUST also resolve. An SDK MUST match the type by the **trailing name** — the segment after the last `#` or `/` — so both forms map identically. Matching the full URI is non-conforming (it breaks against current `tx3c` output, which emits the `tii#/$defs/` form).

| Trailing name | `ParamType` kind |
|---|---|
| `Bytes` | `bytes` |
| `Address` | `address` |
| `UtxoRef` | `utxoRef` |
| `Utxo` | `utxo` |
| `AnyAsset` | `anyAsset` |

A `$ref` of the form `#/components/schemas/<Name>` references a user-defined type; the SDK MUST resolve `<Name>` against the TII's `components.schemas` table and interpret the resolved schema recursively. The `components` table MUST therefore be threaded into the parameter-type builder.

### Primitive types

| Schema | `ParamType` kind |
|---|---|
| `{ "type": "integer" }` | `integer` |
| `{ "type": "boolean" }` | `boolean` |
| `{ "type": "null" }` | `unit` |

### Compound types

| Schema | `ParamType` kind | Carries |
|---|---|---|
| `{ "type": "array", "items": <T> }` | `list` | inner element type |
| `{ "type": "array", "prefixItems": [<T0>, <T1>, …], "items": false }` | `tuple` | positional element types |
| `{ "type": "object", "additionalProperties": <V> }` | `map` | value type (keys are always strings) |
| `{ "type": "object", "properties": {…}, "required": […] }` | `record` | field name → type |
| `{ "oneOf": [<case>, …] }` (externally tagged) | `variant` | case tag → fields |

A variant case has the externally-tagged shape `{ "type": "object", "additionalProperties": false, "required": ["<Tag>"], "properties": { "<Tag>": <fields schema> } }`. The SDK reads the single `required` entry as the case tag and interprets `properties[<Tag>]` (a record) as its fields.

An SDK SHOULD model `list` / `tuple` / `map` / `record` / `variant` as **distinct** kinds carrying their element/field types, so downstream consumers can introspect structure. An SDK that does not yet carry the inner types for a kind MUST still accept and marshal values of that kind, and MUST log the gap in `parity-matrix.md`.

### Never throw; the `unknown` fallback

Building the parameter-type model MUST NOT fail on an unrecognized schema shape (including a bare `{ "type": "string" }`, an unresolved `{ "type": "object" }` fallback that `tx3c` emits for unresolvable forward references, or an unknown `$ref`). Such shapes MUST map to an `unknown` kind that carries the raw schema. A bare `string` MUST NOT be assumed to be an `address` — `tx3c` always emits `Address` as a `$ref`, so a bare string is genuinely untyped.

## Value marshalling

Argument **values** are marshalled to the TRP wire format independently of the parameter-type model (the wire form is a plain JSON value; the TRP resolver performs authoritative type checking). Marshalling MUST be **generic-recursive**: scalar coercions (byte arrays → `0x`-prefixed hex, big integers → their wire encoding) MUST be applied to values nested inside lists, tuples, maps, and records, not only to top-level args. The wire value for a `list`/`tuple` is a JSON array; for a `map`/`record`/`variant` it is a JSON object.

> Type-*directed* validation/encoding (using the resolved `ParamType` to validate each arg) and the variant-construction encoder are not yet required; track them in `parity-matrix.md` when added.

*Rust reference:* `tx3_sdk::core::ArgMap`, `tx3_sdk::tii::ParamType`. *Web reference:* `web-sdk/sdk/src/core/args.ts`, `web-sdk/sdk/src/tii/paramType.ts`.
8 changes: 8 additions & 0 deletions sdks/sdk-spec/test-vectors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ This directory contains canonical, spec-level e2e vectors shared across all Tx3
- `transfer.tx3`
- `transfer.tii`
- `transfer.preprod.env`
- `complex-types/`
- `complex.tii` — schema-only fixture whose `complex` transaction declares one
param of every `ParamType` kind (integer, boolean, unit, Address, UtxoRef,
AnyAsset, list, tuple, map, plus a component-ref record `AssetClass` and a
component-ref variant `Side`), with scalar `$ref`s in the canonical
`tii#/$defs/` form. Used to verify parameter-type interpretation parity
across SDKs (see `api-surface/args.md`). The TIR envelope is a non-resolvable
placeholder — this vector is for type-model tests, not TRP resolution.
Loading