Canton.Ledger.Testing
0.6.0
Prefix Reserved
dotnet add package Canton.Ledger.Testing --version 0.6.0
NuGet\Install-Package Canton.Ledger.Testing -Version 0.6.0
<PackageReference Include="Canton.Ledger.Testing" Version="0.6.0" />
<PackageVersion Include="Canton.Ledger.Testing" Version="0.6.0" />
<PackageReference Include="Canton.Ledger.Testing" />
paket add Canton.Ledger.Testing --version 0.6.0
#r "nuget: Canton.Ledger.Testing, 0.6.0"
#:package Canton.Ledger.Testing@0.6.0
#addin nuget:?package=Canton.Ledger.Testing&version=0.6.0
#tool nuget:?package=Canton.Ledger.Testing&version=0.6.0
Canton.Ledger.Testing
In-memory test doubles and event/result builders for unit-testing business logic against the Canton Ledger API client surfaces without a live participant and without a mocking framework. Part of the Canton .NET SDK.
FakeLedgerClient implements the neutral Canton participant surface
Canton.Ledger.Abstractions.ICantonLedgerClient (and therefore the transport-neutral
Daml.Ledger.Abstractions.ILedgerClient it extends). FakeAdminClient, FakeTokenProvider
and FakePqsClient implement the equally neutral Canton.Ledger.Abstractions.IAdminClient,
Canton.Ledger.Abstractions.ITokenProvider and Canton.Ledger.Abstractions.IPqsClient.
Every fake therefore stands in for a contract declared in one neutral package, and this package
depends only on Canton.Ledger.Abstractions/Daml.Ledger.Abstractions/Daml.Runtime — no
transport, no Canton.Ledger.Kernel, no PostgreSQL driver.
Installation
dotnet add package Canton.Ledger.Testing --version 0.6.0
Every fake replays canned data staged ahead of time — none of them is a semantic ledger/PQS
simulator (no contract-key uniqueness or consuming-choice archival). FakePqsClient does evaluate the
PqsFilter it is given against the staged contracts, with the semantics of the SQL a live PQS runs.
The one exception is FakeLedgerClient's ledger end: it starts at the offset staged through
WithLedgerEnd and advances by one offset per committed write, so a test can read the end, write,
read it again, and get a bounded (fromOffset, toOffset] window that actually contains the write. The Canton participant surface (ICantonLedgerClient — fire-and-forget
submission, the completion stream, synchronizer/version discovery, offset/id point reads,
tree-shaped submission, traffic-cost estimation, interactive submission and package preference) lives
directly on FakeLedgerClient: seed completion events, connected synchronizers, the Ledger API
version, point-read transactions, transaction trees, the traffic-cost estimate, the prepared and executed interactive submissions and the preferred packages the same way, so tests swap Fake ⇆ REST ⇆ gRPC behind one
interface. SubmitAsync/SubmitReassignmentAsync echo the submission's command id (minting one
when omitted).
Key types
| Type | Purpose |
|---|---|
FakeLedgerClient |
Configurable in-memory ICantonLedgerClient (and thus ILedgerClient). Build it with the fluent builder from FakeLedgerClient.Create(). Any member, Daml type, or Canton read you did not stage throws a descriptive NotSupportedException. |
FakeLedgerClientBuilder |
WithLedgerEnd (the ledger end before any write — stage an event for a bounded window opened around the nth write at that offset plus n), WithActiveContracts<T> (the staged snapshot must end on a single terminal Checkpoint or StreamError, as a participant's does — WithMalformedActiveContracts<T> stages one that deliberately does not), WithContractEvents<T>, WithLedgerEffects<T>, WithExerciseResult<TResult>, WithCreateResult<TTemplate>, WithSubmissionOutcome, WithTransactionTree, WithReassignmentResult<T>, WithCompletionEvents, WithConnectedSynchronizers, WithLedgerApiVersion, WithTrafficCostEstimate (staging null replays a participant that served no estimation), WithPreparedSubmission, WithExecutedSubmission, WithExecutedTransaction, WithPreferredPackages, WithPackagePreference (staging null replays no satisfying package), WithUpdateByOffset, WithUpdateById, WithUpdateTreeByOffset, WithContract<T>, WithContractLifecycle<T>, WithDisclosure<T> (the answer GetDisclosureAsync<T> returns for a contract id typed as a template or an interface; an id with no staged disclosure reads as null), WithActiveContractsPages<T>, WithUpdatesPages, WithPrunedOffsets, the interface counterparts WithActiveInterfaceContracts<TInterface, TView>, WithInterfaceEvents<TInterface, TView> and WithInterfaceLedgerEffects<TInterface, TView>, WithThrowingCallFailure (stage a LedgerOperationException that every throwing member raises from its task, as that same instance; Try* members and streams keep returning what you staged for them. The fake raises the instance as staged and does not derive its CommitState from the member called, so build it with the LedgerOperationException(message, status, commitState, category, errorId, metadata, innerException) constructor to carry a Status and the CommitState suited to the members under test: a failed read is NotCommitted), then Build(). |
LedgerEvents |
Factories for AcsSnapshotEntry<T> variants, all constrained where T : ITemplate, IDamlRecord<T>: Created (typed T payload plus a ContractKey? key), Checkpoint, StreamError (an optional DamlErrorCategory?, string? errorId and source Exception? after the TransportStatus and message), Unclassified (a nullable LedgerOffset? and an UnclassifiedKind). |
ContractEvents |
Factories for ContractStreamEvent<T> variants, all constrained where T : ITemplate, IDamlRecord<T>: Created and Assigned (typed T payload plus a ContractKey? key), Archived, Unassigned, Exercised, Checkpoint, StreamError (an optional DamlErrorCategory?, string? errorId and source Exception? after the TransportStatus and message), Unclassified (an UnclassifiedKind). |
LedgerOutcomes |
Factories for ExerciseOutcome<T> variants: One, None, Many, DamlError, InfraError (an optional DamlErrorCategory? before the source Exception?), CommittedUndecodable (the command committed but its response could not be decoded; carries the update id when one was read). |
LedgerResults |
Factories for TransactionResult, SubmitAndWaitResult, Contract<T>. |
FakeAdminClient |
Configurable in-memory IAdminClient. Build it with FakeAdminClient.Create(). Query-style members you did not stage throw NotSupportedException; the void command members (GrantUserRightsAsync, RevokeUserRightsAsync, UploadDarAsync, ValidateDarAsync) always succeed. |
FakeAdminClientBuilder |
WithParticipantId, WithAllocatedParty, WithParties, WithUser, WithUsers, WithUserRights, WithKnownPackages, WithPackage, WithVettedPackages, WithExternalPartyTopology, WithAllocatedExternalParty, then Build(). |
FakePqsClient |
Configurable in-memory IPqsClient. Build it with FakePqsClient.Create(). Query results are staged per Daml type; an unstaged type throws NotSupportedException. Filtered queries return only the staged contracts the filter matches, and QueryOneAsync returns null when none do. Paged queries order the staged contracts by contract id (ordinal comparison) before slicing, as PqsClient does, whatever order you staged them in; stage hex-like ids when a test depends on which contracts land on which page. Unpaged queries return contracts in staging order. |
FakePqsClientBuilder |
WithQueryResults<T>, WithInterfaceQueryResults<TInterface, TView>, then Build(). |
FakeTokenProvider |
In-memory ITokenProvider. FakeTokenProvider.WithToken(token) for the happy path, FakeTokenProvider.WithFailure(exception) to exercise auth-failure paths. No builder — both factories fully configure the fake. |
Usage
Stage the active-contract snapshot your business logic will read, keyed by Daml type, then drive it through the fake:
using Canton.Ledger.Testing;
using Daml.Ledger.Abstractions;
using Daml.Runtime;
using Daml.Runtime.Contracts;
using Daml.Runtime.Data;
var owner = new Party("bob");
var asset = new DemoAsset(new Party("issuer"), owner, "GOLD", 42m);
ILedgerClient client = FakeLedgerClient.Create()
.WithActiveContracts(
LedgerEvents.Created(
new ContractId<DemoAsset>("cid1"),
asset,
key: null,
LedgerOffset.At(1),
(SynchronizerId)"sync1",
[owner]),
LedgerEvents.Checkpoint<DemoAsset>(LedgerOffset.At(2)))
.Build();
// SubscribeActiveAsync<DemoAsset> now replays the staged entries:
await foreach (var entry in client.SubscribeActiveAsync<DemoAsset>(owner))
{
// ... exercise the code under test ...
}
// A member you never staged fails loudly instead of returning empty/null:
// client.TryExerciseAsync<string>(...) -> NotSupportedException("...WithExerciseResult<String>...")
Every unconfigured member and every unstaged Daml type throws a NotSupportedException whose
message names the builder call needed to stage it, so a test never silently exercises
unconfigured behaviour.
Seed the completion stream to drive fire-and-forget submission logic (SubmitAsync then observe
the completion), using the neutral CommandAccepted/CommandRejected/Checkpoint events:
using Canton.Ledger.Abstractions;
using Canton.Ledger.Testing;
using Daml.Runtime;
using Daml.Runtime.Commands;
using Daml.Runtime.Data;
var owner = new Party("bob");
var completion = new Completion(
new CommandId("cmd-1"), Offset: LedgerOffset.At(1), ActAs: [owner],
new SynchronizerTime("sync1", DateTimeOffset.UtcNow),
SubmissionId: null, UserId: null, DeduplicationPeriod: null,
PaidTrafficCost: 0, TraceContext: null);
ICantonLedgerClient client = FakeLedgerClient.Create()
.WithCompletionEvents(
new CompletionStreamEvent.Checkpoint(LedgerOffset.Begin),
new CompletionStreamEvent.CommandAccepted(completion, "update-1"))
.Build();
await foreach (var completionEvent in client.CompletionStreamAsync(owner))
{
// ... assert your code correlates the accepted completion by command id ...
}
Seed the submit-and-wait write path — the command shape every generated choice helper submits
through — with the ExerciseOutcome<TransactionResult> your code under test should observe.
TrySubmitAndWaitForTransactionAsync (both overloads) replies with the staged outcome for every
submission; stage a success or a failure to drive either branch:
using Canton.Ledger.Testing;
using Daml.Ledger.Abstractions;
using Daml.Runtime;
using Daml.Runtime.Commands;
using Daml.Runtime.Contracts;
using Daml.Runtime.Data;
using Daml.Runtime.Outcomes;
ILedgerClient happy = FakeLedgerClient.Create()
.WithSubmissionOutcome(LedgerOutcomes.One(LedgerResults.Transaction(
"update-1",
LedgerOffset.At(5),
[
new CreatedContract(
"event-1",
"cid1",
DemoAsset.TemplateId,
DamlRecord.Create(),
WitnessParties: [],
Signatories: [],
Observers: []),
],
archivedContractIds: [],
(CommandId)"cmd-1")))
.Build();
ILedgerClient failing = FakeLedgerClient.Create()
.WithSubmissionOutcome(LedgerOutcomes.DamlError<TransactionResult>(
DamlErrorCategory.InvalidGivenCurrentSystemStateOther,
"UNHANDLED_EXCEPTION", "assertion failed", new Dictionary<string, string>()))
.Build();
Stage an admin client the same way:
using Canton.Ledger.Abstractions;
using Canton.Ledger.Testing;
using Daml.Runtime.Data;
IAdminClient admin = FakeAdminClient.Create()
.WithParticipantId("participant1")
.WithUser(new UserDetails("alice", new Party("alice::1220")))
.Build();
var participantId = await admin.GetParticipantIdAsync();
var alice = await admin.GetUserAsync("alice"); // staged UserDetails
var bob = await admin.GetUserAsync("bob"); // null — never staged, not an error
Stage a PQS client's query results per Daml type:
using Canton.Ledger.Abstractions;
using Canton.Ledger.Testing;
using Daml.Runtime.Contracts;
IPqsClient pqs = FakePqsClient.Create()
.WithQueryResults(new Contract<Holding>(new ContractId<Holding>("cid1"), holding))
.Build();
var holdings = await pqs.QueryAsync<Holding>();
And a token provider for either the happy path or an auth-failure path:
using Canton.Ledger.Abstractions;
using Canton.Ledger.Testing;
ITokenProvider ok = FakeTokenProvider.WithToken("test-bearer-token");
ITokenProvider failing = FakeTokenProvider.WithFailure(new InvalidOperationException("token endpoint unreachable"));
Stage a failed throwing call to test your failure handling. Both transports report a failed
call as a LedgerOperationException, so one staged failure covers either; build it with the
constructor that takes a TransportStatus and a CommitState when the test branches on them:
using Canton.Ledger.Testing;
using Daml.Ledger.Abstractions;
using Daml.Runtime.Outcomes;
var noAnswer = new LedgerOperationException(
"read got no answer",
new TransportStatus.NoResponse(),
CommitState.NotCommitted,
DamlErrorCategory.TransientServerFailure,
errorId: null,
metadata: null,
innerException: new TimeoutException("no response"));
ILedgerClient failing = FakeLedgerClient.Create()
.WithThrowingCallFailure(noAnswer)
.Build();
var act = () => failing.GetLedgerEndAsync(); // throws noAnswer from its task
Zero mocking-framework dependency
None of the fakes in this package pull in Moq / NSubstitute, so it composes with any (or no)
mocking framework in your own test project. Every fake — FakeLedgerClient, FakeAdminClient,
FakeTokenProvider and FakePqsClient — depends only on Canton.Ledger.Abstractions,
Daml.Ledger.Abstractions, and Daml.Runtime.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Canton.Ledger.Abstractions (>= 0.6.0)
- Daml.Ledger.Abstractions (>= 0.6.0)
- Daml.Runtime (>= 0.6.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.6.0 | 75 | 10/7/2026 |
| 0.6.0-preview.4 | 98 | 10/7/2026 |
| 0.6.0-preview.3 | 165 | 10/1/2026 |
| 0.6.0-preview.2 | 400 | 9/30/2026 |
| 0.6.0-preview.1 | 197 | 9/28/2026 |
| 0.5.0-preview.3 | 208 | 9/24/2026 |
| 0.5.0-preview.2 | 125 | 9/22/2026 |
| 0.5.0-preview.1 | 206 | 9/8/2026 |
| 0.4.1-preview.1 | 106 | 8/29/2026 |