Canton.Ledger.Grpc.Client
0.7.1
Prefix Reserved
dotnet add package Canton.Ledger.Grpc.Client --version 0.7.1
NuGet\Install-Package Canton.Ledger.Grpc.Client -Version 0.7.1
<PackageReference Include="Canton.Ledger.Grpc.Client" Version="0.7.1" />
<PackageVersion Include="Canton.Ledger.Grpc.Client" Version="0.7.1" />
<PackageReference Include="Canton.Ledger.Grpc.Client" />
paket add Canton.Ledger.Grpc.Client --version 0.7.1
#r "nuget: Canton.Ledger.Grpc.Client, 0.7.1"
#:package Canton.Ledger.Grpc.Client@0.7.1
#addin nuget:?package=Canton.Ledger.Grpc.Client&version=0.7.1
#tool nuget:?package=Canton.Ledger.Grpc.Client&version=0.7.1
Canton.Ledger.Grpc.Client
High-level gRPC client for the Canton Ledger API with integration to Daml.Runtime types, part of the Canton .NET SDK.
Installation
dotnet add package Canton.Ledger.Grpc.Client --version 0.7.1
Key Types
| Type | Purpose |
|---|---|
ICantonLedgerClient (from Canton.Ledger.Abstractions) |
The type to resolve from the container. Everything on ILedgerClient plus the Canton-only participant operations: SubmitAsync, SubmitReassignmentAsync, TrySubmitAndWaitForReassignmentAsync, TrySubmitAndWaitForTransactionTreeAsync, CompletionStreamAsync, GetConnectedSynchronizersAsync, GetLedgerApiVersionAsync, GetUpdateByOffsetAsync, GetUpdateByIdAsync, GetDisclosureAsync<T>, EstimateTrafficCostAsync, and typed interface-view queries via QueryActiveAsync<TInterface, TView> |
ILedgerClient (from Daml.Ledger.Abstractions) |
Command operations: TryCreateAsync, TryExerciseAsync, SubmitAndWaitAsync, TrySubmitAndWaitForTransactionAsync, SubscribeAsync, SubscribeActiveAsync, GetLedgerEndAsync |
Daml.Ledger.Abstractions.Extensions |
Convenience extension methods on the client interfaces: ThrowingExercise.ExerciseAsync (wraps TryExerciseAsync, throws on non-One outcomes), CreateByExercise (TryCreateOneByExerciseAsync, TryCreateManyByExerciseAsync and their throwing forms), SingleCommandExtensions.TrySubmitSingleAsync, StreamerSnapshot.SnapshotAsync |
LedgerClient (concrete, gRPC) |
The implementation AddLedgerClient registers behind ICantonLedgerClient — resolve the interface rather than naming this type. Its raw-stub escape hatch is reached through IGrpcCallInvokerFactory, and its ActivitySource name through LedgerActivitySourceNames.GrpcLedgerClient |
IAdminClient (from Canton.Ledger.Abstractions) |
Admin operations: AllocatePartyAsync, CreateUserAsync, GrantUserRightsAsync |
LedgerClientOptions |
Config: GrpcAddress (required), UserId, MaxMessageSize, Timeout, Retry (opt-in retry pipeline, disabled by default), Tls (opt-in mTLS material, unconfigured by default) |
TlsOptions (from Canton.Ledger.Kernel.Security) |
Nested at LedgerClientOptions.Tls and bound from Canton:Ledger:Tls. Client identity as ClientCertificatePemPath (+ ClientCertificateKeyPemPath), ClientCertificatePkcs12Path (+ ClientCertificatePkcs12Password) or an already-loaded ClientCertificate; private-CA trust as CertificateAuthorityBundlePemPath or CertificateAuthorities, with RevocationMode. Left unconfigured the channel is untouched — OS trust store, no client certificate — and it applies to every gRPC registration alike |
Authentication
Clients receive an ITokenProvider (from Canton.Ledger.Abstractions; the providers that implement it ship in Canton.Ledger.Kernel). Four modes:
1. Convention-based (recommended) — AddCantonLedger
services.AddCantonLedger(configuration);
Reads Canton:Ledger for client options, and registers a client-credentials ITokenProvider from Canton:Auth whenever that section has any populated value. Half-configured auth (e.g. ClientSecret without ClientId) then fails loudly at startup rather than silently falling back to unauthenticated. Canton:Auth keys mirror ClientCredentialsOptions (ClientId, ClientSecret, Audience, Domain or TokenEndpoint); in environment variables they appear as Canton__Auth__ClientId and so on. When Audience is set, it must equal the participant's target-audience (by default Canton accepts https://daml.com/jwt/aud/participant/<participant-id>); leave it unset only if your identity provider stamps that audience into the token itself.
2. Client credentials (OAuth2) — explicit sections
services.AddLedgerClient(
configuration.GetSection("Canton:Ledger"),
authConfiguration: configuration.GetSection("Canton:Auth"));
This calls AddCantonAuth(authConfiguration) internally. The ClientCredentialsProvider handles token acquisition and caching.
3. Static token — explicit registration
services.AddCantonStaticAuth("eyJ...");
services.AddLedgerClient(configuration.GetSection("Canton:Ledger"));
AddCantonStaticAuth(...) may be called before or after AddLedgerClient(...): it replaces only the exact unkeyed ITokenProvider.None fallback installed by the client. Register static auth before AddCantonLedger(...) when Canton:Auth is configured, because a selected client-credentials provider is a real provider and remains in place. Any other pre-existing unkeyed provider also wins, and keyed registrations remain independent.
4. Unauthenticated — no auth configured
services.AddLedgerClient(configuration.GetSection("Canton:Ledger"));
// No ITokenProvider registered — defaults to ITokenProvider.None
// Clients skip the Authorization header
Use for local development with unauthenticated Canton nodes.
Transport security
An http:// GrpcAddress opens a cleartext channel, so a token-issuing ITokenProvider sends its bearer tokens readable — and replayable — by anyone on the network path. The client logs a warning at construction when that combination is detected. Use an https:// address for any deployment beyond local development.
Usage
Creating Contracts
// Using generated template types from Daml.Codegen.CSharp
var asset = new Asset(new Party("Alice::1234..."), 100m);
var outcome = await ledgerClient.TryCreateAsync(
asset,
submitter: new Party("Alice::1234..."),
workflowId: "create-asset");
// Outcome is a discriminated union: One / None / Many / DamlError / InfraError / CommittedUndecodable.
var contractId = outcome switch
{
ExerciseOutcome<ContractId<Asset>>.One ok => ok.Result,
ExerciseOutcome<ContractId<Asset>>.DamlError err => throw new InvalidOperationException(err.ErrorId),
_ => throw new InvalidOperationException(outcome.GetType().Name),
};
Exercising Choices
var command = new ExerciseCommand(
Asset.TemplateId,
contractId,
new ChoiceName("Transfer"),
new Asset.Transfer(NewOwner: new Party("Bob::5678...")).ToRecord());
await ledgerClient.ExerciseAsync(
command,
submitter: new Party("Alice::1234..."));
Handling Failures
Every call reports failure through one contract, the same on the gRPC and JSON Ledger API transports:
- A throwing call (
SubmitAndWaitAsync,GetLedgerEndAsync, the point and typed reads, interactive submission,IAdminClient) raisesLedgerOperationException, neverGrpc.Core.RpcException.StatusisTransportStatus.Grpc(code)(a deadline overrun isGrpc(DeadlineExceeded)),Category,ErrorIdandMetadatacarry the participant's structured error, andInnerExceptionis the originalRpcException. A response the participant sent but this client cannot decode isStatusUndecodableBodywith aMalformedResponseExceptionasInnerException. - A
Try*call returns anExerciseOutcome<T>instead of throwing. - A stream ends with a terminal
StreamErrorevent instead of throwing. - Only your own
CancellationTokensurfaces asOperationCanceledException. A null argument or a value that cannot be encoded still throws synchronously with its own type.
CommitState follows the kind of call: a failed read is always NotCommitted, a write the participant may have received without answering is Unknown, and a write it rejected reports from its error (DUPLICATE_COMMAND is Committed unless its accepted metadata is "false", and SUBMISSION_ALREADY_IN_FLIGHT is Unknown). A write that Retry re-sent after a transient failure is Unknown for any rejection other than DUPLICATE_COMMAND, because the first attempt may have committed before its response was lost; the Try* members return that rejection as an InfraError rather than a DamlError. Branch on Category and CommitState rather than on the transport status:
try
{
await ledgerClient.ExerciseAsync(command, submitter: new Party("Alice::1234..."));
}
catch (LedgerOperationException ex) when (ex.CommitState == CommitState.NotCommitted)
{
Console.WriteLine($"Not committed ({ex.Category}); safe to retry");
}
Reading a Disclosure
GetDisclosureAsync<T> reads, by contract id, the data needed to disclose a contract explicitly to another submitter. T may be a template or an interface, so a holding reached through ContractId<IHolding> works like any template. It returns null when the participant answers CONTRACT_EVENTS_NOT_FOUND: the contract is not visible to the submitter, does not exist or is archived. Any other not-found rejection, such as UNSUPPORTED_CONTRACT_ID, throws LedgerOperationException:
var disclosure = await ledgerClient.GetDisclosureAsync(holdingId, submitter);
if (disclosure is not null)
{
var submission = baseSubmission.WithDisclosedContracts(disclosure);
}
The disclosure leaves the synchronizer id unset, so the participant's synchronizer router selects the synchronizer; pin it with CommandsSubmission.WithSynchronizerId when the current assignment matters.
Async Submission + Completions
SubmitAsync is a true fire path: it returns once the participant accepts the commands (yielding the command_id), not when the transaction commits. The verdict arrives separately on CompletionStreamAsync, surfaced as IAsyncEnumerable<CompletionStreamEvent> — a small union where the verdict is the event type: CommandAccepted (the neutral Completion plus the resulting UpdateId), CommandRejected (the neutral Completion plus a CompletionStatus carrying the google.rpc.Code verdict, the Canton ErrorId and the error Metadata decoded from status.details), Checkpoint (the participant's offset checkpoints, so your persisted resume offset keeps advancing during quiet periods instead of falling arbitrarily far behind), and StreamError (a mid-stream transport fault surfaced in-band as a terminal event rather than thrown). The client keeps no pending-set — you correlate completions by command_id/submission_id and own your offset.
var actAs = new Party("Alice::1234...");
// Capture the offset BEFORE submitting — a completion can be emitted
// before the stream is opened.
var beginOffset = await ledgerClient.GetLedgerEndAsync();
var resumeOffset = beginOffset;
// SubmitAsync returns the effective CommandId — minted for you when the submission omits one.
CommandId commandId = await ledgerClient.SubmitAsync(submission);
await foreach (var streamEvent in ledgerClient.CompletionStreamAsync(actAs, beginOffset, ct))
{
switch (streamEvent)
{
case CompletionStreamEvent.Checkpoint checkpoint:
// Persist this even when no completions arrive — it is the offset
// to resume from without re-processing or hitting pruned data.
resumeOffset = checkpoint.Offset;
continue;
case CompletionStreamEvent.CommandAccepted accepted:
resumeOffset = accepted.Completion.Offset;
if (accepted.Completion.CommandId.Value == commandId.Value)
{
// Accepted — accepted.UpdateId is the resulting update id.
return accepted.UpdateId;
}
continue;
case CompletionStreamEvent.CommandRejected rejected:
resumeOffset = rejected.Completion.Offset;
if (rejected.Completion.CommandId.Value == commandId.Value)
{
// Rejected — rejected.Status carries the google.rpc.Code and message.
throw new InvalidOperationException(
$"Command {commandId.Value} rejected ({rejected.Status.Code}): {rejected.Status.Message}");
}
continue;
case CompletionStreamEvent.StreamError error:
// Mid-stream transport fault surfaced in-band — reopen from
// resumeOffset, log, or stop. Terminal: no further events follow.
throw new InvalidOperationException(
$"Completion stream failed ({error.Status}): {error.Message}");
}
}
SubmitAsyncandSubmitAndWaitAsyncreport the effectiveCommandIdback to you — the one you supplied, or the one minted here when you omit it. After a transport failure the first attempt may have been accepted, so do not resubmit blindly:LedgerOperationException.CommandIdnames the failed write (including an id minted here), so look that id up on the completion stream (CompletionStreamAsync). Re-invoking with a fresh, command_id-less submission mints a new id and double-submits. A resubmitted consuming exercise that already committed is rejected withCONTRACT_NOT_FOUND.
To submit and wait for the transaction in one call instead, use SubmitAndWaitAsync.
Party Management
var party = await adminClient.AllocatePartyAsync("alice-hint");
var user = await adminClient.CreateUserAsync(
userId: "alice-user",
primaryParty: party.Party,
rights: [new UserRight.ActAs(party.Party), new UserRight.ReadAs(party.Party)]);
User Management
await adminClient.GrantUserRightsAsync(
"alice-user",
[new UserRight.ReadAs(new Party("Bob::5678..."))]);
var rights = await adminClient.ListUserRightsAsync("alice-user");
var users = await adminClient.ListUsersAsync();
Raw gRPC Stubs (Escape Hatch)
For Ledger API services or overloads the typed surface does not cover, opt into
IGrpcCallInvokerFactory (namespace Canton.Ledger.Grpc.Client.Raw). Its CreateCallInvoker()
returns a Grpc.Core.CallInvoker that reuses the SDK's authentication and deadline
plumbing — no hand-built GrpcChannel, Bearer-header CallOptions, or deadlines needed:
services.AddLedgerRawGrpc(configuration.GetSection("Canton:Ledger"));
var stateService = new StateService.StateServiceClient(invokerFactory.CreateCallInvoker());
var ledgerEnd = await stateService.GetLedgerEndAsync(new GetLedgerEndRequest());
Bearer tokens come from the registered ITokenProvider on every call (ITokenProvider.None
sends no authorization header); unary calls get the configured Timeout as a per-attempt
deadline, while streaming calls attach auth headers but no default deadline and are never
retried. A caller-supplied authorization header or deadline in CallOptions wins over the
SDK's.
A raw unary call is sent exactly once even with Retry enabled, because the SDK cannot know
whether it is safe to repeat. When you know a call is idempotent, start it inside a
RawCallReplay.Allow() scope (namespace Canton.Ledger.Kernel.Resilience) and the Retry
pipeline re-sends it after a transient failure:
using (RawCallReplay.Allow())
{
var ledgerEnd = await stateService.GetLedgerEndAsync(new GetLedgerEndRequest());
}
The factory runs on the one channel the container shares across every client it registers, built
from the same LedgerClientOptions the clients use, so it needs neither client registered and
registration order does not matter. The container owns that channel, so the invoker stays valid
for as long as the provider does: dispose the provider, not the invoker.
Dependency Injection
The recommended DI lifetime is Singleton — gRPC clients share the underlying GrpcChannel lifetime.
// Config-based (recommended)
services.AddLedgerClient(configuration.GetSection("Canton:Ledger"));
services.AddAdminClient(configuration.GetSection("Canton:Ledger"));
// With auth configuration
services.AddLedgerClient(
configuration.GetSection("Canton:Ledger"),
configuration.GetSection("Canton:Auth"));
// Action-based
services.AddLedgerClient(options => options.GrpcAddress = "https://localhost:5001");
// Opt-in raw gRPC escape hatch — registers IGrpcCallInvokerFactory only
services.AddLedgerRawGrpc(configuration.GetSection("Canton:Ledger"));
// Health check — requires ILedgerClient, queries the ledger end to verify connectivity
services.AddHealthChecks().AddLedgerClient(tags: ["grpc", "ready"]);
Resolve the transport-neutral interfaces, not the concrete clients — the container owns the gRPC channel, so nothing here is disposed by hand:
await using var provider = services.BuildServiceProvider();
var ledgerClient = provider.GetRequiredService<ICantonLedgerClient>();
var adminClient = provider.GetRequiredService<IAdminClient>();
OpenTelemetry Tracing
Canton.Ledger.OpenTelemetry registers every Canton client source at once:
tracing.AddCantonLedgerInstrumentation();
To register these two sources by hand, take the names from Canton.Ledger.Kernel — no reference to this package required:
tracing.AddSource(LedgerActivitySourceNames.GrpcLedgerClient);
tracing.AddSource(LedgerActivitySourceNames.GrpcAdminClient);
Related Packages
Canton.Ledger.Abstractions— Transport-neutral Canton contract layer:ICantonLedgerClient,IAdminClient,ITokenProvider,IPqsClient, the completion and reassignment familiesCanton.Ledger.Kernel— Transport-neutral client kernel: theITokenProviderimplementations, telemetry convention, retry pipelineCanton.Ledger.Grpc— Low-level gRPC stubsCanton.Ledger.Pqs.Client— PQS query clientDaml.Runtime— Runtime types for generated Daml contracts
Support
For questions, bug reports and feature requests, open a GitHub issue. This package is maintained by Peaceful Studio. For consulting beyond this SDK (Daml development, .NET integration with Canton, validator operations), please write to info@peaceful.studio.
| 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.7.1)
- Canton.Ledger.Grpc (>= 0.7.1)
- Canton.Ledger.Kernel (>= 0.7.1)
- Daml.Ledger.Abstractions (>= 0.7.1)
- Daml.Runtime (>= 0.7.1)
- Google.Protobuf (>= 3.35.1)
- Grpc.Net.Client (>= 2.84.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.13)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.13)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.13)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.13)
- Microsoft.Extensions.Options (>= 10.0.13)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.13)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.13)
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.7.1 | 59 | 10/23/2026 |
| 0.7.0 | 194 | 10/14/2026 |
| 0.7.0-preview.1 | 402 | 10/13/2026 |
| 0.6.0 | 290 | 10/7/2026 |
| 0.6.0-preview.4 | 162 | 10/7/2026 |
| 0.6.0-preview.3 | 192 | 10/1/2026 |
| 0.6.0-preview.2 | 412 | 9/30/2026 |
| 0.6.0-preview.1 | 216 | 9/28/2026 |
| 0.5.0-preview.3 | 229 | 9/24/2026 |
| 0.5.0-preview.2 | 151 | 9/22/2026 |
| 0.5.0-preview.1 | 250 | 9/8/2026 |
| 0.4.1-preview.1 | 124 | 8/29/2026 |
| 0.4.0-preview.1 | 218 | 7/18/2026 |
| 0.2.0-preview.1 | 133 | 7/6/2026 |
| 0.1.5-preview.1 | 101 | 6/25/2026 |
| 0.1.4-preview.1 | 86 | 6/15/2026 |