From 6d7c11f1b15feeec8575cfd6c681d3cfee4d0f12 Mon Sep 17 00:00:00 2001 From: d11n Date: Mon, 17 Oct 2022 09:51:15 +0200 Subject: [PATCH] Greenfield: Get Lightning invoices (#4180) * Greenfield: Get Lightning invoices Matching the data added in btcpayserver/BTCPayServer.Lightning#98 and btcpayserver/BTCPayServer.Lightning#99. * Small adjustments Co-authored-by: nicolas.dorier --- .../BTCPayServerClient.Lightning.Internal.cs | 18 +++++ .../BTCPayServerClient.Lightning.Store.cs | 18 +++++ .../Models/LightningInvoiceData.cs | 4 + BTCPayServer.Tests/GreenfieldAPITests.cs | 39 +++++++--- ...ieldLightningNodeApiController.Internal.cs | 8 ++ ...enfieldLightningNodeApiController.Store.cs | 8 ++ .../GreenfieldLightningNodeApiController.cs | 16 +++- .../GreenField/LocalBTCPayServerClient.cs | 16 +++- .../v1/swagger.template.lightning.common.json | 5 ++ .../swagger.template.lightning.internal.json | 74 +++++++++++++++++- .../v1/swagger.template.lightning.store.json | 77 +++++++++++++++++++ 11 files changed, 270 insertions(+), 13 deletions(-) diff --git a/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs b/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs index ff5e1fa81..e2ac5ef84 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs @@ -95,6 +95,24 @@ namespace BTCPayServer.Client method: HttpMethod.Get), token); return await HandleResponse(response); } + + public virtual async Task GetLightningInvoices(string cryptoCode, + bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default) + { + var queryPayload = new Dictionary(); + if (pendingOnly is bool v) + { + queryPayload.Add("pendingOnly", v.ToString()); + } + if (offsetIndex is > 0) + { + queryPayload.Add("offsetIndex", offsetIndex); + } + + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices", queryPayload), token); + return await HandleResponse(response); + } public virtual async Task CreateLightningInvoice(string cryptoCode, CreateLightningInvoiceRequest request, CancellationToken token = default) diff --git a/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs b/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs index 72a224ff3..fd07b1eca 100644 --- a/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs +++ b/BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs @@ -97,6 +97,24 @@ namespace BTCPayServer.Client method: HttpMethod.Get), token); return await HandleResponse(response); } + + public virtual async Task GetLightningInvoices(string storeId, string cryptoCode, + bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default) + { + var queryPayload = new Dictionary(); + if (pendingOnly is bool v) + { + queryPayload.Add("pendingOnly", v.ToString()); + } + if (offsetIndex is > 0) + { + queryPayload.Add("offsetIndex", offsetIndex); + } + + var response = await _httpClient.SendAsync( + CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices", queryPayload), token); + return await HandleResponse(response); + } public virtual async Task CreateLightningInvoice(string storeId, string cryptoCode, CreateLightningInvoiceRequest request, CancellationToken token = default) diff --git a/BTCPayServer.Client/Models/LightningInvoiceData.cs b/BTCPayServer.Client/Models/LightningInvoiceData.cs index 7f855902c..c70d54eb3 100644 --- a/BTCPayServer.Client/Models/LightningInvoiceData.cs +++ b/BTCPayServer.Client/Models/LightningInvoiceData.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using BTCPayServer.Client.JsonConverters; using BTCPayServer.Lightning; using Newtonsoft.Json; @@ -26,5 +27,8 @@ namespace BTCPayServer.Client.Models [JsonConverter(typeof(LightMoneyJsonConverter))] public LightMoney AmountReceived { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public Dictionary CustomRecords { get; set; } } } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 970c6b72e..31f9a7855 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -1662,9 +1662,9 @@ namespace BTCPayServer.Tests var merchantClient = await merchant.CreateClient($"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}"); var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(LightMoney.Satoshis(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(Policies.CanUseInternalLightningNode); + var chargeClient = await user.CreateClient(Policies.CanUseInternalLightningNode); - var info = await client.GetLightningNodeInfo("BTC"); + var info = await chargeClient.GetLightningNodeInfo("BTC"); Assert.Single(info.NodeURIs); Assert.NotEqual(0, info.BlockHeight); Assert.NotNull(info.Alias); @@ -1675,10 +1675,10 @@ namespace BTCPayServer.Tests Assert.NotNull(info.InactiveChannelsCount); Assert.NotNull(info.PendingChannelsCount); - await AssertAPIError("lightning-node-unavailable", () => client.GetLightningNodeChannels("BTC")); + await AssertAPIError("lightning-node-unavailable", () => chargeClient.GetLightningNodeChannels("BTC")); // Not permission for the store! - await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels(user.StoreId, "BTC")); - var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest() + await AssertAPIError("missing-permission", () => chargeClient.GetLightningNodeChannels(user.StoreId, "BTC")); + var invoiceData = await chargeClient.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest() { Amount = LightMoney.Satoshis(1000), Description = "lol", @@ -1686,9 +1686,17 @@ namespace BTCPayServer.Tests PrivateRouteHints = false }); var chargeInvoice = invoiceData; - Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id)); + Assert.NotNull(await chargeClient.GetLightningInvoice("BTC", invoiceData.Id)); - client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}"); + // check list for internal node + var invoices = await chargeClient.GetLightningInvoices("BTC"); + var pendingInvoices = await chargeClient.GetLightningInvoices("BTC", true); + Assert.NotEmpty(invoices); + Assert.Contains(invoices, i => i.Id == invoiceData.Id); + Assert.NotEmpty(pendingInvoices); + Assert.Contains(pendingInvoices, i => i.Id == invoiceData.Id); + + var client = await user.CreateClient($"{Policies.CanUseLightningNodeInStore}:{user.StoreId}"); // Not permission for the server await AssertAPIError("missing-permission", () => client.GetLightningNodeChannels("BTC")); @@ -1706,6 +1714,11 @@ namespace BTCPayServer.Tests Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id)); + // check pending list + var merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true); + Assert.NotEmpty(merchantPendingInvoices); + Assert.Contains(merchantPendingInvoices, i => i.Id == merchantInvoice.Id); + await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() { BOLT11 = merchantInvoice.BOLT11 @@ -1726,6 +1739,15 @@ namespace BTCPayServer.Tests var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id); Assert.NotNull(invoice.PaidAt); Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount); + + // check list for store with paid invoice + var merchantInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC"); + merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true); + Assert.NotEmpty(merchantInvoices); + Assert.Empty(merchantPendingInvoices); + // if the test ran too many times the invoice might be on a later page + if (merchantInvoices.Length < 100) Assert.Contains(merchantInvoices, i => i.Id == merchantInvoice.Id); + // Amount received might be bigger because of internal implementation shit from lightning Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived); @@ -1733,7 +1755,6 @@ namespace BTCPayServer.Tests Assert.Single(info.NodeURIs); Assert.NotEqual(0, info.BlockHeight); - // As admin, can use the internal node through our store. await user.MakeAdmin(true); await user.RegisterInternalLightningNodeAsync("BTC"); @@ -1743,7 +1764,7 @@ namespace BTCPayServer.Tests await AssertPermissionError("btcpay.server.canuseinternallightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC")); // However, even as a guest, you should be able to create an invoice var guest = tester.NewAccount(); - guest.GrantAccess(false); + await guest.GrantAccessAsync(); await user.AddGuest(guest.UserId); client = await guest.CreateClient(Policies.CanCreateLightningInvoiceInStore); await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest() diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Internal.cs b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Internal.cs index 37c53358d..c4fd7f987 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Internal.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Internal.cs @@ -101,6 +101,14 @@ namespace BTCPayServer.Controllers.Greenfield return base.GetInvoice(cryptoCode, id, cancellationToken); } + [Authorize(Policy = Policies.CanUseInternalLightningNode, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/server/lightning/{cryptoCode}/invoices")] + public override Task GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default) + { + return base.GetInvoices(cryptoCode, pendingOnly, offsetIndex, cancellationToken); + } + [Authorize(Policy = Policies.CanUseInternalLightningNode, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices/pay")] diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Store.cs b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Store.cs index 9394c5ddf..a3a1063b3 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Store.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Store.cs @@ -111,6 +111,14 @@ namespace BTCPayServer.Controllers.Greenfield return base.GetInvoice(cryptoCode, id, cancellationToken); } + [Authorize(Policy = Policies.CanUseLightningNodeInStore, + AuthenticationSchemes = AuthenticationSchemes.Greenfield)] + [HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")] + public override Task GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default) + { + return base.GetInvoices(cryptoCode, pendingOnly, offsetIndex, cancellationToken); + } + [Authorize(Policy = Policies.CanCreateLightningInvoiceInStore, AuthenticationSchemes = AuthenticationSchemes.Greenfield)] [HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")] diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs index 19bda6338..acb241d10 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs @@ -246,6 +246,14 @@ namespace BTCPayServer.Controllers.Greenfield return inv == null ? this.CreateAPIError(404, "invoice-not-found", "Impossible to find a lightning invoice with this id") : Ok(ToModel(inv)); } + public virtual async Task GetInvoices(string cryptoCode, [FromQuery] bool? pendingOnly, [FromQuery] long? offsetIndex, CancellationToken cancellationToken = default) + { + var lightningClient = await GetLightningClient(cryptoCode, false); + var param = new ListInvoicesParams { PendingOnly = pendingOnly, OffsetIndex = offsetIndex }; + var invoices = await lightningClient.ListInvoices(param, cancellationToken); + return Ok(invoices.Select(ToModel)); + } + public virtual async Task CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request, CancellationToken cancellationToken = default) { var lightningClient = await GetLightningClient(cryptoCode, false); @@ -303,7 +311,7 @@ namespace BTCPayServer.Controllers.Greenfield private LightningInvoiceData ToModel(LightningInvoice invoice) { - return new LightningInvoiceData + var data = new LightningInvoiceData { Amount = invoice.Amount, Id = invoice.Id, @@ -313,6 +321,12 @@ namespace BTCPayServer.Controllers.Greenfield BOLT11 = invoice.BOLT11, ExpiresAt = invoice.ExpiresAt }; + + if (invoice.CustomRecords != null) + { + data.CustomRecords = invoice.CustomRecords; + } + return data; } private LightningPaymentData ToModel(LightningPayment payment) diff --git a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs index 1541a783c..3dc47812c 100644 --- a/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs +++ b/BTCPayServer/Controllers/GreenField/LocalBTCPayServerClient.cs @@ -351,7 +351,7 @@ namespace BTCPayServer.Controllers.Greenfield CancellationToken token = default) { return GetFromActionResult( - await GetController().GetBalance(cryptoCode)); + await GetController().GetBalance(cryptoCode, token)); } public override async Task ConnectToLightningNode(string storeId, string cryptoCode, @@ -394,6 +394,13 @@ namespace BTCPayServer.Controllers.Greenfield await GetController().GetInvoice(cryptoCode, invoiceId, token)); } + public override async Task GetLightningInvoices(string storeId, string cryptoCode, + bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default) + { + return GetFromActionResult( + await GetController().GetInvoices(cryptoCode, pendingOnly, offsetIndex, token)); + } + public override async Task CreateLightningInvoice(string storeId, string cryptoCode, CreateLightningInvoiceRequest request, CancellationToken token = default) { @@ -455,6 +462,13 @@ namespace BTCPayServer.Controllers.Greenfield await GetController().GetInvoice(cryptoCode, invoiceId, token)); } + public override async Task GetLightningInvoices(string cryptoCode, + bool? pendingOnly = null, long? offsetIndex = null, CancellationToken token = default) + { + return GetFromActionResult( + await GetController().GetInvoices(cryptoCode, pendingOnly, offsetIndex, token)); + } + public override async Task CreateLightningInvoice(string cryptoCode, CreateLightningInvoiceRequest request, CancellationToken token = default) diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json index 6040837fe..78f41eb42 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json @@ -148,6 +148,11 @@ "amountReceived": { "type": "string", "description": "The amount received in millisatoshi" + }, + "customRecords": { + "type": "object", + "nullable": true, + "description": "The custom TLV records attached to a keysend payment" } } }, diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.internal.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.internal.json index d33c09224..a426be8fe 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.internal.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.internal.json @@ -389,7 +389,6 @@ } }, - "/api/v1/server/lightning/{cryptoCode}/invoices/{id}": { "get": { "tags": [ @@ -406,7 +405,8 @@ "type": "string" }, "example": "BTC" - } , { + }, + { "name": "id", "in": "path", "required": true, @@ -446,6 +446,7 @@ ] } }, + "/api/v1/server/lightning/{cryptoCode}/invoices/pay": { "post": { "tags": [ @@ -524,7 +525,76 @@ ] } }, + "/api/v1/server/lightning/{cryptoCode}/invoices": { + "get": { + "tags": [ + "Lightning (Internal Node)" + ], + "summary": "Get invoices", + "parameters": [ + { + "name": "cryptoCode", + "in": "path", + "required": true, + "description": "The cryptoCode of the lightning-node to query", + "schema": { + "type": "string" + }, + "example": "BTC" + }, + { + "name": "pendingOnly", + "in": "query", + "required": false, + "description": "Limit to pending invoices only", + "schema": { + "type": "boolean", + "nullable": true, + "default": false + } + }, + { + "name": "offsetIndex", + "in": "query", + "required": false, + "description": "The index of an invoice that will be used as the start of the list", + "schema": { + "type": "number", + "nullable": true, + "default": 0 + } + } + ], + "description": "View information about the lightning invoices", + "operationId": "InternalLightningNodeApi_GetInvoices", + "responses": { + "200": { + "description": "Lightning invoice data", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LightningInvoiceData" + } + } + } + } + }, + "503": { + "description": "Unable to access the lightning node" + } + }, + "security": [ + { + "API_Key": [ + "btcpay.server.canuseinternallightningnode" + ], + "Basic": [] + } + ] + }, "post": { "tags": [ "Lightning (Internal Node)" diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.store.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.store.json index dad643f61..4e727ae70 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.store.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.store.json @@ -609,6 +609,83 @@ } }, "/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices": { + "get": { + "tags": [ + "Lightning (Store)" + ], + "summary": "Get invoices", + "parameters": [ + { + "name": "cryptoCode", + "in": "path", + "required": true, + "description": "The cryptoCode of the lightning-node to query", + "schema": { + "type": "string" + }, + "example": "BTC" + }, + { + "name": "storeId", + "in": "path", + "required": true, + "description": "The store id with the lightning-node configuration to query", + "schema": { + "type": "string" + } + }, + { + "name": "pendingOnly", + "in": "query", + "required": false, + "description": "Limit to pending invoices only", + "schema": { + "type": "boolean", + "nullable": true, + "default": false + } + }, + { + "name": "offsetIndex", + "in": "query", + "required": false, + "description": "The index of an invoice that will be used as the start of the list", + "schema": { + "type": "number", + "nullable": true, + "default": 0 + } + } + ], + "description": "View information about the lightning invoices", + "operationId": "StoreLightningNodeApi_GetInvoices", + "responses": { + "200": { + "description": "Lightning invoice data", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LightningInvoiceData" + } + } + } + } + }, + "503": { + "description": "Unable to access the lightning node" + } + }, + "security": [ + { + "API_Key": [ + "btcpay.store.cancreatelightninginvoice" + ], + "Basic": [] + } + ] + }, "post": { "tags": [ "Lightning (Store)"