diff --git a/OpenPayments.Sdk.HttpSignatureUtils/HttpRequestSigner.cs b/OpenPayments.Sdk.HttpSignatureUtils/HttpRequestSigner.cs index 320f900..a97aec0 100644 --- a/OpenPayments.Sdk.HttpSignatureUtils/HttpRequestSigner.cs +++ b/OpenPayments.Sdk.HttpSignatureUtils/HttpRequestSigner.cs @@ -8,7 +8,6 @@ /// /// Signature headers returned by the HttpRequestSigner. /// - public class SignatureHeaders { /// @@ -31,13 +30,13 @@ private static string BuildSignatureInput(List components, string keyId, var fields = string.Join(" ", components.Select(h => $"\"{h}\"")); return $"({fields});created={created};keyid=\"{keyId}\";alg=\"ed25519\""; } - + private static string ComputeContentDigest(string body) { var hash = SHA512.HashData(Encoding.UTF8.GetBytes(body)); return Convert.ToBase64String(hash); } - + private static async Task TryGetHeaderValueAsync(HttpRequestMessage request, string name) { name = name.ToLowerInvariant(); diff --git a/OpenPayments.Sdk.HttpSignatureUtils/HttpSignatureValidator.cs b/OpenPayments.Sdk.HttpSignatureUtils/HttpSignatureValidator.cs index 3aab70f..003839b 100644 --- a/OpenPayments.Sdk.HttpSignatureUtils/HttpSignatureValidator.cs +++ b/OpenPayments.Sdk.HttpSignatureUtils/HttpSignatureValidator.cs @@ -1,6 +1,5 @@ using System.Text; using NSec.Cryptography; - using OpenPayments.Sdk.HttpSignatureUtils; public class HttpSignatureValidator : IHttpSignatureValidator @@ -38,11 +37,12 @@ public async Task ValidateSignatureAsync(HttpRequestMessage request, Jwk c return false; var challenge = await _builder.BuildBaseAsync(components, request, sigInput); - if (challenge is null) + if (challenge is null) return false; var signatureBytes = Convert.FromBase64String(sig.Replace("sig1=", "").Replace(":", "")); - var publicKey = PublicKey.Import(SignatureAlgorithm.Ed25519, Base64UrlDecode(clientKey.X), KeyBlobFormat.RawPublicKey); + var publicKey = PublicKey.Import(SignatureAlgorithm.Ed25519, Base64UrlDecode(clientKey.X), + KeyBlobFormat.RawPublicKey); return SignatureAlgorithm.Ed25519.Verify(publicKey, Encoding.UTF8.GetBytes(challenge), signatureBytes); } diff --git a/OpenPayments.Sdk.HttpSignatureUtils/KeyUtils.cs b/OpenPayments.Sdk.HttpSignatureUtils/KeyUtils.cs index 1fa2397..f604044 100644 --- a/OpenPayments.Sdk.HttpSignatureUtils/KeyUtils.cs +++ b/OpenPayments.Sdk.HttpSignatureUtils/KeyUtils.cs @@ -59,19 +59,19 @@ public static Key LoadPem(string pem) throw new ArgumentException("Invalid PEM"); // Parse PKCS#8 PrivateKeyInfo - var privInfo = PrivateKeyInfo.GetInstance(Asn1Object.FromByteArray(pemObj.Content)); + var pkInfo = PrivateKeyInfo.GetInstance(Asn1Object.FromByteArray(pemObj.Content)); // Ensure Ed25519 OID (1.3.101.112) - var oid = privInfo.PrivateKeyAlgorithm.Algorithm.Id; + var oid = pkInfo.PrivateKeyAlgorithm.Algorithm.Id; if (oid != "1.3.101.112") throw new ArgumentException($"Unexpected algorithm OID: {oid}. Expected Ed25519 (1.3.101.112)."); // Extract the inner OCTET STRING (seed). For Ed25519 in PKCS#8, this is 32 bytes. - var privateKeyOctets = Asn1OctetString.GetInstance(privInfo.ParsePrivateKey()); + var privateKeyOctets = Asn1OctetString.GetInstance(pkInfo.ParsePrivateKey()); var privateKeyBytes = privateKeyOctets.GetOctets(); // Some toolchains wrap the seed in another OCTET STRING layer: - // If length is not 32, try one more unwrap. + // If lenght is not 32, try one more unwrap. if (privateKeyBytes.Length != 32) { try @@ -85,7 +85,7 @@ public static Key LoadPem(string pem) if (privateKeyBytes.Length != 32) throw new ArgumentException($"Ed25519 seed must be 32 bytes, got {privateKeyBytes.Length}."); - // Import into NSec as raw private key (seed) + // Import into NSec as a raw private key (seed) var algorithm = SignatureAlgorithm.Ed25519; var seed = privateKeyBytes.AsSpan(0, 32); return Key.Import(algorithm, seed, KeyBlobFormat.RawPrivateKey); @@ -214,4 +214,4 @@ public static Key LoadOrGenerateKey(string? keyFilePath = null, GenerateKeyArgs? return GenerateKey(generateKeyArgs); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/OpenPayments.Sdk/Clients/AuthenticatedClient.cs b/OpenPayments.Sdk/Clients/AuthenticatedClient.cs index 4d74fb8..1ff5064 100644 --- a/OpenPayments.Sdk/Clients/AuthenticatedClient.cs +++ b/OpenPayments.Sdk/Clients/AuthenticatedClient.cs @@ -17,6 +17,7 @@ internal sealed class AuthenticatedClient(HttpClient http, Key privateKey, strin private readonly IAuthClientBase _authClient = new AuthClientBase(http, privateKey, keyId, clientUrl); private readonly IResourceClientBase _resClient = new ResourceClientBase(http, privateKey, keyId, clientUrl); + /// public Task RequestGrantAsync(RequestArgs requestArgs, GrantCreateBody body, CancellationToken cancellationToken = default) @@ -24,6 +25,7 @@ public Task RequestGrantAsync(RequestArgs requestArgs, return _authClient.RequestGrantAsync(requestArgs, body, cancellationToken); } + /// public Task ContinueGrantAsync(AuthRequestArgs requestArgs, GrantContinueBody? body, CancellationToken cancellationToken = default) @@ -32,37 +34,71 @@ public Task ContinueGrantAsync(AuthRequestArgs requestArgs, return _authClient.ContinueGrantAsync(requestArgs, body, cancellationToken); } + /// public Task CancelGrantAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default) { return _authClient.CancelGrantAsync(requestArgs, cancellationToken); } + /// public Task RotateTokenAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default) { return _authClient.RotateTokenAsync(requestArgs, cancellationToken); } - + + /// public Task RevokeTokenAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default) { return _authClient.RevokeTokenAsync(requestArgs, cancellationToken); } - public Task CreateIncomingPaymentAsync(AuthRequestArgs requestArgs, IncomingPaymentBody body, + /// + public Task CreateIncomingPaymentAsync(AuthRequestArgs requestArgs, + IncomingPaymentBody body, CancellationToken cancellationToken = default) { return _resClient.CreateIncomingPaymentAsync(requestArgs, body, cancellationToken); } + /// + public Task GetIncomingPaymentAsync(AuthRequestArgs requestArgs, + CancellationToken cancellationToken = default) + { + return _resClient.GetIncomingPaymentAsync(requestArgs, cancellationToken); + } + + /// + public Task GetPublicIncomingPaymentAsync(RequestArgs requestArgs, CancellationToken cancellationToken = default) + { + return base.GetIncomingPaymentAsync(requestArgs.Url.ToString(), cancellationToken); + } + + /// + public Task ListIncomingPaymentsAsync(AuthRequestArgs requestArgs, + ListIncomingPaymentQuery query, CancellationToken cancellationToken = default) + { + return _resClient.ListIncomingPaymentsAsync(requestArgs, query, cancellationToken); + } + + /// + public Task CompleteIncomingPaymentsAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default) + { + return _resClient.CompleteIncomingPaymentAsync(requestArgs, cancellationToken); + } + + /// public Task CreateQuoteAsync(AuthRequestArgs requestArgs, QuoteBody body, CancellationToken cancellationToken = default) { - return _resClient.CreateQuoteAsync(requestArgs, body, cancellationToken); + return _resClient.CreateQuoteAsync(requestArgs, body, cancellationToken); } - public Task CreateOutgoingPaymentAsync(AuthRequestArgs requestArgs, OutgoingPaymentBody body, + /// + public Task CreateOutgoingPaymentAsync(AuthRequestArgs requestArgs, + OutgoingPaymentBody body, CancellationToken cancellationToken = default) { return _resClient.CreateOutgoingPaymentAsync(requestArgs, body, cancellationToken); @@ -78,4 +114,3 @@ public class AuthRequestArgs : RequestArgs { public required string AccessToken { get; set; } } - diff --git a/OpenPayments.Sdk/Clients/IAuthenticatedClient.cs b/OpenPayments.Sdk/Clients/IAuthenticatedClient.cs index c71541e..a0ed9f4 100644 --- a/OpenPayments.Sdk/Clients/IAuthenticatedClient.cs +++ b/OpenPayments.Sdk/Clients/IAuthenticatedClient.cs @@ -10,32 +10,112 @@ namespace OpenPayments.Sdk.Clients; public interface IAuthenticatedClient : IUnauthenticatedClient { /// - /// Resolve a wallet-address URL (or payment pointer) and return its public metadata. + /// Request Grant /// - /// Auth Server URL Address (e.g. https://auth.wallet.example) and access token. + /// Grant URL (e.g. https://ilp.com/grant) and access token. /// Request body /// Optional cancellation token. public Task RequestGrantAsync(RequestArgs requestArgs, GrantCreateBody body, CancellationToken cancellationToken = default); + /// + /// Continue Grant + /// + /// Continue Grant URL (e.g. https://ilp.com/continue/1234) and access token. + /// + /// + /// public Task ContinueGrantAsync(AuthRequestArgs requestArgs, GrantContinueBody? body = null, CancellationToken cancellationToken = default); + /// + /// Cancel Grant + /// + /// Cancel Grant URL (e.g. https://ilp.com/grant/1234) and access token. + /// + /// public Task CancelGrantAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default); - + + /// + /// + /// + /// Auth Token URL Address (e.g. https://ilp.com/token/1234) and access token. + /// + /// public Task RotateTokenAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default); + /// + /// + /// + /// Auth Token URL Address (e.g. https://ilp.com/token/1234) and access token. + /// + /// public Task RevokeTokenAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default); + /// + /// + /// + /// Resource Server URL Address (e.g. https://res.ilp.com/incoming/) and access token. + /// + /// + /// public Task CreateIncomingPaymentAsync(AuthRequestArgs requestArgs, IncomingPaymentBody body, CancellationToken cancellationToken = default); + + /// + /// Get an Incoming Payment + /// + /// + /// + /// IncomingPaymentResponse + public Task GetIncomingPaymentAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default); + + /// + /// Get a Public Incoming Payment + /// + /// + /// + /// + public Task GetPublicIncomingPaymentAsync(RequestArgs requestArgs, CancellationToken cancellationToken = default); + + /// + /// List Incoming Payments + /// + /// + /// + /// + /// ListIncomingPaymentsResponse + public Task ListIncomingPaymentsAsync(AuthRequestArgs requestArgs, ListIncomingPaymentQuery query, CancellationToken cancellationToken = default); + + /// + /// Complete Incoming Payment + /// + /// + /// + /// ListIncomingPaymentsResponse + public Task CompleteIncomingPaymentsAsync(AuthRequestArgs requestArgs, CancellationToken cancellationToken = default); + + /// + /// + /// + /// Resource Server URL Address (e.g. https://res.ilp.com/quote) and access token. + /// + /// + /// public Task CreateQuoteAsync(AuthRequestArgs requestArgs, QuoteBody body, CancellationToken cancellationToken = default); + /// + /// + /// + /// Resource Server URL Address (e.g. https://res.ilp.com/outgoing) and access token. + /// + /// + /// public Task CreateOutgoingPaymentAsync(AuthRequestArgs requestArgs, OutgoingPaymentBody body, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/OpenPayments.Sdk/Clients/ResourceClientBase.cs b/OpenPayments.Sdk/Clients/ResourceClientBase.cs index 56e93bf..9e2fe7b 100644 --- a/OpenPayments.Sdk/Clients/ResourceClientBase.cs +++ b/OpenPayments.Sdk/Clients/ResourceClientBase.cs @@ -21,7 +21,32 @@ public async Task CreateIncomingPaymentAsync(AuthReques { _client.BaseUrl = requestArgs.Url.ToString(); - return await _client.PostIncomingPaymentAsync(body, requestArgs.AccessToken!, cancellationToken); + return await _client.PostIncomingPaymentAsync(body, requestArgs.AccessToken, cancellationToken); + } + + public async Task GetIncomingPaymentAsync(AuthRequestArgs requestArgs, + CancellationToken cancellationToken = default) + { + _client.BaseUrl = requestArgs.Url.ToString(); + + return await _client.GetIncomingPaymentAsync(requestArgs.AccessToken, cancellationToken); + } + + public async Task CompleteIncomingPaymentAsync(AuthRequestArgs requestArgs, + CancellationToken cancellationToken = default) + { + _client.BaseUrl = requestArgs.Url.ToString(); + + return await _client.CompleteIncomingPaymentAsync(requestArgs.AccessToken, cancellationToken); + } + + public async Task ListIncomingPaymentsAsync(AuthRequestArgs requestArgs, + ListIncomingPaymentQuery query, CancellationToken cancellationToken = default) + { + _client.BaseUrl = requestArgs.Url.ToString(); + + return await _client.ListIncomingPaymentsAsync(requestArgs.AccessToken, query.WalletAddress, query.Cursor, + query.First, query.Last, cancellationToken); } public async Task CreateQuoteAsync(AuthRequestArgs requestArgs, QuoteBody body, @@ -29,7 +54,7 @@ public async Task CreateQuoteAsync(AuthRequestArgs requestArgs, Q { _client.BaseUrl = requestArgs.Url.ToString(); - return await _client.PostQuoteAsync(body, requestArgs.AccessToken!, cancellationToken); + return await _client.PostQuoteAsync(body, requestArgs.AccessToken, cancellationToken); } public async Task CreateOutgoingPaymentAsync(AuthRequestArgs requestArgs, @@ -37,7 +62,7 @@ public async Task CreateOutgoingPaymentAsync(AuthReques { _client.BaseUrl = requestArgs.Url.ToString(); - return await _client.PostOutgoingPaymentAsync(body, requestArgs.AccessToken!, cancellationToken); + return await _client.PostOutgoingPaymentAsync(body, requestArgs.AccessToken, cancellationToken); } } @@ -46,9 +71,19 @@ public interface IResourceClientBase public Task CreateIncomingPaymentAsync(AuthRequestArgs requestArgs, Body body, CancellationToken cancellationToken = default); + public Task GetIncomingPaymentAsync(AuthRequestArgs requestArgs, + CancellationToken cancellationToken = default); + + public Task CompleteIncomingPaymentAsync(AuthRequestArgs requestArgs, + CancellationToken cancellationToken = default); + + public Task ListIncomingPaymentsAsync(AuthRequestArgs requestArgs, + ListIncomingPaymentQuery query, CancellationToken cancellationToken = default); + public Task CreateQuoteAsync(AuthRequestArgs requestArgs, QuoteBody body, CancellationToken cancellationToken = default); - public Task CreateOutgoingPaymentAsync(AuthRequestArgs requestArgs, OutgoingPaymentBody body, + public Task CreateOutgoingPaymentAsync(AuthRequestArgs requestArgs, + OutgoingPaymentBody body, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/OpenPayments.Sdk/Clients/UnauthenticatedClient.cs b/OpenPayments.Sdk/Clients/UnauthenticatedClient.cs index 6a13818..501888b 100644 --- a/OpenPayments.Sdk/Clients/UnauthenticatedClient.cs +++ b/OpenPayments.Sdk/Clients/UnauthenticatedClient.cs @@ -1,3 +1,4 @@ +using System.Net.Http.Headers; using Newtonsoft.Json; using OpenPayments.Sdk.Generated.Wallet; using OpenPayments.Sdk.Generated.Resource; @@ -34,10 +35,8 @@ public async Task GetIncomingPaymentAsync(string incoming if (string.IsNullOrWhiteSpace(incomingPaymentUrl)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(incomingPaymentUrl)); - using var request = new HttpRequestMessage(HttpMethod.Get, incomingPaymentUrl) - { - Headers = { Accept = { new("application/json") } } - }; + using var request = new HttpRequestMessage(HttpMethod.Get, incomingPaymentUrl); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); diff --git a/OpenPayments.Sdk/Generated/Resource/ResourceServerClient.Methods.IncomingPayment.cs b/OpenPayments.Sdk/Generated/Resource/ResourceServerClient.Methods.IncomingPayment.cs index f5a61a8..1a54f19 100644 --- a/OpenPayments.Sdk/Generated/Resource/ResourceServerClient.Methods.IncomingPayment.cs +++ b/OpenPayments.Sdk/Generated/Resource/ResourceServerClient.Methods.IncomingPayment.cs @@ -37,7 +37,6 @@ public async Task PostIncomingPaymentAsync(Body body, ArgumentException.ThrowIfNullOrWhiteSpace(accessToken); var client = _httpClient; - using var request = new HttpRequestMessage(); var json = JsonConvert.SerializeObject(body, JsonSerializerSettings); var content = new StringContent(json); @@ -49,7 +48,6 @@ public async Task PostIncomingPaymentAsync(Body body, var urlBuilder = new StringBuilder(); if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder.Append(_baseUrl); - // Operation Path: "incoming-payments" urlBuilder.Append("incoming-payments"); PrepareRequest(client, request, urlBuilder); @@ -97,7 +95,8 @@ await ReadObjectResponseAsync(response, headers, cancellationToke objectResponse.Text, headers, null); } - throw new ApiException(Helpers.StatusAsText(status), status, objectResponse.Text, + throw new ApiException(objectResponse.Object.Error.Description, status, + objectResponse.Text, headers, objectResponse.Object, null); } @@ -116,4 +115,305 @@ await ReadObjectResponseAsync(response, headers, cancellationToke response.Dispose(); } } + + /// + /// Get an Incoming Payment + /// + /// + /// A client can fetch the latest state of an incoming payment to determine the amount received into the wallet address. + /// + /// Access Token. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Incoming Payment Found + /// A server side error occurred. + public virtual async Task GetIncomingPaymentAsync(string accessToken, + CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrWhiteSpace(accessToken); + + var client = _httpClient; + using var request = new HttpRequestMessage(); + request.Method = new HttpMethod("GET"); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + request.Headers.Authorization = new AuthenticationHeaderValue("GNAP", $"{accessToken}"); + + var urlBuilder = new StringBuilder(_baseUrl); + + PrepareRequest(client, request, urlBuilder); + + var url = urlBuilder.ToString(); + request.RequestUri = new Uri(url, UriKind.RelativeOrAbsolute); + + PrepareRequest(client, request, url); + + var response = await client + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + + try + { + var headers = Helpers.ExtractHeaders(response); + + ProcessResponse(client, response); + + var status = (int)response.StatusCode; + switch (status) + { + case 200: + { + var objectResponse = + await ReadObjectResponseAsync(response, headers, cancellationToken) + .ConfigureAwait(false); + if (objectResponse.Object == null) + { + throw new ApiException("Response was null which was not expected.", status, + objectResponse.Text, headers, null); + } + + return objectResponse.Object; + } + case 401: + case 403: + case 404: + { + var objectResponse = + await ReadObjectResponseAsync(response, headers, cancellationToken) + .ConfigureAwait(false); + if (objectResponse.Object == null) + { + throw new ApiException("Response was null which was not expected.", status, + objectResponse.Text, headers, null); + } + + throw new ApiException(objectResponse.Object.Error.Description, status, + objectResponse.Text, + headers, objectResponse.Object, null); + } + default: + { + var responseData = + await ReadAsStringAsync(response.Content, cancellationToken).ConfigureAwait(false); + throw new ApiException( + "The HTTP status code of the response was not expected (" + status + ").", status, + responseData, headers, null); + } + } + } + finally + { + response.Dispose(); + } + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// List Incoming Payments + /// + /// + /// List all incoming payments on the wallet address + /// + /// Access Token + /// URL of a wallet address hosted by a Rafiki instance. + /// The cursor key to list from. + /// The number of items to return after the cursor. + /// The number of items to return before the cursor. + /// OK + /// A server side error occurred. + public virtual async Task ListIncomingPaymentsAsync( + string accessToken, string walletAddress, string? cursor, int? first, int? last, + CancellationToken cancellationToken + ) + { + ArgumentException.ThrowIfNullOrWhiteSpace(accessToken); + ArgumentException.ThrowIfNullOrWhiteSpace(walletAddress); + + var client = _httpClient; + using var request = new HttpRequestMessage(); + request.Method = new HttpMethod("GET"); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + request.Headers.Authorization = new AuthenticationHeaderValue("GNAP", $"{accessToken}"); + + var urlBuilder = new StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder.Append(_baseUrl); + urlBuilder.Append("incoming-payments"); + urlBuilder.Append('?'); + urlBuilder.Append(Uri.EscapeDataString("wallet-address")).Append('=').Append( + Uri.EscapeDataString(ConvertToString(walletAddress, System.Globalization.CultureInfo.InvariantCulture))); + + if (cursor != null) + { + urlBuilder.Append('&').Append(Uri.EscapeDataString("cursor")).Append('=') + .Append( + Uri.EscapeDataString(ConvertToString(cursor, System.Globalization.CultureInfo.InvariantCulture))); + } + + if (first != null) + { + urlBuilder.Append('&').Append(Uri.EscapeDataString("first")).Append('=') + .Append(Uri.EscapeDataString(ConvertToString(first, + System.Globalization.CultureInfo.InvariantCulture))); + } + + if (last != null) + { + urlBuilder.Append('&').Append(Uri.EscapeDataString("last")).Append('=') + .Append(Uri.EscapeDataString(ConvertToString(last, System.Globalization.CultureInfo.InvariantCulture))); + } + + PrepareRequest(client, request, urlBuilder); + + var url = urlBuilder.ToString(); + request.RequestUri = new Uri(url, UriKind.RelativeOrAbsolute); + + PrepareRequest(client, request, url); + + var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + + try + { + var headers = Helpers.ExtractHeaders(response); + + ProcessResponse(client, response); + + var status = (int)response.StatusCode; + switch (status) + { + case 200: + { + var objectResponse = + await ReadObjectResponseAsync(response, headers, + cancellationToken) + .ConfigureAwait(false); + if (objectResponse.Object == null) + { + throw new ApiException("Response was null which was not expected.", status, objectResponse.Text, + headers, null); + } + + return objectResponse.Object; + } + case 401: + case 403: + { + var objectResponse = + await ReadObjectResponseAsync(response, headers, cancellationToken) + .ConfigureAwait(false); + if (objectResponse.Object == null) + { + throw new ApiException("Response was null which was not expected.", status, objectResponse.Text, + headers, null); + } + + throw new ApiException(objectResponse.Object.Error.Description, status, + objectResponse.Text, headers, + objectResponse.Object, null); + } + default: + { + var responseData = + await ReadAsStringAsync(response.Content, cancellationToken).ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status + ").", + status, responseData, headers, null); + } + } + } + finally + { + response.Dispose(); + } + } + + /// + /// Complete an Incoming Payment + /// + /// + /// A client with the appropriate permissions MAY mark a non-expired **incoming payment** as `completed` indicating that the client is not going to make any further payments toward this **incoming payment**, even though the full `incomingAmount` may not have been received. + ///
+ ///
This indicates to the receiving Account Servicing Entity that it can begin any post processing of the payment such as generating account statements or notifying the account holder of the completed payment. + ///
+ /// + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// OK + /// A server side error occurred. + public virtual async Task CompleteIncomingPaymentAsync( + string accessToken, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrWhiteSpace(accessToken); + + var client = _httpClient; + using var request = new HttpRequestMessage(); + request.Content = new StringContent(string.Empty, Encoding.UTF8, "application/json"); + request.Method = new HttpMethod("POST"); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + request.Headers.Authorization = new AuthenticationHeaderValue("GNAP", $"{accessToken}"); + + var urlBuilder = new StringBuilder(_baseUrl); + urlBuilder.Append("/complete"); + + PrepareRequest(client, request, urlBuilder); + + var url = urlBuilder.ToString(); + request.RequestUri = new Uri(url, UriKind.RelativeOrAbsolute); + + PrepareRequest(client, request, url); + + var response = await client + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + + try + { + var headers = Helpers.ExtractHeaders(response); + + ProcessResponse(client, response); + + var status = (int)response.StatusCode; + switch (status) + { + case 200: + { + var objectResponse = + await ReadObjectResponseAsync(response, headers, cancellationToken) + .ConfigureAwait(false); + if (objectResponse.Object == null) + { + throw new ApiException("Response was null which was not expected.", status, + objectResponse.Text, headers, null); + } + + return objectResponse.Object; + } + case 401: + case 403: + case 404: + { + var objectResponse = + await ReadObjectResponseAsync(response, headers, cancellationToken) + .ConfigureAwait(false); + if (objectResponse.Object == null) + { + throw new ApiException("Response was null which was not expected.", status, + objectResponse.Text, headers, null); + } + + throw new ApiException(objectResponse.Object.Error.Description, status, + objectResponse.Text, headers, + objectResponse.Object, null); + } + default: + { + var responseData = + await ReadAsStringAsync(response.Content, cancellationToken).ConfigureAwait(false); + throw new ApiException( + "The HTTP status code of the response was not expected (" + status + ").", status, + responseData, headers, null); + } + } + } + finally + { + response.Dispose(); + } + } } \ No newline at end of file diff --git a/OpenPayments.Sdk/Generated/Resource/Types.cs b/OpenPayments.Sdk/Generated/Resource/Types.cs index cbfaf5e..3b45708 100644 --- a/OpenPayments.Sdk/Generated/Resource/Types.cs +++ b/OpenPayments.Sdk/Generated/Resource/Types.cs @@ -24,16 +24,31 @@ public partial class IncomingPaymentResponse : IncomingPaymentWithMethods public new object? Metadata { get; set; } } + public partial class ListIncomingPaymentQuery + { + public required string WalletAddress { get; set; } + public string? Cursor { get; set; } + public int? First { get; set; } + public int? Last { get; set; } + } + + public partial class ListIncomingPaymentsResponse : Response + { + + } + public partial class QuoteBody : Body3 { /// /// The fixed amount that would be paid into the receiving wallet address given a successful outgoing payment. /// + [JsonProperty("receiveAmount")] public Amount? ReceiveAmount { get; set; } /// /// The fixed amount that would be sent from the sending wallet address given a successful outgoing payment. /// + [JsonProperty("debitAmount")] public Amount? DebitAmount { get; set; } } @@ -69,7 +84,7 @@ public partial class OutgoingPaymentBody : Body2 public partial class OutgoingPaymentResponse : OutgoingPaymentWithSpentAmounts { } - + public partial class Amount { public Amount() {} diff --git a/OpenPayments.Snippets/Program.cs b/OpenPayments.Snippets/Program.cs index 6d746ea..21b2eb6 100644 --- a/OpenPayments.Snippets/Program.cs +++ b/OpenPayments.Snippets/Program.cs @@ -93,21 +93,24 @@ { resourceUrlOption }; - var createIncomingPaymentCommand = new Command("CreateIncomingPayment") { receiverWalletAddressOption, amountOption }; +var listIncomingPaymentsCommand = new Command("ListIncomingPayments") +{ + receiverWalletAddressOption +}; var createQuoteCommand = new Command("CreateQuote") { - senderWalletAddressOption, + senderWalletAddressOption, incomingPaymentIdOption }; var createOutgoingPaymentCommand = new Command("CreateOutgoingPayment") { - senderWalletAddressOption, - quoteUrlOption, + senderWalletAddressOption, + quoteUrlOption, amountOption }; @@ -139,7 +142,6 @@ { var receiver = result.GetValue(receiverWalletAddressOption)!; var amount = result.GetValue(amountOption)!; - var service = provider.GetRequiredService(); await service.CreateIncomingPaymentAsync(receiver, amount); }); @@ -148,7 +150,6 @@ { var incomingPaymentUrl = result.GetValue(incomingPaymentIdOption)!; var sender = result.GetValue(senderWalletAddressOption)!; - var service = provider.GetRequiredService(); await service.CreateQuoteAsync(sender, incomingPaymentUrl); }); @@ -158,7 +159,6 @@ var sender = result.GetValue(senderWalletAddressOption)!; var quoteUrl = result.GetValue(quoteUrlOption)!; var debitAmount = result.GetValue(amountOption)!; - var service = provider.GetRequiredService(); await service.CreateOutgoingPaymentAsync(sender, quoteUrl, debitAmount); }); @@ -181,9 +181,17 @@ } }); +listIncomingPaymentsCommand.SetAction(async result => +{ + var receiver = result.GetValue(receiverWalletAddressOption)!; + var service = provider.GetRequiredService(); + + await service.ListIncomingPaymentsAsync(receiver); +}); + rootCommand.SetAction(_ => { - + }); // Unauthenticated @@ -192,9 +200,9 @@ // Authenticated rootCommand.Add(createIncomingPaymentCommand); +rootCommand.Add(listIncomingPaymentsCommand); rootCommand.Add(createQuoteCommand); rootCommand.Add(createOutgoingPaymentCommand); - var config = new CommandLineConfiguration(rootCommand); return await config.InvokeAsync(args); \ No newline at end of file diff --git a/OpenPayments.Snippets/Services/Authenticated/IncomingPaymentService.cs b/OpenPayments.Snippets/Services/Authenticated/IncomingPaymentService.cs index 6f4a12c..d8871bb 100644 --- a/OpenPayments.Snippets/Services/Authenticated/IncomingPaymentService.cs +++ b/OpenPayments.Snippets/Services/Authenticated/IncomingPaymentService.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json; using OpenPayments.Sdk.Clients; using OpenPayments.Sdk.Generated.Auth; using OpenPayments.Sdk.Generated.Resource; @@ -10,7 +11,6 @@ public class IncomingPaymentService(IAuthenticatedClient client) public async Task CreateIncomingPaymentAsync(string receiver, string amount) { var waDetails = await client.GetWalletAddressAsync(receiver); - var grant = await client.RequestGrantAsync( new RequestArgs() { @@ -49,12 +49,95 @@ public async Task CreateIncomingPaymentAsync(string rec } } ); - + + + Console.WriteLine("===Incoming Payment==="); + Console.WriteLine("grant: {0}", JsonConvert.SerializeObject(grant, Formatting.None)); + Console.WriteLine("AccessToken: {0}", grant.AccessToken!.Value); + Console.WriteLine("Id: {0}", iPaymentResponse.Id); + Console.WriteLine("Amount: {0}", iPaymentResponse.IncomingAmount.Value); + Console.WriteLine("ExpiresAt: {0}", iPaymentResponse.ExpiresAt); + + return iPaymentResponse; + } + + public async Task GetIncomingPaymentAsync(string incomingPaymentUrl, string accessToken, + string tokenUrl) + { + var rotatedToken = await client.RotateTokenAsync(new AuthRequestArgs() + { + Url = new Uri(tokenUrl), + AccessToken = accessToken + }); + + + var iPaymentResponse = await client.GetIncomingPaymentAsync(new AuthRequestArgs() + { + Url = new Uri(incomingPaymentUrl), + AccessToken = rotatedToken.AccessToken.Value + }); + Console.WriteLine("===Incoming Payment==="); Console.WriteLine("Id: {0}", iPaymentResponse.Id); Console.WriteLine("Amount: {0}", iPaymentResponse.IncomingAmount.Value); Console.WriteLine("ExpiresAt: {0}", iPaymentResponse.ExpiresAt); + Console.WriteLine("Access Token Manage: {0}", rotatedToken.AccessToken.Manage); + Console.WriteLine("Access Token Value: {0}", rotatedToken.AccessToken.Value); return iPaymentResponse; } + + public async Task CompleteIncomingPaymentAsync(string incomingPaymentUrl, string accessToken, string tokenUrl) + { + } + + public async Task ListIncomingPaymentsAsync(string walletAddress) + { + var waDetails = await client.GetWalletAddressAsync("https://ilp.interledger-test.dev/cozmin"); + + var grant = await client.RequestGrantAsync( + new RequestArgs() + { + Url = waDetails.AuthServer + }, + new GrantCreateBody() + { + AccessToken = new AccessToken() + { + Access = + [ + new AccessItem() + { + Type = AccessType.IncomingPayment, + Actions = [Actions.List] + } + ] + } + } + ); + + var list = await client.ListIncomingPaymentsAsync(new AuthRequestArgs() + { + Url = waDetails.ResourceServer, + AccessToken = grant.AccessToken!.Value + }, + new ListIncomingPaymentQuery() + { + WalletAddress = waDetails.Id.ToString(), + } + ); + + Console.WriteLine(JsonConvert.SerializeObject(list, Formatting.Indented)); + + foreach (var iPayment in list.Result) + { + Console.WriteLine("===Incoming Payment==="); + Console.WriteLine("Id: {0}", iPayment.Id); + Console.WriteLine("Completed: {0}", iPayment.Completed ? "Yes" : "No"); + Console.WriteLine("Amount: {0}/{1} {2}", iPayment.ReceivedAmount.Value, iPayment.IncomingAmount.Value, iPayment.ReceivedAmount.AssetCode); + Console.WriteLine("ExpiresAt: {0}", iPayment.ExpiresAt); + } + + return list; + } } \ No newline at end of file