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
273 changes: 81 additions & 192 deletions src/KeetaNet.Anchor/Services/AssetMovement/AssetMovementClient.cs

Large diffs are not rendered by default.

128 changes: 124 additions & 4 deletions src/KeetaNet.Anchor/Services/AssetMovement/AssetMovementModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,138 @@ public enum AssetEndpointAuth
public sealed record AssetEndpoint(string Url, AssetEndpointAuth Auth);

/// <summary>
/// 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 <c>AssetMovementProviderInfo</c>). The
/// polymorphic <see cref="SupportedAssets"/>, <see cref="LocationMetadata"/>, and
/// <see cref="Legal"/> 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
/// <see cref="AssetProvider"/> handle bound through
/// <see cref="AssetMovementClient.Provider"/>.
/// </summary>
public sealed record AssetProvider(
public sealed record AssetProviderInfo(
string Id,
IReadOnlyDictionary<string, AssetEndpoint> Operations,
IReadOnlyList<JsonElement>? SupportedAssets = null,
JsonElement? LocationMetadata = null,
JsonElement? Legal = null,
string? Account = null);
string? Account = null)
{
/// <summary>
/// Whether this provider advertises the <paramref name="operation"/>
/// endpoint (e.g. <c>initiateTransfer</c>, <c>createPersistentForwarding</c>).
/// </summary>
public bool IsOperationSupported(string operation) => Operations.ContainsKey(operation);

/// <summary>
/// The advertised legal disclaimers, or null when the metadata carries
/// none. Malformed entries are skipped.
/// </summary>
public IReadOnlyList<AssetDisclaimer>? 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<AssetDisclaimer>();
using JsonElement.ArrayEnumerator enumerated = entries.EnumerateArray();
foreach (JsonElement entry in enumerated)
{
if (TryDeserialize(entry, out AssetDisclaimer? disclaimer))
{
disclaimers.Add(disclaimer!);
}
}

return disclaimers;
}

/// <summary>
/// The identifying details published under <c>legal.anchorDetails</c>, or
/// null when the metadata carries none. A malformed description is dropped
/// while the name and logo are kept.
/// </summary>
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);
}

/// <summary>
/// The display metadata for <paramref name="asset"/> (an external chain
/// asset id) at <paramref name="location"/> (a canonical location string),
/// or null when the provider advertises none or the entry does not parse.
/// </summary>
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;
}

/// <summary>Deserialize one metadata entry, treating malformed JSON as absent.</summary>
private static bool TryDeserialize<T>(JsonElement element, out T? value)
where T : class
{
try
{
value = element.Deserialize<T>(KeetaJson.Options);
}
catch (JsonException)
{
value = null;
}

return value is not null;
}

/// <summary>The member's string value, or null when absent or not a string.</summary>
private static string? ReadOptionalString(JsonElement element, string name)
{
if (!element.TryGetProperty(name, out JsonElement found) || found.ValueKind != JsonValueKind.String)
{
return null;
}

return found.GetString();
}
}

/// <summary>Pagination bounds shared by the list operations.</summary>
public sealed record AssetPagination(uint? Limit = null, uint? Offset = null);
Expand Down
130 changes: 130 additions & 0 deletions src/KeetaNet.Anchor/Services/AssetMovement/AssetProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
namespace KeetaNet.Anchor;

