diff --git a/.editorconfig b/.editorconfig index c910d85..c5c3caf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,6 @@ +# Copyright 2026 Peaceful Studio OÜ +# SPDX-License-Identifier: Apache-2.0 + # EditorConfig - https://editorconfig.org root = true diff --git a/.github/workflows/build-oci-codegen-cs.yaml b/.github/workflows/build-oci-codegen-cs.yaml index 6117eb8..5639d52 100644 --- a/.github/workflows/build-oci-codegen-cs.yaml +++ b/.github/workflows/build-oci-codegen-cs.yaml @@ -118,7 +118,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: jvm-helper-jar - path: jvm-helper/target/scala-2.13/daml-codegen-jvm-helper.jar + path: jvm-helper/target/scala-2.13/daml-dar-to-proto.jar retention-days: 1 if-no-files-found: error @@ -231,7 +231,7 @@ jobs: BUNDLE="bundle/${RID}" mkdir -p "${BUNDLE}/bin" cp "publish/${RID}/${EXE_NAME}" "${BUNDLE}/bin/${EXE_NAME}" - cp jvm-helper-artifact/daml-codegen-jvm-helper.jar "${BUNDLE}/bin/daml-codegen-jvm-helper.jar" + cp jvm-helper-artifact/daml-dar-to-proto.jar "${BUNDLE}/bin/daml-dar-to-proto.jar" cp "scripts/bundle-entrypoint/${ENTRYPOINT_NAME}" "${BUNDLE}/${ENTRYPOINT_NAME}" cp "scripts/bundle-entrypoint/${MANIFEST_NAME}" "${BUNDLE}/component.yaml" if [ "${BUNDLE_OS}" != "windows" ]; then @@ -343,7 +343,7 @@ jobs: "component.yaml:644" "${ENTRYPOINT_NAME}:755" "bin/${EXE_NAME}:755" - "bin/daml-codegen-jvm-helper.jar:644" + "bin/daml-dar-to-proto.jar:644" ) ANNOTATIONS_JSON="${RUNNER_TEMP}/oras-annotations-${RID}.json" @@ -400,7 +400,7 @@ jobs: "component.yaml:${COMPONENT_FILE_MEDIA_TYPE}" \ "${ENTRYPOINT_NAME}:${COMPONENT_FILE_MEDIA_TYPE}" \ "bin/${EXE_NAME}:${COMPONENT_FILE_MEDIA_TYPE}" \ - "bin/daml-codegen-jvm-helper.jar:${COMPONENT_FILE_MEDIA_TYPE}") + "bin/daml-dar-to-proto.jar:${COMPONENT_FILE_MEDIA_TYPE}") popd > /dev/null echo "ref=${REF}" >> "${GITHUB_OUTPUT}" echo "digest=${digest}" >> "${GITHUB_OUTPUT}" diff --git a/.github/workflows/nuget-pack-integration.yaml b/.github/workflows/nuget-pack-integration.yaml index 94dc2fa..2482b54 100644 --- a/.github/workflows/nuget-pack-integration.yaml +++ b/.github/workflows/nuget-pack-integration.yaml @@ -113,7 +113,7 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: jvm-helper-jar - path: jvm-helper/target/scala-2.13/daml-codegen-jvm-helper.jar + path: jvm-helper/target/scala-2.13/daml-dar-to-proto.jar retention-days: 1 if-no-files-found: error @@ -157,7 +157,7 @@ jobs: run: | set -euo pipefail dotnet publish src/Daml.Codegen.CSharp.Cli -c Release -o bundle/bin - cp jvm-helper-artifact/daml-codegen-jvm-helper.jar "bundle/bin/daml-codegen-jvm-helper.jar" + cp jvm-helper-artifact/daml-dar-to-proto.jar "bundle/bin/daml-dar-to-proto.jar" cp "scripts/bundle-entrypoint/${ENTRYPOINT}" "bundle/${ENTRYPOINT}" if [ "${RUNNER_OS}" != "Windows" ]; then chmod +x "bundle/${ENTRYPOINT}" "bundle/bin/${EXE}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 256ad48..0269888 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,6 @@ +# Copyright 2026 Peaceful Studio OÜ +# SPDX-License-Identifier: Apache-2.0 + repos: - repo: local hooks: @@ -6,4 +9,4 @@ repos: entry: scripts/check-license-headers.sh language: script pass_filenames: false - files: \.(cs|scala)$ + files: ^(src|tests|jvm-helper|samples|scripts|conformance|proto|CONTEXT\.md|Directory\.Build\.props|coverage\.settings\.xml|\.editorconfig|\.pre-commit-config\.yaml)(/|$) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63c462b..d8150c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This changelog tracks the packages published from this repo together, because they are versioned in lockstep: - `Daml.Codegen.CSharp` — C# emitter library (NuGet package) +- `Daml.Codegen.Intermediate` — intermediate DAR contract (protobuf types + shared Daml model) - `Daml.Runtime` — runtime types referenced by generated code - `Daml.Ledger.Abstractions` — transport-agnostic ledger client contract (`ILedgerClient`, composed of `ILedgerWriter`/`ILedgerReader`/`ILedgerStreamer`) @@ -36,6 +37,1013 @@ because they are versioned in lockstep: ### Security +## [0.5.0-preview.1] — 2026-09-01 + +### Added + +- `Daml.Codegen.Intermediate` — a new package carrying the intermediate DAR contract on its + own: the C# types generated from `intermediate_dar.proto` plus the shared Daml model + (`DarModel`, `DamlPackage`, `DamlTemplate`, `DamlType`, `PackageVersionParser`, …). + Producers of the intermediate representation can now depend on the contract alone instead + of on the whole emitter. Versioned in lockstep with the other packages. +- **Typed interface/view read surface** (`Daml.Runtime`, `Daml.Ledger.Abstractions`): + `ViewDescriptor` is a pure type witness pairing a Daml interface marker + with its view record, exposed by every generated marker through a static `View` property. + `InterfaceStreamEvent` and `InterfaceAcsSnapshotEntry` + are the interface-family counterparts of `ContractStreamEvent` and `AcsSnapshotEntry`, + carrying the interface's server-computed view record rather than the implementing template's + own record. `ILedgerStreamer.SubscribeAsync`, `SubscribeActiveAsync` and + `SubscribeLedgerEffectsAsync` gain interface-family overloads of the same name, each taking + the witness as its first argument — `client.SubscribeAsync(IHolding.View, parties)` — so both + type parameters are inferred from one argument. A mismatched marker/view pair does not + compile. A matching event delivered without a view is surfaced as `Unclassified` with + `UnclassifiedKind.InterfaceViewUnavailable`, unchanged. + + Two gaps ride with them, both deliberate. There is no interface-family counterpart to + `StreamerSnapshot.SnapshotAsync`: it materializes `IReadOnlyList>`, and + `Contract` is a template shape that cannot hold a view record, so draining + `SubscribeActiveAsync(view, …)` means restating its throw-on-`StreamError`/`Unclassified`/ + missing-checkpoint policy yourself. And `LedgerClientConformanceTests` drives only + the template family, so an implementation of the three new members can be wrong and still + pass the kit; see that package's README. +- Generated interface markers now mirror their view's fields as instance get-only properties, + and the generated view record implements its marker, so a view answers with the *interface's* + identity in identity-keyed generic code and its fields are readable through a marker-typed + variable. `==` between marker-typed variables is reference equality, noted on the marker's + doc-comment; concrete view records keep record value equality. The enrichment is conditional: + where the view record is declared in a different package than its interface, is shared by two + interfaces, or carries a field whose C# name would not agree on both sides of the mirror, the + interface still gets the `View` witness, but the record is not stamped with the marker and the + marker mirrors no fields. +- **Contract keys reach the read surface.** `KeyDescriptor` (`Daml.Runtime`) + pairs a keyed template with its key type and carries the key codec as `KeyEncoder`/`KeyDecoder`, + the way `Choice` carries `ArgumentEncoder`/`ResultDecoder`. Generated + keyed templates carry the `IHasKey` facet and expose the descriptor through a + static `Key` witness, so a generic method constrained on the facet can decode a contract key + from a template type alone and encode one back to the `DamlValue` `ExerciseByKeyCommand` takes, + with no reflection. `TKey` is unconstrained, so a template keyed by a bare `Party` + (`key steward : Party`) gets `KeyDescriptor` on the same footing as a + record-keyed template's `KeyDescriptor`. Where a template's own payload + field maps to the C# member name `Key`, or the template itself is named `Key`, the witness is + emitted as an explicit interface implementation only — reachable through the facet, invisible + on the template type — and codegen logs a warning naming the template and, where there is one, + the field. +- `StreamerSnapshot.SnapshotAsync(T.Key, submitter)` (`Daml.Ledger.Abstractions`): a + descriptor-taking overload returning `IReadOnlyList>` with every key decoded, + so `await client.SnapshotAsync(Account.Key, alice)` hands back contracts whose `Key.Value` is + an `AccountKey`. Passing the template's `Key` witness fixes both type parameters from one + argument — C# performs no partial type-argument inference, so the pair has to travel together. + A create row that carried no key is reported as `LedgerOperationException`, consistent with the + short-list failures the keyless overload already prevents. +- Generated code emits a by-key command builder for every choice on a key-bearing template — + `ByKeyCommand(key, argument)`, returning an `ExerciseByKeyCommand`, beside the existing + `Command(contractId, argument)`. It is a plain static on the template record rather + than an extension method: the key's C# type may be `string` or `Party`, and extending those + would put the method on every value of that type in the consuming project. +- `ContractKey.KeyHash` (`Daml.Runtime`): the ledger's hash of the key, the value Canton indexes + keyed contracts by. It travels beside the key on both wire formats (`contractKeyHash` on the + JSON encoding, `contract_key_hash` on gRPC) and is held as the base64 text the JSON encoding + uses. An init-only property rather than a positional parameter, so `Deconstruct` keeps its two + parameters. A `null` there is a stated absence — the created event carried no hash, or the key + was built by a caller rather than read off the wire. Nothing in this repository computes it: + the hash is the ledger's, and a transport that reads one off a created event populates it. On + a decoded active contract the same hash is reached as `contract.Key.Hash` (see + `ContractKey` under *Changed — BREAKING*). +- **`ContractKey` equality ignores `KeyHash`.** Two keys are the same key when they name the same + value of the same template, so a key read off the wire — which carries a hash — equals the same + key a caller constructed to exercise by it, which does not. `Equals`/`GetHashCode` are written + out rather than compiler-generated for this reason; the generated pair would have compared the + hash and made by-key matching fail quietly depending on where the key came from. `KeyHash` stays + in `ToString()`, so a diagnostic dump still shows it. +- **`DamlLfJsonReader.ReadRecord` (`Daml.Runtime`): type-directed LF-JSON decoding.** Decodes a + JSON object against a generated record type — supplied as `T` or as a runtime `Type`, read from + JSON text or from an already-parsed `JsonElement` — into a `DamlRecord` whose fields carry their + true Daml types (a `Party` field arrives as `DamlParty` rather than the `DamlText` an untyped + decode yields), ready to hand to the generated `FromRecord`. Coverage is the full observed + LF-JSON wire encoding: + - Scalars — `Text`, `Bool`, `Int64` as its wire string, `Numeric` at the declared scale, `Date`, + `Timestamp` (up to seven fractional digits, with `Z`, a numeric offset, or no zone designator, + an unzoned value read as UTC), and `ContractId`. + - Structure — nested records, lists, `TextMap`, `GenMap` (its array of key/value pairs), + `Optional` (`Some` as the bare value, `None` as `null`), variants and enums resolved by the + wire names the emitter writes rather than their C# identifiers, so sanitized or disambiguated + constructors decode correctly, and `Unit`. + - Hand-written stdlib generics — `Tuple2`/`Tuple3` from their wire record form + (`{"_1":…,"_2":…}`, the shape a captured tuple contract key shows) and `Either` from its + `Left`/`Right` tagged-variant form, each component decoded from the CLR generic argument, so + `Optional` components, nested records, lists of tuples and tuples carried by an `Either` arm + all decode in those positions. Generic variants other than `Either` are still refused with + `NotSupportedException`. + + A wire-shape mismatch is reported as `JsonException` carrying the dotted property path. The + reader honours `DamlJsonDeserializationLimits`; the `JsonElement` overloads apply the + array-breadth and value-depth caps but not the input-size cap, which was the caller's to apply + when they parsed. +- `IDamlRecord` (`Daml.Runtime`): self-typed record facet whose + `static abstract TSelf FromRecord(DamlRecord)` factory lets a generated type be materialized + through a `where T : IDamlRecord` constraint instead of by reflection. Generated templates + and non-generic records declare it, as does the hand-written `RelTime`; a non-generic record's + base list becomes `: IDamlRecord` (the plain `IDamlRecord` still holds via + inheritance). Generic records are unchanged. +- `Optional` (`Daml.Runtime.Stdlib`): the representation for a Daml `Optional` in the positions + C# nullable syntax cannot carry — over a type parameter, as a type argument to a generated + generic, and as a `GenMap` key. A closed `Some`/`None` record hierarchy with `Match`, `HasValue`, + and the `GetValueOrDefault`/`GetValueOrThrow`/`TryGetValue` bridges back to idiomatic C#, + mirroring the wire-side `DamlOptional`. +- **Nested Daml `Optional` is generated instead of refused** (`Daml.Codegen.CSharp`). A Daml + `Optional (Optional Text)` becomes a C# `Optional>` — every level of the chain + carried by the wrapper, none of it by C# nullability — so `None`, `Some None` and `Some (Some x)` + stay three distinct values. Where the nesting is written directly in the Daml type, codegen + previously failed the run outright with `NotSupportedException: Codegen does not support nested + Optional types`, so for those shapes this is new capability rather than a changed one: no package + containing them generated before this release. Nesting that arises only by *substitution* is a + different story and did generate before — a field typed `Crate (Optional Text)` against + `data Crate a = Crate with item : Optional a` is nested once substituted, and it generated + silently while writing bytes the participant rejects. Those shapes are now refused instead; see + the breaking entry below for exactly which. A flat `Optional Text` is unchanged and still + generates `string?`, and so does an `Optional` separated from another by an intervening type: + `Optional (Box (Optional Text))` is still `Box>?`. Only a directly nested chain + moves. +- `DamlOptionalChain` (`Daml.Runtime.Data`): the wire node for one level of a directly nested + Optional chain, with `Optional.ToChainValue`/`Optional.FromChainValue` + (`Daml.Runtime.Stdlib`) bridging it to the typed wrapper. It is a separate node from + `DamlOptional` because the two carry different encodings for the same shape: a flat optional + writes JSON `null` or its bare value, while every level of a nested chain writes the array form + — `[]` when absent, `[v]` when present — which is what a participant accepts in a nested + position. For the same reason `DamlValueExtensions.AsOptional` now throws `InvalidCastException` + on a chain level rather than wrapping it as a further `Some`: the level is already an Optional, + in the other encoding. Reaching that throw requires holding a chain, and nothing produced one + before this release. + + One asymmetry rides with it. `DamlJsonSerializer.Deserialize` and `DeserializeRecord` are + untyped: with no schema to consult they reconstruct a chain's array form as a `DamlList`, the + same way they already reconstruct a `TextMap` as a `DamlRecord` and a `Party` as a `DamlText`. + So a `DeserializeRecord` result handed to a generated `FromRecord` throws `InvalidCastException: + Cannot cast DamlList to DamlOptionalChain` on a nested-Optional field, where every other field + type would have decoded. Read such a record with the type-directed `DamlLfJsonReader.ReadRecord` + instead, which has the schema and produces the chain. +- Generated code: every `Async` flavour now carries a `SubmitterInfo` overload beside its + ergonomic single-`Party` shape — the value-returning exerciser on `ContractId` (including + `Archive`), the `T.Contract` sibling of the create-bearing exerciser, and the interface-choice + exerciser. `readAs` and multi-party `actAs` are therefore expressible everywhere the fluent path + reaches, instead of only on the create-bearing `ContractId` overload; a submission needing + either no longer has to drop to a hand-built `ExerciseCommand`. Existing single-`Party` call + sites bind to the same overload as before. +- `ContractIdJsonConverterFactory` and `DamlJsonConverters` (`Daml.Runtime`): the + `System.Text.Json` converters for contract ids, with `DamlJsonConverters.All` and + `options.AddDamlConverters()` registering `Party`, `ContractId` and `SynchronizerId` in one call. + The factory matches the per-template `T.ContractId` record codegen derives from `ContractId` + as well as the closed generic itself, so a property declared as either takes the same wire shape. + A property declared as `ContractId` converts with no registration (`ContractId` carries the + factory as a `[JsonConverter]` attribute, as `Party` and `SynchronizerId` already did); a + property declared as the derived `T.ContractId` — including `T.Contract.Id` — does need + `options.AddDamlConverters()`, because `System.Text.Json` reads `[JsonConverter]` off the + declared type and does not walk its base chain. A contract-id string the target type rejects is + reported as `JsonException`. Consumers that hand-rolled a reflective `JsonConverterFactory` for + contract ids can delete it. +- `ILedgerStreamer.SnapshotAsync()` (`Daml.Ledger.Abstractions`): drains an active-contract-set + snapshot into `IReadOnlyList>`, decoding each row through `T.FromRecord`. Throws + `LedgerOperationException` rather than returning a short list when the snapshot faults, carries + an unclassified row, carries a create row that does not decode into `T`, or ends without its + terminal checkpoint; cancelling the token surfaces as `OperationCanceledException` even when the + transport reports the cancelled call in-band. It consumes and discards the checkpoint's + `StakeholderResume` ticket, so callers needing the gapless snapshot-to-stream handover stay on + `SubscribeActiveAsync`. +- `SingleCommandExtensions.TrySubmitSingleAsync` (`Daml.Ledger.Abstractions`): the single-command + submission path shared by generated exercisers and the hand-written write-path extensions. It + builds the `CommandsSubmission` — single command, caller's command id or a freshly minted one, + caller's workflow id when non-empty — and submits it, so that shape lives in one versioned place + instead of being re-emitted into every consumer's generated code. +- `commandId` parameter on the `CreateByExercise` overloads and on the void + `ThrowingExercise.ExerciseAsync` overloads (`Daml.Ledger.Abstractions`), so a caller can reuse an + id across a retry of a lost-but-accepted submission and have the ledger deduplicate it. + Generated exercisers already accepted one; these did not. Note that a minted id is never reported + back on a failed submission, so an application-level retry is deduplicable only when the caller + supplies and retains the id itself. +- Every write-path extension that accepts a `commandId` now rejects a supplied-but- + `default(CommandId)` with an `ArgumentException` naming the parameter, rather than forwarding it. + `CommandId` is a struct whose `Value` throws on a default instance, so an uninitialised one + previously travelled to the transport and surfaced there as an `InvalidOperationException` out of + a method whose contract is to return a structured outcome instead of throwing. Omitting the + argument still mints an id — only an explicitly passed default is rejected. +- `timeout` parameter on both `ThrowingExercise.ExerciseAsync` overloads + (`Daml.Ledger.Abstractions`), forwarded to the underlying `TryExerciseAsync` / + `TrySubmitSingleAsync` call. The throwing forms were the only unary write-path extensions that + could not carry a per-call deadline, so reaching for one meant dropping down to the structured + `Try*` form. It sits after `commandId` and before `cancellationToken`, matching the + `CreateByExercise` overloads; every existing call site passes `cancellationToken` by name, so the + added parameter does not rebind one. Binary compatibility breaks as it does for the other + parameter additions this cycle; publish in lockstep. +- **`Daml.Ledger.Abstractions.Testing.Conformance`**: an opt-in command-id deduplication check. + Override `CreateCommandIdFixture()` on `LedgerClientConformanceTests` to prove your + `ILedgerWriter` implementation honours the `commandId` obligation that + `TryExerciseAsync`/`TryCreateAsync` document — a caller-supplied id reaches the participant + verbatim, and a fresh one is minted only when the caller omits it, never leaving `command_id` + unset. Both directions are checked separately, since a client that mints over the supplied id and + one that never mints are distinct faults. Leaving it at its `null` default skips the four new + `[Fact]`s, matching the existing opt-in pattern for `CreateWriteFixture()`. +- `CommandsSubmission.WithOptionalWorkflowId(string?)` (`Daml.Runtime`): applies a workflow id when + the caller supplies a non-empty one and returns the submission unchanged otherwise, so the + null/empty branch lives on the submission type instead of being restated at every call site. + Generated exercisers and the `Daml.Ledger.Abstractions` extensions both route through it. +- `CommandId.FromWire(string?)` and `SynchronizerId.FromWire(string?)` (`Daml.Runtime`): project an + optional wire field, returning `null` for a null, empty, or whitespace value and a constructed id + otherwise — the supported way to express "the wire carried none". Hand-rolled fallbacks returning + `default` from a helper typed `CommandId` wrap as a non-null `CommandId?` and still throw on + `?.Value` (`?.Id` for `SynchronizerId`); the factory's only absence value is the literal `null`, + so replace such helpers with `FromWire`. +- `CodeGenOptions.PublishesReferencedPackages` (`Daml.Codegen.CSharp`): declares that this run also + produces the NuGet packages for the other DAR packages the generated code references, so their + ``s are pinned to the exact version this run produces instead of floating over + the generations published against their intrinsic version. Defaults to `false`; the CLI sets it + from `--release-counters`. +- **Supported Daml-LF input is now declared as 2.1 through 2.3**, each carried by a conformance + fixture. Nothing in the toolchain enforced 2.1 before; the declaration was narrower than the + capability. C# is emitted from all three fixtures and the result pinned, so every declared + version is proven on the emit path as well as the read one. Emitting 2.3 needs a 3.5-line Daml + SDK — the fixtures build on 3.5.2. +- A contract-key conformance corpus ships in `Daml.Codegen.Testing.Conformance` (namespace + `Daml.Codegen.Testing.Conformance.Contractkeys`, DAR reachable via + `ConformanceCorpus.OpenDar(ConformancePackage.ContractKeys)`), built at Daml-LF 2.3 — the + earliest 2.x version that can express a contract key. It covers a record key built from several + payload fields, a record key whose field comes from a nested projection, a record key built by a + function in another module, and a bare `Party` key. `ConformanceCorpus.OpenDar()` still returns + the rich-types DAR. +- A default-target conformance corpus ships in `Daml.Codegen.Testing.Conformance` (namespace + `Daml.Codegen.Testing.Conformance.Defaulttarget`, DAR reachable via + `ConformanceCorpus.OpenDar(ConformancePackage.DefaultTarget)`), built with no `--target` so it + carries the Daml-LF version damlc emits for a project that requests none. +- The conformance corpus (`Daml.Codegen.Testing.Conformance`, and the DAR it embeds) gains a + `TypeCorners` template covering the type-system corners the corpus previously omitted: + parameterized records and variants (`Box`, `Slot`) instantiated in a template payload, `GenMap` + keyed by `Party` and by `Int`, `Either`, `Tuple2`/`Tuple3`, a recursive record (`Branch`), an + `Optional` nested inside an `Optional` through a wrapper record, and the `Numeric` scale extremes + 0 and 37. The `Holding` interface gains choices (`Describe`, `Reissue`) alongside its view. + Consumers round-tripping the corpus against a participant get the wider surface; the corpus + package id changes accordingly. +- `Crate` (`Daml.Codegen.Testing.Conformance`, namespace + `Daml.Codegen.Testing.Conformance.Richtypes`): a parameterized record whose one field is an + `Optional` over the record's own type variable, held as `Optional`. It is the first such + shape in the shipped corpus, so the position C# nullable syntax cannot spell is now covered end + to end rather than only in the emitter's unit tests. `TypeCorners` carries one as `crate`. +- NuGet packages now ship a SourceLink'd symbols package (`.snupkg`) beside each `.nupkg`, embed + repository/commit metadata for source-stepping in debuggers, and are gated by NuGet package + validation at pack time; release builds are compiled with `ContinuousIntegrationBuild` for + deterministic, path-normalized PDBs. +- The DAR → IntermediateDar tool (the JVM helper) is now packaged for standalone publication as + `daml-dar-to-proto`: each GitHub release attaches `daml-dar-to-proto-.jar` and the + matching `intermediate_dar-.proto`, so non-C# SDKs can decode a `.dar` to an + Intermediate DAR without the `dpm codegen-cs` bundle. The jar gains a `--version` flag printing + the release version it was built as (`0.0.0-dev` for non-release builds). + +### Changed — BREAKING + +- **The read surface's payloads are typed** (`Daml.Runtime`). `ContractStreamEvent.Created`, + `ContractStreamEvent.Assigned` and `AcsSnapshotEntry.Created` now carry the payload as `T` + rather than a raw `DamlRecord`, and the two unions constrain `T` to the template family + (`where T : ITemplate, IDamlRecord`) instead of the broad `IDamlType`. Subscribing to a Daml + interface moves to the new interface-family overloads, whose payload is the interface's view + record. There is no deprecation period: a C# signature does not include its constraints, so the + old and the new could not overload under one name. Template call sites recompile unchanged after + regenerating; a marker call site takes a one-line fix — pass the marker's `View` witness instead + of naming the marker as a type argument, and read `InterfaceStreamEvent<,>`/ + `InterfaceAcsSnapshotEntry<,>` in place of the template unions. A payload that no longer decodes + is the projector's business now: it surfaces as `Unclassified` with + `UnclassifiedKind.DecodeFailure` instead of throwing out of the consumer's own `FromRecord` call. +- **`ILedgerStreamer` implementations must add three members** — the interface-family + `SubscribeAsync`, `SubscribeActiveAsync` and `SubscribeLedgerEffectsAsync` — and tighten the + three template-family members' `where T : IDamlType` to `where T : ITemplate, IDamlRecord`. + The resume overloads keep their forwarding default implementations. A transport now decodes the + payload before yielding it, which is what moves the decode off every consumer's `switch`. +- **`LedgerClientConformanceTests` constrains `TProbe` to the template family** + (`Daml.Ledger.Abstractions.Testing.Conformance`), matching the surface it exercises. An adopter's + probe type gains `IDamlRecord` — a `static FromRecord` — which a generated template + already carries. +- **`Daml.Ledger.Abstractions.Testing.Conformance` now requires xunit.v3 `4.0.0`** (was `3.2.2`). + The kit references `xunit.v3.extensibility.core` and `xunit.v3.assert` directly, and the `[Fact]` + methods a consumer inherits live on its `LedgerClientConformanceTests` base, so the kit's + xunit major is the consumer's — a consumer test project must move to xunit.v3 `4.0.0` in the same + step. This also moves Microsoft.Testing.Platform `2.0.2` to `2.3.3`, which the pinned coverage + extension already required. +- **Contract keys move off the payload and onto the active contract, typed.** `IHasKey` and + the `Key` member are no longer emitted on the template payload type: a payload is what a caller + constructs locally to build a create command, so it cannot know the key of a contract that does + not exist yet, and the accessor was a stub that threw `NotImplementedException`. Read + `contract.Key` instead of `contract.Data.Key`. The key *type* and its serialization are + unchanged, so caller-constructed key values and `ExerciseByKeyCommand` are untouched. +- **`Contract` loses its `ContractKey? Key` slot, and keyed contracts move to a new + `Contract`** (`Daml.Runtime`). The old slot was the third *positional* parameter, so its + removal breaks twice: `Deconstruct` drops from three parameters to two, and the + compiler-generated equality members stop comparing the key. A template that declares no contract + key stops offering a member that could never be populated; a keyed template's contracts are now + `Contract(ContractId Id, T Data, ContractKey Key)`, whose key is + **non-nullable**. `Contract.FromCreatedEvent` throws `InvalidOperationException` when + the event carries no key, rather than handing back a shape whose type says the key is present. +- **The ledger's key hash is retyped, not dropped.** The new `ContractKey(TKey Value, + string? Hash)` pairs the decoded key with the ledger's hash of it, so `contract.Key.Value` is + `TKey` — no `As()` hop — and `contract.Key.Hash` still reaches the hash. The hash is + Canton-computed over the key and the template id, is not derivable client-side, and no projected + shape exposes the raw `CreatedEvent`, so dropping the slot outright would have destroyed that + access rather than relocating it. Callers reading `contract.Key!.KeyHash` become + `contract.Key.Hash`; callers reading `contract.Key!.Value` as a wire `DamlValue` become + `contract.Key.Value` typed. +- **The generated nested `Contract.Key` becomes non-nullable and gains the hash** + (`Daml.Codegen.CSharp`). A keyed template's nested contract already carried a *typed* key, but as + `required TKey? Key`; it is now `required ContractKey Key`, and its `FromCreatedEvent` + throws when the created event carries no key instead of assigning `null`. A keyless template's + generated output is byte-identical to before. Callers become `contract.Key.Value` for the key and + `contract.Key.Hash` for the hash, and a call site that constructed the contract with `Key = null` + must now supply a `ContractKey`. +- **`IHasKey` becomes `IHasKey`, and its member becomes a `static abstract` + `KeyDescriptor Key`** (`Daml.Runtime`). The facet previously carried an instance + `TKey Key { get; }`, which told generic code the key's type but not how to decode one; a read + surface holding only `TKey` had nowhere to get the decode from, so it could not admit an + unconstrained key type. The descriptor now carries the decode and the pair of types travels + together, so a generic call site infers both type parameters from one argument and `TKey` needs + no `IDamlRecord` constraint — a bare `Party` key is admitted on the same footing as a + record key. Reached through a `static abstract` member rather than a bare static, so generic code + constrained on the facet finds it without reflection, the same shape `ITemplate.TemplateId` uses. + Hand-written implementors give the facet the implementing type as its first argument and replace + the instance property with a static `Key` descriptor. Where the payload already has a member + named `Key`, implement the descriptor explicitly + (`static KeyDescriptor IHasKey.Key { get; } = new() { … }`), which leaves the + name free on the type. +- **`AcsSnapshotEntryExtensions.ToContract()` returns a keyless `Contract`** + (`Daml.Runtime`). A keyed projection goes through the new `ToContract()`, which decodes + the row's key through the template's `Key` witness and throws `InvalidOperationException` when + the row carried none. +- **The four runtime shapes that carry a created contract gain a mandatory `ContractKey? Key` + parameter.** `AcsSnapshotEntry.Created` and `ContractStreamEvent.Created` each take it + immediately after their payload, grouping contract identity, data and key ahead of the ledger + metadata. The parameter is positional rather than an optional init-only property, so this is a + source break and a binary break: every projector constructing one of these shapes — every + `ILedgerClient` implementation included — has to pass the key the created event carried, or an + explicit `null` for a template that has none. The wire-level `ContractKey` travels rather than a + generated key type, which `Daml.Runtime` cannot name; decode it with the hop generated code + already performs, `TKey.FromRecord(key.Value.As())`. An optional slot would have let + every existing projector keep compiling while silently never populating the key. +- **`ContractStreamEvent.Assigned` gains a mandatory `ContractKey? Key`**, positioned + immediately after `Payload` to match the shapes that already carry one. An assignment re-emits + the whole created contract — Canton's assigned event wraps a full created event, key included — + so this variant was the one create-carrying shape with nowhere to put a key, and a consumer + rebuilding state from a single stream lost the key at every reassignment. +- **`CreatedEvent.ContractKey` loses its `= null` default** and becomes a mandatory parameter + (`DateTimeOffset? CreatedAt` stays optional after it). This is the single entry point feeding + every downstream key slot, so a transport could omit it entirely, compile clean, and feed `null` + into every `required` slot below — exactly the failure the mandatory downstream slots were + introduced to prevent. Callers now state the absence: `ContractKey: null` for a template that + declares no key. +- **`ILedgerWriter.TryExerciseAsync` and `ILedgerWriter.TryCreateAsync` take a + new optional `CommandId? commandId = null`** immediately after `workflowId` + (`Daml.Ledger.Abstractions`), so a caller can reuse an id across a retry of a lost-but-accepted + submission and have the ledger deduplicate it — the same parameter the generated exercisers + already accept. Any external implementation of `ILedgerWriter` must add the parameter to both + methods and honour it. Callers are unaffected at the source level unless they passed `timeout` + positionally, which now fails to compile rather than rebinding silently — `TimeSpan` and + `CancellationToken` cannot convert to `CommandId`, whose own conversions are explicit and + `string`-only. Binary compatibility does break: an assembly compiled against the previous package + bakes the old signature into its call sites, so pinning an old `Daml.Ledger.Abstractions` + alongside newly generated code raises `MissingMethodException` at runtime. Publish in lockstep. + The two value-returning `ThrowingExercise.ExerciseAsync` overloads gain the same + parameter and forward it, and the `CreateByExercise` overloads and the void + `ThrowingExercise.ExerciseAsync` overloads take it too — there the break is positional, since + `commandId` precedes `timeout`. Both fail to compile rather than rebinding silently. +- **`CreateAsync` no longer takes a submitter when every signatory is a payload field** — the + emitted wrapper derives the `SubmitterInfo` from the payload's `Party` properties, so the + parameter is removed rather than made optional. It is dropped only when the analysis yields at + least one payload field: a template with any non-payload signatory keeps the explicit + `SubmitterInfo` parameter, and so does one whose signatory clause resolves statically to an empty + list. Callers that passed a submitter equal to the payload's signatories drop the argument; a + caller that passed a different `actAs` was asserting an authority the template does not grant. A + caller that passed a `SubmitterInfo` carrying `readAs` was over-asserting nothing, though — + `readAs` is disclosure, not authority — and the emitted wrapper gives it no way to set one. That + call moves to `ILedgerWriter.TryCreateAsync(payload, submitter, workflowId, commandId, + timeout, cancellationToken)`, which still takes the whole `SubmitterInfo`. +- **The `Party actAs` overloads are removed**, both the emitted ones and their six hand-written + counterparts in `CreateByExercise` and `ThrowingExercise`. A positional `Party` call site keeps + compiling — the implicit `Party` → `SubmitterInfo` conversion still binds — but a call passing + `actAs:` as a named argument does not, and the change is binary-breaking either way. Pass a + `SubmitterInfo`, or let a `Party` convert. The named-controller parameters emitted for + create-bearing choices (`owner`, `custodian`, `provider`, `steward`) are a different family and + are unchanged. +- **The six single-`Party` convenience overloads on `PartyOverloads` + (`Daml.Ledger.Abstractions`) are removed**, and the class with them: the `TryExerciseAsync` / + `TryCreateAsync` pair over `ILedgerWriter` and the four `SubscribeAsync` (offset and + `StakeholderResume` forms), `SubscribeActiveAsync` and `SubscribeLedgerEffectsAsync` members over + `ILedgerStreamer`. No positional call ever reached them: `SubmitterInfo` declares an implicit + conversion from `Party`, which makes the interface member itself applicable to a `Party` + argument, and C# considers extension methods only when no instance member is. So + `client.SubscribeAsync(alice, ...)`, `writer.TryCreateAsync(payload, alice, ...)` and every + other positional call site compile and behave exactly as before — they always bound to the + interface member. Only a caller who named the argument, `actAs: alice`, reached the extension, + and that spelling no longer compiles: drop the name, or use the interface's own parameter name, + `submitter: alice`. Binary compatibility breaks for an assembly compiled against a call to one of + the six. +- **`SingleCommandSubmission` is renamed to `SingleCommandExtensions`** + (`Daml.Ledger.Abstractions`), aligning it with the `