From 1e3f62718d92cb50e3f87a5b2eeebb3a707436a0 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Fri, 29 May 2020 02:00:13 +0200 Subject: [PATCH] GreenField: Cross-implemenation Lightning Node API (#1566) * GreenField: Cross-implemenation Lightning Node API * switch to hard unrsstricted check * fix * set LightningPrivateRouteHints in swagger + stores api * add priv route hint * rename models and add swagger defs to models --- .../BTCPayServer.Client.csproj | 1 + .../BTCPayServerClient.Lightning.Internal.cs | 91 +++++ .../BTCPayServerClient.Lightning.Store.cs | 92 +++++ .../Models/ConnectToNodeRequest.cs | 10 + .../Models/CreateLightningInvoiceRequest.cs | 17 + .../Models/LightningInvoiceData.cs | 28 ++ .../Models/LightningNodeInformationData.cs | 31 ++ .../Models/OpenLightningChannelRequest.cs | 16 + .../Models/PayLightningInvoiceRequest.cs | 7 + BTCPayServer.Client/Models/StoreBaseData.cs | 1 + BTCPayServer.Client/Permissions.cs | 13 +- .../LightningNodeApiController.Internal.cs | 110 ++++++ .../LightningNodeApiController.Store.cs | 124 +++++++ .../GreenField/LightningNodeApiController.cs | 327 ++++++++++++++++++ .../GreenField/StoresController.cs | 4 +- BTCPayServer/Controllers/InvoiceController.cs | 1 + .../Controllers/ManageController.APIKeys.cs | 6 + .../StoresController.LightningLike.cs | 6 +- BTCPayServer/Extensions.cs | 10 + .../GreenFieldAuthorizationHandler.cs | 19 +- .../v1/swagger.template.lightning.common.json | 244 +++++++++++++ .../swagger/v1/swagger.template.stores.json | 3 + 22 files changed, 1140 insertions(+), 21 deletions(-) create mode 100644 BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs create mode 100644 BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs create mode 100644 BTCPayServer.Client/Models/ConnectToNodeRequest.cs create mode 100644 BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs create mode 100644 BTCPayServer.Client/Models/LightningInvoiceData.cs create mode 100644 BTCPayServer.Client/Models/LightningNodeInformationData.cs create mode 100644 BTCPayServer.Client/Models/OpenLightningChannelRequest.cs create mode 100644 BTCPayServer.Client/Models/PayLightningInvoiceRequest.cs create mode 100644 BTCPayServer/Controllers/GreenField/LightningNodeApiController.Internal.cs create mode 100644 BTCPayServer/Controllers/GreenField/LightningNodeApiController.Store.cs create mode 100644 BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs create mode 100644 BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj index 016b28a91..bd2777c9c 100644 --- a/BTCPayServer.Client/BTCPayServer.Client.csproj +++ b/BTCPayServer.Client/BTCPayServer.Client.csproj @@ -6,6 +6,7 @@ + diff --git a/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs b/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs new file mode 100644 index 000000000..bba92128e --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + public async Task GetLightningNodeInfo(string cryptoCode, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/info", + method: HttpMethod.Get), token); + return await HandleResponse(response); + } + + public async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request, + CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/connect", bodyPayload: request, + method: HttpMethod.Post), token); + HandleResponse(response); + } + + public async Task> GetLightningNodeChannels(string cryptoCode, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/channels", + method: HttpMethod.Get), token); + return await HandleResponse>(response); + } + + public async Task OpenLightningChannel(string cryptoCode, OpenLightningChannelRequest request, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/channels", bodyPayload: request, + method: HttpMethod.Post), token); + return await HandleResponse(response); + } + + public async Task GetLightningDepositAddress(string cryptoCode, CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/address", method: HttpMethod.Post), token); + return await HandleResponse(response); + } + + + public async Task PayLightningInvoice(string cryptoCode, PayLightningInvoiceRequest request, + CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/pay", bodyPayload: request, + method: HttpMethod.Post), token); + HandleResponse(response); + } + + public async Task GetLightningInvoice(string cryptoCode, + string invoiceId, CancellationToken token = default) + { + if (invoiceId == null) + throw new ArgumentNullException(nameof(invoiceId)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices/{invoiceId}", + method: HttpMethod.Get), token); + return await HandleResponse(response); + } + + public async Task CreateLightningInvoice(string cryptoCode, CreateLightningInvoiceRequest request, + CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices", bodyPayload: request, + method: HttpMethod.Post), token); + return await HandleResponse(response); + } + } +} diff --git a/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs b/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs new file mode 100644 index 000000000..1b0686315 --- /dev/null +++ b/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; + +namespace BTCPayServer.Client +{ + public partial class BTCPayServerClient + { + public async Task GetLightningNodeInfo(string storeId, string cryptoCode, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/info", + method: HttpMethod.Get), token); + return await HandleResponse(response); + } + + public async Task ConnectToLightningNode(string storeId, string cryptoCode, ConnectToNodeRequest request, + CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/connect", bodyPayload: request, + method: HttpMethod.Post), token); + HandleResponse(response); + } + + public async Task> GetLightningNodeChannels(string storeId, string cryptoCode, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/channels", + method: HttpMethod.Get), token); + return await HandleResponse>(response); + } + + public async Task OpenLightningChannel(string storeId, string cryptoCode, OpenLightningChannelRequest request, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/channels", bodyPayload: request, + method: HttpMethod.Post), token); + return await HandleResponse(response); + } + + public async Task GetLightningDepositAddress(string storeId, string cryptoCode, + CancellationToken token = default) + { + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/address", method: HttpMethod.Post), + token); + return await HandleResponse(response); + } + + public async Task PayLightningInvoice(string storeId, string cryptoCode, PayLightningInvoiceRequest request, + CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/pay", bodyPayload: request, + method: HttpMethod.Post), token); + HandleResponse(response); + } + + public async Task GetLightningInvoice(string storeId, string cryptoCode, + string invoiceId, CancellationToken token = default) + { + if (invoiceId == null) + throw new ArgumentNullException(nameof(invoiceId)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/{invoiceId}", + method: HttpMethod.Get), token); + return await HandleResponse(response); + } + + public async Task CreateLightningInvoice(string storeId, string cryptoCode, + CreateLightningInvoiceRequest request, CancellationToken token = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices", bodyPayload: request, + method: HttpMethod.Post), token); + return await HandleResponse(response); + } + } +} diff --git a/BTCPayServer.Client/Models/ConnectToNodeRequest.cs b/BTCPayServer.Client/Models/ConnectToNodeRequest.cs new file mode 100644 index 000000000..5c5559586 --- /dev/null +++ b/BTCPayServer.Client/Models/ConnectToNodeRequest.cs @@ -0,0 +1,10 @@ +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; } + } +} diff --git a/BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs b/BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs new file mode 100644 index 000000000..0cc1a0d70 --- /dev/null +++ b/BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs @@ -0,0 +1,17 @@ +using System; +using BTCPayServer.Lightning; +using BTCPayServer.Lightning.JsonConverters; +using Newtonsoft.Json; + +namespace BTCPayServer.Client.Models +{ + public class CreateLightningInvoiceRequest + { + [JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))] + public LightMoney Amount { get; set; } + public string Description { get; set; } + public TimeSpan Expiry { get; set; } + public bool LightningPrivateRouteHints { get; set; } + + } +} diff --git a/BTCPayServer.Client/Models/LightningInvoiceData.cs b/BTCPayServer.Client/Models/LightningInvoiceData.cs new file mode 100644 index 000000000..6a76e25f9 --- /dev/null +++ b/BTCPayServer.Client/Models/LightningInvoiceData.cs @@ -0,0 +1,28 @@ +using System; +using BTCPayServer.Lightning; +using BTCPayServer.Lightning.JsonConverters; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace BTCPayServer.Client.Models +{ + public class LightningInvoiceData + { + public string Id { get; set; } + + [JsonProperty(ItemConverterType = typeof(StringEnumConverter))] + public LightningInvoiceStatus Status { get; set; } + + public string BOLT11 { get; set; } + + public DateTimeOffset? PaidAt { get; set; } + + public DateTimeOffset ExpiresAt { get; set; } + + [JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))] + public LightMoney Amount { get; set; } + + [JsonProperty(ItemConverterType = typeof(LightMoneyJsonConverter))] + public LightMoney AmountReceived { get; set; } + } +} diff --git a/BTCPayServer.Client/Models/LightningNodeInformationData.cs b/BTCPayServer.Client/Models/LightningNodeInformationData.cs new file mode 100644 index 000000000..11d83def4 --- /dev/null +++ b/BTCPayServer.Client/Models/LightningNodeInformationData.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using BTCPayServer.Lightning; +using NBitcoin; +using NBitcoin.JsonConverters; +using Newtonsoft.Json; + +namespace BTCPayServer.Client.Models +{ + public class LightningNodeInformationData + { + public IEnumerable NodeInfoList { get; set; } + public int BlockHeight { get; set; } + } + + public class LightningChannelData + { + public string RemoteNode { get; set; } + + public bool IsPublic { get; set; } + + public bool IsActive { get; set; } + + [JsonProperty(ItemConverterType = typeof(MoneyJsonConverter))] + public LightMoney Capacity { get; set; } + + [JsonProperty(ItemConverterType = typeof(MoneyJsonConverter))] + public LightMoney LocalBalance { get; set; } + + public string ChannelPoint { get; set; } + } +} diff --git a/BTCPayServer.Client/Models/OpenLightningChannelRequest.cs b/BTCPayServer.Client/Models/OpenLightningChannelRequest.cs new file mode 100644 index 000000000..192c14fd9 --- /dev/null +++ b/BTCPayServer.Client/Models/OpenLightningChannelRequest.cs @@ -0,0 +1,16 @@ +using NBitcoin; +using NBitcoin.JsonConverters; +using Newtonsoft.Json; + +namespace BTCPayServer.Client.Models +{ + public class OpenLightningChannelRequest + { + public ConnectToNodeRequest Node { get; set; } + [JsonProperty(ItemConverterType = typeof(MoneyJsonConverter))] + public Money ChannelAmount { get; set; } + + [JsonProperty(ItemConverterType = typeof(FeeRateJsonConverter))] + public FeeRate FeeRate { get; set; } + } +} diff --git a/BTCPayServer.Client/Models/PayLightningInvoiceRequest.cs b/BTCPayServer.Client/Models/PayLightningInvoiceRequest.cs new file mode 100644 index 000000000..84a5bef25 --- /dev/null +++ b/BTCPayServer.Client/Models/PayLightningInvoiceRequest.cs @@ -0,0 +1,7 @@ +namespace BTCPayServer.Client.Models +{ + public class PayLightningInvoiceRequest + { + public string Invoice { get; set; } + } +} diff --git a/BTCPayServer.Client/Models/StoreBaseData.cs b/BTCPayServer.Client/Models/StoreBaseData.cs index 5cb07f3c9..25caddbfa 100644 --- a/BTCPayServer.Client/Models/StoreBaseData.cs +++ b/BTCPayServer.Client/Models/StoreBaseData.cs @@ -44,6 +44,7 @@ namespace BTCPayServer.Client.Models public NetworkFeeMode NetworkFeeMode { get; set; } public bool PayJoinEnabled { get; set; } + public bool LightningPrivateRouteHints { get; set; } } diff --git a/BTCPayServer.Client/Permissions.cs b/BTCPayServer.Client/Permissions.cs index 2b937cd85..1ea91e7df 100644 --- a/BTCPayServer.Client/Permissions.cs +++ b/BTCPayServer.Client/Permissions.cs @@ -6,6 +6,10 @@ namespace BTCPayServer.Client { public class Policies { + public const string CanCreateLightningInvoiceInternalNode = "btcpay.server.cancreatelightninginvoiceinternalnode"; + public const string CanCreateLightningInvoiceInStore = "btcpay.store.cancreatelightninginvoice"; + public const string CanUseInternalLightningNode = "btcpay.server.canuseinternallightningnode"; + public const string CanUseLightningNodeInStore = "btcpay.store.canuselightningnode"; public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings"; public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings"; public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings"; @@ -30,6 +34,10 @@ namespace BTCPayServer.Client yield return CanViewProfile; yield return CanCreateUser; yield return Unrestricted; + yield return CanUseInternalLightningNode; + yield return CanCreateLightningInvoiceInternalNode; + yield return CanUseLightningNodeInStore; + yield return CanCreateLightningInvoiceInStore; } } public static bool IsValidPolicy(string policy) @@ -100,8 +108,6 @@ namespace BTCPayServer.Client } } - - internal Permission(string policy, string storeId) { Policy = policy; @@ -147,7 +153,8 @@ namespace BTCPayServer.Client case Policies.CanModifyPaymentRequests when this.Policy == Policies.CanModifyStoreSettings: case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyStoreSettings: case Policies.CanViewPaymentRequests when this.Policy == Policies.CanViewStoreSettings: - case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyPaymentRequests: + case Policies.CanCreateLightningInvoiceInternalNode when this.Policy == Policies.CanUseInternalLightningNode: + case Policies.CanCreateLightningInvoiceInStore when this.Policy == Policies.CanUseLightningNodeInStore: return true; default: return false; diff --git a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Internal.cs b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Internal.cs new file mode 100644 index 000000000..c1f2f9572 --- /dev/null +++ b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Internal.cs @@ -0,0 +1,110 @@ +using System.Threading.Tasks; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Configuration; +using BTCPayServer.HostedServices; +using BTCPayServer.Lightning; +using BTCPayServer.Security; +using BTCPayServer.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers.GreenField +{ + [ApiController] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + public class InternalLightningNodeApiInternalController : LightningNodeApiController + { + private readonly BTCPayServerOptions _btcPayServerOptions; + private readonly BTCPayNetworkProvider _btcPayNetworkProvider; + private readonly LightningClientFactoryService _lightningClientFactory; + + + public InternalLightningNodeApiInternalController(BTCPayServerOptions btcPayServerOptions, + BTCPayNetworkProvider btcPayNetworkProvider, BTCPayServerEnvironment btcPayServerEnvironment, + CssThemeManager cssThemeManager, LightningClientFactoryService lightningClientFactory) : base( + btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager) + { + _btcPayServerOptions = btcPayServerOptions; + _btcPayNetworkProvider = btcPayNetworkProvider; + _lightningClientFactory = lightningClientFactory; + } + + [Authorize(Policy = Policies.CanUseInternalLightningNode, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/server/lightning/{cryptoCode}/info")] + public override Task GetInfo(string cryptoCode) + { + return base.GetInfo(cryptoCode); + } + + [Authorize(Policy = Policies.CanUseInternalLightningNode, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/server/lightning/{cryptoCode}/connect")] + public override Task ConnectToNode(string cryptoCode, ConnectToNodeRequest request) + { + return base.ConnectToNode(cryptoCode, request); + } + + [Authorize(Policy = Policies.CanUseInternalLightningNode, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/server/lightning/{cryptoCode}/channels")] + public override Task GetChannels(string cryptoCode) + { + return base.GetChannels(cryptoCode); + } + + [Authorize(Policy = Policies.CanUseInternalLightningNode, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/server/lightning/{cryptoCode}/channels")] + public override Task OpenChannel(string cryptoCode, OpenLightningChannelRequest request) + { + return base.OpenChannel(cryptoCode, request); + } + + [Authorize(Policy = Policies.CanUseInternalLightningNode, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/server/lightning/{cryptoCode}/address")] + public override Task GetDepositAddress(string cryptoCode) + { + return base.GetDepositAddress(cryptoCode); + } + + [Authorize(Policy = Policies.CanUseInternalLightningNode, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/server/lightning/{cryptoCode}/invoices/{id}")] + public override Task GetInvoice(string cryptoCode, string id) + { + return base.GetInvoice(cryptoCode, id); + } + + [Authorize(Policy = Policies.CanUseInternalLightningNode, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices/pay")] + public override Task PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice) + { + return base.PayInvoice(cryptoCode, lightningInvoice); + } + + [Authorize(Policy = Policies.CanCreateLightningInvoiceInternalNode, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices")] + public override Task CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request) + { + return base.CreateInvoice(cryptoCode, request); + } + + protected override Task GetLightningClient(string cryptoCode, bool doingAdminThings) + { + _btcPayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode, + out var internalLightningNode); + var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); + if (network == null || !CanUseInternalLightning(doingAdminThings) || internalLightningNode == null) + { + return null; + } + + return Task.FromResult(_lightningClientFactory.Create(internalLightningNode, network)); + } + } +} diff --git a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Store.cs b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Store.cs new file mode 100644 index 000000000..1000defc7 --- /dev/null +++ b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.Store.cs @@ -0,0 +1,124 @@ +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Client; +using BTCPayServer.Client.Models; +using BTCPayServer.Configuration; +using BTCPayServer.Data; +using BTCPayServer.HostedServices; +using BTCPayServer.Lightning; +using BTCPayServer.Payments; +using BTCPayServer.Payments.Lightning; +using BTCPayServer.Security; +using BTCPayServer.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers.GreenField +{ + [ApiController] + [Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + public class StoreLightningNodeApiController : LightningNodeApiController + { + private readonly BTCPayServerOptions _btcPayServerOptions; + private readonly LightningClientFactoryService _lightningClientFactory; + private readonly BTCPayNetworkProvider _btcPayNetworkProvider; + + public StoreLightningNodeApiController( + BTCPayServerOptions btcPayServerOptions, + LightningClientFactoryService lightningClientFactory, BTCPayNetworkProvider btcPayNetworkProvider, + BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager) : base( + btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager) + { + _btcPayServerOptions = btcPayServerOptions; + _lightningClientFactory = lightningClientFactory; + _btcPayNetworkProvider = btcPayNetworkProvider; + } + [Authorize(Policy = Policies.CanUseLightningNodeInStore, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/info")] + public override Task GetInfo(string cryptoCode) + { + return base.GetInfo(cryptoCode); + } + + [Authorize(Policy = Policies.CanUseLightningNodeInStore, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/connect")] + public override Task ConnectToNode(string cryptoCode, ConnectToNodeRequest request) + { + return base.ConnectToNode(cryptoCode, request); + } + [Authorize(Policy = Policies.CanUseLightningNodeInStore, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")] + public override Task GetChannels(string cryptoCode) + { + return base.GetChannels(cryptoCode); + } + [Authorize(Policy = Policies.CanUseLightningNodeInStore, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")] + public override Task OpenChannel(string cryptoCode, OpenLightningChannelRequest request) + { + return base.OpenChannel(cryptoCode, request); + } + + [Authorize(Policy = Policies.CanUseLightningNodeInStore, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/address")] + public override Task GetDepositAddress(string cryptoCode) + { + return base.GetDepositAddress(cryptoCode); + } + + [Authorize(Policy = Policies.CanUseLightningNodeInStore, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay")] + public override Task PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice) + { + return base.PayInvoice(cryptoCode, lightningInvoice); + } + + [Authorize(Policy = Policies.CanUseLightningNodeInStore, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/{id}")] + public override Task GetInvoice(string cryptoCode, string id) + { + return base.GetInvoice(cryptoCode, id); + } + + [Authorize(Policy = Policies.CanCreateLightningInvoiceInStore, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")] + public override Task CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request) + { + return base.CreateInvoice(cryptoCode, request); + } + + protected override Task GetLightningClient(string cryptoCode, + bool doingAdminThings) + { + _btcPayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode, + out var internalLightningNode); + var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); + + var store = HttpContext.GetStoreData(); + if (network == null || store == null) + { + return null; + } + + var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); + var existing = store.GetSupportedPaymentMethods(_btcPayNetworkProvider) + .OfType() + .FirstOrDefault(d => d.PaymentId == id); + if (existing == null || (existing.GetLightningUrl().IsInternalNode(internalLightningNode) && + !CanUseInternalLightning(doingAdminThings))) + { + return null; + } + + return Task.FromResult(_lightningClientFactory.Create(existing.GetLightningUrl(), network)); + } + } +} diff --git a/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs new file mode 100644 index 000000000..0cfac1433 --- /dev/null +++ b/BTCPayServer/Controllers/GreenField/LightningNodeApiController.cs @@ -0,0 +1,327 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Client.Models; +using BTCPayServer.HostedServices; +using BTCPayServer.Lightning; +using BTCPayServer.Services; +using Microsoft.AspNetCore.Mvc; +using NBitcoin; + +namespace BTCPayServer.Controllers.GreenField +{ + public abstract class LightningNodeApiController : Controller + { + private readonly BTCPayNetworkProvider _btcPayNetworkProvider; + private readonly BTCPayServerEnvironment _btcPayServerEnvironment; + private readonly CssThemeManager _cssThemeManager; + + protected LightningNodeApiController(BTCPayNetworkProvider btcPayNetworkProvider, + BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager) + { + _btcPayNetworkProvider = btcPayNetworkProvider; + _btcPayServerEnvironment = btcPayServerEnvironment; + _cssThemeManager = cssThemeManager; + } + + public virtual async Task GetInfo(string cryptoCode) + { + var lightningClient = await GetLightningClient(cryptoCode, true); + if (lightningClient == null) + { + return NotFound(); + } + + try + { + var info = await lightningClient.GetInfo(); + return Ok(new LightningNodeInformationData() + { + BlockHeight = info.BlockHeight, + NodeInfoList = info.NodeInfoList.Select(nodeInfo => nodeInfo.ToString()) + }); + } + catch (Exception e) + { + ModelState.AddModelError(string.Empty, e.Message); + return BadRequest(new ValidationProblemDetails(ModelState)); + } + } + + public virtual async Task ConnectToNode(string cryptoCode, ConnectToNodeRequest request) + { + var lightningClient = await GetLightningClient(cryptoCode, true); + if (lightningClient == null) + { + return NotFound(); + } + + if (TryGetNodeInfo(request, out var nodeInfo)) + { + ModelState.AddModelError(nameof(request.NodeId), "A valid node info was not provided to connect to"); + } + + if (CheckValidation(out var errorActionResult)) + { + return errorActionResult; + } + + try + { + await lightningClient.ConnectTo(nodeInfo); + } + catch (Exception e) + { + ModelState.AddModelError(string.Empty, e.Message); + return BadRequest(new ValidationProblemDetails(ModelState)); + } + + return Ok(); + } + + public virtual async Task GetChannels(string cryptoCode) + { + var lightningClient = await GetLightningClient(cryptoCode, true); + if (lightningClient == null) + { + return NotFound(); + } + + try + { + 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) + { + ModelState.AddModelError(string.Empty, e.Message); + return BadRequest(new ValidationProblemDetails(ModelState)); + } + } + + + public virtual async Task OpenChannel(string cryptoCode, OpenLightningChannelRequest request) + { + var lightningClient = await GetLightningClient(cryptoCode, true); + if (lightningClient == null) + { + return NotFound(); + } + + if (TryGetNodeInfo(request.Node, out var nodeInfo)) + { + ModelState.AddModelError(nameof(request.Node), + "A valid node info was not provided to open a channel with"); + } + + if (request.ChannelAmount == null) + { + ModelState.AddModelError(nameof(request.ChannelAmount), "ChannelAmount is missing"); + } + else if (request.ChannelAmount.Satoshi <= 0) + { + ModelState.AddModelError(nameof(request.ChannelAmount), "ChannelAmount must be more than 0"); + } + + if (request.FeeRate == null) + { + ModelState.AddModelError(nameof(request.FeeRate), "FeeRate is missing"); + } + else if (request.FeeRate.SatoshiPerByte <= 0) + { + ModelState.AddModelError(nameof(request.FeeRate), "FeeRate must be more than 0"); + } + + if (CheckValidation(out var errorActionResult)) + { + return errorActionResult; + } + + try + { + var response = await lightningClient.OpenChannel(new Lightning.OpenChannelRequest() + { + ChannelAmount = request.ChannelAmount, FeeRate = request.FeeRate, NodeInfo = nodeInfo + }); + if (response.Result == OpenChannelResult.Ok) + { + return Ok(); + } + + ModelState.AddModelError(string.Empty, response.Result.ToString()); + return BadRequest(new ValidationProblemDetails(ModelState)); + } + catch (Exception e) + { + ModelState.AddModelError(string.Empty, e.Message); + return BadRequest(new ValidationProblemDetails(ModelState)); + } + } + + public virtual async Task GetDepositAddress(string cryptoCode) + { + var lightningClient = await GetLightningClient(cryptoCode, true); + if (lightningClient == null) + { + return NotFound(); + } + + return Ok((await lightningClient.GetDepositAddress()).ToString()); + } + + public virtual async Task PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice) + { + var lightningClient = await GetLightningClient(cryptoCode, true); + var network = _btcPayNetworkProvider.GetNetwork(cryptoCode); + if (lightningClient == null || network == null) + { + return NotFound(); + } + + try + { + BOLT11PaymentRequest.TryParse(lightningInvoice.Invoice, out var bolt11PaymentRequest, network.NBitcoinNetwork); + } + catch (Exception) + { + ModelState.AddModelError(nameof(lightningInvoice), "The BOLT11 invoice was invalid."); + } + + if (CheckValidation(out var errorActionResult)) + { + return errorActionResult; + } + + var result = await lightningClient.Pay(lightningInvoice.Invoice); + switch (result.Result) + { + case PayResult.Ok: + return Ok(); + case PayResult.CouldNotFindRoute: + ModelState.AddModelError(nameof(lightningInvoice.Invoice), "Could not find route"); + break; + case PayResult.Error: + ModelState.AddModelError(nameof(lightningInvoice.Invoice), result.ErrorDetail); + break; + } + + return BadRequest(new ValidationProblemDetails(ModelState)); + } + + 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) + { + return NotFound(); + } + return Ok(ToModel(inv)); + } + catch (Exception e) + { + ModelState.AddModelError(string.Empty, e.Message); + return BadRequest(new ValidationProblemDetails(ModelState)); + } + } + + 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)) + { + return errorActionResult; + } + + try + { + var invoice = await lightningClient.CreateInvoice( + new CreateInvoiceParams(request.Amount, request.Description, request.Expiry) + { + PrivateRouteHints = request.LightningPrivateRouteHints + }, + CancellationToken.None); + + return Ok(ToModel(invoice)); + } + catch (Exception e) + { + ModelState.AddModelError(string.Empty, e.Message); + return BadRequest(new ValidationProblemDetails(ModelState)); + } + } + + private LightningInvoiceData ToModel(LightningInvoice invoice) + { + return new LightningInvoiceData() + { + Amount = invoice.Amount, + Id = invoice.Id, + Status = invoice.Status, + AmountReceived = invoice.AmountReceived, + PaidAt = invoice.PaidAt, + BOLT11 = invoice.BOLT11, + ExpiresAt = invoice.ExpiresAt + }; + } + + private bool CheckValidation(out IActionResult result) + { + if (!ModelState.IsValid) + { + result = BadRequest(new ValidationProblemDetails(ModelState)); + 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/StoresController.cs b/BTCPayServer/Controllers/GreenField/StoresController.cs index e48455f67..12215626b 100644 --- a/BTCPayServer/Controllers/GreenField/StoresController.cs +++ b/BTCPayServer/Controllers/GreenField/StoresController.cs @@ -132,7 +132,8 @@ namespace BTCPayServer.Controllers.GreenField LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate, PaymentTolerance = storeBlob.PaymentTolerance, RedirectAutomatically = storeBlob.RedirectAutomatically, - PayJoinEnabled = storeBlob.PayJoinEnabled + PayJoinEnabled = storeBlob.PayJoinEnabled, + LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints }; } @@ -168,6 +169,7 @@ namespace BTCPayServer.Controllers.GreenField blob.PaymentTolerance = restModel.PaymentTolerance; blob.RedirectAutomatically = restModel.RedirectAutomatically; blob.PayJoinEnabled = restModel.PayJoinEnabled; + blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints; model.SetStoreBlob(blob); } diff --git a/BTCPayServer/Controllers/InvoiceController.cs b/BTCPayServer/Controllers/InvoiceController.cs index b06447563..351c6127e 100644 --- a/BTCPayServer/Controllers/InvoiceController.cs +++ b/BTCPayServer/Controllers/InvoiceController.cs @@ -24,6 +24,7 @@ using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBitpayClient; using Newtonsoft.Json; +using CreateInvoiceRequest = BTCPayServer.Models.CreateInvoiceRequest; using StoreData = BTCPayServer.Data.StoreData; namespace BTCPayServer.Controllers diff --git a/BTCPayServer/Controllers/ManageController.APIKeys.cs b/BTCPayServer/Controllers/ManageController.APIKeys.cs index ec1c5d2cb..70e608a7f 100644 --- a/BTCPayServer/Controllers/ManageController.APIKeys.cs +++ b/BTCPayServer/Controllers/ManageController.APIKeys.cs @@ -368,6 +368,12 @@ namespace BTCPayServer.Controllers {$"{BTCPayServer.Client.Policies.CanModifyPaymentRequests}:", ("Manage selected stores' payment requests", "The app will be able to view, modify, delete and create new payment requests on the selected stores.")}, {BTCPayServer.Client.Policies.CanViewPaymentRequests, ("View your payment requests", "The app will be able to view payment requests.")}, {$"{BTCPayServer.Client.Policies.CanViewPaymentRequests}:", ("View your payment requests", "The app will be able to view the selected stores' payment requests.")}, + {BTCPayServer.Client.Policies.CanUseInternalLightningNode, ("Use the internal lightning node", "The app will be able to use the internal BTCPay Server lightning node to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")}, + {BTCPayServer.Client.Policies.CanCreateLightningInvoiceInternalNode, ("Create invoices with internal lightning node", "The app will be able to use the internal BTCPay Server lightning node to create BOLT11 invoices.")}, + {BTCPayServer.Client.Policies.CanUseLightningNodeInStore, ("Use the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to all your stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")}, + {BTCPayServer.Client.Policies.CanCreateLightningInvoiceInStore, ("Create invoices the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to all your stores to create BOLT11 invoices.")}, + {$"{BTCPayServer.Client.Policies.CanUseLightningNodeInStore}:", ("Use the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to the selected stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")}, + {$"{BTCPayServer.Client.Policies.CanCreateLightningInvoiceInStore}:", ("Create invoices the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to the selected stores to create BOLT11 invoices.")}, }; public string Title { diff --git a/BTCPayServer/Controllers/StoresController.LightningLike.cs b/BTCPayServer/Controllers/StoresController.LightningLike.cs index 8f931c295..2b6530146 100644 --- a/BTCPayServer/Controllers/StoresController.LightningLike.cs +++ b/BTCPayServer/Controllers/StoresController.LightningLike.cs @@ -91,11 +91,7 @@ namespace BTCPayServer.Controllers return View(vm); } - var internalDomain = internalLightning?.BaseUri?.DnsSafeHost; - - bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning || - connectionString.BaseUri.DnsSafeHost == internalDomain || - (internalDomain == "127.0.0.1" || internalDomain == "localhost"); + bool isInternalNode = connectionString.IsInternalNode(internalLightning); if (connectionString.BaseUri.Scheme == "http") { diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index ecbff263f..a5ab20210 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -33,6 +33,7 @@ using BTCPayServer.Data; using Microsoft.EntityFrameworkCore.Infrastructure; using NBXplorer.DerivationStrategy; using System.Net; +using BTCPayServer.Lightning; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Newtonsoft.Json.Linq; @@ -42,6 +43,15 @@ namespace BTCPayServer { public static class Extensions { + public static bool IsInternalNode(this LightningConnectionString connectionString, LightningConnectionString internalLightning) + { + var internalDomain = internalLightning?.BaseUri?.DnsSafeHost; + + return connectionString.ConnectionType == LightningConnectionType.CLightning || + connectionString.BaseUri.DnsSafeHost == internalDomain || + (internalDomain == "127.0.0.1" || internalDomain == "localhost"); + } + public static IQueryable Where(this Microsoft.EntityFrameworkCore.DbSet obj, System.Linq.Expressions.Expression> predicate) where TEntity : class { return System.Linq.Queryable.Where(obj, predicate); diff --git a/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs b/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs index ae325524f..2b4ad7470 100644 --- a/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs +++ b/BTCPayServer/Security/GreenField/GreenFieldAuthorizationHandler.cs @@ -36,16 +36,7 @@ namespace BTCPayServer.Security.GreenField bool success = false; switch (requirement.Policy) { - case Policies.CanModifyProfile: - case Policies.CanViewProfile: - case Policies.Unrestricted: - success = context.HasPermission(Permission.Create(requirement.Policy)); - break; - - case Policies.CanViewPaymentRequests: - case Policies.CanModifyPaymentRequests: - case Policies.CanViewStoreSettings: - case Policies.CanModifyStoreSettings: + case { } policy when Policies.IsStorePolicy(policy): var storeId = _HttpContext.GetImplicitStoreId(); var userid = _userManager.GetUserId(context.User); // Specific store action @@ -75,8 +66,7 @@ namespace BTCPayServer.Security.GreenField success = true; } break; - case Policies.CanCreateUser: - case Policies.CanModifyServerSettings: + case { } policy when Policies.IsServerPolicy(policy): if (context.HasPermission(Permission.Create(requirement.Policy))) { var user = await _userManager.GetUserAsync(context.User); @@ -87,6 +77,11 @@ namespace BTCPayServer.Security.GreenField success = true; } break; + case Policies.CanModifyProfile: + case Policies.CanViewProfile: + case Policies.Unrestricted: + success = context.HasPermission(Permission.Create(requirement.Policy)); + break; } if (success) diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json new file mode 100644 index 000000000..a00ca9bae --- /dev/null +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json @@ -0,0 +1,244 @@ +{ + "components": { + "schemas": { + "ConnectToNodeRequest": { + "oneOf": [ + { + "$ref": "#/components/schemas/ConnectToNodeRequest1" + }, + { + "$ref": "#/components/schemas/ConnectToNodeRequest2" + } + ] + }, + "ConnectToNodeRequest1": { + "type": "object", + "additionalProperties": false, + "properties": { + "nodeInfo": { + "type": "string", + "nullable": true + } + } + }, + "ConnectToNodeRequest2": { + "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": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/LightMoney" + } + ] + }, + "description": { + "type": "string", + "nullable": true + }, + "expiry": { + "type": "string", + "format": "time-span" + }, + "lightningPrivateRouteHints": { + "type": "boolean" + } + } + }, + "LightMoney": { + "type": "integer", + "format": "int64", + "additionalProperties": false + }, + "LightningChannelData": { + "type": "object", + "additionalProperties": false, + "properties": { + "remoteNode": { + "type": "string", + "nullable": true + }, + "isPublic": { + "type": "boolean" + }, + "isActive": { + "type": "boolean" + }, + "capacity": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/LightMoney" + } + ] + }, + "localBalance": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/LightMoney" + } + ] + }, + "channelPoint": { + "type": "string", + "nullable": true + } + } + }, + "LightningInvoiceData": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/LightningInvoiceStatus" + }, + "bolT11": { + "type": "string", + "nullable": true + }, + "paidAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "expiresAt": { + "type": "string", + "format": "date-time" + }, + "amount": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/LightMoney" + } + ] + }, + "amountReceived": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/LightMoney" + } + ] + } + } + }, + "LightningInvoiceStatus": { + "type": "string", + "description": "", + "x-enumNames": [ + "Unpaid", + "Paid", + "Expired" + ], + "enum": [ + "Unpaid", + "Paid", + "Expired" + ] + }, + "LightningNodeInformationData": { + "type": "object", + "additionalProperties": false, + "properties": { + "nodeInfoList": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "blockHeight": { + "type": "integer", + "format": "int32" + } + } + }, + "PayLightningInvoiceRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "invoice": { + "type": "string", + "nullable": true + } + } + }, + "OpenLightningChannelRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "node": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/ConnectToNodeRequest" + } + ] + }, + "channelAmount": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/Money" + } + ] + }, + "feeRate": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/FeeRate" + } + ] + } + } + }, + "Money": { + "type": "integer", + "format": "int64" + }, + "FeeRate": { + "oneOf": [ + { + "type": "integer", + "format": "int64" + }, + { + "type": "number", + "format": "float" + } + ] + } + } + }, + "tags": [ + { + "name": "Lightning" + } + ] +} diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json index 8305cf2c6..312e61b75 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.stores.json @@ -347,6 +347,9 @@ }, "payJoinEnabled": { "type": "boolean" + }, + "lightningPrivateRouteHints": { + "type": "boolean" } } },