mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 14:04:26 +01:00
Greenfield: Add payment hash and preimage to Lightning invoices (#4520)
* Greenfield: Add payment hash and preimage to Lightning invoices Closes #4475. * Greenfield: Add payment hash and preimage to invoice payment method details * Refactor LN payment method details retrieval
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.20" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.21" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.23" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -17,8 +17,14 @@ 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; }
|
||||
|
||||
|
||||
@@ -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,6 +2102,10 @@ 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);
|
||||
|
||||
@@ -2176,6 +2178,14 @@ namespace BTCPayServer.Tests
|
||||
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<GreenfieldValidationException>(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
|
||||
{
|
||||
BOLT11 = "lol"
|
||||
@@ -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")]
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.16" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.18" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Payments
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<bool> 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);
|
||||
|
||||
@@ -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());
|
||||
if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike)
|
||||
switch (details)
|
||||
{
|
||||
case BitcoinLikeOnChainPaymentMethod btcLike:
|
||||
btcLike.NextNetworkFee = NextNetworkFee;
|
||||
btcLike.DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress;
|
||||
btcLike.FeeRate = FeeRate;
|
||||
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;
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
throw new NotSupportedException(PaymentType);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user