Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ Canton API client.
### Open API specification files for Splice and Ledger APIs

* [Splice APIs](https://docs.dev.sync.global/app_dev/overview/splice_app_apis.html#openapi-conventions) (should paste all yaml files into the `OpenApiSpecs` folder)
* [Ledger Json API (REST)](https://dev-canton-api.trakx.io/api/json-api/docs/openapi) (should paste the yaml content into the `OpenApiSpecs/ledger.yaml` file)
* [Ledger Async API (websockets)](https://dev-canton-api.trakx.io/api/json-api/docs/asyncapi) (should paste the yaml content into the `OpenApiSpecs/ledger-async.yaml` file)
* [Ledger Json API (REST)](https://dev-canton-api.trakx.io/api/json-api/docs/openapi) (should paste the yaml content into the `OpenApiSpecs/ledger.yaml` file). Another link is directly in their [github repo](https://github.com/digital-asset/canton/blob/release-line-3.4/community/ledger/ledger-json-api/src/test/resources/json-api-docs/openapi.yaml) - pay attention to the release line!
* [Ledger Async API (websockets)](https://dev-canton-api.trakx.io/api/json-api/docs/asyncapi) (should paste the yaml content into the `OpenApiSpecs/ledger-async.yaml` file). Another link is directly in their [github repo](https://github.com/digital-asset/canton/blob/release-line-3.4/community/ledger/ledger-json-api/src/test/resources/json-api-docs/asyncapi.yaml) - pay attention to the release line!
* [Utilities API](https://api.utilities.digitalasset-dev.com/api/utilities/v0/openapi) (should paste the yaml content into the `OpenApiSpecs/utilities.yaml` file)

## Relevant links
Expand Down
50 changes: 50 additions & 0 deletions src/Trakx.Canton.ApiClient/Auth/StaticJwtCredentialsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Net.Http.Headers;
using Trakx.Authentication;
using Trakx.Authentication.Client;

namespace Trakx.Canton.ApiClient;

/// <summary>
/// Provides static JWT credentials.
/// </summary>
public class StaticJwtCredentialsProvider<TAuthConfiguration> : IAuthCredentialsProvider<TAuthConfiguration>
{
private readonly string _authToken;

/// <summary>Constructor.</summary>
public StaticJwtCredentialsProvider(string authToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(authToken);
_authToken = authToken;
}

/// <inheritdoc />
public void AddCredentials(HttpRequestMessage msg)
{
var credentials = GetCredentials();
msg.Headers.Authorization = new AuthenticationHeaderValue(credentials.Scheme, credentials.Token);
}

/// <inheritdoc />
public Task AddCredentialsAsync(HttpRequestMessage msg, CancellationToken cancellationToken = default)
{
AddCredentials(msg);
return Task.CompletedTask;
}

/// <inheritdoc />
public AuthCredentials GetCredentials()
{
return new AuthCredentials
{
Scheme = AuthenticationConstants.CredentialsScheme.Bearer,
Token = _authToken
};
}

/// <inheritdoc />
public Task<AuthCredentials> GetCredentialsAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult(GetCredentials());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public Uri GetApiUrl(Type clientType)
_ when clientType == typeof(IUtilitiesClient) => UtilitiesApiUrl,
_ when clientType == typeof(ITransferInstructionV1Client) => TokenStandardApiUrl,
_ when clientType == typeof(ILedgerClient) => LedgerApiUrl,
_ when clientType == typeof(ITokenMetadataV1Client) => TokenStandardApiUrl,
_ => SpliceApisUrl
};

Expand Down
51 changes: 39 additions & 12 deletions src/Trakx.Canton.ApiClient/Factories/CantonApiClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class CantonApiClientFactory : ICantonApiClientFactory
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly CantonApiClientConfiguration _configuration;
private readonly SemaphoreSlim _apiKeyAuthGate = new(1, 1);
private readonly SemaphoreSlim _jwtFromRequestAuthGate = new(1, 1);
private readonly SemaphoreSlim _jwtAuthGate = new(1, 1);
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(30);
private readonly RefitSettings _refitSettings;
Expand Down Expand Up @@ -59,9 +60,9 @@ public TClient CreateClient<TClient>(CantonApiClientCredentials credentials) whe
}

/// <inheritdoc />
public TClient CreateClientWithJwtAuth<TClient>() where TClient : ICantonApiClientBase
public TClient CreateClientWithJwtAuth<TClient>(string authToken) where TClient : ICantonApiClientBase
{
var cacheKey = GetJwtAuthCacheKey<TClient>();
var cacheKey = GetJwtAuthCacheKey<TClient>(authToken);

if (_cache.TryGetValue<TClient>(cacheKey, out var cachedClient))
return cachedClient!;
Expand All @@ -72,7 +73,8 @@ public TClient CreateClientWithJwtAuth<TClient>() where TClient : ICantonApiClie
if (_cache.TryGetValue(cacheKey, out cachedClient))
return cachedClient!;

var client = CreateClientWithJwtAuthInternal<TClient>();
var credentialsProvider = new StaticJwtCredentialsProvider<CantonApiClientConfiguration>(authToken);
var client = CreateClientInternal<TClient>(credentialsProvider);
_cache.Set(cacheKey, client, new MemoryCacheEntryOptions { SlidingExpiration = _cacheDuration });
return client;
}
Expand All @@ -82,6 +84,31 @@ public TClient CreateClientWithJwtAuth<TClient>() where TClient : ICantonApiClie
}
}

/// <inheritdoc />
public TClient CreateClientWithJwtAuthFromCurrentRequest<TClient>() where TClient : ICantonApiClientBase
{
var cacheKey = GetJwtAuthFromCurrentCacheKey<TClient>();

if (_cache.TryGetValue<TClient>(cacheKey, out var cachedClient))
return cachedClient!;

_jwtFromRequestAuthGate.Wait();
try
{
if (_cache.TryGetValue(cacheKey, out cachedClient))
return cachedClient!;

var credentialsProvider = new JwtCredentialsProvider<CantonApiClientConfiguration>(_httpContextAccessor);
var client = CreateClientInternal<TClient>(credentialsProvider);
_cache.Set(cacheKey, client, new MemoryCacheEntryOptions { SlidingExpiration = _cacheDuration });
return client;
}
finally
{
_jwtFromRequestAuthGate.Release();
}
}

private TClient CreateClientWithApiKeyAuthInternal<TClient>(CantonApiClientCredentials credentials)
where TClient : ICantonApiClientBase
{
Expand All @@ -95,13 +122,6 @@ private TClient CreateClientWithApiKeyAuthInternal<TClient>(CantonApiClientCrede
return CreateClientInternal<TClient>(credentialsProvider);
}

private TClient CreateClientWithJwtAuthInternal<TClient>()
where TClient : ICantonApiClientBase
{
var credentialsProvider = new JwtCredentialsProvider<CantonApiClientConfiguration>(_httpContextAccessor);
return CreateClientInternal<TClient>(credentialsProvider);
}

private TClient CreateClientInternal<TClient>(IAuthCredentialsProvider<CantonApiClientConfiguration> credentialsProvider) where TClient : ICantonApiClientBase
{
var clientType = typeof(TClient);
Expand All @@ -120,6 +140,13 @@ private TClient CreateClientInternal<TClient>(IAuthCredentialsProvider<CantonApi
internal static string GetApiKeyAuthCacheKey<TClient>(CantonApiClientCredentials credentials)
=> $"Canton_ApiClient_ApiKeyAuth_{typeof(TClient).FullName}_{credentials.ClientId}";

internal static string GetJwtAuthCacheKey<TClient>()
=> $"Canton_ApiClient_JwtAuth_{typeof(TClient).FullName}";
internal static string GetJwtAuthFromCurrentCacheKey<TClient>()
=> $"Canton_ApiClient_JwtAuthFromRequest_{typeof(TClient).FullName}";

internal static string GetJwtAuthCacheKey<TClient>(string authToken)
{
var tokenHash = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(authToken));
var tokenHashString = Convert.ToHexString(tokenHash);
return $"Canton_ApiClient_JwtAuth_{tokenHashString}_{typeof(TClient).FullName}";
}
}
13 changes: 11 additions & 2 deletions src/Trakx.Canton.ApiClient/Factories/ICantonApiClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,19 @@ public interface ICantonApiClientFactory
TClient CreateClient<TClient>(CantonApiClientCredentials credentials) where TClient : ICantonApiClientBase;

/// <summary>
/// Creates an API client with JWT authentication.
/// Creates an API client that will use the given JWT Token as authentication token.
/// Each call creates a new HttpClient instance with its own handler chain.
/// </summary>
/// <typeparam name="TClient">The API client interface type.</typeparam>
/// <param name="authToken">The JWT token to use for authentication.</param>
/// <returns>A new instance of the API client configured with JWT authentication handler.</returns>
TClient CreateClientWithJwtAuth<TClient>() where TClient : ICantonApiClientBase;
TClient CreateClientWithJwtAuth<TClient>(string authToken) where TClient : ICantonApiClientBase;

/// <summary>
/// Creates an API client with JWT authentication using the JWT token from the current request.
/// Each call creates a new HttpClient instance with its own handler chain.
/// </summary>
/// <typeparam name="TClient">The API client interface type.</typeparam>
/// <returns>A new instance of the API client configured with JWT authentication handler.</returns>
TClient CreateClientWithJwtAuthFromCurrentRequest<TClient>() where TClient : ICantonApiClientBase;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ namespace Trakx.Canton.ApiClient.Contracts.Ledger
public partial class AllocateExternalPartyRequest
{
/// <summary>
/// <para>TODO(#27670) support synchronizer aliases Synchronizer ID on which to onboard the party Required</para>
/// <para>TODO(#27670) support synchronizer aliases Synchronizer ID on which to onboard the party</para>
/// <para>Required</para>
/// </summary>
[JsonPropertyName("synchronizer")]
[Required]
Expand All @@ -39,22 +40,25 @@ public partial class AllocateExternalPartyRequest
/// <list type="bullet">
/// <item><description>A PartyToParticipant to register the hosting relationship of the party.</description></item>
/// </list>
/// <para>Must be provided. Required</para>
/// <para>Must be provided.</para>
/// <para>Required: must be non-empty</para>
/// </summary>
[JsonPropertyName("onboardingTransactions")]
[Required]
public System.Collections.Generic.IList<SignedTransaction> OnboardingTransactions { get; set; } = new List<SignedTransaction>();

/// <summary>
/// <para>Optional signatures of the combined hash of all onboarding_transactions This may be used instead of providing signatures on each individual transaction</para>
/// <para>Optional: can be empty</para>
/// </summary>
[JsonPropertyName("multiHashSignatures")]
public System.Collections.Generic.IList<Signature> MultiHashSignatures { get; set; } = new List<Signature>();

/// <summary>
/// <para>The id of the &lt;c&gt;Identity Provider&lt;/c&gt; If not set, assume the party is managed by the default identity provider. Optional</para>
/// <para>The id of the &lt;c&gt;Identity Provider&lt;/c&gt; If not set, assume the party is managed by the default identity provider.</para>
/// <para>Optional</para>
/// </summary>
[JsonPropertyName("identityProviderId")]
[Required]
public string IdentityProviderId { get; set; } = null!;

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ namespace Trakx.Canton.ApiClient.Contracts.Ledger
public partial class AllocateExternalPartyResponse
{
/// <summary>
/// <para>The allocated party id</para>
/// <para>Required</para>
/// </summary>
[JsonPropertyName("partyId")]
[Required]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,38 @@ namespace Trakx.Canton.ApiClient.Contracts.Ledger
public partial class AllocatePartyRequest
{
/// <summary>
/// <para>A hint to the participant which party ID to allocate. It can be ignored. Must be a valid PartyIdString (as described in &lt;c&gt;value.proto&lt;/c&gt;). Optional</para>
/// <para>A hint to the participant which party ID to allocate. It can be ignored. Must be a valid PartyIdString (as described in &lt;c&gt;value.proto&lt;/c&gt;).</para>
/// <para>Optional</para>
/// </summary>
[JsonPropertyName("partyIdHint")]
[Required]
public string PartyIdHint { get; set; } = null!;

/// <summary>
/// <para>Formerly &quot;display_name&quot; Participant-local metadata to be stored in the &lt;c&gt;PartyDetails&lt;/c&gt; of this newly allocated party. Optional</para>
/// <para>Formerly &quot;display_name&quot; Participant-local metadata to be stored in the &lt;c&gt;PartyDetails&lt;/c&gt; of this newly allocated party.</para>
/// <para>Optional</para>
/// </summary>
[JsonPropertyName("localMetadata")]
public ObjectMeta LocalMetadata { get; set; } = null!;

/// <summary>
/// <para>The id of the &lt;c&gt;Identity Provider&lt;/c&gt; Optional, if not set, assume the party is managed by the default identity provider or party is not hosted by the participant.</para>
/// <para>The id of the &lt;c&gt;Identity Provider&lt;/c&gt; If not set, assume the party is managed by the default identity provider or party is not hosted by the participant.</para>
/// <para>Optional</para>
/// </summary>
[JsonPropertyName("identityProviderId")]
[Required]
public string IdentityProviderId { get; set; } = null!;

/// <summary>
/// <para>The synchronizer, on which the party should be allocated. For backwards compatibility, this field may be omitted, if the participant is connected to only one synchronizer. Otherwise a synchronizer must be specified. Optional</para>
/// <para>The synchronizer, on which the party should be allocated. For backwards compatibility, this field may be omitted, if the participant is connected to only one synchronizer. Otherwise a synchronizer must be specified.</para>
/// <para>Optional</para>
/// </summary>
[JsonPropertyName("synchronizerId")]
[Required]
public string SynchronizerId { get; set; } = null!;

/// <summary>
/// <para>The user who will get the act_as rights to the newly allocated party. If set to an empty string (the default), no user will get rights to the party. Optional</para>
/// <para>The user who will get the act_as rights to the newly allocated party. If set to an empty string (the default), no user will get rights to the party.</para>
/// <para>Optional</para>
/// </summary>
[JsonPropertyName("userId")]
[Required]
public string UserId { get; set; } = null!;

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ namespace Trakx.Canton.ApiClient.Contracts.Ledger
public partial class AllocatePartyResponse
{
/// <summary>
/// <para>The allocated party details</para>
/// <para>Required</para>
/// </summary>
[JsonPropertyName("partyDetails")]
[Required]
public PartyDetails PartyDetails { get; set; } = null!;

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ namespace Trakx.Canton.ApiClient.Contracts.Ledger
public partial class ArchivedEvent
{
/// <summary>
/// <para>The offset of origin. Offsets are managed by the participant nodes. Transactions can thus NOT be assumed to have the same offsets on different participant nodes. Required, it is a valid absolute offset (positive integer)</para>
/// <para>The offset of origin. Offsets are managed by the participant nodes. Transactions can thus NOT be assumed to have the same offsets on different participant nodes. It is a valid absolute offset (positive integer)</para>
/// <para>Required</para>
/// </summary>
[JsonPropertyName("offset")]
[Required]
public long Offset { get; set; }

/// <summary>
/// <para>The position of this event in the originating transaction or reassignment. Node IDs are not necessarily equal across participants, as these may see different projections/parts of transactions. Required, must be valid node ID (non-negative integer)</para>
/// <para>The position of this event in the originating transaction or reassignment. Node IDs are not necessarily equal across participants, as these may see different projections/parts of transactions. Must be valid node ID (non-negative integer)</para>
/// <para>Required</para>
/// </summary>
[JsonPropertyName("nodeId")]
[Required]
Expand All @@ -50,13 +52,16 @@ public partial class ArchivedEvent
public string TemplateId { get; set; } = null!;

/// <summary>
/// <para>The parties that are notified of this event. For an &lt;c&gt;ArchivedEvent&lt;/c&gt;, these are the intersection of the stakeholders of the contract in question and the parties specified in the &lt;c&gt;TransactionFilter&lt;/c&gt;. The stakeholders are the union of the signatories and the observers of the contract. Each one of its elements must be a valid PartyIdString (as described in &lt;c&gt;value.proto&lt;/c&gt;). Required</para>
/// <para>The parties that are notified of this event. For an &lt;c&gt;ArchivedEvent&lt;/c&gt;, these are the intersection of the stakeholders of the contract in question and the parties specified in the &lt;c&gt;TransactionFilter&lt;/c&gt;. The stakeholders are the union of the signatories and the observers of the contract. Each one of its elements must be a valid PartyIdString (as described in &lt;c&gt;value.proto&lt;/c&gt;).</para>
/// <para>Required: must be non-empty</para>
/// </summary>
[JsonPropertyName("witnessParties")]
[Required]
public System.Collections.Generic.IList<string> WitnessParties { get; set; } = new List<string>();

/// <summary>
/// <para>The package name of the contract. Required</para>
/// <para>The package name of the contract.</para>
/// <para>Required</para>
/// </summary>
[JsonPropertyName("packageName")]
[Required]
Expand All @@ -65,7 +70,7 @@ public partial class ArchivedEvent
/// <summary>
/// <para>The interfaces implemented by the target template that have been matched from the interface filter query. Populated only in case interface filters with include_interface_view set.</para>
/// <para>If defined, the identifier uses the package-id reference format.</para>
/// <para>Optional</para>
/// <para>Optional: can be empty</para>
/// </summary>
[JsonPropertyName("implementedInterfaces")]
public System.Collections.Generic.IList<string> ImplementedInterfaces { get; set; } = new List<string>();
Expand Down
Loading
Loading