/// <summary>
/// One asset-movement provider bound to its discovering client (the reference
/// provider handle): a metadata snapshot in <see cref="Info"/> plus every
/// per-provider operation, signed and retried by the client it came from.
/// Obtained from the <see cref="AssetMovementClient"/> discovery methods or
/// re-bound from a stored snapshot with
/// <see cref="AssetMovementClient.Provider"/>.
/// </summary>
public sealed class AssetProvider
{
private readonly AssetMovementClient _client;

internal AssetProvider(AssetMovementClient client, AssetProviderInfo info)
{
_client = client;
Info = info;
}

/// <summary>The provider's advertised metadata snapshot.</summary>
public AssetProviderInfo Info { get; }

/// <summary>The provider's id.</summary>
public string Id => Info.Id;

/// <inheritdoc cref="AssetProviderInfo.IsOperationSupported"/>
public bool IsOperationSupported(string operation) => Info.IsOperationSupported(operation);

/// <inheritdoc cref="AssetProviderInfo.GetLegalDisclaimers"/>
public IReadOnlyList<AssetDisclaimer>? GetLegalDisclaimers() => Info.GetLegalDisclaimers();

/// <inheritdoc cref="AssetProviderInfo.GetAnchorDetails"/>
public AssetAnchorDetails? GetAnchorDetails() => Info.GetAnchorDetails();

/// <inheritdoc cref="AssetProviderInfo.GetAssetMetadataForLocation"/>
public AssetTokenMetadata? GetAssetMetadataForLocation(string location, string asset) =>
Info.GetAssetMetadataForLocation(location, asset);

/// <summary>Simulate a transfer, returning a fluent handle over its instruction choices.</summary>
public Task<AssetSimulatedTransfer> SimulateTransfer(
AssetTransferRequest request,
CancellationToken cancellationToken = default) =>
_client.SimulateTransfer(this, request, cancellationToken);

/// <summary>Initiate a transfer, returning a fluent handle. The request's recipient is required.</summary>
public Task<AssetTransfer> InitiateTransfer(
AssetTransferRequest request,
CancellationToken cancellationToken = default) =>
_client.InitiateTransfer(this, request, cancellationToken);

/// <summary>Execute a pull instruction for a transfer.</summary>
public Task<AssetTransferStatus> ExecuteTransfer(
AssetExecuteRequest request,
CancellationToken cancellationToken = default) =>
_client.ExecuteTransfer(Info, request, cancellationToken);

/// <summary>Read the status of transfer <paramref name="id"/>.</summary>
public Task<AssetTransferStatus> GetTransferStatus(string id, CancellationToken cancellationToken = default) =>
_client.GetTransferStatus(Info, id, cancellationToken);

/// <summary>Read whether the signer's account is ready to use this provider.</summary>
public Task<AssetAccountStatus> GetAccountStatus(CancellationToken cancellationToken = default) =>
_client.GetAccountStatus(Info, cancellationToken);

/// <summary>Open a persistent-forwarding template session.</summary>
public Task<AssetTemplateSession> InitiatePersistentForwardingTemplate(
AssetInitiateTemplateRequest request,
CancellationToken cancellationToken = default) =>
_client.InitiatePersistentForwardingTemplate(Info, request, cancellationToken);

/// <summary>Create a persistent-forwarding template.</summary>
public Task<AssetForwardingTemplate> CreatePersistentForwardingTemplate(
AssetCreateTemplateRequest request,
CancellationToken cancellationToken = default) =>
_client.CreatePersistentForwardingTemplate(Info, request, cancellationToken);

/// <summary>List persistent-forwarding templates.</summary>
public Task<AssetTemplatePage> ListForwardingAddressTemplates(
AssetListTemplatesRequest request,
CancellationToken cancellationToken = default) =>
_client.ListForwardingAddressTemplates(Info, request, cancellationToken);

/// <summary>Create a persistent-forwarding address, returning its (obfuscated) details.</summary>
public Task<AssetForwardingAddress> CreatePersistentForwardingAddress(
AssetCreateAddressRequest request,
CancellationToken cancellationToken = default) =>
_client.CreatePersistentForwardingAddress(Info, request, cancellationToken);

/// <summary>List persistent-forwarding addresses.</summary>
public Task<AssetAddressPage> ListForwardingAddresses(
AssetListAddressesRequest request,
CancellationToken cancellationToken = default) =>
_client.ListForwardingAddresses(Info, request, cancellationToken);

/// <summary>Deactivate a persistent-forwarding template by id.</summary>
public Task DeactivatePersistentForwardingTemplate(string id, CancellationToken cancellationToken = default) =>
_client.DeactivatePersistentForwardingTemplate(Info, id, cancellationToken);

/// <summary>Deactivate a persistent-forwarding address by id.</summary>
public Task DeactivatePersistentForwardingAddress(string id, CancellationToken cancellationToken = default) =>
_client.DeactivatePersistentForwardingAddress(Info, id, cancellationToken);

/// <summary>List asset-movement transactions.</summary>
public Task<AssetTransactionPage> ListTransactions(
AssetListTransactionsRequest request,
CancellationToken cancellationToken = default) =>
_client.ListTransactions(Info, request, cancellationToken);

/// <summary>
/// Share KYC attributes with the provider, returning the provider's outcome unchanged.
/// A pending outcome carries the promise URL the caller must poll. Use
/// <see cref="ShareKycAttributesAndWait"/> to poll it automatically.
/// </summary>
public Task<AssetShareKycOutcome> ShareKycAttributes(
AssetShareKycRequest request,
CancellationToken cancellationToken = default) =>
_client.ShareKycAttributes(Info, request, cancellationToken);

/// <summary>
/// Share KYC attributes and, when the outcome is pending with a promise URL,
/// poll that URL inside the core until it resolves.
/// </summary>
public Task<AssetShareKycOutcome> ShareKycAttributesAndWait(
AssetShareKycRequest request,
TimeSpan? pollInterval = null,
TimeSpan? timeout = null,
CancellationToken cancellationToken = default) =>
_client.ShareKycAttributesAndWait(Info, request, pollInterval, timeout, cancellationToken);
}
12 changes: 3 additions & 9 deletions src/KeetaNet.Anchor/Services/AssetMovement/AssetTransfer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,14 @@ namespace KeetaNet.Anchor;
/// </summary>
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<JsonElement> instructionChoices)
{
_client = client;
_provider = provider;
_request = request;
InstructionChoices = instructionChoices;
Expand All @@ -45,7 +42,7 @@ public Task<AssetTransfer> CreateTransfer(
};
AssetTransferRequest request = _request with { To = to };

return _client.InitiateTransfer(_provider, request, cancellationToken);
return _provider.InitiateTransfer(request, cancellationToken);
}
}

Expand All @@ -56,16 +53,13 @@ public Task<AssetTransfer> CreateTransfer(
/// </summary>
public sealed class AssetTransfer
{
private readonly AssetMovementClient _client;
private readonly AssetProvider _provider;

internal AssetTransfer(
AssetMovementClient client,
AssetProvider provider,
string id,
IReadOnlyList<JsonElement> instructionChoices)
{
_client = client;
_provider = provider;
Id = id;
InstructionChoices = instructionChoices;
Expand All @@ -79,14 +73,14 @@ internal AssetTransfer(

/// <summary>Read this transfer's current status.</summary>
public Task<AssetTransferStatus> GetTransferStatus(CancellationToken cancellationToken = default) =>
_client.GetTransferStatus(_provider, Id, cancellationToken);
_provider.GetTransferStatus(Id, cancellationToken);

/// <summary>Execute a fiat pull <paramref name="instruction"/> for this transfer.</summary>
public Task<AssetTransferStatus> ExecuteTransfer(
AssetPullInstruction instruction,
CancellationToken cancellationToken = default)
{
var request = new AssetExecuteRequest(Id, instruction);
return _client.ExecuteTransfer(_provider, request, cancellationToken);
return _provider.ExecuteTransfer(request, cancellationToken);
}
}
Loading
Loading