From 8dd6ecc0b824319bd15e9b1fb5860de71cb16765 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Mon, 8 Jun 2020 23:40:58 +0900 Subject: [PATCH] Fix lightning implementation, docs and tests --- .../BTCPayServer.Client.csproj | 4 +- .../BTCPayServerClient.APIKeys.cs | 4 +- .../BTCPayServerClient.Lightning.Internal.cs | 6 +- .../BTCPayServerClient.Lightning.Store.cs | 7 +- .../BTCPayServerClient.PaymentRequests.cs | 2 +- .../BTCPayServerClient.Stores.cs | 2 +- BTCPayServer.Client/BTCPayServerClient.cs | 15 +- BTCPayServer.Client/GreenFieldAPIException.cs | 18 ++ .../GreenFieldValidationException.cs | 30 +++ .../JsonConverters/NodeUriJsonConverter.cs | 29 +++ .../Models/ConnectToNodeRequest.cs | 22 +- .../Models/CreateLightningInvoiceRequest.cs | 14 +- .../Models/GreenfieldAPIError.cs | 25 ++ .../Models/GreenfieldValidationError.cs | 26 ++ .../Models/LightningInvoiceData.cs | 10 +- .../Models/LightningNodeInformationData.cs | 7 +- .../Models/OpenLightningChannelRequest.cs | 10 +- .../Models/PayLightningInvoiceRequest.cs | 3 +- BTCPayServer.Data/BTCPayServer.Data.csproj | 2 +- BTCPayServer.Tests/GreenfieldAPITests.cs | 34 ++- BTCPayServer.Tests/TestAccount.cs | 30 ++- BTCPayServer.Tests/UnitTest1.cs | 86 +++++++ BTCPayServer/BTCPayServer.csproj | 2 +- .../GreenField/ApiKeysController.cs | 4 +- .../Controllers/GreenField/GreenFieldUtils.cs | 31 +-- .../LightningNodeApiController.Internal.cs | 3 +- .../LightningNodeApiController.Store.cs | 5 +- .../GreenField/LightningNodeApiController.cs | 235 ++++++++---------- .../GreenField/PaymentRequestsController.cs | 2 +- .../GreenField/StoresController.cs | 4 +- .../Controllers/GreenField/UsersController.cs | 12 +- BTCPayServer/DerivationSchemeSettings.cs | 2 +- .../wwwroot/swagger/v1/swagger.template.json | 65 ++--- .../v1/swagger.template.lightning.common.json | 195 +++++---------- .../swagger.template.lightning.internal.json | 99 ++------ .../v1/swagger.template.lightning.store.json | 98 ++------ 36 files changed, 615 insertions(+), 528 deletions(-) create mode 100644 BTCPayServer.Client/GreenFieldAPIException.cs create mode 100644 BTCPayServer.Client/GreenFieldValidationException.cs create mode 100644 BTCPayServer.Client/JsonConverters/NodeUriJsonConverter.cs create mode 100644 BTCPayServer.Client/Models/GreenfieldAPIError.cs create mode 100644 BTCPayServer.Client/Models/GreenfieldValidationError.cs diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj index bd2777c9c..1cc3e7c77 100644 --- a/BTCPayServer.Client/BTCPayServer.Client.csproj +++ b/BTCPayServer.Client/BTCPayServer.Client.csproj @@ -5,8 +5,8 @@ - - + + diff --git a/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs b/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs index 348c470cb..219f026f2 100644 --- a/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs +++ b/BTCPayServer.Client/BTCPayServerClient.APIKeys.cs @@ -25,7 +25,7 @@ namespace BTCPayServer.Client public virtual async Task RevokeCurrentAPIKeyInfo(CancellationToken token = default) { var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current", null, HttpMethod.Delete), token); - HandleResponse(response); + await HandleResponse(response); } public virtual async Task RevokeAPIKey(string apikey, CancellationToken token = default) @@ -33,7 +33,7 @@ namespace BTCPayServer.Client if (apikey == null) throw new ArgumentNullException(nameof(apikey)); var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/api-keys/{apikey}", null, HttpMethod.Delete), token); - HandleResponse(response); + await HandleResponse(response); } } } diff --git a/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs b/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs index bba92128e..953829bb6 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs @@ -26,7 +26,7 @@ namespace BTCPayServer.Client var response = await _httpClient.SendAsync( CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/connect", bodyPayload: request, method: HttpMethod.Post), token); - HandleResponse(response); + await HandleResponse(response); } public async Task> GetLightningNodeChannels(string cryptoCode, @@ -61,9 +61,9 @@ namespace BTCPayServer.Client if (request == null) throw new ArgumentNullException(nameof(request)); var response = await _httpClient.SendAsync( - CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/pay", bodyPayload: request, + CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices/pay", bodyPayload: request, method: HttpMethod.Post), token); - HandleResponse(response); + await HandleResponse(response); } public async Task GetLightningInvoice(string cryptoCode, diff --git a/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs b/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs index 1b0686315..0b48ce774 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Client.Models; +using BTCPayServer.Lightning; namespace BTCPayServer.Client { @@ -26,7 +27,7 @@ namespace BTCPayServer.Client var response = await _httpClient.SendAsync( CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/connect", bodyPayload: request, method: HttpMethod.Post), token); - HandleResponse(response); + await HandleResponse(response); } public async Task> GetLightningNodeChannels(string storeId, string cryptoCode, @@ -62,9 +63,9 @@ namespace BTCPayServer.Client if (request == null) throw new ArgumentNullException(nameof(request)); var response = await _httpClient.SendAsync( - CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/pay", bodyPayload: request, + CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay", bodyPayload: request, method: HttpMethod.Post), token); - HandleResponse(response); + await HandleResponse(response); } public async Task GetLightningInvoice(string storeId, string cryptoCode, diff --git a/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs b/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs index a54edcb12..feeea0151 100644 --- a/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs +++ b/BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs @@ -31,7 +31,7 @@ namespace BTCPayServer.Client var response = await _httpClient.SendAsync( CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}", method: HttpMethod.Delete), token); - HandleResponse(response); + await HandleResponse(response); } public virtual async Task CreatePaymentRequest(string storeId, diff --git a/BTCPayServer.Client/BTCPayServerClient.Stores.cs b/BTCPayServer.Client/BTCPayServerClient.Stores.cs index 4e23b35aa..ce9557a90 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Stores.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Stores.cs @@ -26,7 +26,7 @@ namespace BTCPayServer.Client { var response = await _httpClient.SendAsync( CreateHttpRequest($"api/v1/stores/{storeId}", method: HttpMethod.Delete), token); - HandleResponse(response); + await HandleResponse(response); } public virtual async Task CreateStore(CreateStoreRequest request, CancellationToken token = default) diff --git a/BTCPayServer.Client/BTCPayServerClient.cs b/BTCPayServer.Client/BTCPayServerClient.cs index 23f141a10..c8409e1ec 100644 --- a/BTCPayServer.Client/BTCPayServerClient.cs +++ b/BTCPayServer.Client/BTCPayServerClient.cs @@ -43,14 +43,25 @@ namespace BTCPayServer.Client _httpClient = httpClient ?? new HttpClient(); } - protected void HandleResponse(HttpResponseMessage message) + protected async Task HandleResponse(HttpResponseMessage message) { + if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity) + { + var err = JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync()); ; + throw new GreenFieldValidationException(err); + } + else if (message.StatusCode == System.Net.HttpStatusCode.BadRequest) + { + var err = JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync()); + throw new GreenFieldAPIException(err); + } + message.EnsureSuccessStatusCode(); } protected async Task HandleResponse(HttpResponseMessage message) { - HandleResponse(message); + await HandleResponse(message); return JsonConvert.DeserializeObject(await message.Content.ReadAsStringAsync()); } diff --git a/BTCPayServer.Client/GreenFieldAPIException.cs b/BTCPayServer.Client/GreenFieldAPIException.cs new file mode 100644 index 000000000..964770583 --- /dev/null +++ b/BTCPayServer.Client/GreenFieldAPIException.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml.Linq; + +namespace BTCPayServer.Client +{ + public class GreenFieldAPIException : Exception + { + public GreenFieldAPIException(Models.GreenfieldAPIError error):base(error.Message) + { + if (error == null) + throw new ArgumentNullException(nameof(error)); + APIError = error; + } + public Models.GreenfieldAPIError APIError { get; } + } +} diff --git a/BTCPayServer.Client/GreenFieldValidationException.cs b/BTCPayServer.Client/GreenFieldValidationException.cs new file mode 100644 index 000000000..664a76c1c --- /dev/null +++ b/BTCPayServer.Client/GreenFieldValidationException.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text; +using BTCPayServer.Client.Models; + +namespace BTCPayServer.Client +{ + public class GreenFieldValidationException : Exception + { + public GreenFieldValidationException(Models.GreenfieldValidationError[] errors) : base(BuildMessage(errors)) + { + ValidationErrors = errors; + } + + private static string BuildMessage(GreenfieldValidationError[] errors) + { + if (errors == null) + throw new ArgumentNullException(nameof(errors)); + StringBuilder builder = new StringBuilder(); + foreach (var error in errors) + { + builder.AppendLine($"{error.Path}: {error.Message}"); + } + return builder.ToString(); + } + + public Models.GreenfieldValidationError[] ValidationErrors { get; } + } +} diff --git a/BTCPayServer.Client/JsonConverters/NodeUriJsonConverter.cs b/BTCPayServer.Client/JsonConverters/NodeUriJsonConverter.cs new file mode 100644 index 000000000..229eb6a97 --- /dev/null +++ b/BTCPayServer.Client/JsonConverters/NodeUriJsonConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using BTCPayServer.Client.Models; +using BTCPayServer.Lightning; +using NBitcoin.JsonConverters; +using Newtonsoft.Json; + +namespace BTCPayServer.Client.JsonConverters +{ + public class NodeUriJsonConverter : JsonConverter + { + public override NodeInfo ReadJson(JsonReader reader, Type objectType, [AllowNull] NodeInfo existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.String) + throw new JsonObjectException(reader.Path, "Unexpected token type for NodeUri"); + if (NodeInfo.TryParse((string)reader.Value, out var info)) + return info; + throw new JsonObjectException(reader.Path, "Invalid NodeUri"); + } + + public override void WriteJson(JsonWriter writer, [AllowNull] NodeInfo value, JsonSerializer serializer) + { + if (value is NodeInfo) + writer.WriteValue(value.ToString()); + } + } +} diff --git a/BTCPayServer.Client/Models/ConnectToNodeRequest.cs b/BTCPayServer.Client/Models/ConnectToNodeRequest.cs index 5c5559586..d1813eeab 100644 --- a/BTCPayServer.Client/Models/ConnectToNodeRequest.cs +++ b/BTCPayServer.Client/Models/ConnectToNodeRequest.cs @@ -1,10 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; +using BTCPayServer.Client.JsonConverters; +using BTCPayServer.Lightning; +using Newtonsoft.Json; + namespace BTCPayServer.Client.Models { public class ConnectToNodeRequest { - public string NodeInfo { get; set; } - public string NodeId { get; set; } - public string NodeHost { get; set; } - public int NodePort { get; set; } + public ConnectToNodeRequest() + { + + } + public ConnectToNodeRequest(NodeInfo nodeInfo) + { + NodeURI = nodeInfo; + } + [JsonConverter(typeof(NodeUriJsonConverter))] + [JsonProperty("nodeURI")] + public NodeInfo NodeURI { get; set; } } } diff --git a/BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs b/BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs index 3c7e08489..eb2326a31 100644 --- a/BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs +++ b/BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs @@ -1,4 +1,5 @@ using System; +using System.Security.Cryptography; using BTCPayServer.Lightning; using BTCPayServer.Lightning.JsonConverters; using Newtonsoft.Json; @@ -7,9 +8,20 @@ namespace BTCPayServer.Client.Models { public class CreateLightningInvoiceRequest { - [JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))] + public CreateLightningInvoiceRequest() + { + + } + public CreateLightningInvoiceRequest(LightMoney amount, string description, TimeSpan expiry) + { + Amount = amount; + Description = description; + Expiry = expiry; + } + [JsonConverter(typeof(LightMoneyJsonConverter))] public LightMoney Amount { get; set; } public string Description { get; set; } + [JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter))] public TimeSpan Expiry { get; set; } public bool PrivateRouteHints { get; set; } diff --git a/BTCPayServer.Client/Models/GreenfieldAPIError.cs b/BTCPayServer.Client/Models/GreenfieldAPIError.cs new file mode 100644 index 000000000..58919bb32 --- /dev/null +++ b/BTCPayServer.Client/Models/GreenfieldAPIError.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BTCPayServer.Client.Models +{ + public class GreenfieldAPIError + { + public GreenfieldAPIError() + { + + } + public GreenfieldAPIError(string code, string message) + { + if (code == null) + throw new ArgumentNullException(nameof(code)); + if (message == null) + throw new ArgumentNullException(nameof(message)); + Code = code; + Message = message; + } + public string Code { get; set; } + public string Message { get; set; } + } +} diff --git a/BTCPayServer.Client/Models/GreenfieldValidationError.cs b/BTCPayServer.Client/Models/GreenfieldValidationError.cs new file mode 100644 index 000000000..d02390fae --- /dev/null +++ b/BTCPayServer.Client/Models/GreenfieldValidationError.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BTCPayServer.Client.Models +{ + public class GreenfieldValidationError + { + public GreenfieldValidationError() + { + + } + public GreenfieldValidationError(string path, string message) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + if (message == null) + throw new ArgumentNullException(nameof(message)); + Path = path; + Message = message; + } + + public string Path { get; set; } + public string Message { get; set; } + } +} diff --git a/BTCPayServer.Client/Models/LightningInvoiceData.cs b/BTCPayServer.Client/Models/LightningInvoiceData.cs index 8a22439c4..7f855902c 100644 --- a/BTCPayServer.Client/Models/LightningInvoiceData.cs +++ b/BTCPayServer.Client/Models/LightningInvoiceData.cs @@ -10,19 +10,21 @@ namespace BTCPayServer.Client.Models { public string Id { get; set; } - [JsonProperty(ItemConverterType = typeof(StringEnumConverter))] + [JsonConverter(typeof(StringEnumConverter))] public LightningInvoiceStatus Status { get; set; } + [JsonProperty("BOLT11")] public string BOLT11 { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] public DateTimeOffset? PaidAt { get; set; } - + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] public DateTimeOffset ExpiresAt { get; set; } - [JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))] + [JsonConverter(typeof(LightMoneyJsonConverter))] public LightMoney Amount { get; set; } - [JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))] + [JsonConverter(typeof(LightMoneyJsonConverter))] public LightMoney AmountReceived { get; set; } } } diff --git a/BTCPayServer.Client/Models/LightningNodeInformationData.cs b/BTCPayServer.Client/Models/LightningNodeInformationData.cs index e3e8a7b18..5617bc0b9 100644 --- a/BTCPayServer.Client/Models/LightningNodeInformationData.cs +++ b/BTCPayServer.Client/Models/LightningNodeInformationData.cs @@ -9,7 +9,8 @@ namespace BTCPayServer.Client.Models { public class LightningNodeInformationData { - public IEnumerable NodeInfoList { get; set; } + [JsonProperty("nodeURIs", ItemConverterType = typeof(NodeUriJsonConverter))] + public NodeInfo[] NodeURIs { get; set; } public int BlockHeight { get; set; } } @@ -21,10 +22,10 @@ namespace BTCPayServer.Client.Models public bool IsActive { get; set; } - [JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))] + [JsonConverter(typeof(LightMoneyJsonConverter))] public LightMoney Capacity { get; set; } - [JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))] + [JsonConverter(typeof(LightMoneyJsonConverter))] public LightMoney LocalBalance { get; set; } public string ChannelPoint { get; set; } diff --git a/BTCPayServer.Client/Models/OpenLightningChannelRequest.cs b/BTCPayServer.Client/Models/OpenLightningChannelRequest.cs index 1ee015475..390a72e4e 100644 --- a/BTCPayServer.Client/Models/OpenLightningChannelRequest.cs +++ b/BTCPayServer.Client/Models/OpenLightningChannelRequest.cs @@ -1,3 +1,5 @@ +using BTCPayServer.Client.JsonConverters; +using BTCPayServer.Lightning; using NBitcoin; using NBitcoin.JsonConverters; using Newtonsoft.Json; @@ -7,11 +9,13 @@ namespace BTCPayServer.Client.Models { public class OpenLightningChannelRequest { - public ConnectToNodeRequest Node { get; set; } - [JsonProperty(ItemConverterType = typeof(MoneyJsonConverter))] + [JsonConverter(typeof(NodeUriJsonConverter))] + [JsonProperty("nodeURI")] + public NodeInfo NodeURI { get; set; } + [JsonConverter(typeof(MoneyJsonConverter))] public Money ChannelAmount { get; set; } - [JsonProperty(ItemConverterType = typeof(FeeRateJsonConverter))] + [JsonConverter(typeof(FeeRateJsonConverter))] public FeeRate FeeRate { get; set; } } } diff --git a/BTCPayServer.Client/Models/PayLightningInvoiceRequest.cs b/BTCPayServer.Client/Models/PayLightningInvoiceRequest.cs index 84a5bef25..4cfb0d37c 100644 --- a/BTCPayServer.Client/Models/PayLightningInvoiceRequest.cs +++ b/BTCPayServer.Client/Models/PayLightningInvoiceRequest.cs @@ -2,6 +2,7 @@ namespace BTCPayServer.Client.Models { public class PayLightningInvoiceRequest { - public string Invoice { get; set; } + [Newtonsoft.Json.JsonProperty("BOLT11")] + public string BOLT11 { get; set; } } } diff --git a/BTCPayServer.Data/BTCPayServer.Data.csproj b/BTCPayServer.Data/BTCPayServer.Data.csproj index 1a000adaf..7436b550d 100644 --- a/BTCPayServer.Data/BTCPayServer.Data.csproj +++ b/BTCPayServer.Data/BTCPayServer.Data.csproj @@ -8,7 +8,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index d2bc5d86f..3cf672d56 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Linq; using System.Net.Http; +using System.Reflection.Metadata; using System.Threading.Tasks; using BTCPayServer.Client; using BTCPayServer.Client.Models; @@ -16,6 +17,7 @@ using NBitpayClient; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OpenQA.Selenium; +using Org.BouncyCastle.Utilities.Collections; using Xunit; using Xunit.Abstractions; using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest; @@ -105,13 +107,13 @@ namespace BTCPayServer.Tests tester.PayTester.DisableRegistration = true; await tester.StartAsync(); var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri); - await AssertHttpError(422, + await AssertValidationError(new[] { "Email", "Password" }, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest())); - await AssertHttpError(422, + await AssertValidationError(new[] { "Password" }, async () => await unauthClient.CreateUser( new CreateApplicationUserRequest() {Email = "test@gmail.com"})); // Pass too simple - await AssertHttpError(422, + await AssertValidationError(new[] { "Password" }, async () => await unauthClient.CreateUser( new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "a"})); @@ -123,7 +125,7 @@ namespace BTCPayServer.Tests new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"}); // Duplicate email - await AssertHttpError(422, + await AssertValidationError(new[] { "Email" }, async () => await unauthClient.CreateUser( new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"})); @@ -252,6 +254,18 @@ namespace BTCPayServer.Tests } } + private async Task AssertValidationError(string[] fields, Func act) + { + var remainingFields = fields.ToHashSet(); + var ex = await Assert.ThrowsAsync(act); + foreach (var field in fields) + { + Assert.Contains(field, ex.ValidationErrors.Select(e => e.Path).ToArray()); + remainingFields.Remove(field); + } + Assert.Empty(remainingFields); + } + private async Task AssertHttpError(int code, Func act) { var ex = await Assert.ThrowsAsync(act); @@ -303,17 +317,17 @@ namespace BTCPayServer.Tests }); Assert.NotNull(newUser2); - await Assert.ThrowsAsync(async () => + await AssertValidationError(new[] { "Email" }, async () => await clientServer.CreateUser(new CreateApplicationUserRequest() { Email = $"{Guid.NewGuid()}", Password = Guid.NewGuid().ToString() })); - await Assert.ThrowsAsync(async () => + await AssertValidationError(new[] { "Password" }, async () => await clientServer.CreateUser( new CreateApplicationUserRequest() {Email = $"{Guid.NewGuid()}@g.com",})); - await Assert.ThrowsAsync(async () => + await AssertValidationError(new[] { "Email" }, async () => await clientServer.CreateUser( new CreateApplicationUserRequest() {Password = Guid.NewGuid().ToString()})); } @@ -375,16 +389,16 @@ namespace BTCPayServer.Tests //create payment request //validation errors - await AssertHttpError(422, async () => + await AssertValidationError(new[] { "Amount", "Currency" }, async () => { await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() {Title = "A"}); }); - await AssertHttpError(422, async () => + await AssertValidationError(new[] { "Amount" }, async () => { await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() {Title = "A", Currency = "BTC", Amount = 0}); }); - await AssertHttpError(422, async () => + await AssertValidationError(new[] { "Currency" }, async () => { await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() {Title = "A", Currency = "helloinvalid", Amount = 1}); diff --git a/BTCPayServer.Tests/TestAccount.cs b/BTCPayServer.Tests/TestAccount.cs index 987f027b9..e0b953541 100644 --- a/BTCPayServer.Tests/TestAccount.cs +++ b/BTCPayServer.Tests/TestAccount.cs @@ -252,23 +252,39 @@ namespace BTCPayServer.Tests public bool IsAdmin { get; internal set; } - public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType) + public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true) { - RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult(); + RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult(); } - public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType) + public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true) { var storeController = this.GetController(); string connectionString = null; if (connectionType == LightningConnectionType.Charge) - connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri; + { + if (isMerchant) + connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri; + else + throw new NotSupportedException(); + } else if (connectionType == LightningConnectionType.CLightning) - connectionString = "type=clightning;server=" + - ((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri; + { + if (isMerchant) + connectionString = "type=clightning;server=" + + ((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri; + else + connectionString = "type=clightning;server=" + + ((CLightningClient)parent.CustomerLightningD).Address.AbsoluteUri; + } else if (connectionType == LightningConnectionType.LndREST) - connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true"; + { + if (isMerchant) + connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true"; + else + throw new NotSupportedException(); + } else throw new NotSupportedException(connectionType.ToString()); diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 100cbf56b..afb30a3ba 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -64,6 +64,7 @@ using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache; using Newtonsoft.Json.Schema; using BTCPayServer.Client; using BTCPayServer.Client.Models; +using TwentyTwenty.Storage; namespace BTCPayServer.Tests { @@ -742,6 +743,91 @@ namespace BTCPayServer.Tests } } + [Fact(Timeout = 60 * 2 * 1000)] + [Trait("Integration", "Integration")] + [Trait("Lightning", "Lightning")] + public async Task CanUseLightningAPI() + { + using (var tester = ServerTester.Create()) + { + tester.ActivateLightning(); + await tester.StartAsync(); + await tester.EnsureChannelsSetup(); + var user = tester.NewAccount(); + user.GrantAccess(true); + user.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false); + + var merchant = tester.NewAccount(); + merchant.GrantAccess(true); + merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST); + var merchantClient = await merchant.CreateClient($"btcpay.store.canuselightningnode:{merchant.StoreId}"); + var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(new LightMoney(1_000), "hey", TimeSpan.FromSeconds(60))); + + // The default client is using charge, so we should not be able to query channels + var client = await user.CreateClient("btcpay.server.canuseinternallightningnode"); + var err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels("BTC")); + Assert.Contains("503", err.Message); + // Not permission for the store! + err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels(user.StoreId, "BTC")); + Assert.Contains("403", err.Message); + var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest() + { + Amount = LightMoney.Satoshis(1000), + Description = "lol", + Expiry = TimeSpan.FromSeconds(400), + PrivateRouteHints = false + }); + var chargeInvoice = invoiceData; + Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id)); + + client = await user.CreateClient($"btcpay.store.canuselightningnode:{user.StoreId}"); + // Not permission for the server + err = await Assert.ThrowsAsync(async () => await client.GetLightningNodeChannels("BTC")); + Assert.Contains("403", err.Message); + + var data = await client.GetLightningNodeChannels(user.StoreId, "BTC"); + Assert.Equal(2, data.Count()); + BitcoinAddress.Create(await client.GetLightningDepositAddress(user.StoreId, "BTC"), Network.RegTest); + + invoiceData = await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() + { + Amount = LightMoney.Satoshis(1000), + Description = "lol", + Expiry = TimeSpan.FromSeconds(400), + PrivateRouteHints = false + }); + + Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id)); + + await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() + { + BOLT11 = merchantInvoice.BOLT11 + }); + await Assert.ThrowsAsync(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() + { + BOLT11 = "lol" + })); + + var validationErr = await Assert.ThrowsAsync(async () => await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() + { + Amount = -1, + Expiry = TimeSpan.FromSeconds(-1), + Description = null + })); + Assert.Equal(2, validationErr.ValidationErrors.Length); + + var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id); + Assert.NotNull(invoice.PaidAt); + Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount); + // Amount received might be bigger because of internal implementation shit from lightning + Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived); + + var info = await client.GetLightningNodeInfo(user.StoreId, "BTC"); + Assert.Single(info.NodeURIs); + Assert.NotEqual(0, info.BlockHeight); + } + } + async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user) { var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice() diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index c00b76bf9..c5cef3520 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -30,7 +30,7 @@ - + diff --git a/BTCPayServer/Controllers/GreenField/ApiKeysController.cs b/BTCPayServer/Controllers/GreenField/ApiKeysController.cs index b50efe9ba..99e4844cc 100644 --- a/BTCPayServer/Controllers/GreenField/ApiKeysController.cs +++ b/BTCPayServer/Controllers/GreenField/ApiKeysController.cs @@ -42,7 +42,7 @@ namespace BTCPayServer.Controllers.GreenField public async Task> CreateKey(CreateApiKeyRequest request) { if (request is null) - return BadRequest(); + return NotFound(); var key = new APIKeyData() { Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)), @@ -74,7 +74,7 @@ namespace BTCPayServer.Controllers.GreenField public async Task RevokeKey(string apikey) { if (string.IsNullOrEmpty(apikey)) - return BadRequest(); + return NotFound(); if (await _apiKeyRepository.Remove(apikey, _userManager.GetUserId(User))) return Ok(); else diff --git a/BTCPayServer/Controllers/GreenField/GreenFieldUtils.cs b/BTCPayServer/Controllers/GreenField/GreenFieldUtils.cs index 4b330f5e3..996ed7a30 100644 --- a/BTCPayServer/Controllers/GreenField/GreenFieldUtils.cs +++ b/BTCPayServer/Controllers/GreenField/GreenFieldUtils.cs @@ -1,26 +1,29 @@ using System; +using System.Collections.Generic; +using System.Linq; +using BTCPayServer.Client.Models; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace BTCPayServer.Controllers.GreenField { public static class GreenFieldUtils { - public static IActionResult GetValidationResponse(this ControllerBase controller) + public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState) { - return controller.UnprocessableEntity( new ValidationProblemDetails(controller.ModelState)); - } - public static IActionResult GetExceptionResponse(this ControllerBase controller, Exception e) - { - return GetGeneralErrorResponse(controller, e.Message); - } - - public static IActionResult GetGeneralErrorResponse(this ControllerBase controller, string error) - { - return controller.BadRequest( new ProblemDetails() + List errors = new List(); + foreach (var error in modelState) { - Detail = error - }); + foreach (var errorMessage in error.Value.Errors) + { + errors.Add(new GreenfieldValidationError(error.Key, errorMessage.ErrorMessage)); + } + } + return controller.UnprocessableEntity(errors.ToArray()); + } + public static IActionResult CreateAPIError(this ControllerBase controller, string errorCode, string errorMessage) + { + return controller.BadRequest(new GreenfieldAPIError(errorCode, errorMessage)); } - } } diff --git a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Internal.cs b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Internal.cs index 2933e1b07..14dfc6f1c 100644 --- a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Internal.cs +++ b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Internal.cs @@ -13,6 +13,7 @@ namespace BTCPayServer.Controllers.GreenField { [ApiController] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [LightningUnavailableExceptionFilter] public class InternalLightningNodeApiController : LightningNodeApiController { private readonly BTCPayServerOptions _btcPayServerOptions; @@ -64,7 +65,7 @@ namespace BTCPayServer.Controllers.GreenField [Authorize(Policy = Policies.CanUseInternalLightningNode, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - [HttpGet("~/api/v1/server/lightning/{cryptoCode}/address")] + [HttpPost("~/api/v1/server/lightning/{cryptoCode}/address")] public override Task GetDepositAddress(string cryptoCode) { return base.GetDepositAddress(cryptoCode); diff --git a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Store.cs b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Store.cs index 1000defc7..342ba0d5a 100644 --- a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Store.cs +++ b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Store.cs @@ -17,6 +17,7 @@ namespace BTCPayServer.Controllers.GreenField { [ApiController] [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [LightningUnavailableExceptionFilter] public class StoreLightningNodeApiController : LightningNodeApiController { private readonly BTCPayServerOptions _btcPayServerOptions; @@ -65,7 +66,7 @@ namespace BTCPayServer.Controllers.GreenField [Authorize(Policy = Policies.CanUseLightningNodeInStore, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - [HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/address")] + [HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/address")] public override Task GetDepositAddress(string cryptoCode) { return base.GetDepositAddress(cryptoCode); @@ -81,7 +82,7 @@ namespace BTCPayServer.Controllers.GreenField [Authorize(Policy = Policies.CanUseLightningNodeInStore, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] - [HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/{id}")] + [HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/{id}")] public override Task GetInvoice(string cryptoCode, string id) { return base.GetInvoice(cryptoCode, id); diff --git a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs index 89a62ae3c..fdd89dbfb 100644 --- a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs +++ b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs @@ -5,12 +5,30 @@ using System.Threading.Tasks; using BTCPayServer.Client.Models; using BTCPayServer.HostedServices; using BTCPayServer.Lightning; +using BTCPayServer.Payments.Changelly.Models; using BTCPayServer.Services; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; using NBitcoin; +using Newtonsoft.Json.Linq; namespace BTCPayServer.Controllers.GreenField { + public class LightningUnavailableExceptionFilter : Attribute, IExceptionFilter + { + public void OnException(ExceptionContext context) + { + if (context.Exception is NBitcoin.JsonConverters.JsonObjectException jsonObject) + { + context.Result = new ObjectResult(new GreenfieldValidationError(jsonObject.Path, jsonObject.Message)); + } + else + { + context.Result = new StatusCodeResult(503); + } + context.ExceptionHandled = true; + } + } public abstract class LightningNodeApiController : Controller { private readonly BTCPayNetworkProvider _btcPayNetworkProvider; @@ -32,20 +50,12 @@ namespace BTCPayServer.Controllers.GreenField { return NotFound(); } - - try + var info = await lightningClient.GetInfo(); + return Ok(new LightningNodeInformationData() { - var info = await lightningClient.GetInfo(); - return Ok(new LightningNodeInformationData() - { - BlockHeight = info.BlockHeight, - NodeInfoList = info.NodeInfoList.Select(nodeInfo => nodeInfo.ToString()) - }); - } - catch (Exception e) - { - return this.GetExceptionResponse(e); - } + BlockHeight = info.BlockHeight, + NodeURIs = info.NodeInfoList.Select(nodeInfo => nodeInfo).ToArray() + }); } public virtual async Task ConnectToNode(string cryptoCode, ConnectToNodeRequest request) @@ -56,23 +66,23 @@ namespace BTCPayServer.Controllers.GreenField return NotFound(); } - if (TryGetNodeInfo(request, out var nodeInfo)) + if (request?.NodeURI is null) { - ModelState.AddModelError(nameof(request.NodeId), "A valid node info was not provided to connect to"); + ModelState.AddModelError(nameof(request.NodeURI), "A valid node info was not provided to connect to"); } - if (CheckValidation(out var errorActionResult)) + if (!ModelState.IsValid) { - return errorActionResult; + return this.CreateValidationError(ModelState); } - try + var result = await lightningClient.ConnectTo(request.NodeURI); + switch (result) { - await lightningClient.ConnectTo(nodeInfo); - } - catch (Exception e) - { - return this.GetExceptionResponse(e); + case ConnectionResult.Ok: + return Ok(); + case ConnectionResult.CouldNotConnect: + return this.CreateAPIError("could-not-connect", "Could not connect to the remote node"); } return Ok(); @@ -86,26 +96,19 @@ namespace BTCPayServer.Controllers.GreenField return NotFound(); } - try + var channels = await lightningClient.ListChannels(); + return Ok(channels.Select(channel => new LightningChannelData() { - var channels = await lightningClient.ListChannels(); - return Ok(channels.Select(channel => new LightningChannelData() - { - Capacity = channel.Capacity, - ChannelPoint = channel.ChannelPoint.ToString(), - IsActive = channel.IsActive, - IsPublic = channel.IsPublic, - LocalBalance = channel.LocalBalance, - RemoteNode = channel.RemoteNode.ToString() - })); - } - catch (Exception e) - { - return this.GetExceptionResponse(e); - } + Capacity = channel.Capacity, + ChannelPoint = channel.ChannelPoint.ToString(), + IsActive = channel.IsActive, + IsPublic = channel.IsPublic, + LocalBalance = channel.LocalBalance, + RemoteNode = channel.RemoteNode.ToString() + })); } - + public virtual async Task OpenChannel(string cryptoCode, OpenLightningChannelRequest request) { var lightningClient = await GetLightningClient(cryptoCode, true); @@ -114,9 +117,9 @@ namespace BTCPayServer.Controllers.GreenField return NotFound(); } - if (TryGetNodeInfo(request.Node, out var nodeInfo)) + if (request?.NodeURI is null) { - ModelState.AddModelError(nameof(request.Node), + ModelState.AddModelError(nameof(request.NodeURI), "A valid node info was not provided to open a channel with"); } @@ -138,28 +141,43 @@ namespace BTCPayServer.Controllers.GreenField ModelState.AddModelError(nameof(request.FeeRate), "FeeRate must be more than 0"); } - if (CheckValidation(out var errorActionResult)) + if (ModelState.IsValid) { - return errorActionResult; + return this.CreateValidationError(ModelState); } - try + var response = await lightningClient.OpenChannel(new Lightning.OpenChannelRequest() { - var response = await lightningClient.OpenChannel(new Lightning.OpenChannelRequest() - { - ChannelAmount = request.ChannelAmount, FeeRate = request.FeeRate, NodeInfo = nodeInfo - }); - if (response.Result == OpenChannelResult.Ok) - { + ChannelAmount = request.ChannelAmount, + FeeRate = request.FeeRate, + NodeInfo = request.NodeURI + }); + + string errorCode, errorMessage; + switch (response.Result) + { + case OpenChannelResult.Ok: return Ok(); - } - - return this.GetGeneralErrorResponse(response.Result.ToString()); - } - catch (Exception e) - { - return this.GetExceptionResponse(e); + case OpenChannelResult.AlreadyExists: + errorCode = "channel-already-exists"; + errorMessage = "The channel already exists"; + break; + case OpenChannelResult.CannotAffordFunding: + errorCode = "cannot-afford-funding"; + errorMessage = "Not enough money to open a channel"; + break; + case OpenChannelResult.NeedMoreConf: + errorCode = "need-more-confirmations"; + errorMessage = "Need to wait for more confirmations"; + break; + case OpenChannelResult.PeerNotConnected: + errorCode = "peer-not-connected"; + errorMessage = "Not connected to peer"; + break; + default: + throw new NotSupportedException("Unknown OpenChannelResult"); } + return this.CreateAPIError(errorCode, errorMessage); } public virtual async Task GetDepositAddress(string cryptoCode) @@ -170,7 +188,7 @@ namespace BTCPayServer.Controllers.GreenField return NotFound(); } - return Ok((await lightningClient.GetDepositAddress()).ToString()); + return Ok(new JValue((await lightningClient.GetDepositAddress()).ToString())); } public virtual async Task PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice) @@ -182,85 +200,78 @@ namespace BTCPayServer.Controllers.GreenField return NotFound(); } - try + if (lightningInvoice?.BOLT11 is null || + !BOLT11PaymentRequest.TryParse(lightningInvoice.BOLT11, out _, network.NBitcoinNetwork)) { - BOLT11PaymentRequest.TryParse(lightningInvoice.Invoice, out var bolt11PaymentRequest, network.NBitcoinNetwork); - } - catch (Exception) - { - ModelState.AddModelError(nameof(lightningInvoice.Invoice), "The BOLT11 invoice was invalid."); + ModelState.AddModelError(nameof(lightningInvoice.BOLT11), "The BOLT11 invoice was invalid."); } - if (CheckValidation(out var errorActionResult)) + if (!ModelState.IsValid) { - return errorActionResult; + return this.CreateValidationError(ModelState); } - var result = await lightningClient.Pay(lightningInvoice.Invoice); + var result = await lightningClient.Pay(lightningInvoice.BOLT11); switch (result.Result) { case PayResult.CouldNotFindRoute: - return this.GetGeneralErrorResponse("Could not find route"); + return this.CreateAPIError("could-not-find-route", "Impossible to find a route to the peer"); case PayResult.Error: - return this.GetGeneralErrorResponse(result.ErrorDetail); + return this.CreateAPIError("generic-error", result.ErrorDetail); + case PayResult.Ok: + return Ok(); + default: + throw new NotSupportedException("Unsupported Payresult"); } - - return Ok(); } public virtual async Task GetInvoice(string cryptoCode, string id) { var lightningClient = await GetLightningClient(cryptoCode, false); - + if (lightningClient == null) { return NotFound(); } - try + var inv = await lightningClient.GetInvoice(id); + if (inv == null) { - var inv = await lightningClient.GetInvoice(id); - if (inv == null) - { - return NotFound(); - } - return Ok(ToModel(inv)); - } - catch (Exception e) - { - return this.GetExceptionResponse(e); + return NotFound(); } + return Ok(ToModel(inv)); } public virtual async Task CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request) { var lightningClient = await GetLightningClient(cryptoCode, false); - + if (lightningClient == null) { return NotFound(); } - if (CheckValidation(out var errorActionResult)) + if (request.Amount < LightMoney.Zero) { - return errorActionResult; + ModelState.AddModelError(nameof(request.Amount), "Amount should be more or equals to 0"); } - try + if (request.Expiry <= TimeSpan.Zero) { - var invoice = await lightningClient.CreateInvoice( - new CreateInvoiceParams(request.Amount, request.Description, request.Expiry) - { - PrivateRouteHints = request.PrivateRouteHints - }, - CancellationToken.None); + ModelState.AddModelError(nameof(request.Expiry), "Expiry should be more than 0"); + } - return Ok(ToModel(invoice)); - } - catch (Exception e) + if (!ModelState.IsValid) { - return this.GetExceptionResponse(e); + return this.CreateValidationError(ModelState); } + var invoice = await lightningClient.CreateInvoice( + new CreateInvoiceParams(request.Amount, request.Description, request.Expiry) + { + PrivateRouteHints = request.PrivateRouteHints + }, + CancellationToken.None); + return Ok(ToModel(invoice)); } private LightningInvoiceData ToModel(LightningInvoice invoice) @@ -277,40 +288,12 @@ namespace BTCPayServer.Controllers.GreenField }; } - private bool CheckValidation(out IActionResult result) - { - if (!ModelState.IsValid) - { - result = this.GetValidationResponse(); - return true; - } - - result = null; - return false; - } - protected bool CanUseInternalLightning(bool doingAdminThings) { return (_btcPayServerEnvironment.IsDevelopping || User.IsInRole(Roles.ServerAdmin) || (_cssThemeManager.AllowLightningInternalNodeForAll && !doingAdminThings)); } - - private bool TryGetNodeInfo(ConnectToNodeRequest request, out NodeInfo nodeInfo) - { - nodeInfo = null; - if (!string.IsNullOrEmpty(request.NodeInfo)) return NodeInfo.TryParse(request.NodeInfo, out nodeInfo); - try - { - nodeInfo = new NodeInfo(new PubKey(request.NodeId), request.NodeHost, request.NodePort); - return true; - } - catch (Exception) - { - return false; - } - } - protected abstract Task GetLightningClient(string cryptoCode, bool doingAdminThings); } } diff --git a/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs b/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs index 75e4539e2..5125131ec 100644 --- a/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs +++ b/BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs @@ -137,7 +137,7 @@ namespace BTCPayServer.Controllers.GreenField if (!string.IsNullOrEmpty(data.CustomCSSLink) && data.CustomCSSLink.Length > 500) ModelState.AddModelError(nameof(data.CustomCSSLink), "CustomCSSLink is 500 chars max"); - return !ModelState.IsValid ? this.GetValidationResponse() :null; + return !ModelState.IsValid ? this.CreateValidationError(ModelState) :null; } private static Client.Models.PaymentRequestData FromModel(PaymentRequestData data) diff --git a/BTCPayServer/Controllers/GreenField/StoresController.cs b/BTCPayServer/Controllers/GreenField/StoresController.cs index 89af4d935..80215b0d0 100644 --- a/BTCPayServer/Controllers/GreenField/StoresController.cs +++ b/BTCPayServer/Controllers/GreenField/StoresController.cs @@ -57,7 +57,7 @@ namespace BTCPayServer.Controllers.GreenField if (!_storeRepository.CanDeleteStores()) { - return this.GetGeneralErrorResponse( + return this.CreateAPIError("unsupported", "BTCPay Server is using a database server that does not allow you to remove stores."); } await _storeRepository.RemoveStore(storeId, _userManager.GetUserId(User)); @@ -195,7 +195,7 @@ namespace BTCPayServer.Controllers.GreenField if(request.PaymentTolerance < 0 && request.PaymentTolerance > 100) ModelState.AddModelError(nameof(request.PaymentTolerance), "PaymentTolerance can only be between 0 and 100 percent"); - return !ModelState.IsValid ? this.GetValidationResponse() : null; + return !ModelState.IsValid ? this.CreateValidationError(ModelState) : null; } } } diff --git a/BTCPayServer/Controllers/GreenField/UsersController.cs b/BTCPayServer/Controllers/GreenField/UsersController.cs index 8f3e1ac20..fa9584458 100644 --- a/BTCPayServer/Controllers/GreenField/UsersController.cs +++ b/BTCPayServer/Controllers/GreenField/UsersController.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using NicolasDorier.RateLimits; using BTCPayServer.Client; +using System.Reflection; namespace BTCPayServer.Controllers.GreenField { @@ -75,7 +76,7 @@ namespace BTCPayServer.Controllers.GreenField if (!ModelState.IsValid) { - return this.GetValidationResponse(); + return this.CreateValidationError(ModelState); } var anyAdmin = (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any(); var policies = await _settingsRepository.GetSettingAsync() ?? new PoliciesSettings(); @@ -118,7 +119,7 @@ namespace BTCPayServer.Controllers.GreenField { ModelState.AddModelError(nameof(request.Password), error.Description); } - return this.GetValidationResponse(); + return this.CreateValidationError(ModelState); } if (!isAdmin) { @@ -130,9 +131,12 @@ namespace BTCPayServer.Controllers.GreenField { foreach (var error in identityResult.Errors) { - ModelState.AddModelError(string.Empty, error.Description); + if (error.Code == "DuplicateUserName") + ModelState.AddModelError(nameof(request.Email), error.Description); + else + ModelState.AddModelError(string.Empty, error.Description); } - return this.GetValidationResponse(); + return this.CreateValidationError(ModelState); } if (request.IsAdministrator is true) diff --git a/BTCPayServer/DerivationSchemeSettings.cs b/BTCPayServer/DerivationSchemeSettings.cs index 23a0805ec..78a8223d0 100644 --- a/BTCPayServer/DerivationSchemeSettings.cs +++ b/BTCPayServer/DerivationSchemeSettings.cs @@ -51,7 +51,7 @@ namespace BTCPayServer derivationSchemeSettings.AccountOriginal = null; // Saving this would be confusing for user, as xpub of electrum is legacy derivation, but for btcpay, it is segwit derivation return true; } - catch (Exception e) + catch (Exception) { return false; } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json index 6f687602a..86e18684e 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.json @@ -14,57 +14,38 @@ "components": { "schemas": { "ValidationProblemDetails": { - "allOf": [ - { - "$ref": "#/components/schemas/ProblemDetails" - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "errors": { - "type": "object", - "nullable": true, - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - } + "type": "array", + "description": "An array of validation errors of the request", + "items": { + "type": "object", + "description": "A specific validation error on a json property", + "properties": { + "path": { + "type": "string", + "nullable": false, + "description": "The json path of the property which failed validation" + }, + "message": { + "type": "string", + "nullable": false, + "description": "User friendly error message about the validation" } } - ] + } }, "ProblemDetails": { "type": "object", - "additionalProperties": false, + "description": "Description of an error happening during processing of the request", "properties": { - "type": { + "code": { "type": "string", - "nullable": true + "nullable": false, + "description": "An error code describing the error" }, - "title": { + "message": { "type": "string", - "nullable": true - }, - "status": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "detail": { - "type": "string", - "nullable": true - }, - "instance": { - "type": "string", - "nullable": true - }, - "extensions": { - "type": "object", - "nullable": true, - "additionalProperties": {} + "nullable": false, + "description": "User friendly error message about the error" } } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json index 121b0eebb..cf0ebb2a4 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json @@ -2,68 +2,40 @@ "components": { "schemas": { "ConnectToNodeRequest": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "properties": { - "nodeInfo": { - "type": "string", - "nullable": true - } - } - }, - { - "type": "object", - "additionalProperties": false, - "properties": { - "nodeId": { - "type": "string", - "nullable": true - }, - "nodeHost": { - "type": "string", - "nullable": true - }, - "nodePort": { - "type": "integer", - "format": "int32" - } - } - } - ] - }, - "CreateLightningInvoiceRequest": { "type": "object", "additionalProperties": false, "properties": { - "amount": { + "nodeURI": { + "type": "string", "nullable": true, - "oneOf": [ - { - "$ref": "#/components/schemas/LightMoney" - } - ] - }, - "description": { - "type": "string", - "nullable": true - }, - "expiry": { - "type": "string", - "format": "time-span" - }, - "privateRouteHints": { - "type": "boolean", - "nullable": true + "description": "Node URI in the form `pubkey@endpoint[:port]`" } } }, - "LightMoney": { - "type": "string", - "format": "int64", - "description": "a number amount wrapped in a string, represented in millistatoshi (00000000001BTC = 1 mSAT)", - "additionalProperties": false + "CreateLightningInvoiceRequest": { + "type": "object", + "properties": { + "amount": { + "type": "string", + "description": "Amount wrapped in a string, represented in a millistatoshi string. (1000 millisatoshi = 1 satoshi)", + "nullable": false + }, + "description": { + "type": "string", + "nullable": true, + "description": "Description of the invoice in the BOLT11" + }, + "expiry": { + "type": "integer", + "description": "Expiration time in seconds" + }, + "privateRouteHints": { + "type": "boolean", + "nullable": true, + "default": false, + "description": "True if the invoice should include private route hints" + } + } }, "LightningChannelData": { "type": "object", @@ -71,29 +43,26 @@ "properties": { "remoteNode": { "type": "string", - "nullable": true + "nullable": false, + "description": "The public key of the node (Node ID)" }, "isPublic": { - "type": "boolean" + "type": "boolean", + "description": "Whether the node is public" }, "isActive": { - "type": "boolean" + "type": "boolean", + "description": "Whether the node is online" }, "capacity": { - "nullable": true, - "oneOf": [ - { - "$ref": "#/components/schemas/LightMoney" - } - ] + "type": "string", + "description": "The capacity of the channel in millisatoshi", + "nullable": false }, "localBalance": { - "nullable": true, - "oneOf": [ - { - "$ref": "#/components/schemas/LightMoney" - } - ] + "type": "string", + "description": "The local balance of the channel in millisatoshi", + "nullable": false }, "channelPoint": { "type": "string", @@ -107,39 +76,32 @@ "properties": { "id": { "type": "string", - "nullable": true + "description": "The invoice's ID" }, "status": { "$ref": "#/components/schemas/LightningInvoiceStatus" }, - "bolT11": { + "BOLT11": { "type": "string", - "nullable": true + "description": "The BOLT11 representation of the invoice", + "nullable": false }, "paidAt": { - "type": "string", - "format": "date-time", + "type": "integer", + "description": "The unix timestamp when the invoice got paid", "nullable": true }, "expiresAt": { - "type": "string", - "format": "date-time" + "type": "integer", + "description": "The unix timestamp when the invoice expires" }, "amount": { - "nullable": true, - "oneOf": [ - { - "$ref": "#/components/schemas/LightMoney" - } - ] + "type": "string", + "description": "The amount of the invoice in millisatoshi" }, "amountReceived": { - "nullable": true, - "oneOf": [ - { - "$ref": "#/components/schemas/LightMoney" - } - ] + "type": "string", + "description": "The amount received in millisatoshi" } } }, @@ -159,28 +121,26 @@ }, "LightningNodeInformationData": { "type": "object", - "additionalProperties": false, "properties": { - "nodeInfoList": { + "nodeURIs": { "type": "array", - "nullable": true, + "description": "Node URIs to connect to this node in the form `pubkey@endpoint[:port]`", "items": { "type": "string" } }, "blockHeight": { "type": "integer", - "format": "int32" + "description": "The block height of the lightning node" } } }, "PayLightningInvoiceRequest": { "type": "object", - "additionalProperties": false, "properties": { - "invoice": { + "BOLT11": { "type": "string", - "nullable": true + "description": "The BOLT11 of the invoice to pay" } } }, @@ -188,48 +148,19 @@ "type": "object", "additionalProperties": false, "properties": { - "node": { - "nullable": true, - "oneOf": [ - { - "$ref": "#/components/schemas/ConnectToNodeRequest" - } - ] + "nodeURI": { + "type": "string", + "description": "Node URI in the form `pubkey@endpoint[:port]`" }, "channelAmount": { - "nullable": true, - "oneOf": [ - { - "$ref": "#/components/schemas/Money" - } - ] + "type": "string", + "description": "The amount to fund (in satoshi)" }, "feeRate": { - "nullable": true, - "oneOf": [ - { - "$ref": "#/components/schemas/FeeRate" - } - ] + "type": "number", + "description": "The amount to fund (in satoshi per byte)" } } - }, - "Money": { - "type": "string", - "format": "int64", - "description": "a number amount wrapped in a string, represented in satoshi (00000001BTC = 1 sat)" - }, - "FeeRate": { - "oneOf": [ - { - "type": "integer", - "format": "int64" - }, - { - "type": "number", - "format": "float" - } - ] } } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.internal.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.internal.json index a7c99f680..8d9b0bce0 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.internal.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.internal.json @@ -30,28 +30,8 @@ } } }, - "422": { - "description": "A list of errors that occurred", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ValidationProblemDetails" - } - } - } - }, - "400": { - "description": "An error occurred", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "403": { - "description": "If you are authenticated but forbidden" + "503": { + "description": "Unable to access the lightning node" }, "404": { "description": "The lightning node configuration was not found" @@ -91,7 +71,7 @@ "description": "Successfully connected" }, "422": { - "description": "A list of errors that occurred", + "description": "Unable to validate the request", "content": { "application/json": { "schema": { @@ -101,7 +81,7 @@ } }, "400": { - "description": "An error occurred", + "description": "Wellknown error codes are: `could-not-connect`", "content": { "application/json": { "schema": { @@ -110,8 +90,8 @@ } } }, - "403": { - "description": "If you are authenticated but forbidden" + "503": { + "description": "Unable to access the lightning node" }, "404": { "description": "The lightning node configuration was not found" @@ -170,19 +150,6 @@ } } }, - "400": { - "description": "An error occurred", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "403": { - "description": "If you are authenticated but forbidden" - }, "404": { "description": "The lightning node configuration was not found" } @@ -219,7 +186,7 @@ "description": "Successfully opened" }, "422": { - "description": "A list of errors that occurred", + "description": "Unable to validate the request", "content": { "application/json": { "schema": { @@ -229,7 +196,7 @@ } }, "400": { - "description": "An error occurred", + "description": "Wellknown error codes are: `channel-already-exists`, `cannot-afford-funding`, `need-more-confirmations`, `peer-not-connected`", "content": { "application/json": { "schema": { @@ -238,9 +205,6 @@ } } }, - "403": { - "description": "If you are authenticated but forbidden" - }, "404": { "description": "The lightning node configuration was not found" } @@ -266,7 +230,7 @@ } }, "/api/v1/server/lightning/{cryptoCode}/address": { - "get": { + "post": { "tags": [ "Lightning (Internal Node)" ], @@ -290,13 +254,14 @@ "content": { "application/json": { "schema": { - "type": "string" + "type": "string", + "description": "A bitcoin address belonging to the lightning node" } } } }, - "403": { - "description": "If you are authenticated but forbidden" + "503": { + "description": "Unable to access the lightning node" }, "404": { "description": "The lightning node configuration was not found" @@ -351,8 +316,8 @@ } } }, - "403": { - "description": "If you are authenticated but forbidden" + "503": { + "description": "Unable to access the lightning node" }, "404": { "description": "The lightning node configuration or the specified invoice was not found " @@ -392,7 +357,7 @@ "description": "Successfully paid" }, "422": { - "description": "A list of errors that occurred", + "description": "Unable to validate the request", "content": { "application/json": { "schema": { @@ -402,7 +367,7 @@ } }, "400": { - "description": "An error occurred", + "description": "Wellknown error codes are: `could-not-find-route`, `generic-error`", "content": { "application/json": { "schema": { @@ -411,8 +376,8 @@ } } }, - "403": { - "description": "If you are authenticated but forbidden" + "503": { + "description": "Unable to access the lightning node" }, "404": { "description": "The lightning node configuration was not found" @@ -468,28 +433,8 @@ } } }, - "422": { - "description": "A list of errors that occurred", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ValidationProblemDetails" - } - } - } - }, - "400": { - "description": "An error occurred", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "403": { - "description": "If you are authenticated but forbidden" + "503": { + "description": "Unable to access the lightning node" }, "404": { "description": "The lightning node configuration was not found" @@ -500,7 +445,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PayLightningInvoiceRequest" + "$ref": "#/components/schemas/CreateLightningInvoiceRequest" } } } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.store.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.store.json index 0c08f0399..2aabc3764 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.store.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.store.json @@ -39,28 +39,8 @@ } } }, - "422": { - "description": "A list of errors that occurred", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ValidationProblemDetails" - } - } - } - }, - "400": { - "description": "An error occurred", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "403": { - "description": "If you are authenticated but forbidden" + "503": { + "description": "Unable to access the lightning node" }, "404": { "description": "The lightning node configuration was not found" @@ -109,7 +89,7 @@ "description": "Successfully connected" }, "422": { - "description": "A list of errors that occurred", + "description": "Unable to validate the request", "content": { "application/json": { "schema": { @@ -119,7 +99,7 @@ } }, "400": { - "description": "An error occurred", + "description": "Wellknown error codes are: `could-not-connect`", "content": { "application/json": { "schema": { @@ -128,8 +108,8 @@ } } }, - "403": { - "description": "If you are authenticated but forbidden" + "503": { + "description": "Unable to access the lightning node" }, "404": { "description": "The lightning node configuration was not found" @@ -197,19 +177,6 @@ } } }, - "400": { - "description": "An error occurred", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "403": { - "description": "If you are authenticated but forbidden" - }, "404": { "description": "The lightning node configuration was not found" } @@ -255,7 +222,7 @@ "description": "Successfully opened" }, "422": { - "description": "A list of errors that occurred", + "description": "Unable to validate the request", "content": { "application/json": { "schema": { @@ -265,7 +232,7 @@ } }, "400": { - "description": "An error occurred", + "description": "Wellknown error codes are: `channel-already-exists`, `cannot-afford-funding`, `need-more-confirmations`, `peer-not-connected`", "content": { "application/json": { "schema": { @@ -274,8 +241,8 @@ } } }, - "403": { - "description": "If you are authenticated but forbidden" + "503": { + "description": "Unable to access the lightning node" }, "404": { "description": "The lightning node configuration was not found" @@ -302,7 +269,7 @@ } }, "/api/v1/stores/{storeId}/lightning/{cryptoCode}/address": { - "get": { + "post": { "tags": [ "Lightning (Store)" ], @@ -314,7 +281,8 @@ "required": true, "description": "The cryptoCode of the lightning-node to query", "schema": { - "type": "string" + "type": "string", + "description": "A bitcoin address belonging to the lightning node" } }, { @@ -340,8 +308,8 @@ } } }, - "403": { - "description": "If you are authenticated but forbidden" + "503": { + "description": "Unable to access the lightning node" }, "404": { "description": "The lightning node configuration was not found" @@ -406,8 +374,8 @@ } } }, - "403": { - "description": "If you are authenticated but forbidden" + "503": { + "description": "Unable to access the lightning node" }, "404": { "description": "The lightning node configuration or the specified invoice was not found " @@ -456,7 +424,7 @@ "description": "Successfully paid" }, "422": { - "description": "A list of errors that occurred", + "description": "Unable to validate the request", "content": { "application/json": { "schema": { @@ -466,7 +434,7 @@ } }, "400": { - "description": "An error occurred", + "description": "Wellknown error codes are: `could-not-find-route`, `generic-error`", "content": { "application/json": { "schema": { @@ -475,8 +443,8 @@ } } }, - "403": { - "description": "If you are authenticated but forbidden" + "503": { + "description": "Unable to access the lightning node" }, "404": { "description": "The lightning node configuration was not found" @@ -541,28 +509,8 @@ } } }, - "422": { - "description": "A list of errors that occurred", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ValidationProblemDetails" - } - } - } - }, - "400": { - "description": "An error occurred", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "403": { - "description": "If you are authenticated but forbidden" + "503": { + "description": "Unable to access the lightning node" }, "404": { "description": "The lightning node configuration was not found"