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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Canton.Ledger.Grpc.Client" Version="0.7.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Canton.Ledger.Grpc.Client" Version="0.7.1" />
                    
Directory.Packages.props
<PackageReference Include="Canton.Ledger.Grpc.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Canton.Ledger.Grpc.Client --version 0.7.1
                    
#r "nuget: Canton.Ledger.Grpc.Client, 0.7.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Canton.Ledger.Grpc.Client@0.7.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Canton.Ledger.Grpc.Client&version=0.7.1
                    
Install as a Cake Addin
#tool nuget:?package=Canton.Ledger.Grpc.Client&version=0.7.1
                    
Install as a Cake Tool

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:

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) raises LedgerOperationException, never Grpc.Core.RpcException. Status is TransportStatus.Grpc(code) (a deadline overrun is Grpc(DeadlineExceeded)), Category, ErrorId and Metadata carry the participant's structured error, and InnerException is the original RpcException. A response the participant sent but this client cannot decode is Status UndecodableBody with a MalformedResponseException as InnerException.
  • A Try* call returns an ExerciseOutcome<T> instead of throwing.
  • A stream ends with a terminal StreamError event instead of throwing.
  • Only your own CancellationToken surfaces as OperationCanceledException. 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}");
    }
}

SubmitAsync and SubmitAndWaitAsync report the effective CommandId back 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.CommandId names 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 with CONTRACT_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);
  • Canton.Ledger.Abstractions — Transport-neutral Canton contract layer: ICantonLedgerClient, IAdminClient, ITokenProvider, IPqsClient, the completion and reassignment families
  • Canton.Ledger.Kernel — Transport-neutral client kernel: the ITokenProvider implementations, telemetry convention, retry pipeline
  • Canton.Ledger.Grpc — Low-level gRPC stubs
  • Canton.Ledger.Pqs.Client — PQS query client
  • Daml.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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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