diff --git a/.github/workflows/dx-e2e.yml b/.github/workflows/dx-e2e.yml index acc2a5e..572ac35 100644 --- a/.github/workflows/dx-e2e.yml +++ b/.github/workflows/dx-e2e.yml @@ -7,11 +7,9 @@ name: DX E2E # - stable: the offline scaffold gate + lang-surface journeys. Edge journeys # whose `#@ min-tx3c` exceeds stable's tx3c install + skip (green), and # auto-run once the feature graduates to stable. -# - beta: edge-feature journeys (e.g. 02-lang-tour's tuples), plus the devnet -# round-trip (04) and the invoke arg-type journey (05). Both currently *fail* -# on released channels (known, tracked trix gaps fixed on main but unreleased) -# — intentionally red, not tolerated xfails; each goes green once its fix -# ships to a channel. +# - beta: edge-feature and runtime journeys — tuples (02), the devnet +# round-trip (04), and the invoke journey (05) — that need a beta-channel +# install to exercise their feature. # # Compat lives in one place: the journey's `#@ min-tx3c` header, enforced by the # runner's skip gate. Native runners only — a DX test must exercise the real @@ -59,9 +57,8 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest] - # edge-feature journeys + the devnet round-trip (04) and the invoke - # arg-type journey (05). 04 and 05 fail on released channels until their - # trix fixes ship (intentionally red, not xfails). + # edge-feature journeys plus the runtime journeys: the devnet + # round-trip (04) and invoke (05). journey: [02-lang-tour, 03-lang-edge, 04-devnet-roundtrip, 05-invoke] steps: - uses: actions/checkout@v4 diff --git a/e2e/journeys/05-invoke/journey.sh b/e2e/journeys/05-invoke/journey.sh index 6713c0e..329d561 100755 --- a/e2e/journeys/05-invoke/journey.sh +++ b/e2e/journeys/05-invoke/journey.sh @@ -36,15 +36,9 @@ for _ in $(seq 1 30); do done sleep 3 -# 4. Invoke the transaction — passing the fixture's full argument set — and -# require it to resolve to an unsigned tx. --skip-submit keeps it headless: -# invoke never signs without a TTY (see README), so resolve-only is the mode. -# -# This currently FAILS on released channels because the toolchain can't yet -# bind the fixture's full argument set (resolve errors with "invalid param -# type"). The assertion is kept strict — an intentional red, not an xfail — -# so the gap stays visible and the journey goes green the moment it's fixed. -# Tracked in plans/invoke-arg-type-support-gap.md. +# 4. Invoke the transaction with the fixture's full argument set and require it +# to resolve to an unsigned tx. --skip-submit keeps it headless: invoke never +# signs without a TTY (see README), so resolve-only is the mode. run_cmd "trix invoke — resolve the transaction" \ "${TRIX}" invoke --skip-submit \ --args-json "{\"sender\":\"${ALICE}\",\"receiver\":\"${BOB}\",\"quantity\":2000000,\"urgent\":true,\"memo\":\"deadbeef\",\"meta\":{\"tags\":[1,2,3],\"level\":7}}" diff --git a/manifest-beta.json b/manifest-beta.json index 29aeb56..f2e503a 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -22,7 +22,7 @@ "description": "The tx3 package manager", "repo_name": "trix", "repo_owner": "tx3-lang", - "version": "^0.26.1" + "version": "^0.26.2" }, { "name": "tx3-mcp", diff --git a/plans/complex-arg-type-support.md b/plans/complex-arg-type-support.md new file mode 100644 index 0000000..d9127f7 --- /dev/null +++ b/plans/complex-arg-type-support.md @@ -0,0 +1,113 @@ +# Plan: complex argument-type support through `trix invoke` / TRP resolve + +Status: **open / not started.** +Scope: cross-cutting — `core/tir` (value model + reduction), `lang/tx3` +(`crates/tx3-resolver`), the **dolos** release that embeds the resolver, and the +toolchain manifests. Pairs with the SDK-side encoder tracked in +[`sdk-complex-type-followups.md`](./sdk-complex-type-followups.md) (§1/§2). +Related: [`tx3-protocol-limitations.md`](./tx3-protocol-limitations.md), +[`dx-e2e-journey-roadmap.md`](./dx-e2e-journey-roadmap.md) (the `05-invoke` journey). + +## Context + +A consumer can declare a transaction parameter of a complex type — a record +(custom type), `List`, `Map`, `Tuple` — and `trix invoke` cannot bind a value to +it. Resolution fails with: + +``` +❗️ error: target type not supported: Custom("Meta") (JSON-RPC -32005) +``` + +This is the capability the `05-invoke` DX e2e journey asserts (its fixture passes +a record nesting a `List`), which is why that journey is intentionally red. + +### Where the support actually breaks (verified layer-by-layer) + +The arg flows: SDK reads the TII param types → sends args as JSON over TRP → +the resolver coerces each JSON arg to the param's type → reduction substitutes it +into the transaction template. + +| Layer | Component | State | +|---|---|---| +| TII-load type recognition | `tx3-sdk::tii::from_json_schema` | ✅ reads list/tuple/map/record/variant (tx3-sdk 0.13.0) | +| SDK arg value encoding | per-SDK `ArgValue`/`fromJson` | ⚠️ generic recursive JSON pass-through; no type-directed encoding — see [`sdk-complex-type-followups.md`](./sdk-complex-type-followups.md) §1/§2 | +| **TRP arg coercion** | `tx3-resolver::interop::from_json` (`lang/tx3/crates/tx3-resolver/src/interop.rs:265`) | ❌ matches only `Int/Bool/Bytes/Address/UtxoRef/Undefined`; everything else → `Error::TargetTypeNotSupported` | +| **Resolver value model** | `ArgValue` (`core/tir/crates/tx3-tir/src/reduce/mod.rs:1514`) | ❌ scalar-only: `Int/Bool/String/Bytes/Address/UtxoRef/UtxoSet` — no variant can hold a record/list/map/tuple | +| Arg → template substitution | `arg_value_into_expr` (`core/tir/.../reduce/mod.rs:393`) | ❌ maps only the scalar `ArgValue`s to `Expression` | +| TIR target representation | `Expression` (`core/tir/.../model/v1beta0.rs:187`) | ✅ already has `List`, `Map`, `Tuple`, `Struct(StructExpr)` | +| Running TRP | **dolos** devnet | ❌ pins published `tx3-resolver 0.21.0`; a merged fix reaches `trix invoke` only after a dolos release + manifest bump | + +The SDK layer is done; the wall is the **resolver + the value model** beneath it. +Note the architectural assumption recorded in `sdk-complex-type-followups.md`: +the SDKs deliberately pass complex values as generic JSON because "the TRP +resolver performs authoritative type checking." The resolver does not yet hold up +that contract — closing it here is what makes that assumption true. + +## The central design question (records/variants) + +`Expression` already models the targets, so the mechanical cases are +straightforward — a JSON array → `Expression::List`/`Tuple`, a JSON object → +`Expression::Map` — none need a type definition; the structure is in the JSON. + +Records and variants are the hard case. `StructExpr { constructor: usize, fields: +Vec }` is **positional** (a Plutus-data constr), but a JSON record arg +is **by-name and unordered**, and `Type::Custom("Meta")` carries only the type +*name* — the reduced `Tx` TIR (`core/tir/.../model/v1beta0.rs:342`) holds **no +custom-type registry** to recover the constructor index and field order from. +So something must supply that mapping. Candidate approaches (decide before coding): + +- **(A) Type-directed encoding in the SDK** — the SDK has the full `ParamType` + (record field order via the schema `properties` / component), so it lowers a + by-name record value into a positional/constr wire form; the resolver then only + builds `StructExpr`/`List`/etc. from an already-positional payload. This is the + value-side counterpart of `sdk-complex-type-followups.md` §1/§2 and keeps the + resolver schemaless. **Recommended** — it matches the existing layering and the + SDKs are where the schema already lives. +- **(B) Carry type definitions into resolution** — embed custom-type defs + (constructor + ordered field types) in the TIR or the resolve request so the + resolver maps by-name JSON → positional `StructExpr` itself. Heavier: changes the + TIR/resolve wire shape and the compiler emission. + +Either way, `List`/`Tuple`/`Map`/`Bytes`-nested coercion is recursive and shared. + +## Workstreams + +### 1. `core/tir` — value model + reduction +Add structured variants to `ArgValue` (record/list/map/tuple, mirroring the +existing `Expression` shapes) and extend `arg_value_into_expr` to build +`Expression::Struct`/`List`/`Map`/`Tuple` from them. Cover the new variants in the +`reduce` `apply_args` paths and tests. Under approach (A), the record variant +carries already-ordered positional fields. + +### 2. `lang/tx3` `tx3-resolver::interop::from_json` — recursive coercion +Replace the catch-all `TargetTypeNotSupported` for the complex `Type`s with +recursive coercion into the new `ArgValue`s: arrays → `List`/`Tuple` (per +`Type`), objects → `Map`/record. Resolve the record/variant ordering per the +chosen approach (A: consume the SDK's positional form; B: look up the type def). +Add tests beside the existing `from_json_*` cases (`interop.rs` test module), +including a nested record-with-`List` matching the `05-invoke` fixture. + +### 3. SDK-side encoder (only under approach A) +Implement `sdk-complex-type-followups.md` §1 (type-directed value +validation/encoding) and §2 (variant constructor) so each SDK lowers complex args +to the positional wire form the resolver expects. Sequence with that plan. + +### 4. Release chain +tx3-tir → tx3-resolver (`lang/tx3`) → **dolos** (bump its `tx3-resolver` pin) → +dolos release → `manifest-beta.json` (dolos pin) → `05-invoke` greens on beta. +Per `AGENTS.md` dependency order; use the per-grouping release skills and +`channel-version-update`. This is a language/runtime feature — route via +`skills/add-language-feature/`. + +## Verification + +- `core/tir`: unit tests for each new `ArgValue` → `Expression` mapping and + `apply_args` substitution. +- `tx3-resolver`: `from_json` accept tests for `List`/`Tuple`/`Map`/record, + including the nested `record { …, List }` shape from the `05-invoke` fixture. +- End-to-end: once dolos ships, `./e2e/run.sh --channel beta --journey 05-invoke` + resolves to a `cbor` and the journey goes green (its strict assertion is the + acceptance check); then it can graduate from the beta matrix toward stable as the + channels carry the fix. +- If approach A: the SDK accept/reject + variant round-trip tests from + `sdk-complex-type-followups.md` §1/§2. diff --git a/plans/invoke-arg-type-support-gap.md b/plans/invoke-arg-type-support-gap.md deleted file mode 100644 index 5ac9700..0000000 --- a/plans/invoke-arg-type-support-gap.md +++ /dev/null @@ -1,92 +0,0 @@ -# Bug: `trix invoke` only binds `Int` / `Bool` / `Address` args — everything else fails - -Status: **tracked, not yet fixed.** Reproduces on `stable` (trix 0.25.1) and the default/`beta` -channel (trix 0.26.0). Surfaced by the `05-invoke` DX e2e journey, which invokes a tx with a -diverse arg spread and strictly asserts it resolves — so it's **intentionally red** on released -channels (parked on the `beta` CI job, like `04-devnet-roundtrip`) until the gap below closes. - -## Symptom - -`trix check` passes and `trix build` produces a valid TII, but `trix invoke` (and `cshell tx -invoke` underneath) fails the moment any non-`Int`/`Bool`/`Address` argument is supplied via -`--args-json` / `--args-json-path`: - -``` -❗️ error: invalid param type -``` - -Verified failing arg types: `Bytes`, and the complex types `record` (custom type), `List<_>`, -`Map<_,_>`, `Tuple<_>`, and `AnyAsset`. `UtxoRef` goes through the same path and is expected to -fail too (it additionally needs pre-existing on-chain state to resolve). - -## Root cause - -The error is `tx3_sdk::tii::Error::InvalidParamType`, returned from `ParamType::from_json_schema` -at protocol-load time (`Protocol::from_file`, before any network call). That function is narrow — -it only recognizes three core-type `$ref`s plus scalar instance types, and errors on everything -else: - -```rust -pub fn from_json_schema(schema: Schema) -> Result { - let as_object = schema.into_object(); - if let Some(reference) = &as_object.reference { - return match reference.as_str() { - "https://tx3.land/specs/v1beta0/core#Bytes" => Ok(ParamType::Bytes), - "https://tx3.land/specs/v1beta0/core#Address" => Ok(ParamType::Address), - "https://tx3.land/specs/v1beta0/core#UtxoRef" => Ok(ParamType::UtxoRef), - _ => Err(Error::InvalidParamType), - }; - } - if let Some(inner) = as_object.instance_type { - return match inner { - SingleOrVec::Single(x) => Self::from_json_type(*x), // only Integer / Boolean - SingleOrVec::Vec(_) => Err(Error::InvalidParamType), - }; - } - Err(Error::InvalidParamType) -} -``` - -This yields **two distinct failure modes**, both surfacing as the same `invalid param type`: - -1. **`Bytes` / `UtxoRef` — `$ref` mismatch (a typo-class bug).** tx3c emits the param schema as - `"$ref": "https://tx3.land/specs/v1beta0/tii#/$defs/Bytes"`, but the matcher above keys on - `…/v1beta0/core#Bytes`. `…/tii#/$defs/Bytes` ≠ `…/core#Bytes`, so it falls through. - -2. **Complex types — unimplemented.** A record param emits `"$ref": "#/components/schemas/Meta"`; - a `List<_>` emits an `array`/`Vec` instance type. `from_json_schema` has no branch for local - `#/components/schemas/…` (or `#/$defs/…`) references, no array/`List` handling, and no object - handling — so records, lists, maps, tuples and `AnyAsset` all hit a catch-all `InvalidParamType`. - -Why `Int`/`Bool`/`Address` survive: `Int`→`{"type":"integer"}` and `Bool`→`{"type":"boolean"}` are -matched by instance type; `Address` works *as an argument* only because parties are injected as -`ParamType::Address` directly (`tii/mod.rs`, `out.params.insert(party.to_lowercase(), -ParamType::Address)`), never routed through `from_json_schema`. A non-party `Address` param would -hit the same wall. - -## Secondary bug: `trix invoke` exits 0 on a resolve error - -Independently of the above, `trix invoke` prints `❗️ error: invalid param type` and then **exits -`0`**. A failed resolve should be a non-zero exit. This shapes the `05-invoke` journey: because the -invoke exits 0, the runner's `run_cmd` doesn't abort, so the strict `"cbor"` assertion is what turns -the journey red (and it's why the exit-code-based `xfail_cmd` helper can't be used here — it would -misread the zero exit as success). Worth fixing alongside the type support so resolve failures are -detectable by exit code. - -## Fix options (for whoever picks this up) - -- **`Bytes` / `UtxoRef`:** align the URIs — either tx3c emits `…/core#Bytes`, or `from_json_schema` - also accepts `…/tii#/$defs/` (accept both during the transition). -- **Complex types:** teach `from_json_schema` to resolve local component/`$defs` refs into - `ParamType::Custom(Schema)`, handle `array` → `ParamType::List`, and cover map/tuple/`AnyAsset`. -- **Exit code:** return non-zero from `trix invoke` (and `cshell tx invoke`) on a resolve error. - -Whichever side moves, bump the toolchain so a released channel carries the fix. - -## Verification when fixed - -The `05-invoke` journey auto-detects the fix: its diverse-typed invoke starts resolving to a -`cbor`, the strict assertion passes, and the journey goes green — at which point move it from the -`beta` CI matrix onto `stable` and delete this note. The fine-grained per-type matrix (one case per -`Bytes`/`UtxoRef`/record/`List`/`Map`/`Tuple`/`AnyAsset`) is better covered by trix's own unit -tests than by the e2e journey, which only needs the one end-to-end resolve. diff --git a/tooling/trix b/tooling/trix index 057be51..c4c6107 160000 --- a/tooling/trix +++ b/tooling/trix @@ -1 +1 @@ -Subproject commit 057be51ab624836066aac1b4ba4d15757aaf7b85 +Subproject commit c4c61079f29ddafe208aa28648c6324ad6f6848c