feat(decman-lib): extract the governance domain model and gRPC codecs - #382
Draft
hubagaspar91 wants to merge 28 commits into
Draft
feat(decman-lib): extract the governance domain model and gRPC codecs#382hubagaspar91 wants to merge 28 commits into
hubagaspar91 wants to merge 28 commits into
Conversation
The decman-lib extraction reshapes ProposalType to newtype variants and moves ActionType into the new crate. These insta snapshots pin the HTTP wire JSON of every variant, populated and minimal, so every extraction commit can prove the wire shape never moves.
New workspace crate for the extracted governance domain model and gRPC codecs. Two public modules split by one rule: framework never names a Daml template, catalog does. The thiserror Error keeps decman's exact validation and package-not-configured strings.
Lib-owned template id: a #package-name ref plus module and entity. Owns the two wire conventions: Display as pkg:Module:Entity, and event matching on module+entity only, because Canton echoes resolved package hashes back in events.
The Daml Value constructors, the extraArgs builders, and TransferValidity move from decman's action_serializer to decman_lib::framework::encode, as pub. Integrators cannot implement DamlProtoEncode without them. decman now depends on decman-lib.
One shared module now holds both record-reading shapes: the strict Result readers and the lenient Option readers, plus the Set Party, GenMap, and RelTime extractors. Sources: decman record.rs, the action_serializer extract helpers, and the queries field helpers.
The EventFormat builders move whole. Integrators need them to build the same ACS and update reads decman runs.
One capability per trait, plus the GrpcPayload combination with a blanket impl. Validate takes a ValidationCtx: the governance party and now_micros enter as parameters, so checks stay deterministic. The extension test doubles as the integrator example.
commands_envelope replaces decman's seven duplicated Commands literals; command_id is a parameter so golden tests stay deterministic and the lib needs no uuid dep. build_propose serves any TemplateInfo+DamlProtoEncode payload, injecting the two party fields every GovernableAction template carries.
The payload support types move to the catalog with feature-gated utoipa/ts-rs derives; decman re-exports them so paths and the HTTP JSON stay identical. The protocol validators move to the framework; validate_future_micros now takes now_micros as a parameter, and every message string stays byte-identical inside Error::Validation.
The closed Daml unions map to a Rust enum with methods, so the enum moves whole: both encode forms, both decode dispatches, and validate. The two production panics become Encode errors. Integrators now parse vault and self confirmations with one call. decman re-exports the enum, so the HTTP JSON is unchanged by construction.
Five payloads move to catalog structs with the capability traits; their ProposalType variants become newtype wrappers. The JSON snapshots pin the wire shape; the ts-rs output changes shape (inline fields become intersections) with unchanged field names and optionality. ProvisionProviderService becomes an empty-braces struct because serde cannot internally-tag a unit struct.
Transfer and AcceptTransfer split into wire structs (Validate + TemplateInfo) and wrapper encode structs that carry the registry choice context, the validity window, and the sender. The type system now forces the context decision at the call site instead of an empty-context default that fails on-ledger at execute.
The four rewards payloads move with their validate bodies. The minting-delegation expiry check now reads now_micros from the ValidationCtx instead of the clock, so the check is deterministic.
The three credential payloads move with their validate bodies and the BillingParams encoder.
The last thirteen payloads move. Every ProposalType variant is now a newtype over a catalog struct, every validate arm delegates through the ValidationCtx, and every build_proposal_create_args arm is glue that Task 23 deletes.
The rules, confirmation, and interface template ids move behind Result accessors that error where decman used to 503. Exact-string tests pin every payload struct's template id.
Pure per-event parses over CreatedEvent. The domain confirmation type carries no action, because the on-ledger template stores none; decman's HTTP placeholder stays a decman mapping concern. Task 20 switches queries.rs onto these.
The generic GovernableAction interface-view parse and the three detail extractors move as pure functions over CreatedEvent. The detail DTOs move to the catalog; decman re-exports them so the HTTP JSON and generated TypeScript names stay put.
The dedupe, liveness, and orphan-rule logic leaves the fetch loops as pure functions with time as a parameter. The semantics tests pin the boundaries: newest-per-party, expiry at now, threshold edges, orphans never execute, synthesis only on two complete fetches.
The governance read path now fetches with decman's client, parses with the lib, and maps to the HTTP DTOs. The placeholder action on domain confirmations survives only inside the one mapping function, so the wire JSON is unchanged while the lib types tell the truth.
Thirteen pure builders cover confirm, execute, expire, and cancel for all three governance kinds, plus the atomic proposal retraction. Each returns a Commands ready for submit_and_wait; package-ref resolution and submission stay with the caller.
Each write handler keeps its GovernanceType match, but every arm is now accessor, package-ref resolve, one lib builder call, submit. The seven duplicated Commands literals are gone.
The propose handler keeps auth, validation, the singleton guard, the registry fetches, and submission; the lib builds the create and self-confirm commands and extracts the created cid. The interim build_proposal_create_args glue and the ProposalPackage enum die, and CI gains the decman-lib forbidden-dependency gate.
GitHub runs run-blocks with bash -e, which exempts !-inverted commands, so the gate's first line could never fail the job. The fail-closed if-forms restore the depth-1 reqwest check, and pipefail makes a cargo-tree failure fatal instead of a vacuous pass. The docs that named the deleted action_serializer.rs now point at the lib.
The crate-level rustdoc gains a compiling quick-start: a custom proposal through build_propose, plus the read side and a by-cid confirm. The CI dependency gate widens actix-web to the actix family and documents the tonic-transport blind spot. Two doc provenance notes and one zero-constant cleanup ride along.
TemplateInfo now takes a PackageResolver, so external integrators resolve package refs from their own config, a map, or a bare str — PackageConfig demotes to one shipped convenience impl. DecMan call sites are unchanged: the reference coerces.
The CI dependency gate goes away per review. One macro-generated projection replaces the repeated validate arms and the grpc_payload match in ProposalType. The what-moved-where comments go; comments now describe what the code is, not the extraction.
The stale test-mode/wildcard comments now state the live reason the view fallback exists. The PackageConfig resolver destructures without `..`, so a new config field is a compile error here. The projection macro unrolls into two explicit matches per review preference, and a guard test pins which variants carry a GrpcPayload.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR creates
crates/decman-liband moves decman's governance protocolknowledge into it. The crate holds the typed models for every governance
action and proposal, the gRPC codecs that turn them into Ledger API values,
and the pure functions that parse governance state back out of events.
DecMan now calls the lib. The wire behavior does not change.
Three codebases implement this protocol today: decman (gRPC), the attestor
stack (JSON), and soon the token launchpad. The lib ends the duplication for
the gRPC side and gives external integrators one crate to build governance
clients with: propose, confirm, execute, expire, and cancel — for the core
actions and for custom
GovernableActiontemplates they write themselves.The JSON codec and the attestor stay out of scope (phase 2).
The design doc behind this PR was reviewed before implementation. Its core
decision is an asymmetric type model that mirrors the Daml:
ActionTypemoves whole and gains codec methods:to_vault_proto,to_self_proto,from_vault_proto,from_self_proto,validate. Thetwo production panics became typed errors.
payload (29 of them) plus three capability traits (
TemplateInfo,DamlProtoEncode,Validate) and a frozenGrpcPayloadcombinationtrait. Third parties implement the traits for their own templates and get
the same builders decman uses.
Implementation details
decman_lib::framework(template-agnostic; never names a Daml template):ValidationCtx. Validation takes the governanceparty and
now_microsas parameters — the lib never reads a clock.TemplateInforesolves package refs through aPackageResolvertrait;the lib ships impls for
PackageConfigand for a bare&str, so anintegrator needs no decman config type to name their package.
TemplateId: a#package-nameref plus module and entity. It renders aspkg:Module:Entityand matches events on module+entity only, becauseCanton echoes resolved package hashes back.
make_party,make_record, theextraArgsbuilders,
TransferValidity) and the record-reading toolkit (strictResultreaders and lenientOptionreaders, plus theSet Party,GenMap, and RelTime extractors).
commands_envelope(replaces the duplicatedCommandsliterals;command_idis a parameter, so the lib needs no uuid dependency) andbuild_propose, which injects thegovernancePartyandproposerfields every
GovernableActiontemplate carries.decman_lib::catalog(decman's protocol content):ActionType(21 variants) with the four codec methods; decode dispatchlives here, so integrators parse confirmations with one call.
credential, utility). serde attributes moved byte-identically; ts-rs and
utoipa derives sit behind
typegen/openapifeatures.TransferandAcceptTransfersplit into wire structs and wrapper encodestructs (
TransferWithContext,AcceptTransferWithContext). The wrapperscarry the registry choice context and the validity window, so the type
system forces the context decision; an empty-context transfer cannot be
encoded by accident.
PackageNotConfiguredwhere decmanused to 503, and the template lists the fetch loops iterate.
the three confirmation cancels, and the atomic two-command proposal
retraction.
parse_confirmation,parse_domain_confirmation(honestly action-less — the on-ledger template stores no action),
extract_governance_state,extract_proposal_info, the dedupe andliveness helpers, and the orphan-rule assembly.
DecMan changes:
ProposalTypestays in decman as the HTTP enum, reshaped to newtypevariants that wrap the lib structs. The serde tag layout keeps the JSON
byte-identical.
to the HTTP DTOs. The legacy placeholder action on domain confirmations
survives in exactly one mapping function, for wire compatibility.
GovernanceTypematch; each arm is nowaccessor → package-ref resolve → one builder call → submit.
propose_actionkeeps auth, validation, the delegation-singleton guard,and the registry fetches; the lib builds the commands.
action_serializer.rs(3.6k lines) is deleted.One edge worth knowing:
crates/commongained eight additivePartialEqderives on wire DTOs(nothing else changed there). The lib structs derive
PartialEqfor theround-trip tests and need the embedded types to match.
Related issues
None.
Type of change
Checklist
cargo fmt -- --checkpassescargo clippy --all-targets --all-features -- -D warningsis cleancargo testpasses<type>(<scope>): <subject>conventionNotes for reviewers
How this was tested. The first commit snapshots the HTTP JSON of every
enum variant with insta, before any conversion. Every later commit keeps
those snapshots byte-identical; that is the wire-compatibility proof. The
full localnet e2e (
governance_workflows_e2e) ran green six times acrossthe branch, and it re-runs on the head of every review round. The lib
carries 110 tests plus a doctest: codec
round-trips for both encode forms, wrong-form and unknown-constructor error
tests, exact
pkg:Module:Entitystrings for all 29 payloads, encodesnapshots with populated and minimal fixtures, interpretation boundary
tests (expiry at
now, threshold edges, orphans never execute, bothSet Partywire shapes), and a compiling integrator doctest in the craterustdoc.
Where to look first.
crates/decman-lib/src/lib.rs(the quick-startdoctest),
catalog/action.rs(the enum move),catalog/proposals/custody.rs(the wrapper pattern),
server/queries.rs(the read-path adoption and theone placeholder mapping), and
handlers/governance.rs(the write-pathadoption).
Size. The PR is far over the usual ~400-line norm. It is a reviewed
extraction executed as 28 stepwise commits; each commit compiles, passes
the full suite, and moves one bounded slice. Reviewing commit-by-commit is
the intended path.
Review rounds so far. The first review reshaped three things on the
branch:
TemplateInfonow resolves package refs through thePackageResolvertrait (thePackageConfigimpl destructures without.., so a new config field is a compile error there); the CI dependencygate came and went, so the branch's net CI diff is empty; and the payload
projections in
ProposalTypeare explicit matches with a guard test thatpins which variants carry a
GrpcPayload. Extraction-changelog commentsare gone throughout.
Follow-ups (non-blocking). An all-
Nonecodec round-trip fixture forActionType; a decman unit test for theDomainAction→ DTO mapping; onemanual OpenAPI diff against main; feature-gating
canton-api-clientincanton-lib's
common(it pullsreqwestinto every consumer); phase 2adds the JSON codec and the attestor.