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
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,18 @@ public static IServiceCollection AddKeetaNetAnchor(this IServiceCollection servi
}

/// <summary>
/// Register a <see cref="NodeClient"/> for the node API at
/// Register a <see cref="KeetaClient"/> for the node API at
/// <paramref name="nodeUrl"/> as a typed HTTP client, so its
/// <see cref="System.Net.Http.HttpClient"/> comes from
/// <c>IHttpClientFactory</c> (pooled handlers, policy-friendly).
/// </summary>
/// <returns>The same collection, for chaining.</returns>
public static IServiceCollection AddKeetaNetAnchorNodeClient(this IServiceCollection services, string nodeUrl)
public static IServiceCollection AddKeetaNetAnchorKeetaClient(this IServiceCollection services, string nodeUrl)
{
services.AddKeetaNetAnchor();
services.AddHttpClient(nameof(NodeClient))
services.AddHttpClient(nameof(KeetaClient))
.AddTypedClient((http, provider) =>
provider.GetRequiredService<WasmRuntime>().CreateNodeClient(nodeUrl, http));
provider.GetRequiredService<WasmRuntime>().CreateKeetaClient(nodeUrl, http));
return services;
}
}
2 changes: 1 addition & 1 deletion src/KeetaNet.Anchor/Crypto/Certificate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public DateTimeOffset NotAfter
/// <summary>
/// The SHA3-256 of the certificate's DER: the key the ledger stores a
/// published certificate under, so it feeds
/// <see cref="NodeClient.GetCertificateByHash(Account, CertificateHash, CancellationToken)"/>.
/// <see cref="KeetaClient.GetCertificateByHash(Account, CertificateHash, CancellationToken)"/>.
/// </summary>
public CertificateHash Hash => CertificateHash.Parse(Runtime.CertificateHash(Convert.ToHexString(ToDer())));

