DPM component POC for participant-scoped Canton transaction visualization.
It demonstrates the proposal surface:
trace: inspect a successful transaction by update id.trace --command-id: inspect a failed submission through completion data.open: reopen an exported trace artifact.prepare: prepare a command without committing it.submit: submit a command (submit-and-wait) and print the resulting update id.compare: compare prepared transactions, successful transactions, or completions.test: run Daml Script unit tests (unit mode) or an lit suite against a managed local Canton (integration mode).
.venv/bin/python -m pip install -e .
./scripts/install-local-dpm-trace.shOptional local config:
cp .dpm-trace.example.json .dpm-trace.jsonExample config:
{
"ledgerUrl": "http://localhost:<json-ledger-api-port>",
"readAs": "<party-id>",
"darPaths": ["./path/to/app.dar"]
}Inspect a successful transaction:
dpm trace <update-id>With explicit participant context:
dpm trace <update-id> \
--submitter http://localhost:<json-ledger-api-port> \
--read-as '<party-id>' \
--access-token-file ./token.txtThe bearer token can also be passed with --token, DPM_TRACE_TOKEN, or DPM_TRACE_TOKEN_FILE.
Inspect a failed submission by command id:
dpm trace --command-id <command-id> \
--submitter http://localhost:<json-ledger-api-port> \
--act-as '<party-id>' \
--log-file /tmp/canton-participant.log \
--access-token-file ./token.txtOr inspect captured completion JSON:
dpm trace --completion-file completion.json \
--log-file /tmp/canton-participant.logWith a local Daml project and DAR available, failed completions can point back
to the contract line and column. When --dar is provided, dpm trace uses
damlc inspect to confirm the failure text exists in the compiled package
before resolving it against local sources.
dpm trace --completion-file completion.json \
--daml-yaml <path-to-daml-project>/daml.yaml \
--dar <path-to-daml-project>/.daml/dist/app.dar \
--damlc damlExport a trace artifact:
dpm trace <update-id> --export trace.jsonOpen the interactive transaction visualizer:
dpm trace <update-id> --visualizeReopen an exported trace artifact:
dpm trace open trace.json
dpm trace open trace.json --visualizePrepare a command without committing it:
dpm trace prepare \
--submitter http://localhost:<json-ledger-api-port> \
--act-as '<party-id>' \
--template '<package-id>:Counter:Counter' \
--arg owner='<party-id>' \
--arg count=0 \
--export prepared.jsonOr pass a command file:
dpm trace prepare \
--submitter http://localhost:<json-ledger-api-port> \
--act-as '<party-id>' \
--commands commands.json \
--export prepared.jsonprepare calls Canton's non-committing prepare API. It does not submit to the ledger.
Compare a prepared transaction with a successful transaction:
dpm trace compare \
--prepared prepared.json \
--update <update-id> \
--submitter http://localhost:<json-ledger-api-port> \
--read-as '<party-id>'Compare a prepared transaction with a failed submission:
dpm trace compare \
--prepared prepared.json \
--command-id <command-id> \
--submitter http://localhost:<json-ledger-api-port> \
--act-as '<party-id>' \
--log-file /tmp/canton-participant.logCompare two successful transactions:
dpm trace compare <update-id-a> <update-id-b> \
--submitter http://localhost:<json-ledger-api-port> \
--read-as '<party-id>'Compare a prepared transaction with a captured completion JSON:
dpm trace compare \
--prepared prepared.json \
--completion-file completion.jsonRun a package's Daml Script unit tests, render each script's transaction tree,
and map any failed test back to source. It wraps daml test on the in-memory
IDE ledger, so it needs no Canton node and runs locally in CI/CD.
dpm trace test <package-dir> --daml damldaml test already runs the tests and gates CI by exit code. dpm trace test
adds what it does not:
- Failure triage. A red test is resolved to source and rendered with a caret
— both the test call site and the contract invariant (
assertMsg/abort/ensure) that rejected it. With--dar, the contract match is verified against the compiled package usingdamlc inspect, not just grepped from local files. - Transaction trees in the terminal and as JSON.
daml testonly writes these to IDE-only HTML; here they appear inline and in--print-json. - A structured report to build CI automation on (PR comments, custom gates).
dpm trace test . # run all Script tests in the current package
dpm trace test . --no-trees # summary + failures only (compact CI logs)
dpm trace test . --print-json # machine-readable report (dpm-trace/test-report/v0)
dpm trace test . --junit out.xml # also write JUnit XML for CI
dpm trace test . -p testSplit # run a subset by test pattern
dpm trace test . --dar .daml/dist/<pkg>.dar # damlc-inspect-verified failure mapping--daml selects the toolchain (daml, damlc, or dpm) and defaults to daml.
A passing run renders each script's decoded transaction tree and a per-test summary:
DPM trace test
package: daml-tests
command: daml test
result: all 7 passed (7 total)
Results
PASS testTransfer 2 tx +2 create >1 exercise x1 archive
PASS testSplit 2 tx +3 create >1 exercise x1 archive
PASS testCannotIssueZero 1 tx !1 expected-fail
...
A failing run pinpoints the source and returns a non-zero exit code. When a contract guard rejects a submission, the report shows both where the test failed and why:
FAIL testSplitContractGuard
message: ... AssertionFailed: splitQuantity must be between 1 and quantity - 1 ...
daml/Test.daml:14:8 basis: daml test: Test (where the test failed)
daml/Asset.daml:37:20 basis: damlc inspect: Asset (the invariant that rejected it)
> 37 | assertMsg "splitQuantity must be between 1 and quantity - 1"
^
The command exits non-zero on any failure, so a CI step is a single line:
dpm trace test . --dar .daml/dist/<pkg>.dar --junit results.xml --no-treesA worked example — an Asset contract, a Daml Script test suite, a GitHub Actions
workflow, and a regression demo — lives in the sibling daml-tests package.
Unit tests run on the in-memory IDE ledger. For integration tests against a
real local Canton node, point test at an lit suite with --integration.
Scaffold the test layout once with --init — it writes itests/ (the lit
integration suite) and a self-contained unittests/ package:
dpm trace test . --init
# created itests/lit.cfg.py, itests/example.test
# created unittests/daml.yaml, unittests/daml/Example.daml
# created .github/workflows/dpm-trace.yml (unit + integration jobs)--no-unittests / --no-ci skip those parts.
Then run the integration suite:
dpm trace test . --integration itests \
--canton-jar "$DPM_TRACE_CANTON_JAR" \
--daml damlMultiple participants. --parties places parties on participants with
Name@N, and the harness provisions that many participant nodes:
dpm trace test . --integration itests --canton-jar "$DPM_TRACE_CANTON_JAR" \
--parties Alice@1,Bob@2Tests then reach the second participant via %ledger2. Because a committed
update reaches the other participant asynchronously, trace it with --wait <s>
(retries until it is visible). See daml-tests/itests/asset-issue-to-bob.test
for a cross-participant example.
This builds the package DAR, boots an in-memory Canton on random ports, uploads
the DAR, allocates parties (default Alice,Bob), then runs lit over the test
directory against the live node and tears Canton down. One boot serves the whole
suite, and the lit exit code gates CI.
Connection details are exposed to tests as lit substitutions: %dpm
(the CLI), %ledger (the participant JSON Ledger API URL), %alice, %bob,
and %dar. A test submits against the live ledger and asserts on the trace:
# REQUIRES: canton
# RUN: ID=$(%dpm submit --submitter %ledger --act-as %alice \
# RUN: --template '#asset-tests:Asset:Asset' \
# RUN: --arg issuer=%alice --arg owner=%alice --arg name=GOLD --arg quantity=100) \
# RUN: && %dpm trace "$ID" --submitter %ledger --read-as %alice --color never | FileCheck %s
# CHECK: CREATE Asset:Asset
# CHECK: name: GOLD{{.*}}quantity: 100
It needs a Canton jar (--canton-jar or DPM_TRACE_CANTON_JAR), plus lit and
FileCheck on PATH. See daml-tests/itests/ for a working suite.
Submit a command to a participant (submit-and-wait) and print the update id — the primitive integration tests use to create state and then trace it:
dpm trace submit \
--submitter http://localhost:<json-ledger-api-port> \
--act-as '<party-id>' \
--template '#<package-name>:Mod:Template' \
--arg owner='<party-id>' --arg count=0Use --print-json for the full submit-and-wait response, or --allow-fail to
capture a rejected submission as JSON (which dpm trace --completion-file then
maps back to source) instead of erroring out.
This fixture shows the CI-style path: consume a captured completion/error JSON,
verify it against a compiled DAR with damlc inspect, and resolve it against
local Daml sources.
dpm trace --completion-file examples/failed-with-source.completion.json \
--daml-yaml <path-to-daml-project>/daml.yaml \
--dar <path-to-daml-project>/.daml/dist/app.dar \
--damlc damlThe output includes a Source diagnostics block with file:line:column and a
caret under the matching Daml code.
Fast source-diagnostic tests:
lit testsInspect-backed source diagnostic test:
DPM_TRACE_RUN_DAMLC_INSPECT=1 \
DPM_TRACE_DAMLC=daml \
lit tests/completion-source-inspect.testOpt-in Daml Script test-runner integration test (uses a real Daml toolchain
against the sibling daml-tests package):
DPM_TRACE_RUN_DAML_TEST=1 \
DPM_TRACE_DAML=daml \
lit tests/daml-script-test.testOpt-in local Canton integration test:
DPM_TRACE_RUN_REAL_CANTON=1 \
DPM_TRACE_DAML=daml \
DPM_TRACE_DAMLC=daml \
DPM_TRACE_CANTON_JAR=<path-to-canton.jar> \
DPM_TRACE_DAML_HELPER=<path-to-daml-helper> \
lit tests/real-canton-failed-completion.test- Output is participant-scoped. It is not a global Canton transaction.
- Failed submissions may not have an update id. In that case comparison uses completion/error data.
- Source diagnostics use
damlc inspectplus local source/project metadata when available, with local source matching as a fallback. Compiler debug-info generation is out of scope for this PoC.