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,