@distrohelena/canton-typescript-sdk
v1.0.6
Published
TypeScript SDK for Canton with:
Maintainers
Readme
Canton TypeScript SDK
TypeScript SDK for Canton with:
- a shared
CantonClient - gRPC and JSON transports
- gRPC Ledger API-shaped service boundaries
- PQS-backed relational queries
- external-party lifecycle and signing
- authenticated and TLS-secured channels
- experimental tooling for invariant testing, DAML-LF inspection, interface generation, and replay debugging
Install
npm install @distrohelena/canton-typescript-sdkExperimental invariant testing
@distrohelena/canton-typescript-sdk/testing is an experimental, opt-in
Foundry-style fuzzing surface for Canton. It provides semantic parity for
campaign runs, exact depth, handlers, permissive or strict protocol reverts,
invariants, shrinking, replay traces, and safe artifacts. It does not attempt
to reproduce Foundry's EVM, ABI, PRNG, or shrink sequence byte-for-byte.
Define the campaign once, keep ledger I/O in explicit runtime hooks, and use safe isolation. Shared or production ledgers must use an explicit external or snapshot policy; cleanup policies need contract discovery for ambiguous submissions.
import * as fc from "fast-check";
import {
defineInvariantCampaign,
runInvariantCampaignCheckAsync,
} from "@distrohelena/canton-typescript-sdk/testing";
const campaign = defineInvariantCampaign<{ total: number }>({
runtime: {
actors: {
issuer: { party: "Issuer", participant: "participant-a" },
},
isolation: { kind: "external" },
},
config: { runs: 100, depth: 8, failOnRevert: false, seed: 42 },
targets: [{ key: "Main:Iou:Create", actors: ["issuer"] }],
invariants: [async ({ model }) => {
if (model.total < 0) throw new Error("negative total");
}],
});
await runInvariantCampaignCheckAsync({
campaign,
arbitrary: fc.constant([{ actor: "issuer", targetKey: "Main:Iou:Create" }]),
key: (actions) => JSON.stringify(actions),
setupAsync: async () => ({ model: { total: 0 }, ghost: {} }),
executeAsync: async () => ({ kind: "accepted", updateId: "update-1" }),
});Use createDamlTestingCatalog, targetTemplate, and
resolveDeclarativeTargets to discover targets. targetTemplate(id).create()
and .choice(name) select create and exercise actions; pair the resolved
targets with createDeclarativeCampaignArbitrary for an exact-depth action
sequence. In executeAsync, pass each action to executeDeclarativeActionAsync
with the campaign runtime and an explicit resolveContractIdAsync callback for
choices. The callback is intentional: the SDK will not guess an active contract
from stale local state. handler, bound, and handler assumptions support
custom operations alongside declarative actions. A failed check returns the
shrunk counterexample trace; InvariantCampaignFailure and replay artifacts
expose only allowlisted diagnostics.
Automatic Party fields require an explicit valueParties list. When writing
commands by hand, use DamlParty and DamlNumeric for Party and exact decimal
Numeric values; a plain string is DAML Text and a plain JavaScript number is
not an exact Numeric value.
Live Integration Tests
The repository also supports a live SDK validation suite against an already-running CN quickstart localnet.
The current party-creation flows were developed and live-tested against Canton
Participant 3.5.7. In particular, external-party allocation signs the generated
combined topology hash (multi_hash) rather than each topology transaction
individually.
The decentralized Ed25519 example was also run end-to-end against an isolated Canton Participant 3.5.8 sidecar. On both 3.5.7 and 3.5.8 it creates the same topology shape: a serial-1 decentralized namespace definition, one self-root namespace certificate per owner, and a PartyToParticipant containing the party signing key. No deprecated PartyToKeyMapping is created.
The SDK preserves identityProviderId, userId, and waitForAllocation
through decentralized prepare/finalize. Canton ignores waitForAllocation for
decentralized parties, so the example explicitly calls
topologyAggregationService.waitForPartyHostingAsync(...) to prove that the
expected participant is hosting the party on the expected synchronizer. That
aggregate topology check is the same on Participant 3.5.7 and 3.5.8.
Standalone TypeScript examples
The repository includes runnable TypeScript examples for a local Canton participant. Check them before running a lifecycle:
npm run examples:checkThe setup and party examples are available individually:
npm run example:init
npm run example:tls
npm run example:jwt
npm run example:party:hosted
npm run example:party:external
npm run example:party:decentralizedFor the application lifecycle, run these in order:
npm run example:dar:upload
npm run example:contract:create-exercise
npm run example:contract:query
npm run example:updates:stream
npm run example:user:rights
npm run example:topology:party-hostingWorkflow examples
The eight stateful workflow examples are standalone proofs, not a sequence:
each uploads or verifies the fixture DAR, resolves a party, reads the
participant status, and creates its own run-scoped data. First make a Canton
participant available and verify the source with npm run examples:check; the
default endpoints and authentication environment variables are documented below.
The pruning preflight is a separate read-only operator check and does none of
those setup or mutation steps. Run any example independently:
npm run example:workflow:atomic
npm run example:workflow:retry
npm run example:workflow:resume
npm run example:workflow:stale-contract
npm run example:workflow:command-completion
npm run example:workflow:contract-lifecycle-audit
npm run example:workflow:update-lookup-reconciliation
npm run example:workflow:pruning-preflight
npm run example:workflow:participant-localThose eight established stateful workflows intentionally leave durable state behind. A missing SDK_EXAMPLE_PARTY
causes fallback party allocation, which creates durable topology state; every
one also creates durable contracts. Set SDK_EXAMPLE_PARTY to an existing
party to rerun the established workflows against that party and avoid fallback
allocation. The fixture DAR remains installed after a run.
The participant-local command-submission workflow
(npm run example:workflow:participant-local) is a standalone gRPC-only proof
of the ordinary participant submission authorization route. It uses the normal
SDK_EXAMPLE_* endpoint, authentication, party, and timeout configuration:
SDK_EXAMPLE_LEDGER_ENDPOINT, SDK_EXAMPLE_LEDGER_ADMIN_ENDPOINT,
SDK_EXAMPLE_PARTICIPANT_ADMIN_ENDPOINT, SDK_EXAMPLE_BEARER_TOKEN,
SDK_EXAMPLE_LEDGER_BEARER_TOKEN, SDK_EXAMPLE_LEDGER_ADMIN_BEARER_TOKEN,
SDK_EXAMPLE_PARTICIPANT_ADMIN_BEARER_TOKEN, SDK_EXAMPLE_PARTY,
SDK_EXAMPLE_PARTY_PREFIX, SDK_EXAMPLE_TIMEOUT_MS, and
SDK_EXAMPLE_TLS_ROOT_CERTIFICATE. It configures a command signer that throws
if touched, then successfully creates and finds one exact active Message through
commandService.submitParticipantLocalAndWaitAsync; that success proves the
configured external signer was bypassed. An explicit party is reused, while
fallback allocation creates durable topology; the fixture upload leaves a
durable DAR and the proof leaves a durable contract. The unchanged
implementation uses the common compatibility path on Participant 3.5.7 and
the isolated Participant 3.5.8 sidecar.
The completion-correlation workflow (npm run example:workflow:command-completion)
is a standalone successful proof that creates durable Message state. It uses the
same normal SDK_EXAMPLE_* configuration: SDK_EXAMPLE_LEDGER_ENDPOINT,
SDK_EXAMPLE_LEDGER_ADMIN_ENDPOINT, SDK_EXAMPLE_PARTICIPANT_ADMIN_ENDPOINT,
SDK_EXAMPLE_BEARER_TOKEN, SDK_EXAMPLE_LEDGER_BEARER_TOKEN,
SDK_EXAMPLE_LEDGER_ADMIN_BEARER_TOKEN,
SDK_EXAMPLE_PARTICIPANT_ADMIN_BEARER_TOKEN,
SDK_EXAMPLE_TLS_ROOT_CERTIFICATE, SDK_EXAMPLE_PARTY,
SDK_EXAMPLE_PARTY_PREFIX, and SDK_EXAMPLE_TIMEOUT_MS.
SDK_EXAMPLE_USER_ID is mandatory: absent or blank input is rejected, while
every nonblank value is preserved untrimmed and exactly submitted and exactly
matched in the completion. With bearer authentication, the configured declared
user must equal the token's Ledger API user/subject; the example does not inspect
the token. It keeps the ledger end as its saved exclusive offset and begins the
first stream read before submission.
No public wait-for-command-completion helper or API is introduced; the
correlation helper remains example-only.
The completion-correlation example asserts successful correlation only; it does
not assert rejected-command correlation. On both participant observations, the
stream-first rejected-command probes observed no exact completion before their
bounded stream transport errors.
The contract-lifecycle audit workflow (npm run example:workflow:contract-lifecycle-audit) is a standalone gRPC-only proof. It uses the normal SDK_EXAMPLE_* endpoint, authentication, party, and timeout configuration: SDK_EXAMPLE_LEDGER_ENDPOINT, SDK_EXAMPLE_LEDGER_ADMIN_ENDPOINT, SDK_EXAMPLE_PARTICIPANT_ADMIN_ENDPOINT, SDK_EXAMPLE_BEARER_TOKEN, SDK_EXAMPLE_LEDGER_BEARER_TOKEN, SDK_EXAMPLE_LEDGER_ADMIN_BEARER_TOKEN, SDK_EXAMPLE_PARTICIPANT_ADMIN_BEARER_TOKEN, SDK_EXAMPLE_PARTY, SDK_EXAMPLE_PARTY_PREFIX, and SDK_EXAMPLE_TIMEOUT_MS. An explicit SDK_EXAMPLE_PARTY is reused; otherwise fallback allocation creates durable topology. The fixture upload leaves a durable DAR, and the workflow leaves durable contracts. It uses the alpha ContractService to prove the original active Message before replacement and the replacement active Message afterward, then uses EventQuery for the original contract's create/archive history. It makes no post-archive ContractService claim for the original.
The update-lookup reconciliation workflow (npm run example:workflow:update-lookup-reconciliation) is a standalone gRPC-only proof that observes one exact self-party Message transaction from UpdateService.GetUpdates, then immediately reconciles it through getUpdateById and getUpdateByOffset. It uses the normal SDK_EXAMPLE_* endpoint, authentication, party, and timeout configuration, including SDK_EXAMPLE_PARTY and SDK_EXAMPLE_TIMEOUT_MS; first run npm run examples:check and make an authenticated participant available. An explicit party is reused, while fallback allocation creates durable topology; the fixture upload leaves a durable DAR and the workflow leaves durable contracts. The same unchanged implementation is tested against authenticated Participant 3.5.7 and the isolated Participant 3.5.8 sidecar, in default-party and explicit-party modes.
The pruning-preflight workflow (npm run example:workflow:pruning-preflight) is a standalone gRPC-only, read-only operator check. It requires SDK_EXAMPLE_OFFSET to be a canonical positive decimal integer and uses the normal endpoint, authentication, and timeout variables: SDK_EXAMPLE_LEDGER_ENDPOINT, SDK_EXAMPLE_LEDGER_ADMIN_ENDPOINT, SDK_EXAMPLE_PARTICIPANT_ADMIN_ENDPOINT, SDK_EXAMPLE_BEARER_TOKEN, SDK_EXAMPLE_LEDGER_BEARER_TOKEN, SDK_EXAMPLE_LEDGER_ADMIN_BEARER_TOKEN, SDK_EXAMPLE_PARTICIPANT_ADMIN_BEARER_TOKEN, and SDK_EXAMPLE_TIMEOUT_MS. The participant-admin credential is required for the schedule and safe-pruning context reads. Its safe-pruning context request sends the saved ledger end and a validated current timestamp encoded in beforeOrAt; it leaves commitment-state absent. It does not mutate the participant or create durable state: no party, DAR, command, update query, schedule change, or pruning request is made. The later participant watermark classifies the supplied offset as alreadyPruned, beyondLedgerEnd, or notObservedPruned; all-divulged watermarks and schedule/safe-pruning context are reported separately and do not alter that result. notObservedPruned is not proven queryable, because pruning can race after the later observation and other query preconditions can still fail. The unchanged implementation is supported on Participant 3.5.7 and the isolated Participant 3.5.8 sidecar.
Each of the eight established stateful workflows prints its actor plus the full participant version returned by the
authenticated status API, its parsed release core, and its selected path:
Participant version:, Release core:, and Compatibility path:. The current
stateful-workflow implementation uses one common-code path for release cores 3.5.7 and 3.5.8. It
adds a version-specific behavioral difference only after live evidence proves
one; it does not infer compatibility from a container tag or endpoint.
example:workflow:atomicfirst proves that a two-command batch with an invalid second command is rejected without creating its valid first Message, then proves that two independent creates commit atomically and remain active with their exact payloads.example:workflow:retrysubmits a caller-controlled command ID with a deduplication duration, retries the exact same request, classifies the duplicate outcome, and proves that exactly one matching contract is active.example:workflow:resumesaves the ledger end before its post-offset create, proves the intentionally idle stream timeout, then resumes exclusively after that saved offset and rejects a pre-offset contract if it appears.example:workflow:stale-contractproves archive/replacement state and then proves that exercising the archived contract is rejected.
Expected failures are accepted only through structured error classification: gRPC status code, decoded status, operation, and the selected compatibility path—not prose matching. The retry example uses one explicit command ID and deduplication period for the exact same request; changing either value is a new request and is not a retry proof. The resume example treats the saved offset as exclusive, so its resumed stream must observe only updates after the saved ledger end.
The eight established stateful workflow sources and unit contracts were developed
and live-tested against authenticated Participant 3.5.7 and the isolated
Participant 3.5.8. Both final-tree matrices selected the same unchanged implementation and common
compatibility path. The normalized outcome comparison is identical: atomic
reports invalidChoice before its replacement proof, retry reports
duplicateCommand with one active contract, resume reports idle-timeout and
a post-offset update, and stale-contract reports staleContract.
For those eight established stateful workflows, the best multi-version path reads the authenticated full version, parses its
release core, and uses data-only structured compatibility for observed
outcomes. A behavioral difference is introduced only after live proof; it is
never inferred from a container tag, endpoint, or prose error message. For the
isolated 3.5.8 sidecar, use the SDK sidecar launcher's protected documented
credential refresh flow in a local child shell and refresh before expiry. Never
log its output or expose refreshed credentials beyond that child. Use the
documented SDK_EXAMPLE_* environment variables when running the workflow
commands.
By default, Ledger and Ledger Admin use localhost:3901; Participant Admin
uses localhost:3902. Override them with
SDK_EXAMPLE_LEDGER_ENDPOINT, SDK_EXAMPLE_LEDGER_ADMIN_ENDPOINT, and
SDK_EXAMPLE_PARTICIPANT_ADMIN_ENDPOINT. Use SDK_EXAMPLE_BEARER_TOKEN for a
shared bearer token, or set SDK_EXAMPLE_LEDGER_BEARER_TOKEN,
SDK_EXAMPLE_LEDGER_ADMIN_BEARER_TOKEN, and
SDK_EXAMPLE_PARTICIPANT_ADMIN_BEARER_TOKEN per surface. The examples also
accept SDK_EXAMPLE_PARTY, SDK_EXAMPLE_USER_ID, SDK_EXAMPLE_SYNCHRONIZER,
and SDK_EXAMPLE_TIMEOUT_MS; generated fallback party names use
SDK_EXAMPLE_PARTY_PREFIX when supplied. For TLS, set
SDK_EXAMPLE_TLS_ROOT_CERTIFICATE to a custom CA certificate path.
Some commands intentionally create durable localnet state: DAR upload installs a package; create/exercise, query, and stream may allocate a fallback party and create contracts; topology inspection may allocate a fallback party. User-rights is read-only. These examples do not clean up durable state.
The uploaded asset is
canton-explorer-debug-playground-0.1.0.dar, the normal (not debug) Canton
Explorer Debug Playground DAR from
/home/helena/dev/daml/canton-explorer/debug-playground/.daml/dist/canton-explorer-debug-playground-0.1.0.dar.
Its Canton Explorer checkout commit is 750b28dd0ce4674e4368c12a6da1b5b5cbb00f88,
its package-introduction commit is abde077, it is Apache-2.0 licensed, and
its SHA-256 is
307cf7c52ac2770d1d1a2c5e1ec56a78ab7c70e7809c0cfb419abadb93cc6e29.
The examples and their DAR are repository-only and excluded from the npm tarball. The existing setup and decentralized-party examples were developed and live-tested against Participant 3.5.7, then live-tested unchanged against the isolated Participant 3.5.8 sidecar. The workflow examples have completed 3.5.7 and 3.5.8 workflow matrices with authenticated status evidence, the same unchanged implementation, and the common compatibility path.
Localnet launchers
The published package includes launchers for an existing CN Quickstart checkout.
Docker Compose must be installed and available. Set CN_QUICKSTART_DIR to the
checkout (or its quickstart/ directory) when it is not in a supported relative
location.
After installing the package, run:
canton-localnet-start
canton-localnet-stopYou can also run them without a global install:
npm exec --package @distrohelena/canton-typescript-sdk canton-localnet-start
npm exec --package @distrohelena/canton-typescript-sdk canton-localnet-stopThese commands launch and stop CN Quickstart; they do not provision a Quickstart checkout.
Optional Canton 3.5.8 participant sidecar
For SDK compatibility work, an isolated Canton 3.5.8 participant can join an already-running CN Quickstart localnet without changing its files or Compose project:
canton-localnet-participant-358-start
canton-localnet-participant-358-stopThe sidecar owns .generated/participant-358, its canton-participant-358
Compose project, its Postgres container, and host ports 8901 (Ledger), 8902
(Admin), and 8975 (JSON). The start launcher reads the existing localnet's
registered synchronizer via its Admin API, stores the exported connection
configuration in the sidecar runtime directory, and connects the sidecar with
its own Admin API. It generates a five-minute 3.5.8-compatible development JWT
at .generated/participant-358/ledger-api-user.token. When the connection is
healthy, use the protected child-shell credential flow below to make the
live-test and example endpoint/token variables available without logging them.
Canton 3.5.8 enforces this short lifetime for the unsafe development JWT.
cn-quickstart is strictly read-only: this launcher never writes to, starts,
or stops the normal Quickstart stack.
Refresh short-lived credentials before expiry without restarting or otherwise
touching the running containers. Use the launcher's protected documented
credential refresh flow only in a local child shell; do not log its output or
expose refreshed credentials beyond that child. The runtime credential file
remains protected with mode 0600.
After the protected credential flow completes, run the example inside that same short-lived credential-scoped child shell:
npm run example:party:decentralizedThe defaults target the normal insecure shared-secret localnet at
localhost:3902 on its quickstart Docker network. Override them when your
localnet differs:
PARTICIPANT_358_SOURCE_ADMIN_ENDPOINT=localhost:3902 \
PARTICIPANT_358_NETWORK=quickstart \
canton-localnet-participant-358-startProvide any required source-admin credential only through the protected child-scoped credential mechanism described above; do not place credentials in command lines or logs.
Use PARTICIPANT_358_CANTON_IMAGE, PARTICIPANT_358_PROJECT_NAME,
PARTICIPANT_358_RUNTIME_DIR, PARTICIPANT_358_LEDGER_PORT,
PARTICIPANT_358_ADMIN_PORT, and PARTICIPANT_358_JSON_PORT to make an
explicitly isolated variant. The opt-in Docker check requires a running
localnet and is never part of the normal test suite:
PARTICIPANT_358_SMOKE_TEST=1 npm run test:participant-358-sidecar-smokeOptional Splice image overrides
canton-localnet-start normally launches the Canton and Splice images pinned
by the CN Quickstart checkout's own .env (SPLICE_VERSION). To exercise the
SDK against a different Splice release instead, without editing that
checkout, use one of:
canton-localnet-splice-0.7.0-start
canton-localnet-splice-0.7.0-stop
canton-localnet-splice-0.6.14-start
canton-localnet-splice-0.6.14-stop
canton-localnet-splice-0.6.13-start
canton-localnet-splice-0.6.13-stop
canton-localnet-splice-0.6.12-start
canton-localnet-splice-0.6.12-stopThese are thin wrappers around the normal launchers that default IMAGE_TAG
to 0.7.0, 0.6.14, 0.6.13, or 0.6.12 respectively before delegating to
them; every other launcher option (extra participants, ES256, TLS,
AUTH_MODE) still applies. Set IMAGE_TAG yourself beforehand to target a
different version through the same wrappers.
npm run test:live (with EXTRA_PARTICIPANTS=4) has been run against each of
0.7.0, 0.6.14, 0.6.13, and 0.6.12 and passes on all four. Two
intermittent failures were observed and are environment-timing issues
unrelated to the Splice version under test: a PQS/scribe first-boot ingestion
race (live-query-parity, self-resolves on retry) and a dedicated
participant needing the domain's ACS reconciliation interval to elapse before
a safe pruning point exists (live-query-pruning). Separately, Splice 0.7.0
ships a Canton participant reporting Ledger API version 3.5.11, which the
workflow examples' compatibility guard (examples/shared/workflow-compatibility.ts)
does not yet accept (it allows only 3.5.7/3.5.8); those examples fail
against the 0.7.0 launcher until that guard is updated.
Optional ES256 bearer tokens
Set LOCALNET_ES256_JWT=1 when starting the localnet to add ES256 JWT
verification to the primary participants and any EXTRA_PARTICIPANTS. The
existing AUTH_MODE stays active for Quickstart's internal services.
By default, the launcher creates reusable P-256 development key material and
a self-signed certificate in .generated/localnet-es256 at the package root. Set
LOCALNET_ES256_ROTATE=1 to replace generated material, or set both
LOCALNET_ES256_PRIVATE_KEY_PATH and LOCALNET_ES256_CERTIFICATE_PATH to
use your own matching PEM private key and certificate.
The launcher writes a short-lived (ten-minute) token for ledger-api-user to
ledger-api-user.token in that runtime directory and prints its path. In a
short-lived child shell, use a protected local credential mechanism to make the
token available without placing its value or a token-file read in a command
line or log. From within that same child shell, run the live SDK suite:
npm run test:liveThis is development-only key material. A custom LOCALNET_ES256_SUBJECT must
already be a Ledger API user with the appropriate rights on each participant.
Extra participants use the existing shared-secret onboarding flow; the current
OAuth2-plus-extras limitation still applies.
Optional localnet TLS
Set LOCALNET_TLS=1 to enable TLS on every participant Ledger API and Admin API
listener, including generated extra participants. TLS is disabled by default,
so LOCALNET_TLS=0 preserves the existing Quickstart behavior. The launcher
uses direct Compose mode when TLS is enabled so it can apply the generated
configuration overlay.
By default, development-only material is generated in
.generated/localnet-tls: ca.crt, server.crt, and server.key. Set
LOCALNET_TLS_ROTATE=1 to replace it. To provide your own material, set all
three variables together:
LOCALNET_TLS=1 \
LOCALNET_TLS_CERT_CHAIN_PATH=/path/to/server-chain.pem \
LOCALNET_TLS_PRIVATE_KEY_PATH=/path/to/server-key.pem \
LOCALNET_TLS_CA_CERT_PATH=/path/to/root-ca.pem \
canton-localnet-startThe server certificate must cover the hostname used by the client, normally
localhost for host-side SDK calls. Client certificate authentication is not
enabled; authentication remains controlled by the existing Quickstart
AUTH_MODE and optional ES256 settings.
The SDK gRPC channels remain TLS by default. For generated localnet material, pass the generated CA to the client:
import { readFileSync } from "node:fs";
const client = new CantonClient(new CantonClientOptions({
transportKind: TransportKind.grpc,
ledgerEndpoint: "localhost:3901",
ledgerAdminEndpoint: "localhost:3902",
participantAdminEndpoint: "localhost:3902",
grpcTlsRootCertificates: readFileSync(
".generated/localnet-tls/ca.crt",
),
}));The live suite runs single-worker with an extended timeout because it mutates and reads a shared localnet.
Prerequisites:
- CN quickstart is already running on your machine
- the suite is expected to fail fast if the configured node is unreachable
Default local endpoints:
- gRPC ledger:
http://localhost:3901 - gRPC ledger admin:
http://localhost:3901 - gRPC participant admin:
http://localhost:3902 - JSON ledger and ledger admin:
http://localhost:3975
Override environment variables:
SDK_TEST_LEDGER_ENDPOINTSDK_TEST_LEDGER_ADMIN_ENDPOINTSDK_TEST_PARTICIPANT_ADMIN_ENDPOINTSDK_TEST_SECONDARY_LEDGER_ENDPOINTSDK_TEST_SECONDARY_LEDGER_ADMIN_ENDPOINTSDK_TEST_SECONDARY_PARTICIPANT_ADMIN_ENDPOINTSDK_TEST_TERTIARY_LEDGER_ENDPOINTSDK_TEST_TERTIARY_LEDGER_ADMIN_ENDPOINTSDK_TEST_TERTIARY_PARTICIPANT_ADMIN_ENDPOINT
The live harness also supports bearer-token overrides:
SDK_TEST_LEDGER_BEARER_TOKENSDK_TEST_LEDGER_ADMIN_BEARER_TOKENSDK_TEST_PARTICIPANT_ADMIN_BEARER_TOKEN
For CN quickstart shared-secret mode, the harness generates a default bearer token automatically using:
- subject
ledger-api-user - audience
https://canton.network.global - shared secret
unsafe
Run:
npm run test:liveThe opt-in live stateful fuzz campaign uses the two-participant CN quickstart
Main:Iou fixture. It is disabled by default and must be enabled explicitly:
SDK_TEST_ENABLE_LIVE_FUZZING=1 \
FUZZ_NUM_RUNS=20 \
npm run test:live:fuzzThe campaign requires both gRPC participants, with node 0 using
SDK_TEST_LEDGER_ENDPOINT, SDK_TEST_LEDGER_ADMIN_ENDPOINT, and
SDK_TEST_PARTICIPANT_ADMIN_ENDPOINT, and node 1 using the corresponding
SDK_TEST_SECONDARY_* variables. It allocates an issuer on participant A and
an owner on participant B unless FUZZ_LIVE_ISSUER_PARTY and
FUZZ_LIVE_OWNER_PARTY are both supplied. For exact replay, keep those party
IDs, FUZZ_LIVE_RUN_ID, FUZZ_SEED, and FUZZ_PATH unchanged.
Campaign controls include:
FUZZ_LIVE_DEPTH=Nfor exact-depth Foundry-style runs. If it is absent,FUZZ_LIVE_MAX_COMMANDS=Nretains the legacy variable-length behavior; equal values are accepted when both are supplied, while conflicting values fail fast.FUZZ_LIVE_FAIL_ON_REVERT=true|falsecontrols protocol reverts. It defaults tofalse; transport errors, timeouts, malformed responses, and ambiguous commit outcomes remain fatal.FUZZ_LIVE_REQUIRE_ARCHIVE=true|falsealso accepts legacy1|0and requires strict reverts for archive smoke mode.FUZZ_LIVE_ACTION_WEIGHTS=query=30,fetch=20,events=20,exercise=10,probe=20sets non-negative action weights. Exact-depth campaigns always retain a no-contractprobefallback and a post-archive read action.FUZZ_LIVE_ACTORS=issuer,ownerselects eligible actors.issueris mandatory; omittingownerremoves owner-targeted generated reads while retaining the cross-participant fixture checks.FUZZ_LIVE_POLL_TIMEOUT_MS,FUZZ_LIVE_POLL_INTERVAL_MS,FUZZ_LIVE_TEST_TIMEOUT_MS, andFUZZ_LIVE_CLEANUP_TIMEOUT_MScontrol polling and timeouts.FUZZ_LIVE_FAILURE_DIRdefaults totests/live/.artifacts/failures.FUZZ_LIVE_REPLAY_FAILURES=true|falseenables automatic replay of valid artifacts in that directory; stale or corrupt automatic artifacts are reported and skipped.FUZZ_LIVE_REPLAY_FILE=/path/to/failure.jsonperforms explicit replay and validates run ID, party IDs, and fingerprints before connecting to participants.
Artifacts contain allowlisted campaign data only: endpoints, credentials, headers, and arbitrary error objects are never serialized. They are written with restrictive permissions and no-clobber atomic persistence. For a strict four-step smoke run, use:
SDK_TEST_ENABLE_LIVE_FUZZING=1 \
FUZZ_NUM_RUNS=1 \
FUZZ_LIVE_DEPTH=4 \
FUZZ_LIVE_FAIL_ON_REVERT=true \
FUZZ_LIVE_REQUIRE_ARCHIVE=1 \
FUZZ_LIVE_FAILURE_DIR=tests/live/.artifacts/smoke \
npm run test:live:fuzzThis fixture assumes the CN quickstart already has the Main:Iou package on
both participants. The ledger-only DAML Ops localnet launcher is not a
substitute: open ports are insufficient without the quickstart Ledger API,
package, party, and cross-participant visibility checks.
Experimental multi-host external-party coverage is opt-in:
- set
SDK_TEST_ENABLE_MULTI_HOST_EXTERNAL_PARTY=1to enable the multi-host live spec - the default quickstart assumptions cover 2 nodes (
390xand490x) - configure the tertiary endpoint variables above to enable the 3-host scenario
Shared Client
import {
AllocatePartyRequest,
BearerTokenAuthProvider,
CantonClient,
CantonClientOptions,
HealthCheckRequest,
GetLedgerApiVersionRequest,
TransportKind,
} from "@distrohelena/canton-typescript-sdk";
import { ledgerApiV2 } from "@distrohelena/canton-typescript-sdk/protobuf";
const client = new CantonClient(
new CantonClientOptions({
transportKind: TransportKind.json,
ledgerEndpoint: "https://ledger.example.com",
ledgerAdminEndpoint: "https://ledger-admin.example.com",
participantAdminEndpoint: "https://participant-admin.example.com",
ledgerAuthProvider: new BearerTokenAuthProvider("ledger-token"),
ledgerAdminAuthProvider: new BearerTokenAuthProvider(
"ledger-admin-token",
),
participantAdminAuthProvider: new BearerTokenAuthProvider(
"participant-admin-token",
),
}),
);
const version = await client.versionService.getLedgerApiVersionAsync(
new GetLedgerApiVersionRequest(),
);
const health = await client.healthService.checkAsync(
new HealthCheckRequest({
service: "grpc.health.v1.Health",
}),
);
const party = await client.partyManagementService.allocatePartyAsync(
new AllocatePartyRequest({
partyIdHint: "Alice",
displayName: "Alice",
}),
);
const contracts = await client.stateService.getActiveContractsPageAsync(
ledgerApiV2.GetActiveContractsPageRequest.create({
eventFormat: {
filtersByParty: {
Alice: { cumulative: [] },
},
verbose: true,
},
}),
);stateService.getActiveContractsPageAsync(...) is gRPC-only and accepts the generated Ledger API request, including its eventFormat, activeAtOffset, maxPageSize, and pageToken fields. Build party, template, and interface filters in eventFormat.
stateService.getActiveContractsPagesAsync(...) is the gRPC-only lazy, raw, bounded traversal API. It starts from the same generated request and yields raw GetActiveContractsPageResponse values one page at a time. The caller selects the shared OperationDeadline, maximum pages, and maximum contracts with ActiveContractsTraversalOptions; there is no collect-all wrapper. Transport errors from dispatched RPCs propagate unchanged. Traversal safety, invariant, and bound failures use ActiveContractsTraversalError codes, such as an inconsistent offset, repeated page token, or exceeded bound.
JSON does not implement either paginated gRPC API. Its existing stateService.getActiveContractsAsync(...) behavior remains the distinct JSON streaming read.
For interface views, do not use contractService.getContractAsync(...). That contract lookup surface cannot return interface views; use stateService or updateService instead.
CantonClient now splits its public surface across the real API boundaries:
- ledger services use
ledgerEndpoint - ledger admin services use
ledgerAdminEndpoint - participant admin services use
participantAdminEndpoint
For gRPC, channel security resolves per surface:
- ledger services use
ledgerGrpcChannelSecurity ?? grpcChannelSecurity ?? GrpcChannelSecurity.tls - ledger admin services use
ledgerAdminGrpcChannelSecurity ?? grpcChannelSecurity ?? GrpcChannelSecurity.tls - participant admin services use
participantAdminGrpcChannelSecurity ?? grpcChannelSecurity ?? GrpcChannelSecurity.tls
gRPC error handling
gRPC failures reject with GrpcTransportError, a TransportError subclass
with the gRPC status code, service/method, copied metadata, and decoded
google.rpc.Status trailer when Canton provides one. Use onGrpcError for
centralized logging or telemetry; it observes the error but cannot replace the
rejection if the callback itself fails.
import { GrpcTransportError } from "@distrohelena/canton-typescript-sdk";
const client = new CantonClient(new CantonClientOptions({
// existing gRPC connection options,
onGrpcError: (error) => {
logger.error({ code: error.grpcCode, status: error.status });
},
}));
try {
await client.userManagementService.listUsersAsync(/* request */);
} catch (error) {
if (error instanceof GrpcTransportError) {
console.error(error.grpcCode, error.serviceName, error.methodName);
}
}Application-specific google.protobuf.Any values in error.status.details
remain opaque (typeUrl and bytes) unless the application knows that type.
External party lifecycle
For an externally controlled party, provide the public key and a callback that delegates signing to your HSM, KMS, wallet, or other key service. The SDK generates the Canton topology, requests signatures for each topology transaction and its multihash, then allocates the party. It never receives a private key.
import {
CreateExternalPartyRequest,
ExternalPartyCryptoKeyFormat,
ExternalPartySignatureFormat,
ExternalPartySigningAlgorithmSpec,
ExternalPartySigningKeySpec,
ExternalPartySigningPublicKey,
} from "@distrohelena/canton-typescript-sdk";
const party = await client.partyManagementService.createExternalPartyAsync(
new CreateExternalPartyRequest({
synchronizer: "sync::sandbox",
partyHint: "alice",
publicKey: new ExternalPartySigningPublicKey({
format: ExternalPartyCryptoKeyFormat.raw,
keyData: ed25519PublicKeyBytes,
keySpec: ExternalPartySigningKeySpec.ecCurve25519,
}),
sign: async ({ payload }) => ({
signature: await keyService.sign(payload),
format: ExternalPartySignatureFormat.raw,
signingAlgorithmSpec: ExternalPartySigningAlgorithmSpec.ed25519,
}),
}),
);Use the same flow for secp256k1 by supplying
ExternalPartySigningKeySpec.ecSecp256k1 and the signer’s compatible Canton
signature format and algorithm. This convenience operation is gRPC-only.
Service Map
Canton Manager queries
CantonManager keeps gRPC as the write path and selects one typed-query source
at initialization. The same typed query function works with either gRPC or
PQS; source selection changes how the data is read, not the query grammar.
import {
CantonClientOptions,
CantonManager,
CreateCommand,
DamlRecord,
MemoryQueryCache,
QuerySource,
SubmitCommandsRequest,
TransportKind,
} from "@distrohelena/canton-typescript-sdk";
const options = {
grpc: new CantonClientOptions({
transportKind: TransportKind.grpc,
ledgerEndpoint: "localhost:6865",
}),
pqs: { connectionString: process.env.PQS_URL!, schema: "public" },
cache: { store: new MemoryQueryCache(), ttlMs: 5_000 },
};
const readIous = (manager: CantonManager) => manager.query.contracts.findMany({
where: { templateId: { moduleName: { equals: "Main" }, entityName: { equals: "Iou" } } },
orderBy: [{ createdAt: "desc" }],
take: 50,
});
const pqsManager = new CantonManager({ ...options, querySource: QuerySource.pqs });
const grpcManager = new CantonManager({ ...options, querySource: QuerySource.grpc });
const pqsContracts = await readIous(pqsManager);
const grpcContracts = await readIous(grpcManager);
// gRPC caching is explicit and point-in-time; ordinary reads never populate it.
await grpcManager.query.cacheContracts({ parties: ["Alice"] });
await grpcManager.query.invalidateContractsCache({ parties: ["Alice"] });
// A SubmitCommandsRequest contains a non-empty ordered atomic command batch:
// all commands commit together, or none of them do.
await grpcManager.grpc.commandService.submitAndWaitAsync(
new SubmitCommandsRequest({
applicationId: "example-app",
actAs: ["Alice"],
commands: [
new CreateCommand({
templateId: { packageId: "", moduleName: "Main", entityName: "Message" },
createArguments: new DamlRecord({ author: "Alice", body: "Hello" }),
}),
],
}),
);
await Promise.all([pqsManager.disposeAsync(), grpcManager.disposeAsync()]);To migrate a prior singleton submission, pass commands: [previousCommand].
There is no compatibility alias; put multiple independent commands in that
array in their required atomic order.
An options object may contain both grpc and pqs; the PQS setting is used
only when querySource is pqs. Both sources expose contracts,
contractTypes, events, exercises, exerciseTypes, packages,
transactions, and watermark, including filters, includes, ordering,
pagination, projections, grouping, and aggregates. Literal parity requires
PQS and gRPC to observe the same participant-visible data; independently
configured participants can legitimately see different contracts.
cacheContracts is a gRPC-only explicit prewarm of active contracts for one
party scope. It needs a cache store and a positive TTL; reads may use a valid
entry but never renew it. A cached gRPC result is internally consistent at its
activeAtOffset, so it can be stale until expiry, refresh, or
invalidateContractsCache. Under PQS the same lifecycle calls are safe no-ops
and cacheContracts returns { source: QuerySource.pqs, cached: false }.
Typed-query pk/ix values are canonical and source-independent. Transaction
and watermark keys are ledger offsets; event, package, contract-type, and
exercise-type keys are lossless positive-decimal encodings of their stable
semantic identities. PQS physical keys are retained only for internal joins.
If pruning has removed history needed by a gRPC typed query, it rejects with
QuerySnapshotIncompleteError rather than returning partial rows, groups, or
aggregates.
Raw SQL is the sole PQS-only query operation: use
pqsManager.query.$queryRaw(...) for one read-only statement with positional
parameters and a read-only PostgreSQL role. $queryRaw on a gRPC manager
rejects with QueryCapabilityError.
The typed relation delegates use a Prisma-like surface: findMany({ where,
select, orderBy, skip, take }), findUnique({ where, select }), count, and
aggregate({ count, min, max, sum }). Filters support equals, in,
{ is: null }, { isNot: null }, and { has: party } on array fields.
Ordering accepts an ordered list of one-field entries, so multi-field ordering
uses [{ createdAt: "desc" }, { contractId: "asc" }]. exercises intentionally has no findUnique
because the v1 PQS profile does not declare a stable key. The manager validates
the selected PQS schema profile before its first PQS query.
Ledger endpoint:
versionService.getLedgerApiVersionAsync(...):json,grpchealthService.checkAsync(...):grpconlypackageService.listPackagesAsync(...):grpconlypackageService.getPackageAsync(...):grpconlypackageService.getPackageStatusAsync(...):grpconlypackageService.listVettedPackagesAsync(...):grpconlycommandService.submitAndWaitAsync(...):json,grpccommandSubmissionService.submitAsync(...): reserved, currently unsupportedstateService.getActiveContractsPageAsync(...):grpconlystateService.getActiveContractsPagesAsync(...):grpconly, lazy raw bounded traversalstateService.getActiveContractsAsync(...):jsononly, existing distinct streaming readupdateService.getUpdatesAsync(...):grpconlycommandCompletionService.getCompletionsAsync(...):grpconly, existing streaming APIeventQueryService.getEventsByContractIdAsync(...):grpconly; JSON rejects this requestcontractService.getContractAsync(...):grpconly; JSON rejects this requestLedger Admin endpoint:
partyManagementService.allocatePartyAsync(...):json,grpcpartyManagementService.listKnownPartiesAsync(...):json,grpcpartyManagementService.getParticipantIdAsync(...):grpconlypartyManagementService.getPartiesAsync(...):grpconlypartyManagementService.generateExternalPartyTopologyAsync(...):grpconlypartyManagementService.allocateExternalPartyAsync(...):grpconlypartyManagementService.createExternalPartyAsync(...):grpconlyuserManagementService.grantUserRightsAsync(...):json,grpcpackageManagementService.uploadDarFileAsync(...):json,grpcParticipant Admin endpoint:
participantPackageService.listPackagesAsync(...):grpconlyparticipantPackageService.getPackageContentsAsync(...):grpconlyparticipantPackageService.getPackageReferencesAsync(...):grpconlyparticipantStatusService.getParticipantStatusAsync(...):grpconlytopologyManagerReadService.*:grpconlytopologyAggregationService.*:grpconlytopologyManagerWriteService.authorizeAsync(...):grpconlytopologyManagerWriteService.addTransactionsAsync(...):grpconlytopologyManagerWriteService.importTopologySnapshotAsync(...):grpconlytopologyManagerWriteService.importTopologySnapshotV2Async(...):grpconlytopologyManagerWriteService.signTransactionsAsync(...):grpconlytopologyManagerWriteService.generateTransactionsAsync(...):grpconlytopologyManagerWriteService.createTemporaryTopologyStoreAsync(...):grpconlytopologyManagerWriteService.dropTemporaryTopologyStoreAsync(...):grpconlytopologyManagerWriteService.assembleSignedTransactions(...): SDK-local on any client
Raw topology-write mapping support currently starts with PartyToParticipant. The detached-signature assembler is transport-independent, but the actual participant-admin write RPCs are grpc only and JSON rejects them with NotSupportedError.
Protocol-Specific Clients
Subpath exports are available when you want to construct directly over a transport adapter:
@distrohelena/canton-typescript-sdk/grpc@distrohelena/canton-typescript-sdk/json@distrohelena/canton-typescript-sdk/daml-lf@distrohelena/canton-typescript-sdk/debugger@distrohelena/canton-typescript-sdk/daml-interface
GrpcLedgerClient and JsonLedgerClient expose the same service properties as CantonClient.
JSON does not provide a grpc.health.v1.Health.Check equivalent. The shared SDK still exposes healthService, but JSON rejects calls with NotSupportedError.
JSON also does not provide a participant-admin status equivalent, so participantStatusService is currently grpc only.
JSON also does not expose the ledger-admin external-party RPCs, so partyManagementService.generateExternalPartyTopologyAsync(...) and partyManagementService.allocateExternalPartyAsync(...) are grpc only.
DAML-LF Parser
The package also exposes a separate DAML-LF front-end at @distrohelena/canton-typescript-sdk/daml-lf.
Current scope:
- artifact-centric
DARandDALFloading - LF
2.xdecoding - immutable package/module/definition model
- workspace, compilation, and symbol resolution
- semantic queries over the compiled model
- evaluator core and trace-sink contracts
- replay-effect tracing for debugger-owned sessions
Example:
import {
DarArchiveLoader,
DamlLfCompilation,
DamlLfPackageLoader,
DamlLfWorkspace,
} from "@distrohelena/canton-typescript-sdk/daml-lf";
const archive = await new DarArchiveLoader().loadDarOrThrowAsync(darBytes);
const packageLoader = new DamlLfPackageLoader();
const packageModel = packageLoader.loadPackageOrThrow(
archive.mainPackageEntry.bytes,
);
const workspace = new DamlLfWorkspace([packageModel]);
const compilation = DamlLfCompilation.createOrThrow(workspace);
const semanticModel = compilation.createSemanticModel();Replay Debugger
The package also exposes an experimental replay debugger at @distrohelena/canton-typescript-sdk/debugger.
Current scope:
- load a replay session from a committed update offset
- hydrate referenced contracts through the gRPC contract and event-query services
- precompute a stepwise LF trace and expose stepping/session APIs
- replay LF update bodies for the supported evaluator subset, including nested exercise-driven effects
- validate replay determinism against the observed update payload
Current limits:
- replay depends on gRPC-visible create/exercise payloads
- source-aware replay expects DAR provenance with debugger source-map metadata
- unsupported LF constructs still fail fast with
ReplayUnsupportedLfConstructException - source locations currently map to executable definition spans from the DAR source map
Example:
import {
LedgerReplayDebuggerClient,
ReplaySessionRequest,
} from "@distrohelena/canton-typescript-sdk/debugger";
const debuggerClient = new LedgerReplayDebuggerClient({
sessionLoader,
});
const session = await debuggerClient.loadSessionAsync(
new ReplaySessionRequest({ offset: "42" }),
);DAML Interface Generator
The @distrohelena/canton-typescript-sdk/daml-interface subpath exposes a generator that turns compiled DAR or DALF artifacts into an in-memory TypeScript binding project.
Current generated output shape:
- one file per template
- shared support files
- a registry file
- an index file
Example:
import { DamlInterfaceGenerator } from "@distrohelena/canton-typescript-sdk/daml-interface";
const project = await new DamlInterfaceGenerator().generateFromDalfOrThrowAsync(
dalfBytes,
);
console.log(project.templateFiles[0].path);
console.log(project.registryFile?.path);
console.log(project.indexFile?.path);You can also write the generated project to disk:
import {
DamlInterfaceGenerator,
DamlInterfaceWriter,
} from "@distrohelena/canton-typescript-sdk/daml-interface";
const generator = new DamlInterfaceGenerator();
const writer = new DamlInterfaceWriter();
const project = await generator.generateFromDarOrThrowAsync(darBytes);
await writer.writeProjectAsync(project, "./artifacts");CLI:
npm run generate:daml-interface -- --input ./sample.dalf --output ./artifactsCurrent limits:
- generation is strict and throws when a template shape is not supported yet
- milestone 1 supports the current
daml-lftext-based analyzer surface only - the generator works from compiled artifacts, not
.damlsource files
External Signing
External signing is supported on grpc only through ICommandSigner.
import {
ICommandSigner,
SignCommandRequest,
SignCommandResult,
} from "@distrohelena/canton-typescript-sdk";
class ExampleSigner implements ICommandSigner {
public async signAsync(
request: SignCommandRequest,
): Promise<SignCommandResult> {
return new SignCommandResult({
algorithm: "ed25519",
signature: request.payload,
});
}
}See DOCUMENTATION.md for the full function-by-function reference.