Expand Down
23 changes: 19 additions & 4 deletions src/KeetaNet.Anchor/Interop/WasmRuntime.Surface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,28 @@ public AssetMovementClient CreateAssetMovementClient(string nodeUrl, string root
AssetMovementClient.WithAccount(this, nodeUrl, root, account);

/// <summary>
/// Create a lite client for the node API at <paramref name="nodeUrl"/>. An
/// injected <paramref name="httpClient"/> (for example from
/// Create the base client for the node API at <paramref name="nodeUrl"/>.
/// An injected <paramref name="httpClient"/> (for example from
/// <c>IHttpClientFactory</c>) is borrowed, not disposed. Absent one the
/// client owns its own. Binding <paramref name="network"/> enables the
/// write path (<see cref="NodeClient.Transmit(Crypto.Block, TransmitOptions?, CancellationToken)"/>
/// write path (<see cref="KeetaClient.Transmit(Crypto.Block, TransmitOptions?, CancellationToken)"/>
/// and fee blocks). A client without one stays read-only.
/// </summary>
public NodeClient CreateNodeClient(string nodeUrl, HttpClient? httpClient = null, long? network = null) =>
public KeetaClient CreateKeetaClient(string nodeUrl, HttpClient? httpClient = null, long? network = null) =>
new(this, nodeUrl, httpClient, network);

/// <summary>
/// Create a client bound to <paramref name="signer"/> (null for a
/// read-only client), operating as <paramref name="account"/> when given
/// and as the signer itself otherwise. Both accounts are borrowed, not
/// disposed. See <see cref="CreateKeetaClient"/> for the remaining
/// parameters.
/// </summary>
public UserClient CreateUserClient(
string nodeUrl,
Account? signer,
HttpClient? httpClient = null,
long? network = null,
Account? account = null) =>
new(this, nodeUrl, httpClient, network, signer, account);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
namespace KeetaNet.Anchor;

/// <summary>
/// A lite, read-only client for the KeetaNet node API: the ledger reads the
/// reference node client performs, over the transport generated from the
/// canonical OpenAPI spec.
/// The base client for the KeetaNet node API: ledger reads and the two-round
/// transmit flow, over the transport generated from the canonical OpenAPI
/// spec. Account-bound conveniences live on <see cref="UserClient"/>.
/// </summary>
public sealed class NodeClient : IDisposable
public sealed class KeetaClient : IDisposable
{
/// <summary>The block version the reference clients build.</summary>
private const int BlockVersion = 2;
internal const int BlockVersion = 2;

private readonly WasmRuntime _runtime;

Expand All @@ -37,7 +37,7 @@ public sealed class NodeClient : IDisposable
/// borrowed, not disposed. A bound <paramref name="network"/> enables the
/// write path; without one the client stays read-only.
/// </summary>
internal NodeClient(WasmRuntime runtime, string nodeUrl, HttpClient? http = null, long? network = null)
internal KeetaClient(WasmRuntime runtime, string nodeUrl, HttpClient? http = null, long? network = null)
{
_runtime = runtime;
_network = network;
Expand Down Expand Up @@ -186,6 +186,25 @@ public async Task<BigInteger> GetAccountBalance(
return OptionalHexAmount(response.Balance) ?? BigInteger.Zero;
}

/// <summary>
/// A builder pre-set with the reference block version, the bound network,
/// <paramref name="account"/> as originator, <paramref name="signer"/>
/// (the account itself when null) signing, and the current moment. The
/// caller positions it, appends operations, and builds. Requires a bound
/// network.
/// </summary>
internal Crypto.BlockBuilder InitBuilder(Crypto.Account account, Crypto.Account? signer = null)
{
(long network, _) = RequireNetwork();

return _runtime.Blocks.NewBuilder()
.WithVersion(BlockVersion)
.WithNetwork(network)
.WithAccount(account)
.WithSigner(signer ?? account)
.WithDate(DateTimeOffset.UtcNow);
}

/// <summary>Publish one signed block as its own staple. See the list overload.</summary>
public Task<bool> Transmit(
Crypto.Block block,
Expand Down Expand Up @@ -515,7 +534,7 @@ private async Task<bool> PublishStaple(
/// Position <paramref name="builder"/> atop <paramref name="previous"/>, or
/// as an opening block when the account has no chain yet.
/// </summary>
private static void PositionAfter(Crypto.BlockBuilder builder, string? previous)
internal static void PositionAfter(Crypto.BlockBuilder builder, string? previous)
{
if (string.IsNullOrEmpty(previous))
{
Expand Down
2 changes: 1 addition & 1 deletion src/KeetaNet.Anchor/Services/Node/TransmitOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace KeetaNet.Anchor;
/// with <c>FEE_REQUIRED</c> before anything is published.
/// </summary>
public delegate Task<Crypto.Block?> GenerateFeeBlock(
NodeClient client,
KeetaClient client,
Crypto.VoteStaple staple,
IReadOnlyList<Crypto.Account> feeTokenPriority,
CancellationToken cancellationToken);
Expand Down
189 changes: 189 additions & 0 deletions src/KeetaNet.Anchor/Services/Node/UserClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
using System.Numerics;

namespace KeetaNet.Anchor;

/// <summary>
/// A <see cref="KeetaClient"/> bound to an operating account: reads imply the
/// account, writes originate from it and are signed by the bound signer, which
/// also pays any required fee by default. Without a signer the client is
/// read-only and writes throw <c>SIGNER_REQUIRED</c>.
/// </summary>
public sealed class UserClient : IDisposable
{
private readonly WasmRuntime _runtime;

private readonly KeetaClient _client;

/// <summary>The operating account when it differs from the signer.</summary>
private readonly Crypto.Account? _account;

private readonly Crypto.Account? _signer;

/// <summary>
/// An owned <see cref="KeetaClient"/> for <paramref name="nodeUrl"/>
/// bound to <paramref name="signer"/>, operating as
/// <paramref name="account"/> when given and as the signer itself
/// otherwise. Both accounts are borrowed, not disposed.
/// </summary>
internal UserClient(
WasmRuntime runtime,
string nodeUrl,
HttpClient? http,
long? network,
Crypto.Account? signer,
Crypto.Account? account)
{
_runtime = runtime;
_client = new KeetaClient(runtime, nodeUrl, http, network);
_signer = signer;
_account = account;
}

/// <summary>The underlying client, for reads beyond the operating account.</summary>
public KeetaClient Client => _client;

/// <summary>The bound signer, if any.</summary>
public Crypto.Account? Signer => _signer;

/// <summary>Whether this client has no signer and therefore rejects writes.</summary>
public bool IsReadOnly => _signer is null;

/// <summary>
/// The operating account: the configured account, then the signer.
/// Throws <c>SIGNER_REQUIRED</c> when neither is bound.
/// </summary>
public Crypto.Account Account =>
_account
?? _signer
?? throw new KeetaException("SIGNER_REQUIRED", "bind a signer or an operating account to the user client");

/// <summary>The full state of the operating account.</summary>
public Task<AccountState> GetState(CancellationToken cancellationToken = default) =>
_client.GetAccountState(Account, cancellationToken);

/// <summary>The settled balance of <paramref name="token"/> held by the operating account.</summary>
public Task<BigInteger> GetBalance(Crypto.Account token, CancellationToken cancellationToken = default) =>
_client.GetAccountBalance(Account, token, cancellationToken);

/// <summary>Every token balance held by the operating account.</summary>
public Task<IReadOnlyList<TokenBalance>> GetAllBalances(CancellationToken cancellationToken = default) =>
_client.GetAccountBalances(Account, cancellationToken);

/// <summary>The certificates published by the operating account.</summary>
public Task<IReadOnlyList<Certificate>> GetAllCertificates(CancellationToken cancellationToken = default) =>
_client.GetAllCertificates(Account, cancellationToken);

/// <summary>
/// The certificate the operating account published under
/// <paramref name="certificateHash"/>, or null when it never did.
/// </summary>
public Task<Certificate?> GetCertificateByHash(
Crypto.CertificateHash certificateHash,
CancellationToken cancellationToken = default) =>
_client.GetCertificateByHash(Account, certificateHash, cancellationToken);

/// <summary>
/// A builder for the operating account, signed by the bound signer and
/// pre-set with the client's defaults. The caller positions it, appends
/// operations, and builds. Requires a signer and a bound network.
/// </summary>
public Crypto.BlockBuilder InitBuilder() => _client.InitBuilder(Account, RequireSigner());

/// <summary>
/// Publish one signed block, paying any required fee with the bound
/// signer unless <paramref name="options"/> carries a fee-block factory.
/// </summary>
public Task<bool> Transmit(
Crypto.Block block,
TransmitOptions? options = null,
CancellationToken cancellationToken = default) =>
Transmit(new[] { block }, options, cancellationToken);

/// <summary>
/// Publish <paramref name="blocks"/> as one atomic staple, paying any
/// required fee with the bound signer unless <paramref name="options"/>
/// carries a fee-block factory.
/// </summary>
public Task<bool> Transmit(
IReadOnlyList<Crypto.Block> blocks,
TransmitOptions? options = null,
CancellationToken cancellationToken = default)
{
_ = RequireSigner();

return _client.Transmit(blocks, OrDefaultFeePayer(options), cancellationToken);
}

/// <summary>
/// Send <paramref name="amount"/> of <paramref name="token"/> to
/// <paramref name="to"/>, carrying an optional <paramref name="external"/>
/// reference.
/// </summary>
public async Task<bool> Send(
Crypto.Account to,
BigInteger amount,
Crypto.Account token,
string? external = null,
TransmitOptions? options = null,
CancellationToken cancellationToken = default)
{
using Crypto.BlockOperation send = _runtime.Blocks.Send(to, amount, token, external);
return await BuildAndTransmit(send, options, cancellationToken).ConfigureAwait(false);
}

/// <summary>Set the operating account's representative to <paramref name="representative"/>.</summary>
public async Task<bool> SetRep(
Crypto.Account representative,
TransmitOptions? options = null,
CancellationToken cancellationToken = default)
{
using Crypto.BlockOperation setRep = _runtime.Blocks.SetRep(representative);
return await BuildAndTransmit(setRep, options, cancellationToken).ConfigureAwait(false);
}

/// <summary>Release the owned <see cref="KeetaClient"/>; the bound accounts stay with the caller.</summary>
public void Dispose() => _client.Dispose();

/// <summary>
/// Build the operating account's one-operation block against its ledger
/// head (opening a fresh chain when it has none) and transmit it.
/// </summary>
private async Task<bool> BuildAndTransmit(
Crypto.BlockOperation operation,
TransmitOptions? options,
CancellationToken cancellationToken)
{
TransmitOptions resolved = OrDefaultFeePayer(options);
AccountState state = await GetState(cancellationToken).ConfigureAwait(false);

using Crypto.BlockBuilder builder = InitBuilder();
KeetaClient.PositionAfter(builder, state.HeadBlock?.ToString());
using Crypto.Block block = builder.AddOperation(operation).Build();

return await _client.Transmit(block, resolved, cancellationToken).ConfigureAwait(false);
}

/// <summary>Absent a fee-block factory, the bound signer pays any required fee itself.</summary>
private TransmitOptions OrDefaultFeePayer(TransmitOptions? options)
{
if (options?.FeeBlockFactory is not null)
{
return options;
}

TransmitOptions resolved = TransmitOptions.WithFeeSigner(RequireSigner());
if (options is not null)
{
foreach (Crypto.Account token in options.FeeTokenPriority)
{
resolved.FeeTokenPriority.Add(token);
}
}

return resolved;
}

/// <summary>The bound signer, required by every write.</summary>
private Crypto.Account RequireSigner() =>
_signer ?? throw new KeetaException("SIGNER_REQUIRED", "bind a signer to the user client to build or transmit blocks");
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
sephynox marked this conversation as resolved.
6 changes: 3 additions & 3 deletions tests/KeetaNet.Anchor.E2eTests/KycFlowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public async Task LedgerReadServesEveryPublishedCertificateRecord()

using var runtime = WasmRuntime.Load();
using Account observer = runtime.Accounts.FromSeed(E2eSeeds.Caller, 0, E2eSeeds.Secp256k1);
using NodeClient client = runtime.CreateNodeClient(anchor.NodeApi);
using KeetaClient client = runtime.CreateKeetaClient(anchor.NodeApi);

// An account that never published anything reads back as an empty list.
// The Account overload resolves the address itself, as the reference does.
Expand Down Expand Up @@ -138,7 +138,7 @@ public async Task BasicLedgerReadsReportTheHolderStateAndBalances()
KycAnchor anchor = KycAnchor.Start(harness);

using var runtime = WasmRuntime.Load();
using NodeClient client = runtime.CreateNodeClient(anchor.NodeApi);
using KeetaClient client = runtime.CreateKeetaClient(anchor.NodeApi);

string version = await client.GetNodeVersion(cancellationToken);
Assert.NotEmpty(version);
Expand Down Expand Up @@ -175,7 +175,7 @@ public async Task PublishedChainStatusMatchesTheTrustSet()
KycAnchor anchor = KycAnchor.Start(harness);

using var runtime = WasmRuntime.Load();
using NodeClient client = runtime.CreateNodeClient(anchor.NodeApi);
using KeetaClient client = runtime.CreateKeetaClient(anchor.NodeApi);

PublishedChain chain = PublishedChain.Publish(harness);
using Account holder = runtime.Accounts.FromPublicKeyString(chain.Account);
Expand Down
Loading
Loading