diff --git a/BTCPayServer.Client/BTCPayServer.Client.csproj b/BTCPayServer.Client/BTCPayServer.Client.csproj index 3d9b6d174..4fa4ae3ea 100644 --- a/BTCPayServer.Client/BTCPayServer.Client.csproj +++ b/BTCPayServer.Client/BTCPayServer.Client.csproj @@ -28,7 +28,7 @@ - + diff --git a/BTCPayServer.Client/Models/LightningInvoiceData.cs b/BTCPayServer.Client/Models/LightningInvoiceData.cs index c70d54eb3..814113f38 100644 --- a/BTCPayServer.Client/Models/LightningInvoiceData.cs +++ b/BTCPayServer.Client/Models/LightningInvoiceData.cs @@ -16,9 +16,15 @@ namespace BTCPayServer.Client.Models [JsonProperty("BOLT11")] public string BOLT11 { get; set; } + + public string PaymentHash { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string Preimage { get; set; } [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] public DateTimeOffset? PaidAt { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))] public DateTimeOffset ExpiresAt { get; set; } diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index ef35ed671..f68079219 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -19,8 +19,6 @@ using BTCPayServer.Services; using BTCPayServer.Services.Custodian.Client.MockCustodian; using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications.Blobs; -using BTCPayServer.Services.Stores; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NBitcoin; @@ -1723,7 +1721,7 @@ namespace BTCPayServer.Tests using var tester = CreateServerTester(); await tester.StartAsync(); var user = tester.NewAccount(); - await user.GrantAccessAsync(); + await user.GrantAccessAsync(true); await user.MakeAdmin(); await user.SetupWebhook(); var client = await user.CreateClient(Policies.Unrestricted); @@ -2104,9 +2102,13 @@ namespace BTCPayServer.Tests merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST); 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))); + Assert.NotNull(merchantInvoice.Id); + Assert.NotNull(merchantInvoice.PaymentHash); + Assert.Equal(merchantInvoice.Id, merchantInvoice.PaymentHash); + // The default client is using charge, so we should not be able to query channels var chargeClient = await user.CreateClient(Policies.CanUseInternalLightningNode); - + var info = await chargeClient.GetLightningNodeInfo("BTC"); Assert.Single(info.NodeURIs); Assert.NotEqual(0, info.BlockHeight); @@ -2175,6 +2177,14 @@ namespace BTCPayServer.Tests Assert.NotNull(payResponse.FeeAmount); Assert.NotNull(payResponse.TotalAmount); Assert.NotNull(payResponse.PaymentHash); + + // check the get invoice response + var merchInvoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id); + Assert.NotNull(merchInvoice); + Assert.NotNull(merchInvoice.Preimage); + Assert.NotNull(merchInvoice.PaymentHash); + Assert.Equal(payResponse.Preimage, merchInvoice.Preimage); + Assert.Equal(payResponse.PaymentHash, merchInvoice.PaymentHash); await Assert.ThrowsAsync(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest() { @@ -2191,6 +2201,8 @@ namespace BTCPayServer.Tests var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id); Assert.NotNull(invoice.PaidAt); + Assert.NotNull(invoice.PaymentHash); + Assert.NotNull(invoice.Preimage); Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount); // check list for store with paid invoice @@ -2232,6 +2244,45 @@ namespace BTCPayServer.Tests await AssertPermissionError("btcpay.store.canuselightningnode", () => client.GetLightningNodeInfo(user.StoreId, "BTC")); } + [Fact(Timeout = 60 * 20 * 1000)] + [Trait("Integration", "Integration")] + [Trait("Lightning", "Lightning")] + public async Task CanAccessInvoiceLightningPaymentMethodDetails() + { + using var tester = CreateServerTester(); + tester.ActivateLightning(); + await tester.StartAsync(); + await tester.EnsureChannelsSetup(); + var user = tester.NewAccount(); + await user.GrantAccessAsync(true); + user.RegisterLightningNode("BTC", LightningConnectionType.CLightning); + + var client = await user.CreateClient(Policies.Unrestricted); + var invoice = await client.CreateInvoice(user.StoreId, + new CreateInvoiceRequest + { + Currency = "USD", + Amount = 100, + Checkout = new CreateInvoiceRequest.CheckoutOptions + { + PaymentMethods = new[] { "BTC-LightningNetwork" }, + DefaultPaymentMethod = "BTC_LightningLike" + } + }); + var pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)); + Assert.False(pm.AdditionalData.HasValues); + + var resp = await tester.CustomerLightningD.Pay(pm.Destination); + Assert.Equal(PayResult.Ok, resp.Result); + Assert.NotNull(resp.Details.PaymentHash); + Assert.NotNull(resp.Details.Preimage); + + pm = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, invoice.Id)); + Assert.True(pm.AdditionalData.HasValues); + Assert.Equal(resp.Details.PaymentHash.ToString(), pm.AdditionalData.GetValue("paymentHash")); + Assert.Equal(resp.Details.Preimage.ToString(), pm.AdditionalData.GetValue("preimage")); + } + [Fact(Timeout = 60 * 20 * 1000)] [Trait("Integration", "Integration")] [Trait("Lightning", "Lightning")] diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index bc2d33354..656186350 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -47,7 +47,7 @@ - + diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs index 20a7e4392..f85a95a5f 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs @@ -534,7 +534,7 @@ namespace BTCPayServer.Controllers.Greenfield var payments = method.ParentEntity.GetPayments(includeAccountedPaymentOnly).Where(paymentEntity => paymentEntity.GetPaymentMethodId() == method.GetId()); - return new InvoicePaymentMethodDataModel() + return new InvoicePaymentMethodDataModel { Activated = details.Activated, PaymentMethod = method.GetId().ToStringNormalized(), diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs index b3bd8d6fd..e485bc5d2 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs @@ -264,9 +264,12 @@ namespace BTCPayServer.Controllers.Greenfield }), PayResult.Ok => Ok(new LightningPaymentData { + BOLT11 = bolt11?.ToString(), Status = LightningPaymentStatus.Complete, TotalAmount = result.Details?.TotalAmount, - FeeAmount = result.Details?.FeeAmount + FeeAmount = result.Details?.FeeAmount, + PaymentHash = result.Details?.PaymentHash.ToString(), + Preimage = result.Details?.Preimage.ToString() }), _ => throw new NotSupportedException("Unsupported PayResult") }; @@ -353,7 +356,9 @@ namespace BTCPayServer.Controllers.Greenfield AmountReceived = invoice.AmountReceived, PaidAt = invoice.PaidAt, BOLT11 = invoice.BOLT11, - ExpiresAt = invoice.ExpiresAt + ExpiresAt = invoice.ExpiresAt, + PaymentHash = invoice.PaymentHash, + Preimage = invoice.Preimage }; if (invoice.CustomRecords != null) diff --git a/BTCPayServer/Controllers/UILNURLController.cs b/BTCPayServer/Controllers/UILNURLController.cs index 45540600a..d838a2872 100644 --- a/BTCPayServer/Controllers/UILNURLController.cs +++ b/BTCPayServer/Controllers/UILNURLController.cs @@ -594,6 +594,8 @@ namespace BTCPayServer } paymentMethodDetails.BOLT11 = invoice.BOLT11; + paymentMethodDetails.PaymentHash = string.IsNullOrEmpty(invoice.PaymentHash) ? null : uint256.Parse(invoice.PaymentHash); + paymentMethodDetails.Preimage = string.IsNullOrEmpty(invoice.Preimage) ? null : uint256.Parse(invoice.Preimage); paymentMethodDetails.InvoiceId = invoice.Id; paymentMethodDetails.GeneratedBoltAmount = new LightMoney(amount.Value); if (lnurlSupportedPaymentMethod.LUD12Enabled) @@ -604,7 +606,6 @@ namespace BTCPayServer lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails); await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod); - _eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(invoiceId, paymentMethodDetails, pmi)); return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse diff --git a/BTCPayServer/Payments/IPaymentMethodDetails.cs b/BTCPayServer/Payments/IPaymentMethodDetails.cs index 7d537d39c..2dbca5a2f 100644 --- a/BTCPayServer/Payments/IPaymentMethodDetails.cs +++ b/BTCPayServer/Payments/IPaymentMethodDetails.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using BTCPayServer.Services.Invoices; using Newtonsoft.Json.Linq; namespace BTCPayServer.Payments diff --git a/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentMethodDetails.cs b/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentMethodDetails.cs index 99201fc85..47e2a08b5 100644 --- a/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentMethodDetails.cs +++ b/BTCPayServer/Payments/LNURLPay/LNURLPayPaymentMethodDetails.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using BTCPayServer.Client.JsonConverters; using BTCPayServer.Lightning; using BTCPayServer.Payments.Lightning; +using BTCPayServer.Services.Invoices; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs index aed52bd31..6bf5d8415 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs @@ -11,11 +11,19 @@ namespace BTCPayServer.Payments.Lightning { [JsonIgnore] public BTCPayNetworkBase Network { get; set; } + [JsonConverter(typeof(LightMoneyJsonConverter))] public LightMoney Amount { get; set; } + public string BOLT11 { get; set; } + [JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))] public uint256 PaymentHash { get; set; } + + [JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))] + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public uint256 Preimage { get; set; } + public string PaymentType { get; set; } public string GetDestination() @@ -25,7 +33,6 @@ namespace BTCPayServer.Payments.Lightning public decimal NetworkFee { get; set; } - public string GetPaymentId() { // Legacy, some old payments don't have the PaymentHash set diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index e376c2c32..62c0cf8e8 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -89,7 +89,7 @@ namespace BTCPayServer.Payments.Lightning if (expiry < TimeSpan.Zero) expiry = TimeSpan.FromSeconds(1); - LightningInvoice? lightningInvoice = null; + LightningInvoice? lightningInvoice; string description = storeBlob.LightningDescriptionTemplate; description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase) @@ -118,6 +118,7 @@ namespace BTCPayServer.Payments.Lightning Activated = true, BOLT11 = lightningInvoice.BOLT11, PaymentHash = BOLT11PaymentRequest.Parse(lightningInvoice.BOLT11, network.NBitcoinNetwork).PaymentHash, + Preimage = string.IsNullOrEmpty(lightningInvoice.Preimage) ? null : uint256.Parse(lightningInvoice.Preimage), InvoiceId = lightningInvoice.Id, NodeInfo = (await nodeInfo).FirstOrDefault()?.ToString() }; diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs index 6863487eb..920ebceb3 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentMethodDetails.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Linq; using BTCPayServer.Lightning; +using BTCPayServer.Services.Invoices; using NBitcoin; using Newtonsoft.Json.Linq; @@ -9,6 +11,7 @@ namespace BTCPayServer.Payments.Lightning { public string BOLT11 { get; set; } public uint256 PaymentHash { get; set; } + public uint256 Preimage { get; set; } public string InvoiceId { get; set; } public string NodeInfo { get; set; } @@ -45,7 +48,12 @@ namespace BTCPayServer.Payments.Lightning public virtual JObject GetAdditionalData() { - return new(); + var result = new JObject(); + if (PaymentHash != null && PaymentHash != default) + result.Add("paymentHash", new JValue(PaymentHash.ToString())); + if (Preimage != null && Preimage != default) + result.Add("preimage", new JValue(Preimage.ToString())); + return result; } } } diff --git a/BTCPayServer/Payments/Lightning/LightningListener.cs b/BTCPayServer/Payments/Lightning/LightningListener.cs index f9ebe5b85..985ffd10f 100644 --- a/BTCPayServer/Payments/Lightning/LightningListener.cs +++ b/BTCPayServer/Payments/Lightning/LightningListener.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using NBitcoin; using NBXplorer; namespace BTCPayServer.Payments.Lightning @@ -529,10 +530,11 @@ namespace BTCPayServer.Payments.Lightning public async Task AddPayment(LightningInvoice notification, string invoiceId, PaymentType paymentType) { - var payment = await _paymentService.AddPayment(invoiceId, notification.PaidAt.Value, new LightningLikePaymentData() + var payment = await _paymentService.AddPayment(invoiceId, notification.PaidAt.Value, new LightningLikePaymentData { BOLT11 = notification.BOLT11, PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, _network.NBitcoinNetwork).PaymentHash, + Preimage = string.IsNullOrEmpty(notification.Preimage) ? null : uint256.Parse(notification.Preimage), Amount = notification.AmountReceived ?? notification.Amount, // if running old version amount received might be unavailable, PaymentType = paymentType.ToString() }, _network, accounted: true); diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index 071494486..116fed723 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -9,6 +9,7 @@ using BTCPayServer.JsonConverters; using BTCPayServer.Models; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; +using BTCPayServer.Payments.Lightning; using NBitcoin; using NBitcoin.DataEncoders; using NBitpayClient; @@ -989,25 +990,35 @@ namespace BTCPayServer.Services.Invoices // Legacy, old code does not have PaymentMethods if (string.IsNullOrEmpty(PaymentType) || PaymentMethodDetails == null) { - return new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() + return new BitcoinLikeOnChainPaymentMethod { FeeRate = FeeRate, DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress, NextNetworkFee = NextNetworkFee }; } - else + + IPaymentMethodDetails details = GetId().PaymentType.DeserializePaymentMethodDetails(Network, PaymentMethodDetails.ToString()); + switch (details) { - IPaymentMethodDetails details = GetId().PaymentType.DeserializePaymentMethodDetails(Network, PaymentMethodDetails.ToString()); - if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike) - { + case BitcoinLikeOnChainPaymentMethod btcLike: btcLike.NextNetworkFee = NextNetworkFee; btcLike.DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress; btcLike.FeeRate = FeeRate; - } - return details; + break; + case LightningLikePaymentMethodDetails lnLike: + // use set properties and fall back to values from payment data + var payments = ParentEntity.GetPayments(true).Where(paymentEntity => + paymentEntity.GetPaymentMethodId() == GetId()); + var payment = payments.Select(p => p.GetCryptoPaymentData() as LightningLikePaymentData).FirstOrDefault(); + var paymentHash = payment?.PaymentHash != null && payment.PaymentHash != default ? payment.PaymentHash : null; + var preimage = payment?.Preimage != null && payment.Preimage != default ? payment.Preimage : null; + lnLike.PaymentHash = lnLike.PaymentHash != null && lnLike.PaymentHash != default ? lnLike.PaymentHash : paymentHash; + lnLike.Preimage = lnLike.Preimage != null && lnLike.Preimage != default ? lnLike.Preimage : preimage; + break; } - throw new NotSupportedException(PaymentType); + + return details; #pragma warning restore CS0618 // Type or member is obsolete } diff --git a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json index 0d875a154..21edb2ae4 100644 --- a/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json +++ b/BTCPayServer/wwwroot/swagger/v1/swagger.template.lightning.common.json @@ -149,6 +149,15 @@ "type": "string", "description": "The amount received in millisatoshi" }, + "paymentHash": { + "type": "string", + "description": "The payment hash" + }, + "preimage": { + "type": "string", + "nullable": true, + "description": "The payment preimage (available when status is complete)" + }, "customRecords": { "type": "object", "nullable": true,