diff --git a/src/KeetaNet.Anchor/Services/AssetMovement/AssetMovementClient.cs b/src/KeetaNet.Anchor/Services/AssetMovement/AssetMovementClient.cs
index 54b13dc..345a4be 100644
--- a/src/KeetaNet.Anchor/Services/AssetMovement/AssetMovementClient.cs
+++ b/src/KeetaNet.Anchor/Services/AssetMovement/AssetMovementClient.cs
@@ -5,9 +5,11 @@ namespace KeetaNet.Anchor;
///
/// An asset-movement anchor client bound to a signer and a metadata root.
/// Discovery, request signing, retries, and the account-status blocker fold all
-/// run inside the wasm core. The client is thread-safe: operations serialize
-/// onto the runtime's dispatcher, and every networked method honors its
-/// before dispatch and during host HTTP and sleeps.
+/// run inside the wasm core. Discovery returns
+/// handles carrying every per-provider operation. The client is thread-safe:
+/// operations serialize onto the runtime's dispatcher, and every networked
+/// method honors its before dispatch and during
+/// host HTTP and sleeps.
///
public sealed class AssetMovementClient : WasmObject
{
@@ -32,14 +34,14 @@ internal static AssetMovementClient WithAccount(WasmRuntime runtime, string node
public async Task> GetProviders(CancellationToken cancellationToken = default)
{
byte[] payload = await Runtime.AssetProviders(Handle, cancellationToken).ConfigureAwait(false);
- return KeetaJson.ReadList(payload);
+ return BindAll(KeetaJson.ReadList(payload));
}
/// The provider with , or null when none advertises it.
public async Task GetProviderById(string id, CancellationToken cancellationToken = default)
{
byte[] payload = await Runtime.AssetProviderById(Handle, id, cancellationToken).ConfigureAwait(false);
- return ParseOptionalProvider(payload);
+ return BindOptional(payload);
}
/// The provider signed by , or null when absent.
@@ -52,7 +54,7 @@ public async Task> GetProviders(CancellationToken c
public async Task GetProviderByAccount(string account, CancellationToken cancellationToken = default)
{
byte[] payload = await Runtime.AssetProviderByAccount(Handle, account, cancellationToken).ConfigureAwait(false);
- return ParseOptionalProvider(payload);
+ return BindOptional(payload);
}
///
@@ -68,80 +70,11 @@ public async Task> GetProvidersForTransfer(
.AssetProvidersForTransfer(Handle, searchJson, cancellationToken)
.ConfigureAwait(false);
- return KeetaJson.ReadList(payload);
+ return BindAll(KeetaJson.ReadList(payload));
}
- ///
- /// Whether advertises the
- /// endpoint (e.g. initiateTransfer,
- /// createPersistentForwarding).
- ///
- public bool IsOperationSupported(AssetProvider provider, string operation) => provider.Operations.ContainsKey(operation);
-
- ///
- /// The provider's advertised legal disclaimers, or null when its metadata
- /// carries none. Malformed entries are skipped.
- ///
- public IReadOnlyList? GetLegalDisclaimers(AssetProvider provider)
- {
- if (provider.Legal is not { } legal
- || legal.ValueKind != JsonValueKind.Object
- || !legal.TryGetProperty("disclaimers", out JsonElement entries)
- || entries.ValueKind != JsonValueKind.Array)
- {
- return null;
- }
-
- var disclaimers = new List();
- using JsonElement.ArrayEnumerator enumerated = entries.EnumerateArray();
- foreach (JsonElement entry in enumerated)
- {
- if (TryDeserialize(entry, out AssetDisclaimer? disclaimer))
- {
- disclaimers.Add(disclaimer!);
- }
- }
-
- return disclaimers;
- }
-
- ///
- /// The provider's identifying details published under
- /// legal.anchorDetails, or null when its metadata carries none. A
- /// malformed description is dropped while the name and logo are kept.
- ///
- public AssetAnchorDetails? GetProviderAnchorDetails(AssetProvider provider)
- {
- if (provider.Legal is not { } legal
- || legal.ValueKind != JsonValueKind.Object
- || !legal.TryGetProperty("anchorDetails", out JsonElement details)
- || details.ValueKind != JsonValueKind.Object)
- {
- return null;
- }
-
- string? name = ReadOptionalString(details, "name");
- string? logo = ReadOptionalString(details, "logo");
-
- AssetRenderableContent? description = null;
- if (details.TryGetProperty("description", out JsonElement rawDescription))
- {
- TryDeserialize(rawDescription, out description);
- }
-
- return new AssetAnchorDetails(name, description, logo);
- }
-
- /// The member's string value, or null when absent or not a string.
- private static string? ReadOptionalString(JsonElement element, string name)
- {
- if (!element.TryGetProperty(name, out JsonElement found) || found.ValueKind != JsonValueKind.String)
- {
- return null;
- }
-
- return found.GetString();
- }
+ /// Bind a stored metadata snapshot back to this client as an operable handle.
+ public AssetProvider Provider(AssetProviderInfo info) => new(this, info);
///
/// The legal disclaimers advertised by the provider with
@@ -153,98 +86,47 @@ public async Task> GetProvidersForTransfer(
CancellationToken cancellationToken = default)
{
AssetProvider? provider = await GetProviderById(id, cancellationToken).ConfigureAwait(false);
- if (provider is null)
- {
- return null;
- }
-
- return GetLegalDisclaimers(provider);
+ return provider?.GetLegalDisclaimers();
}
- ///
- /// The provider's display metadata for (an external
- /// chain asset id) at (a canonical location
- /// string), or null when the provider advertises none or the entry does not
- /// parse.
- ///
- public AssetTokenMetadata? GetAssetMetadataForLocation(AssetProvider provider, string location, string asset)
- {
- if (provider.LocationMetadata is not { } metadata || metadata.ValueKind != JsonValueKind.Object)
- {
- return null;
- }
-
- if (!metadata.TryGetProperty(location, out JsonElement forLocation)
- || forLocation.ValueKind != JsonValueKind.Object
- || !forLocation.TryGetProperty("assets", out JsonElement assets)
- || assets.ValueKind != JsonValueKind.Object)
- {
- return null;
- }
-
- if (!assets.TryGetProperty(asset, out JsonElement found)
- || !TryDeserialize(found, out AssetTokenMetadata? parsed))
- {
- return null;
- }
-
- return parsed;
- }
-
- /// Deserialize one metadata entry, treating malformed JSON as absent.
- private static bool TryDeserialize(JsonElement element, out T? value)
- where T : class
- {
- try
- {
- value = element.Deserialize(KeetaJson.Options);
- }
- catch (JsonException)
- {
- value = null;
- }
-
- return value is not null;
- }
-
- /// Simulate a transfer, returning a fluent handle over its instruction choices.
- public async Task SimulateTransfer(
+ /// Simulate a transfer for .
+ internal async Task SimulateTransfer(
AssetProvider provider,
AssetTransferRequest request,
- CancellationToken cancellationToken = default)
+ CancellationToken cancellationToken)
{
- var transport = await ReadOperationAsync(Runtime.AssetSimulateTransfer, provider, request, cancellationToken).ConfigureAwait(false);
- return new AssetSimulatedTransfer(this, provider, request, transport.InstructionChoices);
+ var transport = await ReadOperationAsync(Runtime.AssetSimulateTransfer, provider.Info, request, cancellationToken).ConfigureAwait(false);
+ return new AssetSimulatedTransfer(provider, request, transport.InstructionChoices);
}
- /// Initiate a transfer, returning a fluent handle. The request's recipient is required.
- public async Task InitiateTransfer(
+ /// Initiate a transfer for .
+ internal async Task InitiateTransfer(
AssetProvider provider,
AssetTransferRequest request,
- CancellationToken cancellationToken = default)
+ CancellationToken cancellationToken)
{
- var transport = await ReadOperationAsync(Runtime.AssetInitiateTransfer, provider, request, cancellationToken).ConfigureAwait(false);
- return new AssetTransfer(this, provider, transport.Id, transport.InstructionChoices);
+ var transport = await ReadOperationAsync(Runtime.AssetInitiateTransfer, provider.Info, request, cancellationToken).ConfigureAwait(false);
+ return new AssetTransfer(provider, transport.Id, transport.InstructionChoices);
}
/// Execute a pull instruction for a transfer.
- public Task ExecuteTransfer(
- AssetProvider provider,
+ internal Task ExecuteTransfer(
+ AssetProviderInfo provider,
AssetExecuteRequest request,
- CancellationToken cancellationToken = default) =>
+ CancellationToken cancellationToken) =>
ReadOperationAsync(Runtime.AssetExecuteTransfer, provider, request, cancellationToken);
/// Read the status of transfer .
- public Task GetTransferStatus(
- AssetProvider provider,
+ internal Task GetTransferStatus(
+ AssetProviderInfo provider,
string id,
- CancellationToken cancellationToken = default) =>
+ CancellationToken cancellationToken) =>
ReadOperationForIdAsync(Runtime.AssetTransferStatus, provider, id, cancellationToken);
- /// Read whether the signer's account is ready to use this provider.
- public async Task GetAccountStatus(
- AssetProvider provider,
- CancellationToken cancellationToken = default)
+ /// Read whether the signer's account is ready to use .
+ internal async Task GetAccountStatus(
+ AssetProviderInfo provider,
+ CancellationToken cancellationToken)
{
string providerJson = Serialize(provider);
byte[] payload = await Runtime.AssetAccountStatus(Handle, providerJson, cancellationToken).ConfigureAwait(false);
@@ -253,82 +135,78 @@ public async Task GetAccountStatus(
}
/// Open a persistent-forwarding template session.
- public Task InitiatePersistentForwardingTemplate(
- AssetProvider provider,
+ internal Task InitiatePersistentForwardingTemplate(
+ AssetProviderInfo provider,
AssetInitiateTemplateRequest request,
- CancellationToken cancellationToken = default) =>
+ CancellationToken cancellationToken) =>
ReadOperationAsync(Runtime.AssetInitiatePersistentForwardingTemplate, provider, request, cancellationToken);
/// Create a persistent-forwarding template.
- public Task CreatePersistentForwardingTemplate(
- AssetProvider provider,
+ internal Task CreatePersistentForwardingTemplate(
+ AssetProviderInfo provider,
AssetCreateTemplateRequest request,
- CancellationToken cancellationToken = default) =>
+ CancellationToken cancellationToken) =>
ReadOperationAsync(Runtime.AssetCreatePersistentForwardingTemplate, provider, request, cancellationToken);
/// List persistent-forwarding templates.
- public Task ListForwardingAddressTemplates(
- AssetProvider provider,
+ internal Task ListForwardingAddressTemplates(
+ AssetProviderInfo provider,
AssetListTemplatesRequest request,
- CancellationToken cancellationToken = default) =>
+ CancellationToken cancellationToken) =>
ReadOperationAsync(Runtime.AssetListForwardingAddressTemplates, provider, request, cancellationToken);
/// Create a persistent-forwarding address, returning its (obfuscated) details.
- public Task CreatePersistentForwardingAddress(
- AssetProvider provider,
+ internal Task CreatePersistentForwardingAddress(
+ AssetProviderInfo provider,
AssetCreateAddressRequest request,
- CancellationToken cancellationToken = default) =>
+ CancellationToken cancellationToken) =>
ReadOperationAsync(Runtime.AssetCreatePersistentForwardingAddress, provider, request, cancellationToken);
/// List persistent-forwarding addresses.
- public Task ListForwardingAddresses(
- AssetProvider provider,
+ internal Task ListForwardingAddresses(
+ AssetProviderInfo provider,
AssetListAddressesRequest request,
- CancellationToken cancellationToken = default) =>
+ CancellationToken cancellationToken) =>
ReadOperationAsync(Runtime.AssetListForwardingAddresses, provider, request, cancellationToken);
/// Deactivate a persistent-forwarding template by id.
- public Task DeactivatePersistentForwardingTemplate(
- AssetProvider provider,
+ internal Task DeactivatePersistentForwardingTemplate(
+ AssetProviderInfo provider,
string id,
- CancellationToken cancellationToken = default) =>
+ CancellationToken cancellationToken) =>
RunOperationForIdAsync(Runtime.AssetDeactivatePersistentForwardingTemplate, provider, id, cancellationToken);
/// Deactivate a persistent-forwarding address by id.
- public Task DeactivatePersistentForwardingAddress(
- AssetProvider provider,
+ internal Task DeactivatePersistentForwardingAddress(
+ AssetProviderInfo provider,
string id,
- CancellationToken cancellationToken = default) =>
+ CancellationToken cancellationToken) =>
RunOperationForIdAsync(Runtime.AssetDeactivatePersistentForwardingAddress, provider, id, cancellationToken);
/// List asset-movement transactions.
- public Task ListTransactions(
- AssetProvider provider,
+ internal Task ListTransactions(
+ AssetProviderInfo provider,
AssetListTransactionsRequest request,
- CancellationToken cancellationToken = default) =>
+ CancellationToken cancellationToken) =>
ReadOperationAsync(Runtime.AssetListTransactions, provider, request, cancellationToken);
- ///
- /// Share KYC attributes with the provider, returning the provider's outcome unchanged.
- /// A pending outcome carries the promise URL the caller must poll. Use
- /// to poll it automatically.
- ///
- public Task ShareKycAttributes(
- AssetProvider provider,
+ /// Share KYC attributes with , returning its outcome unchanged.
+ internal Task ShareKycAttributes(
+ AssetProviderInfo provider,
AssetShareKycRequest request,
- CancellationToken cancellationToken = default) =>
+ CancellationToken cancellationToken) =>
ReadOperationAsync(Runtime.AssetShareKycAttributes, provider, request, cancellationToken);
///
/// Share KYC attributes and, when the outcome is pending with a promise URL,
/// poll that URL inside the core until it resolves.
///
- public async Task ShareKycAttributesAndWait(
- AssetProvider provider,
+ internal async Task ShareKycAttributesAndWait(
+ AssetProviderInfo provider,
AssetShareKycRequest request,
- TimeSpan? pollInterval = null,
- TimeSpan? timeout = null,
- CancellationToken cancellationToken = default)
+ TimeSpan? pollInterval,
+ TimeSpan? timeout,
+ CancellationToken cancellationToken)
{
string providerJson = Serialize(provider);
string requestJson = Serialize(request);
@@ -344,7 +222,7 @@ public async Task ShareKycAttributesAndWait(
/// Drive a provider operation whose crosses as JSON.
private async Task ReadOperationAsync(
Func> operation,
- AssetProvider provider,
+ AssetProviderInfo provider,
object request,
CancellationToken cancellationToken)
{
@@ -358,7 +236,7 @@ private async Task ReadOperationAsync(
/// Drive a provider operation keyed by a raw .
private async Task ReadOperationForIdAsync(
Func> operation,
- AssetProvider provider,
+ AssetProviderInfo provider,
string id,
CancellationToken cancellationToken)
{
@@ -371,7 +249,7 @@ private async Task ReadOperationForIdAsync(
/// Drive a provider operation keyed by a raw , discarding the response.
private async Task RunOperationForIdAsync(
Func> operation,
- AssetProvider provider,
+ AssetProviderInfo provider,
string id,
CancellationToken cancellationToken)
{
@@ -396,7 +274,12 @@ private static T Read(byte[] payload) =>
JsonSerializer.Deserialize(payload, KeetaJson.Options)
?? throw new KeetaException("DECODE", $"could not decode a {typeof(T).Name} from the asset-movement response");
- private static AssetProvider? ParseOptionalProvider(byte[] payload)
+ /// Bind every discovered snapshot to this client.
+ private AssetProvider[] BindAll(IReadOnlyList infos) =>
+ infos.Select(Provider).ToArray();
+
+ /// Bind an optional discovery payload, mapping JSON null to no provider.
+ private AssetProvider? BindOptional(byte[] payload)
{
using var document = JsonDocument.Parse(payload);
JsonElement root = document.RootElement;
@@ -405,7 +288,13 @@ private static T Read(byte[] payload) =>
return null;
}
- return root.Deserialize(KeetaJson.Options);
+ AssetProviderInfo? info = root.Deserialize(KeetaJson.Options);
+ if (info is null)
+ {
+ return null;
+ }
+
+ return Provider(info);
}
private protected override void Release(WasmRuntime runtime, int handle) => runtime.AssetFree(handle);
diff --git a/src/KeetaNet.Anchor/Services/AssetMovement/AssetMovementModels.cs b/src/KeetaNet.Anchor/Services/AssetMovement/AssetMovementModels.cs
index c467e15..1902a6b 100644
--- a/src/KeetaNet.Anchor/Services/AssetMovement/AssetMovementModels.cs
+++ b/src/KeetaNet.Anchor/Services/AssetMovement/AssetMovementModels.cs
@@ -23,18 +23,138 @@ public enum AssetEndpointAuth
public sealed record AssetEndpoint(string Url, AssetEndpointAuth Auth);
///
-/// An asset-movement provider discovered from on-chain service metadata. The
+/// An asset-movement provider's advertised metadata, discovered from on-chain
+/// service metadata (the reference AssetMovementProviderInfo). The
/// polymorphic , , and
/// members are carried as raw JSON so the value round-trips
-/// unchanged when handed back to an operation.
+/// unchanged when handed back to an operation. Operations live on the
+/// handle bound through
+/// .
///
-public sealed record AssetProvider(
+public sealed record AssetProviderInfo(
string Id,
IReadOnlyDictionary Operations,
IReadOnlyList? SupportedAssets = null,
JsonElement? LocationMetadata = null,
JsonElement? Legal = null,
- string? Account = null);
+ string? Account = null)
+{
+ ///
+ /// Whether this provider advertises the
+ /// endpoint (e.g. initiateTransfer, createPersistentForwarding).
+ ///
+ public bool IsOperationSupported(string operation) => Operations.ContainsKey(operation);
+
+ ///
+ /// The advertised legal disclaimers, or null when the metadata carries
+ /// none. Malformed entries are skipped.
+ ///
+ public IReadOnlyList? GetLegalDisclaimers()
+ {
+ if (Legal is not { } legal
+ || legal.ValueKind != JsonValueKind.Object
+ || !legal.TryGetProperty("disclaimers", out JsonElement entries)
+ || entries.ValueKind != JsonValueKind.Array)
+ {
+ return null;
+ }
+
+ var disclaimers = new List();
+ using JsonElement.ArrayEnumerator enumerated = entries.EnumerateArray();
+ foreach (JsonElement entry in enumerated)
+ {
+ if (TryDeserialize(entry, out AssetDisclaimer? disclaimer))
+ {
+ disclaimers.Add(disclaimer!);
+ }
+ }
+
+ return disclaimers;
+ }
+
+ ///
+ /// The identifying details published under legal.anchorDetails, or
+ /// null when the metadata carries none. A malformed description is dropped
+ /// while the name and logo are kept.
+ ///
+ public AssetAnchorDetails? GetAnchorDetails()
+ {
+ if (Legal is not { } legal
+ || legal.ValueKind != JsonValueKind.Object
+ || !legal.TryGetProperty("anchorDetails", out JsonElement details)
+ || details.ValueKind != JsonValueKind.Object)
+ {
+ return null;
+ }
+
+ string? name = ReadOptionalString(details, "name");
+ string? logo = ReadOptionalString(details, "logo");
+
+ AssetRenderableContent? description = null;
+ if (details.TryGetProperty("description", out JsonElement rawDescription))
+ {
+ TryDeserialize(rawDescription, out description);
+ }
+
+ return new AssetAnchorDetails(name, description, logo);
+ }
+
+ ///
+ /// The display metadata for (an external chain
+ /// asset id) at (a canonical location string),
+ /// or null when the provider advertises none or the entry does not parse.
+ ///
+ public AssetTokenMetadata? GetAssetMetadataForLocation(string location, string asset)
+ {
+ if (LocationMetadata is not { } metadata || metadata.ValueKind != JsonValueKind.Object)
+ {
+ return null;
+ }
+
+ if (!metadata.TryGetProperty(location, out JsonElement forLocation)
+ || forLocation.ValueKind != JsonValueKind.Object
+ || !forLocation.TryGetProperty("assets", out JsonElement assets)
+ || assets.ValueKind != JsonValueKind.Object)
+ {
+ return null;
+ }
+
+ if (!assets.TryGetProperty(asset, out JsonElement found)
+ || !TryDeserialize(found, out AssetTokenMetadata? parsed))
+ {
+ return null;
+ }
+
+ return parsed;
+ }
+
+ /// Deserialize one metadata entry, treating malformed JSON as absent.
+ private static bool TryDeserialize(JsonElement element, out T? value)
+ where T : class
+ {
+ try
+ {
+ value = element.Deserialize(KeetaJson.Options);
+ }
+ catch (JsonException)
+ {
+ value = null;
+ }
+
+ return value is not null;
+ }
+
+ /// The member's string value, or null when absent or not a string.
+ private static string? ReadOptionalString(JsonElement element, string name)
+ {
+ if (!element.TryGetProperty(name, out JsonElement found) || found.ValueKind != JsonValueKind.String)
+ {
+ return null;
+ }
+
+ return found.GetString();
+ }
+}
/// Pagination bounds shared by the list operations.
public sealed record AssetPagination(uint? Limit = null, uint? Offset = null);
diff --git a/src/KeetaNet.Anchor/Services/AssetMovement/AssetProvider.cs b/src/KeetaNet.Anchor/Services/AssetMovement/AssetProvider.cs
new file mode 100644
index 0000000..747d5fa
--- /dev/null
+++ b/src/KeetaNet.Anchor/Services/AssetMovement/AssetProvider.cs
@@ -0,0 +1,130 @@
+namespace KeetaNet.Anchor;
+
+///
+/// One asset-movement provider bound to its discovering client (the reference
+/// provider handle): a metadata snapshot in plus every
+/// per-provider operation, signed and retried by the client it came from.
+/// Obtained from the discovery methods or
+/// re-bound from a stored snapshot with
+/// .
+///
+public sealed class AssetProvider
+{
+ private readonly AssetMovementClient _client;
+
+ internal AssetProvider(AssetMovementClient client, AssetProviderInfo info)
+ {
+ _client = client;
+ Info = info;
+ }
+
+ /// The provider's advertised metadata snapshot.
+ public AssetProviderInfo Info { get; }
+
+ /// The provider's id.
+ public string Id => Info.Id;
+
+ ///
+ public bool IsOperationSupported(string operation) => Info.IsOperationSupported(operation);
+
+ ///
+ public IReadOnlyList? GetLegalDisclaimers() => Info.GetLegalDisclaimers();
+
+ ///
+ public AssetAnchorDetails? GetAnchorDetails() => Info.GetAnchorDetails();
+
+ ///
+ public AssetTokenMetadata? GetAssetMetadataForLocation(string location, string asset) =>
+ Info.GetAssetMetadataForLocation(location, asset);
+
+ /// Simulate a transfer, returning a fluent handle over its instruction choices.
+ public Task SimulateTransfer(
+ AssetTransferRequest request,
+ CancellationToken cancellationToken = default) =>
+ _client.SimulateTransfer(this, request, cancellationToken);
+
+ /// Initiate a transfer, returning a fluent handle. The request's recipient is required.
+ public Task InitiateTransfer(
+ AssetTransferRequest request,
+ CancellationToken cancellationToken = default) =>
+ _client.InitiateTransfer(this, request, cancellationToken);
+
+ /// Execute a pull instruction for a transfer.
+ public Task ExecuteTransfer(
+ AssetExecuteRequest request,
+ CancellationToken cancellationToken = default) =>
+ _client.ExecuteTransfer(Info, request, cancellationToken);
+
+ /// Read the status of transfer .
+ public Task GetTransferStatus(string id, CancellationToken cancellationToken = default) =>
+ _client.GetTransferStatus(Info, id, cancellationToken);
+
+ /// Read whether the signer's account is ready to use this provider.
+ public Task GetAccountStatus(CancellationToken cancellationToken = default) =>
+ _client.GetAccountStatus(Info, cancellationToken);
+
+ /// Open a persistent-forwarding template session.
+ public Task InitiatePersistentForwardingTemplate(
+ AssetInitiateTemplateRequest request,
+ CancellationToken cancellationToken = default) =>
+ _client.InitiatePersistentForwardingTemplate(Info, request, cancellationToken);
+
+ /// Create a persistent-forwarding template.
+ public Task CreatePersistentForwardingTemplate(
+ AssetCreateTemplateRequest request,
+ CancellationToken cancellationToken = default) =>
+ _client.CreatePersistentForwardingTemplate(Info, request, cancellationToken);
+
+ /// List persistent-forwarding templates.
+ public Task ListForwardingAddressTemplates(
+ AssetListTemplatesRequest request,
+ CancellationToken cancellationToken = default) =>
+ _client.ListForwardingAddressTemplates(Info, request, cancellationToken);
+
+ /// Create a persistent-forwarding address, returning its (obfuscated) details.
+ public Task CreatePersistentForwardingAddress(
+ AssetCreateAddressRequest request,
+ CancellationToken cancellationToken = default) =>
+ _client.CreatePersistentForwardingAddress(Info, request, cancellationToken);
+
+ /// List persistent-forwarding addresses.
+ public Task ListForwardingAddresses(
+ AssetListAddressesRequest request,
+ CancellationToken cancellationToken = default) =>
+ _client.ListForwardingAddresses(Info, request, cancellationToken);
+
+ /// Deactivate a persistent-forwarding template by id.
+ public Task DeactivatePersistentForwardingTemplate(string id, CancellationToken cancellationToken = default) =>
+ _client.DeactivatePersistentForwardingTemplate(Info, id, cancellationToken);
+
+ /// Deactivate a persistent-forwarding address by id.
+ public Task DeactivatePersistentForwardingAddress(string id, CancellationToken cancellationToken = default) =>
+ _client.DeactivatePersistentForwardingAddress(Info, id, cancellationToken);
+
+ /// List asset-movement transactions.
+ public Task ListTransactions(
+ AssetListTransactionsRequest request,
+ CancellationToken cancellationToken = default) =>
+ _client.ListTransactions(Info, request, cancellationToken);
+
+ ///
+ /// Share KYC attributes with the provider, returning the provider's outcome unchanged.
+ /// A pending outcome carries the promise URL the caller must poll. Use
+ /// to poll it automatically.
+ ///
+ public Task ShareKycAttributes(
+ AssetShareKycRequest request,
+ CancellationToken cancellationToken = default) =>
+ _client.ShareKycAttributes(Info, request, cancellationToken);
+
+ ///
+ /// Share KYC attributes and, when the outcome is pending with a promise URL,
+ /// poll that URL inside the core until it resolves.
+ ///
+ public Task ShareKycAttributesAndWait(
+ AssetShareKycRequest request,
+ TimeSpan? pollInterval = null,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default) =>
+ _client.ShareKycAttributesAndWait(Info, request, pollInterval, timeout, cancellationToken);
+}
diff --git a/src/KeetaNet.Anchor/Services/AssetMovement/AssetTransfer.cs b/src/KeetaNet.Anchor/Services/AssetMovement/AssetTransfer.cs
index 18e8c2d..c2d1fdc 100644
--- a/src/KeetaNet.Anchor/Services/AssetMovement/AssetTransfer.cs
+++ b/src/KeetaNet.Anchor/Services/AssetMovement/AssetTransfer.cs
@@ -9,17 +9,14 @@ namespace KeetaNet.Anchor;
///
public sealed class AssetSimulatedTransfer
{
- private readonly AssetMovementClient _client;
private readonly AssetProvider _provider;
private readonly AssetTransferRequest _request;
internal AssetSimulatedTransfer(
- AssetMovementClient client,
AssetProvider provider,
AssetTransferRequest request,
IReadOnlyList instructionChoices)
{
- _client = client;
_provider = provider;
_request = request;
InstructionChoices = instructionChoices;
@@ -45,7 +42,7 @@ public Task CreateTransfer(
};
AssetTransferRequest request = _request with { To = to };
- return _client.InitiateTransfer(_provider, request, cancellationToken);
+ return _provider.InitiateTransfer(request, cancellationToken);
}
}
@@ -56,16 +53,13 @@ public Task CreateTransfer(
///
public sealed class AssetTransfer
{
- private readonly AssetMovementClient _client;
private readonly AssetProvider _provider;
internal AssetTransfer(
- AssetMovementClient client,
AssetProvider provider,
string id,
IReadOnlyList instructionChoices)
{
- _client = client;
_provider = provider;
Id = id;
InstructionChoices = instructionChoices;
@@ -79,7 +73,7 @@ internal AssetTransfer(
/// Read this transfer's current status.
public Task GetTransferStatus(CancellationToken cancellationToken = default) =>
- _client.GetTransferStatus(_provider, Id, cancellationToken);
+ _provider.GetTransferStatus(Id, cancellationToken);
/// Execute a fiat pull for this transfer.
public Task ExecuteTransfer(
@@ -87,6 +81,6 @@ public Task ExecuteTransfer(
CancellationToken cancellationToken = default)
{
var request = new AssetExecuteRequest(Id, instruction);
- return _client.ExecuteTransfer(_provider, request, cancellationToken);
+ return _provider.ExecuteTransfer(request, cancellationToken);
}
}
diff --git a/src/KeetaNet.Anchor/Services/Kyc/KycClient.cs b/src/KeetaNet.Anchor/Services/Kyc/KycClient.cs
index 27e21d3..7752853 100644
--- a/src/KeetaNet.Anchor/Services/Kyc/KycClient.cs
+++ b/src/KeetaNet.Anchor/Services/Kyc/KycClient.cs
@@ -4,10 +4,11 @@ namespace KeetaNet.Anchor;
///
/// A KYC anchor client bound to a signer and a metadata root. Discovery, request
-/// signing, retries, and polling all run inside the wasm core. The client is
-/// thread-safe: operations serialize onto the runtime's dispatcher, and every
-/// networked method honors its before dispatch
-/// and during host HTTP and sleeps.
+/// signing, retries, and polling all run inside the wasm core. Discovery returns
+/// handles carrying the verification operations. The
+/// client is thread-safe: operations serialize onto the runtime's dispatcher,
+/// and every networked method honors its before
+/// dispatch and during host HTTP and sleeps.
///
public sealed class KycClient : WasmObject
{
@@ -33,10 +34,8 @@ public async Task> GetProviders(
IEnumerable countries,
CancellationToken cancellationToken = default)
{
- string countriesJson = SerializeCountries(countries);
-
- byte[] payload = await Runtime.KycProviders(Handle, countriesJson, cancellationToken).ConfigureAwait(false);
- return KeetaJson.ReadList(payload);
+ IReadOnlyList infos = await GetProviderInfos(countries, cancellationToken).ConfigureAwait(false);
+ return infos.Select(Provider).ToArray();
}
///
@@ -45,20 +44,23 @@ public async Task> GetProviders(
///
public async Task GetSupportedCountries(CancellationToken cancellationToken = default)
{
- IReadOnlyList providers = await GetProviders(Array.Empty(), cancellationToken).ConfigureAwait(false);
- return SupportedCountries.FromProviders(providers);
+ IReadOnlyList infos = await GetProviderInfos(Array.Empty(), cancellationToken).ConfigureAwait(false);
+ return SupportedCountries.FromProviders(infos);
}
+ /// Bind a stored metadata snapshot back to this client as an operable handle.
+ public KycProvider Provider(KycProviderInfo info) => new(this, info);
+
///
/// Start a verification with for
/// , optionally redirecting the user to
/// when the flow ends.
///
- public async Task StartVerification(
- KycProvider provider,
+ internal async Task StartVerification(
+ KycProviderInfo provider,
IEnumerable countries,
- string? redirect = null,
- CancellationToken cancellationToken = default)
+ string? redirect,
+ CancellationToken cancellationToken)
{
string providerJson = JsonSerializer.Serialize(provider, KeetaJson.Options);
string countriesJson = SerializeCountries(countries);
@@ -71,10 +73,10 @@ public async Task StartVerification(
}
/// Fetch the certificates issued for verification .
- public async Task GetCertificates(
- KycProvider provider,
+ internal async Task GetCertificates(
+ KycProviderInfo provider,
string id,
- CancellationToken cancellationToken = default)
+ CancellationToken cancellationToken)
{
string providerJson = JsonSerializer.Serialize(provider, KeetaJson.Options);
byte[] payload = await Runtime
@@ -85,15 +87,14 @@ public async Task GetCertificates(
}
/// Parse 's advertised issuer CA certificate.
- /// Use it as a trusted root when verifying an issued .
- public Crypto.Certificate GetCA(KycProvider provider) =>
+ internal Crypto.Certificate GetCA(KycProviderInfo provider) =>
Runtime.Certificates.Parse(provider.Ca);
/// Read the status of verification .
- public async Task GetVerificationStatus(
- KycProvider provider,
+ internal async Task GetVerificationStatus(
+ KycProviderInfo provider,
string id,
- CancellationToken cancellationToken = default)
+ CancellationToken cancellationToken)
{
string providerJson = JsonSerializer.Serialize(provider, KeetaJson.Options);
byte[] payload = await Runtime
@@ -103,6 +104,17 @@ public async Task GetVerificationStatus(
return ParseOutcome(payload, "status", ready => new StatusOutcome(ready, null), retry => new StatusOutcome(null, retry));
}
+ /// The raw discovery payload decoded to metadata snapshots.
+ private async Task> GetProviderInfos(
+ IEnumerable countries,
+ CancellationToken cancellationToken)
+ {
+ string countriesJson = SerializeCountries(countries);
+
+ byte[] payload = await Runtime.KycProviders(Handle, countriesJson, cancellationToken).ConfigureAwait(false);
+ return KeetaJson.ReadList(payload);
+ }
+
private static string SerializeCountries(IEnumerable countries) =>
JsonSerializer.Serialize(countries.ToArray(), KeetaJson.Options);
diff --git a/src/KeetaNet.Anchor/Services/Kyc/KycModels.cs b/src/KeetaNet.Anchor/Services/Kyc/KycModels.cs
index 645a787..5bc640e 100644
--- a/src/KeetaNet.Anchor/Services/Kyc/KycModels.cs
+++ b/src/KeetaNet.Anchor/Services/Kyc/KycModels.cs
@@ -10,9 +10,14 @@ public sealed record KycOperations(
string? CheckLocality,
string? GetEstimate);
-/// A KYC provider discovered from on-chain service metadata.
+///
+/// A KYC provider's advertised metadata, discovered from on-chain service
+/// metadata (the reference KycProviderInfo). Operations live on the
+/// handle bound through
+/// .
+///
/// is null for a worldwide provider.
-public sealed record KycProvider(
+public sealed record KycProviderInfo(
string Id,
string Ca,
KycOperations Operations,
@@ -27,10 +32,10 @@ public sealed record KycProvider(
public sealed record SupportedCountries(bool Worldwide, IReadOnlyList Countries)
{
/// Fold discovered into their aggregate coverage.
- public static SupportedCountries FromProviders(IEnumerable providers)
+ public static SupportedCountries FromProviders(IEnumerable providers)
{
var countries = new List();
- foreach (KycProvider provider in providers)
+ foreach (KycProviderInfo provider in providers)
{
if (provider.CountryCodes is null)
{
diff --git a/src/KeetaNet.Anchor/Services/Kyc/KycProvider.cs b/src/KeetaNet.Anchor/Services/Kyc/KycProvider.cs
new file mode 100644
index 0000000..1b7d266
--- /dev/null
+++ b/src/KeetaNet.Anchor/Services/Kyc/KycProvider.cs
@@ -0,0 +1,47 @@
+namespace KeetaNet.Anchor;
+
+///
+/// One KYC provider bound to its discovering client (the reference provider
+/// handle): a metadata snapshot in plus the verification
+/// operations, signed and retried by the client it came from. Obtained from
+/// or re-bound from a stored snapshot
+/// with .
+///
+public sealed class KycProvider
+{
+ private readonly KycClient _client;
+
+ internal KycProvider(KycClient client, KycProviderInfo info)
+ {
+ _client = client;
+ Info = info;
+ }
+
+ /// The provider's advertised metadata snapshot.
+ public KycProviderInfo Info { get; }
+
+ /// The provider's id.
+ public string Id => Info.Id;
+
+ ///
+ /// Start a verification for , optionally
+ /// redirecting the user to when the flow ends.
+ ///
+ public Task StartVerification(
+ IEnumerable countries,
+ string? redirect = null,
+ CancellationToken cancellationToken = default) =>
+ _client.StartVerification(Info, countries, redirect, cancellationToken);
+
+ /// Fetch the certificates issued for verification .
+ public Task GetCertificates(string id, CancellationToken cancellationToken = default) =>
+ _client.GetCertificates(Info, id, cancellationToken);
+
+ /// Read the status of verification .
+ public Task GetVerificationStatus(string id, CancellationToken cancellationToken = default) =>
+ _client.GetVerificationStatus(Info, id, cancellationToken);
+
+ /// Parse this provider's advertised issuer CA certificate.
+ /// Use it as a trusted root when verifying an issued .
+ public Crypto.Certificate GetCA() => _client.GetCA(Info);
+}
diff --git a/tests/KeetaNet.Anchor.E2eTests/AssetFlowTests.cs b/tests/KeetaNet.Anchor.E2eTests/AssetFlowTests.cs
index 0f66a7c..533c198 100644
--- a/tests/KeetaNet.Anchor.E2eTests/AssetFlowTests.cs
+++ b/tests/KeetaNet.Anchor.E2eTests/AssetFlowTests.cs
@@ -27,7 +27,7 @@ public async Task DiscoveryReadsThePublishedProvider()
IReadOnlyList providers = await client.GetProviders(cancellationToken);
AssetProvider provider = Assert.Single(providers);
Assert.Equal(anchor.ProviderId, provider.Id);
- Assert.True(client.IsOperationSupported(provider, "simulateTransfer"));
+ Assert.True(provider.IsOperationSupported("simulateTransfer"));
// The Account overload resolves the public-key string itself, so one
// call covers both lookup forms.
@@ -54,10 +54,10 @@ public async Task TransfersRunEndToEndAgainstTheLiveAnchor()
(AssetMovementClient client, AssetAnchor anchor, CancellationToken cancellationToken) = session;
AssetProvider provider = await session.DiscoveredProviderAsync();
- AssetAccountStatus status = await client.GetAccountStatus(provider, cancellationToken);
+ AssetAccountStatus status = await provider.GetAccountStatus(cancellationToken);
Assert.False(status.ActionRequired);
- AssetSimulatedTransfer simulated = await client.SimulateTransfer(provider, PushTransfer(anchor, anchor.SendToAddress), cancellationToken);
+ AssetSimulatedTransfer simulated = await provider.SimulateTransfer(PushTransfer(anchor, anchor.SendToAddress), cancellationToken);
JsonElement simulatedInstruction = Assert.Single(simulated.InstructionChoices);
Assert.Equal("KEETA_SEND", simulatedInstruction.GetProperty("type").GetString());
@@ -75,20 +75,20 @@ public async Task TransfersRunEndToEndAgainstTheLiveAnchor()
$"123:{anchor.Signer}",
Assert.Single(redirected.InstructionChoices).GetProperty("external").GetString());
- AssetTransfer transfer = await client.InitiateTransfer(provider, PushTransfer(anchor, anchor.SendToAddress), cancellationToken);
+ AssetTransfer transfer = await provider.InitiateTransfer(PushTransfer(anchor, anchor.SendToAddress), cancellationToken);
Assert.Equal("123", transfer.Id);
Assert.Equal(
anchor.SendToAddress,
transfer.InstructionChoices[0].GetProperty("sendToAddress").GetString());
await Assert.ThrowsAsync(
- () => client.InitiateTransfer(provider, PushTransfer(anchor, recipient: null), cancellationToken));
+ () => provider.InitiateTransfer(PushTransfer(anchor, recipient: null), cancellationToken));
AssetTransferStatus completed = await transfer.GetTransferStatus(cancellationToken);
Assert.Equal("123", completed.Transaction.GetProperty("id").GetString());
Assert.Equal("COMPLETED", completed.Transaction.GetProperty("status").GetString());
- AssetTransfer pull = await client.InitiateTransfer(provider, PullTransfer(anchor), cancellationToken);
+ AssetTransfer pull = await provider.InitiateTransfer(PullTransfer(anchor), cancellationToken);
JsonElement pullInstruction = Assert.Single(pull.InstructionChoices);
Assert.Equal("ACH_DEBIT", pullInstruction.GetProperty("type").GetString());
@@ -106,7 +106,7 @@ public async Task AccountStatusServesTypedBlockersForABlockedCaller()
(AssetMovementClient client, AssetAnchor anchor, CancellationToken cancellationToken) = session;
AssetProvider provider = await session.DiscoveredProviderAsync();
- AssetAccountStatus status = await client.GetAccountStatus(provider, cancellationToken);
+ AssetAccountStatus status = await provider.GetAccountStatus(cancellationToken);
Assert.True(status.ActionRequired);
Assert.Equal(2, status.Blockers!.Count);
@@ -136,7 +136,7 @@ public async Task PublishedLegalAndTokenMetadataRoundTrip()
(AssetMovementClient client, AssetAnchor anchor, CancellationToken cancellationToken) = session;
AssetProvider provider = await session.DiscoveredProviderAsync();
- IReadOnlyList? disclaimers = client.GetLegalDisclaimers(provider);
+ IReadOnlyList? disclaimers = provider.GetLegalDisclaimers();
Assert.NotNull(disclaimers);
AssetDisclaimer disclaimer = Assert.Single(disclaimers!);
Assert.Equal(AssetDisclaimerPurpose.General, disclaimer.Purpose);
@@ -147,7 +147,7 @@ public async Task PublishedLegalAndTokenMetadataRoundTrip()
IReadOnlyList? byId = await client.GetProviderLegalDisclaimersById(anchor.ProviderId, cancellationToken);
Assert.Equal(disclaimers, byId);
- AssetTokenMetadata? metadata = client.GetAssetMetadataForLocation(provider, EvmLocation, EvmAsset);
+ AssetTokenMetadata? metadata = provider.GetAssetMetadataForLocation(EvmLocation, EvmAsset);
Assert.NotNull(metadata);
Assert.Equal(18u, metadata!.DecimalPlaces);
Assert.Equal("Test Token", metadata.DisplayName);
@@ -156,10 +156,10 @@ public async Task PublishedLegalAndTokenMetadataRoundTrip()
// An asset the anchor publishes no display metadata for reports absent,
// not an error.
- Assert.Null(client.GetAssetMetadataForLocation(provider, EvmLocation, "evm:0xdeadbeef"));
+ Assert.Null(provider.GetAssetMetadataForLocation(EvmLocation, "evm:0xdeadbeef"));
// The identifying details under legal.anchorDetails decode typed.
- AssetAnchorDetails? details = client.GetProviderAnchorDetails(provider);
+ AssetAnchorDetails? details = provider.GetAnchorDetails();
Assert.NotNull(details);
Assert.Equal("Test Anchor", details!.Name);
Assert.NotNull(details.Description);
@@ -177,13 +177,12 @@ public async Task ForwardingAndListingRunAgainstTheLiveAnchor()
(AssetMovementClient client, AssetAnchor anchor, CancellationToken cancellationToken) = session;
AssetProvider provider = await session.DiscoveredProviderAsync();
- AssetTemplateSession templateSession = await client.InitiatePersistentForwardingTemplate(
- provider, new AssetInitiateTemplateRequest(anchor.Asset, EvmLocation), cancellationToken);
+ AssetTemplateSession templateSession = await provider.InitiatePersistentForwardingTemplate(
+ new AssetInitiateTemplateRequest(anchor.Asset, EvmLocation), cancellationToken);
Assert.Equal("test-session-id", templateSession.Id);
Assert.Equal("link-sandbox-test-token", templateSession.Data.GetProperty("plaidLinkToken").GetString());
- AssetForwardingTemplate template = await client.CreatePersistentForwardingTemplate(
- provider,
+ AssetForwardingTemplate template = await provider.CreatePersistentForwardingTemplate(
new AssetCreateTemplateRequest(Asset: anchor.Asset, Location: EvmLocation, Address: anchor.SendToAddress),
cancellationToken);
Assert.Equal("template-id", template.Id);
@@ -194,19 +193,17 @@ public async Task ForwardingAndListingRunAgainstTheLiveAnchor()
plaidPublicToken = "public-sandbox-token",
plaidAccountId = "account-1",
};
- AssetForwardingTemplate completed = await client.CreatePersistentForwardingTemplate(
- provider, new AssetCreateTemplateRequest(Id: templateSession.Id, Data: completionData), cancellationToken);
+ AssetForwardingTemplate completed = await provider.CreatePersistentForwardingTemplate(
+ new AssetCreateTemplateRequest(Id: templateSession.Id, Data: completionData), cancellationToken);
Assert.Equal("template-id", completed.Id);
- AssetTemplatePage templates = await client.ListForwardingAddressTemplates(
- provider,
+ AssetTemplatePage templates = await provider.ListForwardingAddressTemplates(
new AssetListTemplatesRequest(new[] { anchor.Asset }, new[] { EvmLocation }),
cancellationToken);
Assert.Single(templates.Templates);
Assert.Equal("1", templates.Total);
- AssetForwardingAddress created = await client.CreatePersistentForwardingAddress(
- provider,
+ AssetForwardingAddress created = await provider.CreatePersistentForwardingAddress(
new AssetCreateAddressRequest(
EvmLocation,
anchor.Asset,
@@ -225,14 +222,12 @@ public async Task ForwardingAndListingRunAgainstTheLiveAnchor()
Assert.Equal(50d, lineItem.BasisPoints);
Assert.Equal(AssetContentType.Markdown, lineItem.Details!.Type);
- AssetForwardingAddress fromTemplate = await client.CreatePersistentForwardingAddress(
- provider,
+ AssetForwardingAddress fromTemplate = await provider.CreatePersistentForwardingAddress(
new AssetCreateAddressRequest(EvmLocation, anchor.Asset, PersistentAddressTemplateId: template.Id),
cancellationToken);
Assert.Equal(anchor.SendToAddress, fromTemplate.Address.GetString());
- AssetAddressPage addresses = await client.ListForwardingAddresses(
- provider,
+ AssetAddressPage addresses = await provider.ListForwardingAddresses(
new AssetListAddressesRequest(
new[] { new AssetAddressFilter(SourceLocation: EvmLocation, Asset: anchor.Asset) },
new AssetPagination(10, 0)),
@@ -251,16 +246,14 @@ public async Task ForwardingAndListingRunAgainstTheLiveAnchor()
// A conversion-pair filter crosses the wire in the reference `{ from,
// to }` form and passes the live anchor's request validation.
- AssetAddressPage paired = await client.ListForwardingAddresses(
- provider,
+ AssetAddressPage paired = await provider.ListForwardingAddresses(
new AssetListAddressesRequest(
new[] { new AssetAddressFilter(SourceLocation: EvmLocation, Asset: AssetOrPair.Pair(anchor.Asset, "USD")) },
new AssetPagination(10, 0)),
cancellationToken);
Assert.Single(paired.Addresses);
- AssetTransactionPage transactions = await client.ListTransactions(
- provider,
+ AssetTransactionPage transactions = await provider.ListTransactions(
new AssetListTransactionsRequest(
new[] { new AssetPersistentAddressFilter(EvmLocation, anchor.SendToAddress) },
new AssetEndpointFilter(EvmLocation, anchor.SendToAddress, anchor.Asset),
@@ -269,20 +262,21 @@ public async Task ForwardingAndListingRunAgainstTheLiveAnchor()
JsonElement transaction = Assert.Single(transactions.Transactions);
Assert.Equal("123", transaction.GetProperty("id").GetString());
- await client.DeactivatePersistentForwardingTemplate(provider, template.Id, cancellationToken);
- await client.DeactivatePersistentForwardingAddress(provider, template.Id, cancellationToken);
+ await provider.DeactivatePersistentForwardingTemplate(template.Id, cancellationToken);
+ await provider.DeactivatePersistentForwardingAddress(template.Id, cancellationToken);
await Assert.ThrowsAsync(
- () => client.DeactivatePersistentForwardingTemplate(provider, "does-not-exist", cancellationToken));
+ () => provider.DeactivatePersistentForwardingTemplate("does-not-exist", cancellationToken));
// An operation the provider does not advertise must surface a typed
- // error before any request leaves the client.
- Dictionary narrowedOperations = provider.Operations
+ // error before any request leaves the client. A stored snapshot
+ // re-binds through the client's Provider factory.
+ Dictionary narrowedOperations = provider.Info.Operations
.Where(operation => operation.Key != "listTransactions")
.ToDictionary(operation => operation.Key, operation => operation.Value);
- AssetProvider narrowed = provider with { Operations = narrowedOperations };
+ AssetProvider narrowed = client.Provider(provider.Info with { Operations = narrowedOperations });
await Assert.ThrowsAsync(
- () => client.ListTransactions(narrowed, new AssetListTransactionsRequest(), cancellationToken));
+ () => narrowed.ListTransactions(new AssetListTransactionsRequest(), cancellationToken));
session.Shutdown();
}
@@ -294,18 +288,17 @@ public async Task ShareKycSettlesAndPollsAgainstTheLiveAnchor()
(AssetMovementClient client, _, CancellationToken cancellationToken) = session;
AssetProvider provider = await session.DiscoveredProviderAsync();
- AssetShareKycOutcome settled = await client.ShareKycAttributes(
- provider, new AssetShareKycRequest("exported-attributes"), cancellationToken);
+ AssetShareKycOutcome settled = await provider.ShareKycAttributes(
+ new AssetShareKycRequest("exported-attributes"), cancellationToken);
Assert.False(settled.IsPending);
- AssetShareKycOutcome withoutPolling = await client.ShareKycAttributesAndWait(
- provider, new AssetShareKycRequest("exported-attributes"), cancellationToken: cancellationToken);
+ AssetShareKycOutcome withoutPolling = await provider.ShareKycAttributesAndWait(
+ new AssetShareKycRequest("exported-attributes"), cancellationToken: cancellationToken);
Assert.False(withoutPolling.IsPending);
// The promise route reports pending (202 + Retry-After) for the first
// two polls and settles on the third.
- AssetShareKycOutcome polled = await client.ShareKycAttributesAndWait(
- provider,
+ AssetShareKycOutcome polled = await provider.ShareKycAttributesAndWait(
new AssetShareKycRequest("promise-flow"),
pollInterval: TimeSpan.FromMilliseconds(1),
timeout: TimeSpan.FromMinutes(1),
@@ -313,8 +306,7 @@ public async Task ShareKycSettlesAndPollsAgainstTheLiveAnchor()
Assert.False(polled.IsPending);
await Assert.ThrowsAsync(
- () => client.ShareKycAttributesAndWait(
- provider,
+ () => provider.ShareKycAttributesAndWait(
new AssetShareKycRequest("promise-stall"),
pollInterval: TimeSpan.FromSeconds(1),
timeout: TimeSpan.FromMilliseconds(500),
@@ -332,7 +324,7 @@ public async Task ARefusedShareSurfacesTheTypedKycBlocker()
// The anchor refuses the magic attributes with a 403 blocker envelope
KeetaBlockerException refusal = await Assert.ThrowsAsync(
- () => client.ShareKycAttributes(provider, new AssetShareKycRequest("blocked"), cancellationToken));
+ () => provider.ShareKycAttributes(new AssetShareKycRequest("blocked"), cancellationToken));
Assert.Equal("KEETA_ANCHOR_ASSET_MOVEMENT_KYC_SHARE_NEEDED", refusal.Code);
var share = Assert.IsType(refusal.Blocker);
diff --git a/tests/KeetaNet.Anchor.E2eTests/KycFlowTests.cs b/tests/KeetaNet.Anchor.E2eTests/KycFlowTests.cs
index 6d9b98f..f206629 100644
--- a/tests/KeetaNet.Anchor.E2eTests/KycFlowTests.cs
+++ b/tests/KeetaNet.Anchor.E2eTests/KycFlowTests.cs
@@ -38,10 +38,10 @@ public async Task VerificationPathRunsAgainstTheLiveAnchor(string algorithm)
Assert.False(supported.Worldwide);
Assert.Equal(Countries, supported.Countries);
- using CryptoCertificate ca = client.GetCA(provider);
+ using CryptoCertificate ca = provider.GetCA();
Assert.NotEmpty(ca.SubjectPublicKey);
- VerificationOutcome created = await client.StartVerification(provider, Countries, cancellationToken: cancellationToken);
+ VerificationOutcome created = await provider.StartVerification(Countries, cancellationToken: cancellationToken);
Assert.NotNull(created.Ready);
Verification verification = created.Ready!;
Assert.NotEmpty(verification.Id);
@@ -50,20 +50,20 @@ public async Task VerificationPathRunsAgainstTheLiveAnchor(string algorithm)
// A redirect URL rides the signed create body. The server must accept
// the extra field and still assign a verification.
- VerificationOutcome redirected = await client.StartVerification(provider, Countries, "https://example.test/done", cancellationToken);
+ VerificationOutcome redirected = await provider.StartVerification(Countries, "https://example.test/done", cancellationToken);
Assert.NotNull(redirected.Ready);
Assert.NotEmpty(redirected.Ready!.Id);
- StatusOutcome status = await client.GetVerificationStatus(provider, verification.Id, cancellationToken);
+ StatusOutcome status = await provider.GetVerificationStatus(verification.Id, cancellationToken);
Assert.NotNull(status.Ready);
Assert.Equal("pending", status.Ready!.Status);
Assert.True(status.Ready.RequiresManualVerification);
- CertificatesOutcome pending = await client.GetCertificates(provider, "pending", cancellationToken);
+ CertificatesOutcome pending = await provider.GetCertificates("pending", cancellationToken);
Assert.Null(pending.Ready);
Assert.NotNull(pending.RetryAfterMs);
- CertificatesOutcome ready = await client.GetCertificates(provider, "ready", cancellationToken);
+ CertificatesOutcome ready = await provider.GetCertificates("ready", cancellationToken);
Assert.NotNull(ready.Ready);
Assert.NotEmpty(ready.Ready!.Results);
@@ -71,7 +71,7 @@ public async Task VerificationPathRunsAgainstTheLiveAnchor(string algorithm)
// `[leaf, ca]` chain over the same signed-URL certificate path.
IssuedLeaf issued = IssuedLeaf.Issue(harness);
- CertificatesOutcome chain = await client.GetCertificates(provider, issued.VerificationId, cancellationToken);
+ CertificatesOutcome chain = await provider.GetCertificates(issued.VerificationId, cancellationToken);
Assert.NotNull(chain.Ready);
Assert.Equal(2, chain.Ready!.Results.Count);
diff --git a/tests/KeetaNet.Anchor.Tests/AssetModelTests.cs b/tests/KeetaNet.Anchor.Tests/AssetModelTests.cs
index 364a3dd..4439f19 100644
--- a/tests/KeetaNet.Anchor.Tests/AssetModelTests.cs
+++ b/tests/KeetaNet.Anchor.Tests/AssetModelTests.cs
@@ -78,12 +78,8 @@ public void AnUnknownBlockerTypeRefusesToDecode()
[Fact]
public void LegalDisclaimersDecodeAndSkipMalformedEntries()
{
- using var runtime = WasmRuntime.Load();
- using Account account = runtime.Accounts.FromSeed(TestSeeds.Subject, 0, TestSeeds.DefaultAlgorithm);
- using AssetMovementClient client = runtime.CreateAssetMovementClient(TestSeeds.NonRoutableAnchor, account.PublicKeyString, account);
-
// One well-formed markdown disclaimer and one with an unknown purpose
- AssetProvider provider = Provider(legal: """
+ AssetProviderInfo provider = Provider(legal: """
{
"disclaimers": [
{ "purpose": "general", "content": { "type": "markdown", "content": "# Terms" } },
@@ -92,7 +88,7 @@ public void LegalDisclaimersDecodeAndSkipMalformedEntries()
}
""");
- IReadOnlyList? disclaimers = client.GetLegalDisclaimers(provider);
+ IReadOnlyList? disclaimers = provider.GetLegalDisclaimers();
Assert.NotNull(disclaimers);
AssetDisclaimer disclaimer = Assert.Single(disclaimers!);
Assert.Equal(AssetDisclaimerPurpose.General, disclaimer.Purpose);
@@ -100,19 +96,15 @@ public void LegalDisclaimersDecodeAndSkipMalformedEntries()
Assert.Equal("# Terms", disclaimer.Content.Content);
// A provider without legal metadata reports none, not an empty list.
- Assert.Null(client.GetLegalDisclaimers(Provider(legal: null)));
+ Assert.Null(Provider(legal: null).GetLegalDisclaimers());
}
[Fact]
public void TokenMetadataDecodesNumberAndStringDecimalPlaces()
{
- using var runtime = WasmRuntime.Load();
- using Account account = runtime.Accounts.FromSeed(TestSeeds.Subject, 0, TestSeeds.DefaultAlgorithm);
- using AssetMovementClient client = runtime.CreateAssetMovementClient(TestSeeds.NonRoutableAnchor, account.PublicKeyString, account);
-
// The reference TokenMetadataJSON publishes decimalPlaces as a number
// or a numeric string; both must decode, and garbage must read absent.
- AssetProvider provider = Provider(locationMetadata: """
+ AssetProviderInfo provider = Provider(locationMetadata: """
{
"chain:evm:100": {
"assets": {
@@ -124,21 +116,21 @@ public void TokenMetadataDecodesNumberAndStringDecimalPlaces()
}
""");
- AssetTokenMetadata? full = client.GetAssetMetadataForLocation(provider, "chain:evm:100", "text-places");
+ AssetTokenMetadata? full = provider.GetAssetMetadataForLocation("chain:evm:100", "text-places");
Assert.NotNull(full);
Assert.Equal(18u, full!.DecimalPlaces);
Assert.Equal("https://logo.test/t.png", full.LogoUri);
Assert.Equal("Token", full.DisplayName);
Assert.Equal("$TOK", full.Ticker);
- AssetTokenMetadata? bare = client.GetAssetMetadataForLocation(provider, "chain:evm:100", "numeric-places");
+ AssetTokenMetadata? bare = provider.GetAssetMetadataForLocation("chain:evm:100", "numeric-places");
Assert.NotNull(bare);
Assert.Equal(6u, bare!.DecimalPlaces);
Assert.Null(bare.LogoUri);
- Assert.Null(client.GetAssetMetadataForLocation(provider, "chain:evm:100", "garbage-places"));
- Assert.Null(client.GetAssetMetadataForLocation(provider, "chain:evm:100", "absent-asset"));
- Assert.Null(client.GetAssetMetadataForLocation(provider, "chain:solana:1", "text-places"));
+ Assert.Null(provider.GetAssetMetadataForLocation("chain:evm:100", "garbage-places"));
+ Assert.Null(provider.GetAssetMetadataForLocation("chain:evm:100", "absent-asset"));
+ Assert.Null(provider.GetAssetMetadataForLocation("chain:solana:1", "text-places"));
}
[Fact]
@@ -216,11 +208,7 @@ public void LocatedAssetsRoundTripTheirCanonicalTransportForms(string id, string
[Fact]
public void AnchorDetailsDecodeAndDropAMalformedDescription()
{
- using var runtime = WasmRuntime.Load();
- using Account account = runtime.Accounts.FromSeed(TestSeeds.Subject, 0, TestSeeds.DefaultAlgorithm);
- using AssetMovementClient client = runtime.CreateAssetMovementClient(TestSeeds.NonRoutableAnchor, account.PublicKeyString, account);
-
- AssetProvider provider = Provider(legal: """
+ AssetProviderInfo provider = Provider(legal: """
{
"anchorDetails": {
"name": "Anchor Under Test",
@@ -230,7 +218,7 @@ public void AnchorDetailsDecodeAndDropAMalformedDescription()
}
""");
- AssetAnchorDetails? details = client.GetProviderAnchorDetails(provider);
+ AssetAnchorDetails? details = provider.GetAnchorDetails();
Assert.NotNull(details);
Assert.Equal("Anchor Under Test", details!.Name);
Assert.Equal(AssetContentType.Plaintext, details.Description!.Type);
@@ -238,22 +226,22 @@ public void AnchorDetailsDecodeAndDropAMalformedDescription()
Assert.Equal("https://logo.test/a.svg", details.Logo);
// A malformed description drops while the identifying fields survive.
- AssetProvider malformed = Provider(legal: """
+ AssetProviderInfo malformed = Provider(legal: """
{ "anchorDetails": { "name": "Partial", "description": { "type": "unknown-kind", "content": 5 } } }
""");
- AssetAnchorDetails? partial = client.GetProviderAnchorDetails(malformed);
+ AssetAnchorDetails? partial = malformed.GetAnchorDetails();
Assert.NotNull(partial);
Assert.Equal("Partial", partial!.Name);
Assert.Null(partial.Description);
Assert.Null(partial.Logo);
// Legal metadata without anchor details reports none.
- Assert.Null(client.GetProviderAnchorDetails(Provider(legal: """{ "disclaimers": [] }""")));
- Assert.Null(client.GetProviderAnchorDetails(Provider(legal: null)));
+ Assert.Null(Provider(legal: """{ "disclaimers": [] }""").GetAnchorDetails());
+ Assert.Null(Provider(legal: null).GetAnchorDetails());
}
- /// A minimal provider carrying only the polymorphic metadata under test.
- private static AssetProvider Provider(string? legal = null, string? locationMetadata = null)
+ /// A minimal provider snapshot carrying only the polymorphic metadata under test.
+ private static AssetProviderInfo Provider(string? legal = null, string? locationMetadata = null)
{
JsonElement? legalElement = null;
if (legal is not null)
@@ -267,7 +255,7 @@ private static AssetProvider Provider(string? legal = null, string? locationMeta
locationElement = JsonSerializer.Deserialize(locationMetadata);
}
- return new AssetProvider(
+ return new AssetProviderInfo(
"provider-under-test",
new Dictionary(),
LocationMetadata: locationElement,
diff --git a/tests/KeetaNet.Anchor.Tests/KycModelTests.cs b/tests/KeetaNet.Anchor.Tests/KycModelTests.cs
index 998f509..068a546 100644
--- a/tests/KeetaNet.Anchor.Tests/KycModelTests.cs
+++ b/tests/KeetaNet.Anchor.Tests/KycModelTests.cs
@@ -37,13 +37,13 @@ public void AWorldwideProviderFoldsToWorldwide()
[Fact]
public void NoProvidersFoldToAnEmptyUnion()
{
- SupportedCountries folded = SupportedCountries.FromProviders(Array.Empty());
+ SupportedCountries folded = SupportedCountries.FromProviders(Array.Empty());
Assert.False(folded.Worldwide);
Assert.Empty(folded.Countries);
}
- /// A provider advertising , or worldwide when null.
- private static KycProvider Provider(string id, string[]? countryCodes) =>
+ /// A provider snapshot advertising , or worldwide when null.
+ private static KycProviderInfo Provider(string id, string[]? countryCodes) =>
new(id, "ca-pem", new KycOperations(null, null, null, null, null), countryCodes);
}