LNURL Payment Method Support (#2897)

* LNURL Payment Method Support

* Merge recent Lightning controller related changes

* Fix build

* Create separate payment settings section for stores

* Improve LNURL configuration

* Prevent duplicate array entries when merging Swagger JSON

* Fix CanSetPaymentMethodLimitsLightning

* Fix CanUsePayjoinViaUI

* Adapt test for new cancel bolt invoice feature

* rebase fixes

* Fixes after rebase

* Test fixes

* Do not turn LNURL on by default, Off-Chain payment criteria should affects both BOLT11 and LNURL, Payment criteria of unset payment method shouldn't be shown

* Send better error if payment method not found

* Revert "Prevent duplicate array entries when merging Swagger JSON"

This reverts commit 5783db9eda.

* Fix LNUrl doc

* Fix some warnings

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
Andrew Camilleri 2021-10-25 08:18:02 +02:00 committed by GitHub
parent fbdd2fc470
commit 951bfeefb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1996 additions and 480 deletions

View file

@ -29,7 +29,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="NBitcoin" Version="6.0.15" /> <PackageReference Include="NBitcoin" Version="6.0.15" />
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.6" /> <PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="icon.png" Pack="true" PackagePath="\" /> <None Include="icon.png" Pack="true" PackagePath="\" />

View file

@ -0,0 +1,59 @@
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 virtual async Task<IEnumerable<LNURLPayPaymentMethodData>>
GetStoreLNURLPayPaymentMethods(string storeId, bool? enabled = null,
CancellationToken token = default)
{
var query = new Dictionary<string, object>();
if (enabled != null)
{
query.Add(nameof(enabled), enabled);
}
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay",
query), token);
return await HandleResponse<IEnumerable<LNURLPayPaymentMethodData>>(response);
}
public virtual async Task<LNURLPayPaymentMethodData> GetStoreLNURLPayPaymentMethod(
string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}"), token);
return await HandleResponse<LNURLPayPaymentMethodData>(response);
}
public virtual async Task RemoveStoreLNURLPayPaymentMethod(string storeId,
string cryptoCode, CancellationToken token = default)
{
var response =
await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}",
method: HttpMethod.Delete), token);
await HandleResponse(response);
}
public virtual async Task<LNURLPayPaymentMethodData> UpdateStoreLNURLPayPaymentMethod(
string storeId,
string cryptoCode, LNURLPayPaymentMethodData paymentMethod,
CancellationToken token = default)
{
var response = await _httpClient.SendAsync(
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}",
bodyPayload: paymentMethod, method: HttpMethod.Put), token);
return await HandleResponse<LNURLPayPaymentMethodData>(response);
}
}
}

View file

@ -0,0 +1,14 @@
namespace BTCPayServer.Client.Models
{
public class LNURLPayPaymentMethodBaseData
{
public bool UseBech32Scheme { get; set; }
public bool EnableForStandardInvoices { get; set; }
public bool LUD12Enabled { get; set; }
public LNURLPayPaymentMethodBaseData()
{
}
}
}

View file

@ -0,0 +1,27 @@
namespace BTCPayServer.Client.Models
{
public class LNURLPayPaymentMethodData: LNURLPayPaymentMethodBaseData
{
/// <summary>
/// Whether the payment method is enabled
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Crypto code of the payment method
/// </summary>
public string CryptoCode { get; set; }
public LNURLPayPaymentMethodData()
{
}
public LNURLPayPaymentMethodData(string cryptoCode, bool enabled, bool useBech32Scheme, bool enableForStandardInvoices)
{
Enabled = enabled;
CryptoCode = cryptoCode;
UseBech32Scheme = useBech32Scheme;
EnableForStandardInvoices = enableForStandardInvoices;
}
}
}

View file

@ -26,7 +26,6 @@ namespace BTCPayServer.Data
public string? Destination { get; set; } public string? Destination { get; set; }
#nullable restore #nullable restore
internal static void OnModelCreating(ModelBuilder builder) internal static void OnModelCreating(ModelBuilder builder)
{ {
builder.Entity<PayoutData>() builder.Entity<PayoutData>()

View file

@ -7,7 +7,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.11.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.11.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" /> <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="NBitcoin" Version="6.0.10" /> <PackageReference Include="NBitcoin" Version="6.0.10" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" /> <PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" />
</ItemGroup> </ItemGroup>

View file

@ -77,7 +77,7 @@ namespace BTCPayServer.Tests
// Get enabled state from overview action // Get enabled state from overview action
StoreViewModel storeModel; StoreViewModel storeModel;
response = await controller.UpdateStore(); response = controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model; storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
var lnNode = storeModel.LightningNodes.Find(node => node.CryptoCode == cryptoCode); var lnNode = storeModel.LightningNodes.Find(node => node.CryptoCode == cryptoCode);
Assert.NotNull(lnNode); Assert.NotNull(lnNode);
@ -89,7 +89,7 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(response); Assert.IsType<ViewResult>(response);
// Get enabled state from overview action // Get enabled state from overview action
response = await controller.UpdateStore(); response = controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model; storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
var derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode); var derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode);
Assert.NotNull(derivationScheme); Assert.NotNull(derivationScheme);
@ -98,7 +98,7 @@ namespace BTCPayServer.Tests
// Disable wallet // Disable wallet
response = controller.SetWalletEnabled(storeId, cryptoCode, false).GetAwaiter().GetResult(); response = controller.SetWalletEnabled(storeId, cryptoCode, false).GetAwaiter().GetResult();
Assert.IsType<RedirectToActionResult>(response); Assert.IsType<RedirectToActionResult>(response);
response = await controller.UpdateStore(); response = controller.UpdateStore();
storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model; storeModel = (StoreViewModel)Assert.IsType<ViewResult>(response).Model;
derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode); derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode);
Assert.NotNull(derivationScheme); Assert.NotNull(derivationScheme);

View file

@ -20,7 +20,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" /> <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
<PackageReference Include="Selenium.Support" Version="3.141.0" /> <PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" /> <PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="94.0.4606.6100" /> <PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="94.0.4606.6100" />

View file

@ -137,10 +137,10 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(true); s.RegisterNewUser(true);
var store = s.CreateNewStore(); var store = s.CreateNewStore();
s.AddLightningNode(); s.AddLightningNode();
s.GoToStore(store.storeId); s.GoToStore(store.storeId, StoreNavPages.Payment);
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true); s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
s.Driver.FindElement(By.Id("Save")).Click(); s.Driver.FindElement(By.Id("Save")).Click();
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text); Assert.Contains("Payment settings successfully updated", s.FindAlertMessage().Text);
var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com"); var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com");
s.GoToInvoiceCheckout(invoiceId); s.GoToInvoiceCheckout(invoiceId);

View file

@ -165,7 +165,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(); user.GrantAccess();
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
await user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never); await user.SetNetworkFeeMode(NetworkFeeMode.Never);
var apps = user.GetController<AppsController>(); var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model); var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test"; vm.Name = "test";

View file

@ -2042,7 +2042,7 @@ namespace BTCPayServer.Tests
void VerifyLightning(Dictionary<string, GenericPaymentMethodData> dictionary) void VerifyLightning(Dictionary<string, GenericPaymentMethodData> dictionary)
{ {
Assert.True(dictionary.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToStringNormalized(), out var item)); Assert.True(dictionary.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToStringNormalized(), out var item));
var lightningNetworkPaymentMethodBaseData =Assert.IsType<JObject>(item.Data).ToObject<LightningNetworkPaymentMethodBaseData>(); var lightningNetworkPaymentMethodBaseData = Assert.IsType<JObject>(item.Data).ToObject<LightningNetworkPaymentMethodBaseData>();
Assert.Equal("Internal Node", lightningNetworkPaymentMethodBaseData.ConnectionString); Assert.Equal("Internal Node", lightningNetworkPaymentMethodBaseData.ConnectionString);
} }
@ -2057,7 +2057,7 @@ namespace BTCPayServer.Tests
void VerifyOnChain(Dictionary<string, GenericPaymentMethodData> dictionary) void VerifyOnChain(Dictionary<string, GenericPaymentMethodData> dictionary)
{ {
Assert.True(dictionary.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), out var item)); Assert.True(dictionary.TryGetValue(new PaymentMethodId("BTC", PaymentTypes.BTCLike).ToStringNormalized(), out var item));
var paymentMethodBaseData =Assert.IsType<JObject>(item.Data).ToObject<OnChainPaymentMethodBaseData>(); var paymentMethodBaseData = Assert.IsType<JObject>(item.Data).ToObject<OnChainPaymentMethodBaseData>();
Assert.Equal(randK, paymentMethodBaseData.DerivationScheme); Assert.Equal(randK, paymentMethodBaseData.DerivationScheme);
} }
@ -2091,6 +2091,5 @@ namespace BTCPayServer.Tests
} }
} }
} }

View file

@ -20,6 +20,7 @@ using BTCPayServer.Views.Wallets;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using NBitcoin; using NBitcoin;
using BTCPayServer.BIP78.Sender; using BTCPayServer.BIP78.Sender;
using BTCPayServer.Views.Stores;
using NBitcoin.Payment; using NBitcoin.Payment;
using NBitpayClient; using NBitpayClient;
using NBXplorer.DerivationStrategy; using NBXplorer.DerivationStrategy;
@ -301,7 +302,7 @@ namespace BTCPayServer.Tests
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21); Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
s.GoToHome(); s.GoToHome();
s.GoToStore(receiver.storeId); s.GoToStore(receiver.storeId, StoreNavPages.Payment);
Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected); Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
var sender = s.CreateNewStore(); var sender = s.CreateNewStore();
@ -570,9 +571,9 @@ namespace BTCPayServer.Tests
address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address; address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address;
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m)); tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m));
await notifications.NextEventAsync(); await notifications.NextEventAsync();
await bob.ModifyStore(s => s.PayJoinEnabled = true); await bob.ModifyPayment(p => p.PayJoinEnabled = true);
var invoice = bob.BitPay.CreateInvoice( var invoice = bob.BitPay.CreateInvoice(
new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true }); new Invoice { Price = 0.1m, Currency = "BTC", FullNotifications = true });
var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21, var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
tester.ExplorerClient.Network.NBitcoinNetwork); tester.ExplorerClient.Network.NBitcoinNetwork);

View file

@ -109,7 +109,7 @@ namespace BTCPayServer.Tests
{ {
await RegisterAsync(isAdmin); await RegisterAsync(isAdmin);
await CreateStoreAsync(); await CreateStoreAsync();
var store = this.GetController<StoresController>(); var store = GetController<StoresController>();
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant); var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString())); Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId); await store.Pair(pairingCode.ToString(), StoreId);
@ -127,19 +127,19 @@ namespace BTCPayServer.Tests
public async Task SetNetworkFeeMode(NetworkFeeMode mode) public async Task SetNetworkFeeMode(NetworkFeeMode mode)
{ {
await ModifyStore(store => await ModifyPayment(payment =>
{ {
store.NetworkFeeMode = mode; payment.NetworkFeeMode = mode;
}); });
} }
public async Task ModifyStore(Action<StoreViewModel> modify) public async Task ModifyPayment(Action<PaymentViewModel> modify)
{ {
var storeController = GetController<StoresController>(); var storeController = GetController<StoresController>();
var response = await storeController.UpdateStore(); var response = await storeController.Payment();
StoreViewModel store = (StoreViewModel)((ViewResult)response).Model; PaymentViewModel payment = (PaymentViewModel)((ViewResult)response).Model;
modify(store); modify(payment);
storeController.UpdateStore(store).GetAwaiter().GetResult(); await storeController.Payment(payment);
} }
public T GetController<T>(bool setImplicitStore = true) where T : Controller public T GetController<T>(bool setImplicitStore = true) where T : Controller
@ -190,7 +190,7 @@ namespace BTCPayServer.Tests
public Task EnablePayJoin() public Task EnablePayJoin()
{ {
return ModifyStore(s => s.PayJoinEnabled = true); return ModifyPayment(p => p.PayJoinEnabled = true);
} }
public GenerateWalletResponse GenerateWalletResponseV { get; set; } public GenerateWalletResponse GenerateWalletResponseV { get; set; }
@ -240,23 +240,26 @@ namespace BTCPayServer.Tests
public bool IsAdmin { get; internal set; } public bool IsAdmin { get; internal set; }
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true) public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true, Action<LightningNodeViewModel> setViewModel = null)
{ {
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult(); RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant, setViewModel: setViewModel).GetAwaiter().GetResult();
} }
public Task RegisterLightningNodeAsync(string cryptoCode, bool isMerchant = true, string storeId = null) public Task RegisterLightningNodeAsync(string cryptoCode, bool isMerchant = true, string storeId = null, Action<LightningNodeViewModel> setViewModel = null)
{ {
return RegisterLightningNodeAsync(cryptoCode, null, isMerchant, storeId); return RegisterLightningNodeAsync(cryptoCode, null, isMerchant, storeId, setViewModel);
} }
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType? connectionType, bool isMerchant = true, string storeId = null) public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType? connectionType, bool isMerchant = true, string storeId = null, Action<LightningNodeViewModel> setViewModel = null)
{ {
var storeController = GetController<StoresController>(); var storeController = GetController<StoresController>();
var connectionString = parent.GetLightningConnectionString(connectionType, isMerchant); var connectionString = parent.GetLightningConnectionString(connectionType, isMerchant);
var nodeType = connectionString == LightningSupportedPaymentMethod.InternalNode ? LightningNodeType.Internal : LightningNodeType.Custom; var nodeType = connectionString == LightningSupportedPaymentMethod.InternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
var vm = new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true };
if (setViewModel != null)
setViewModel(vm);
await storeController.SetupLightningNode(storeId ?? StoreId, await storeController.SetupLightningNode(storeId ?? StoreId,
new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true }, "save", cryptoCode); vm, "save", cryptoCode);
if (storeController.ModelState.ErrorCount != 0) if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage); Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
} }

View file

@ -24,6 +24,7 @@ using BTCPayServer.Fido2.Models;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Hosting; using BTCPayServer.Hosting;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels; using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.AppViewModels;
@ -818,11 +819,11 @@ namespace BTCPayServer.Tests
// Set tolerance to 50% // Set tolerance to 50%
var stores = user.GetController<StoresController>(); var stores = user.GetController<StoresController>();
var response = await stores.UpdateStore(); var response = await stores.Payment();
var vm = Assert.IsType<StoreViewModel>(Assert.IsType<ViewResult>(response).Model); var vm = Assert.IsType<PaymentViewModel>(Assert.IsType<ViewResult>(response).Model);
Assert.Equal(0.0, vm.PaymentTolerance); Assert.Equal(0.0, vm.PaymentTolerance);
vm.PaymentTolerance = 50.0; vm.PaymentTolerance = 50.0;
Assert.IsType<RedirectToActionResult>(stores.UpdateStore(vm).Result); Assert.IsType<RedirectToActionResult>(stores.Payment(vm).Result);
var invoice = user.BitPay.CreateInvoice( var invoice = user.BitPay.CreateInvoice(
new Invoice() new Invoice()
@ -996,8 +997,7 @@ namespace BTCPayServer.Tests
Assert.Equal(4, tor.Services.Length); Assert.Equal(4, tor.Services.Length);
} }
[Fact(Timeout = 60 * 2 * 1000)] [Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")] [Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")] [Trait("Lightning", "Lightning")]
@ -1012,7 +1012,7 @@ namespace BTCPayServer.Tests
await user.RegisterDerivationSchemeAsync("BTC"); await user.RegisterDerivationSchemeAsync("BTC");
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning); await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
await user.SetNetworkFeeMode(NetworkFeeMode.Never); await user.SetNetworkFeeMode(NetworkFeeMode.Never);
await user.ModifyStore(model => model.SpeedPolicy = SpeedPolicy.HighSpeed); await user.ModifyPayment(p => p.SpeedPolicy = SpeedPolicy.HighSpeed);
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC")); var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.0001m, "BTC"));
await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () => await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () =>
{ {
@ -1042,14 +1042,22 @@ namespace BTCPayServer.Tests
Assert.Contains(fetchedInvoice.Status, new[] { InvoiceStatusLegacy.Complete, InvoiceStatusLegacy.Confirmed }); Assert.Contains(fetchedInvoice.Status, new[] { InvoiceStatusLegacy.Complete, InvoiceStatusLegacy.Confirmed });
Assert.Equal(InvoiceExceptionStatus.None, fetchedInvoice.ExceptionStatus); Assert.Equal(InvoiceExceptionStatus.None, fetchedInvoice.ExceptionStatus);
Logs.Tester.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice "); //BTCPay will attempt to cancel previous bolt11 invoices so that there are less weird edge case scenarios
evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () => Logs.Tester.LogInformation($"Attempting to pay invoice {invoice.Id} original full amount bolt11 invoice ");
await Assert.ThrowsAsync<LightningRPCException>(async () =>
{ {
await tester.SendLightningPaymentAsync(invoice); await tester.SendLightningPaymentAsync(invoice);
}, evt => evt.InvoiceId == invoice.Id); });
Assert.Equal(evt.InvoiceId, invoice.Id);
fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId); //NOTE: Eclair does not support cancelling invoice so the below test case would make sense for it
Assert.Equal(3, fetchedInvoice.Payments.Count); // Logs.Tester.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice ");
// evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
// {
// await tester.SendLightningPaymentAsync(invoice);
// }, evt => evt.InvoiceId == invoice.Id);
// Assert.Equal(evt.InvoiceId, invoice.Id);
// fetchedInvoice = await tester.PayTester.InvoiceRepository.GetInvoice(evt.InvoiceId);
// Assert.Equal(3, fetchedInvoice.Payments.Count);
} }
[Fact(Timeout = 60 * 2 * 1000)] [Fact(Timeout = 60 * 2 * 1000)]
@ -1065,7 +1073,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(true); user.GrantAccess(true);
var storeController = user.GetController<StoresController>(); var storeController = user.GetController<StoresController>();
var storeResponse = await storeController.UpdateStore(); var storeResponse = storeController.UpdateStore();
Assert.IsType<ViewResult>(storeResponse); Assert.IsType<ViewResult>(storeResponse);
Assert.IsType<ViewResult>(await storeController.SetupLightningNode(user.StoreId, "BTC")); Assert.IsType<ViewResult>(await storeController.SetupLightningNode(user.StoreId, "BTC"));
@ -1089,7 +1097,7 @@ namespace BTCPayServer.Tests
new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri }, new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri },
"save", "BTC").GetAwaiter().GetResult()); "save", "BTC").GetAwaiter().GetResult());
storeResponse = await storeController.UpdateStore(); storeResponse = storeController.UpdateStore();
var storeVm = var storeVm =
Assert.IsType<StoreViewModel>(Assert Assert.IsType<StoreViewModel>(Assert
.IsType<ViewResult>(storeResponse).Model); .IsType<ViewResult>(storeResponse).Model);
@ -1205,7 +1213,7 @@ namespace BTCPayServer.Tests
var acc = tester.NewAccount(); var acc = tester.NewAccount();
acc.GrantAccess(); acc.GrantAccess();
acc.RegisterDerivationScheme("BTC"); acc.RegisterDerivationScheme("BTC");
await acc.ModifyStore(s => s.SpeedPolicy = SpeedPolicy.LowSpeed); await acc.ModifyPayment(p => p.SpeedPolicy = SpeedPolicy.LowSpeed);
var invoice = acc.BitPay.CreateInvoice(new Invoice var invoice = acc.BitPay.CreateInvoice(new Invoice
{ {
Price = 5.0m, Price = 5.0m,
@ -2032,7 +2040,7 @@ namespace BTCPayServer.Tests
}); });
Assert.Equal(404, (int)response.StatusCode); Assert.Equal(404, (int)response.StatusCode);
await user.ModifyStore(s => s.AnyoneCanCreateInvoice = true); await user.ModifyPayment(p => p.AnyoneCanCreateInvoice = true);
Logs.Tester.LogInformation("Bad store with anyone can create invoice = 403"); Logs.Tester.LogInformation("Bad store with anyone can create invoice = 403");
response = await tester.PayTester.HttpClient.SendAsync( response = await tester.PayTester.HttpClient.SendAsync(
@ -2306,7 +2314,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(true); user.GrantAccess(true);
user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("BTC");
await user.ModifyStore(s => await user.ModifyPayment(s =>
{ {
Assert.Equal("USD", s.DefaultCurrency); Assert.Equal("USD", s.DefaultCurrency);
s.DefaultCurrency = "EUR"; s.DefaultCurrency = "EUR";
@ -2357,7 +2365,7 @@ namespace BTCPayServer.Tests
// We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice // We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model); .IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
Assert.Equal(2, vm.PaymentMethodCriteria.Count); Assert.Equal(3, vm.PaymentMethodCriteria.Count);
var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString())); var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString()));
Assert.Equal(new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), criteria.PaymentMethod); Assert.Equal(new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), criteria.PaymentMethod);
criteria.Value = "5 USD"; criteria.Value = "5 USD";
@ -2448,12 +2456,12 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR); Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR);
// enable unified QR code in settings // enable unified QR code in settings
var vm = Assert.IsType<StoreViewModel>(Assert var vm = Assert.IsType<PaymentViewModel>(Assert
.IsType<ViewResult>(await user.GetController<StoresController>().UpdateStore()).Model .IsType<ViewResult>(await user.GetController<StoresController>().Payment()).Model
); );
vm.OnChainWithLnInvoiceFallback = true; vm.OnChainWithLnInvoiceFallback = true;
Assert.IsType<RedirectToActionResult>( Assert.IsType<RedirectToActionResult>(
user.GetController<StoresController>().UpdateStore(vm).Result user.GetController<StoresController>().Payment(vm).Result
); );
// validate that QR code now has both onchain and offchain payment urls // validate that QR code now has both onchain and offchain payment urls
@ -2470,7 +2478,7 @@ namespace BTCPayServer.Tests
Assert.True($"bitcoin:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split); Assert.True($"bitcoin:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split);
// Fallback lightning invoice should be uppercase inside the QR code. // Fallback lightning invoice should be uppercase inside the QR code.
var lightningFallback = paymentMethodSecond.InvoiceBitcoinUrlQR.Split(new string[] { "&lightning=" }, StringSplitOptions.None)[1]; var lightningFallback = paymentMethodSecond.InvoiceBitcoinUrlQR.Split(new [] { "&lightning=" }, StringSplitOptions.None)[1];
Assert.True(lightningFallback.ToUpperInvariant() == lightningFallback); Assert.True(lightningFallback.ToUpperInvariant() == lightningFallback);
} }
} }
@ -2488,10 +2496,8 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount(); var user = tester.NewAccount();
user.GrantAccess(true); user.GrantAccess(true);
user.RegisterLightningNode("BTC", LightningConnectionType.Charge); user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert var vm = user.GetController<StoresController>().CheckoutExperience().AssertViewModel<CheckoutExperienceViewModel>();
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model); var criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Single(vm.PaymentMethodCriteria);
var criteria = vm.PaymentMethodCriteria.First();
Assert.Equal(new PaymentMethodId("BTC", LightningPaymentType.Instance).ToString(), criteria.PaymentMethod); Assert.Equal(new PaymentMethodId("BTC", LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
criteria.Value = "2 USD"; criteria.Value = "2 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan; criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan;
@ -2499,18 +2505,42 @@ namespace BTCPayServer.Tests
.Result); .Result);
var invoice = user.BitPay.CreateInvoice( var invoice = user.BitPay.CreateInvoice(
new Invoice() new Invoice
{ {
Price = 1.5m, Price = 1.5m,
Currency = "USD", Currency = "USD"
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant); }, Facade.Merchant);
Assert.Single(invoice.CryptoInfo); Assert.Single(invoice.CryptoInfo);
Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType); Assert.Equal(PaymentTypes.LightningLike.ToString(), invoice.CryptoInfo[0].PaymentType);
// Activating LNUrl, we should still have only 1 payment criteria that can be set.
user.RegisterLightningNode("BTC", LightningConnectionType.Charge, setViewModel: vm =>
{
vm.LNURLEnabled = true;
vm.LNURLStandardInvoiceEnabled = true;
});
vm = user.GetController<StoresController>().CheckoutExperience().AssertViewModel<CheckoutExperienceViewModel>();
criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(new PaymentMethodId("BTC", LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().CheckoutExperience(vm).Result);
// However, creating an invoice should show LNURL
invoice = user.BitPay.CreateInvoice(
new Invoice
{
Price = 1.5m,
Currency = "USD"
}, Facade.Merchant);
Assert.Equal(2, invoice.CryptoInfo.Length);
// Make sure this throw: Since BOLT11 and LN Url share the same criteria, there should be no payment method available
Assert.Throws<BitPayException>(() => user.BitPay.CreateInvoice(
new Invoice
{
Price = 2.5m,
Currency = "USD"
}, Facade.Merchant));
} }
} }

View file

@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using NBitcoin; using NBitcoin;
using NBXplorer.Models; using NBXplorer.Models;
using YamlDotNet.Core.Tokens;
using InvoiceData = BTCPayServer.Client.Models.InvoiceData; using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
using Language = BTCPayServer.Client.Models.Language; using Language = BTCPayServer.Client.Models.Language;
using NotificationData = BTCPayServer.Client.Models.NotificationData; using NotificationData = BTCPayServer.Client.Models.NotificationData;
@ -37,6 +36,7 @@ namespace BTCPayServer.Controllers.GreenField
private readonly StoreOnChainPaymentMethodsController _chainPaymentMethodsController; private readonly StoreOnChainPaymentMethodsController _chainPaymentMethodsController;
private readonly StoreOnChainWalletsController _storeOnChainWalletsController; private readonly StoreOnChainWalletsController _storeOnChainWalletsController;
private readonly StoreLightningNetworkPaymentMethodsController _storeLightningNetworkPaymentMethodsController; private readonly StoreLightningNetworkPaymentMethodsController _storeLightningNetworkPaymentMethodsController;
private readonly StoreLNURLPayPaymentMethodsController _storeLnurlPayPaymentMethodsController;
private readonly HealthController _healthController; private readonly HealthController _healthController;
private readonly GreenFieldPaymentRequestsController _paymentRequestController; private readonly GreenFieldPaymentRequestsController _paymentRequestController;
private readonly ApiKeysController _apiKeysController; private readonly ApiKeysController _apiKeysController;
@ -58,6 +58,7 @@ namespace BTCPayServer.Controllers.GreenField
StoreOnChainPaymentMethodsController chainPaymentMethodsController, StoreOnChainPaymentMethodsController chainPaymentMethodsController,
StoreOnChainWalletsController storeOnChainWalletsController, StoreOnChainWalletsController storeOnChainWalletsController,
StoreLightningNetworkPaymentMethodsController storeLightningNetworkPaymentMethodsController, StoreLightningNetworkPaymentMethodsController storeLightningNetworkPaymentMethodsController,
StoreLNURLPayPaymentMethodsController storeLnurlPayPaymentMethodsController,
HealthController healthController, HealthController healthController,
GreenFieldPaymentRequestsController paymentRequestController, GreenFieldPaymentRequestsController paymentRequestController,
ApiKeysController apiKeysController, ApiKeysController apiKeysController,
@ -79,6 +80,7 @@ namespace BTCPayServer.Controllers.GreenField
_chainPaymentMethodsController = chainPaymentMethodsController; _chainPaymentMethodsController = chainPaymentMethodsController;
_storeOnChainWalletsController = storeOnChainWalletsController; _storeOnChainWalletsController = storeOnChainWalletsController;
_storeLightningNetworkPaymentMethodsController = storeLightningNetworkPaymentMethodsController; _storeLightningNetworkPaymentMethodsController = storeLightningNetworkPaymentMethodsController;
_storeLnurlPayPaymentMethodsController = storeLnurlPayPaymentMethodsController;
_healthController = healthController; _healthController = healthController;
_paymentRequestController = paymentRequestController; _paymentRequestController = paymentRequestController;
_apiKeysController = apiKeysController; _apiKeysController = apiKeysController;
@ -141,6 +143,7 @@ namespace BTCPayServer.Controllers.GreenField
_storeLightningNodeApiController, _storeLightningNodeApiController,
_internalLightningNodeApiController, _internalLightningNodeApiController,
_storeLightningNetworkPaymentMethodsController, _storeLightningNetworkPaymentMethodsController,
_storeLnurlPayPaymentMethodsController,
_greenFieldInvoiceController, _greenFieldInvoiceController,
_greenFieldServerInfoController, _greenFieldServerInfoController,
_storeWebhooksController, _storeWebhooksController,
@ -165,6 +168,7 @@ namespace BTCPayServer.Controllers.GreenField
private readonly StoreLightningNodeApiController _storeLightningNodeApiController; private readonly StoreLightningNodeApiController _storeLightningNodeApiController;
private readonly InternalLightningNodeApiController _lightningNodeApiController; private readonly InternalLightningNodeApiController _lightningNodeApiController;
private readonly StoreLightningNetworkPaymentMethodsController _storeLightningNetworkPaymentMethodsController; private readonly StoreLightningNetworkPaymentMethodsController _storeLightningNetworkPaymentMethodsController;
private readonly StoreLNURLPayPaymentMethodsController _storeLnurlPayPaymentMethodsController;
private readonly GreenFieldInvoiceController _greenFieldInvoiceController; private readonly GreenFieldInvoiceController _greenFieldInvoiceController;
private readonly GreenFieldServerInfoController _greenFieldServerInfoController; private readonly GreenFieldServerInfoController _greenFieldServerInfoController;
private readonly StoreWebhooksController _storeWebhooksController; private readonly StoreWebhooksController _storeWebhooksController;
@ -183,6 +187,7 @@ namespace BTCPayServer.Controllers.GreenField
StoreLightningNodeApiController storeLightningNodeApiController, StoreLightningNodeApiController storeLightningNodeApiController,
InternalLightningNodeApiController lightningNodeApiController, InternalLightningNodeApiController lightningNodeApiController,
StoreLightningNetworkPaymentMethodsController storeLightningNetworkPaymentMethodsController, StoreLightningNetworkPaymentMethodsController storeLightningNetworkPaymentMethodsController,
StoreLNURLPayPaymentMethodsController storeLnurlPayPaymentMethodsController,
GreenFieldInvoiceController greenFieldInvoiceController, GreenFieldInvoiceController greenFieldInvoiceController,
GreenFieldServerInfoController greenFieldServerInfoController, GreenFieldServerInfoController greenFieldServerInfoController,
StoreWebhooksController storeWebhooksController, StoreWebhooksController storeWebhooksController,
@ -202,6 +207,7 @@ namespace BTCPayServer.Controllers.GreenField
_storeLightningNodeApiController = storeLightningNodeApiController; _storeLightningNodeApiController = storeLightningNodeApiController;
_lightningNodeApiController = lightningNodeApiController; _lightningNodeApiController = lightningNodeApiController;
_storeLightningNetworkPaymentMethodsController = storeLightningNetworkPaymentMethodsController; _storeLightningNetworkPaymentMethodsController = storeLightningNetworkPaymentMethodsController;
_storeLnurlPayPaymentMethodsController = storeLnurlPayPaymentMethodsController;
_greenFieldInvoiceController = greenFieldInvoiceController; _greenFieldInvoiceController = greenFieldInvoiceController;
_greenFieldServerInfoController = greenFieldServerInfoController; _greenFieldServerInfoController = greenFieldServerInfoController;
_storeWebhooksController = storeWebhooksController; _storeWebhooksController = storeWebhooksController;
@ -746,7 +752,39 @@ namespace BTCPayServer.Controllers.GreenField
{ {
return GetFromActionResult<StoreData>(await _storesController.UpdateStore(storeId, request)); return GetFromActionResult<StoreData>(await _storesController.UpdateStore(storeId, request));
} }
public override Task<IEnumerable<LNURLPayPaymentMethodData>>
GetStoreLNURLPayPaymentMethods(string storeId, bool? enabled,
CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult(
_storeLnurlPayPaymentMethodsController.GetLNURLPayPaymentMethods(storeId, enabled)));
}
public override Task<LNURLPayPaymentMethodData> GetStoreLNURLPayPaymentMethod(
string storeId, string cryptoCode, CancellationToken token = default)
{
return Task.FromResult(GetFromActionResult<LNURLPayPaymentMethodData>(
_storeLnurlPayPaymentMethodsController.GetLNURLPayPaymentMethod(storeId, cryptoCode)));
}
public override async Task RemoveStoreLNURLPayPaymentMethod(string storeId, string cryptoCode,
CancellationToken token = default)
{
HandleActionResult(
await _storeLnurlPayPaymentMethodsController.RemoveLNURLPayPaymentMethod(storeId,
cryptoCode));
}
public override async Task<LNURLPayPaymentMethodData> UpdateStoreLNURLPayPaymentMethod(
string storeId, string cryptoCode,
LNURLPayPaymentMethodData paymentMethod, CancellationToken token = default)
{
return GetFromActionResult<LNURLPayPaymentMethodData>(await
_storeLnurlPayPaymentMethodsController.UpdateLNURLPayPaymentMethod(storeId, cryptoCode,
paymentMethod));
}
public override Task<IEnumerable<LightningNetworkPaymentMethodData>> public override Task<IEnumerable<LightningNetworkPaymentMethodData>>
GetStoreLightningNetworkPaymentMethods(string storeId, bool? enabled, GetStoreLightningNetworkPaymentMethods(string storeId, bool? enabled,
CancellationToken token = default) CancellationToken token = default)

View file

@ -0,0 +1,180 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
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.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers.GreenField
{
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public class StoreLNURLPayPaymentMethodsController : ControllerBase
{
private StoreData Store => HttpContext.GetStoreData();
private readonly StoreRepository _storeRepository;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly IAuthorizationService _authorizationService;
private readonly ISettingsRepository _settingsRepository;
public StoreLNURLPayPaymentMethodsController(
StoreRepository storeRepository,
BTCPayNetworkProvider btcPayNetworkProvider,
IAuthorizationService authorizationService,
ISettingsRepository settingsRepository)
{
_storeRepository = storeRepository;
_btcPayNetworkProvider = btcPayNetworkProvider;
_authorizationService = authorizationService;
_settingsRepository = settingsRepository;
}
public static IEnumerable<LNURLPayPaymentMethodData> GetLNURLPayPaymentMethods(StoreData store,
BTCPayNetworkProvider networkProvider, bool? enabled)
{
var blob = store.GetStoreBlob();
var excludedPaymentMethods = blob.GetExcludedPaymentMethods();
return store.GetSupportedPaymentMethods(networkProvider)
.Where((method) => method.PaymentId.PaymentType == PaymentTypes.LNURLPay)
.OfType<LNURLPaySupportedPaymentMethod>()
.Select(paymentMethod =>
new LNURLPayPaymentMethodData(
paymentMethod.PaymentId.CryptoCode,
!excludedPaymentMethods.Match(paymentMethod.PaymentId),
paymentMethod.UseBech32Scheme, paymentMethod.EnableForStandardInvoices
)
)
.Where((result) => enabled is null || enabled == result.Enabled)
.ToList();
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LNURLPay")]
public ActionResult<IEnumerable<LNURLPayPaymentMethodData>> GetLNURLPayPaymentMethods(
string storeId,
[FromQuery] bool? enabled)
{
return Ok(GetLNURLPayPaymentMethods(Store, _btcPayNetworkProvider, enabled));
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
public IActionResult GetLNURLPayPaymentMethod(string storeId, string cryptoCode)
{
if (!GetNetwork(cryptoCode, out BTCPayNetwork _))
{
return NotFound();
}
var method = GetExistingLNURLPayPaymentMethod(cryptoCode);
if (method is null)
{
return this.CreateAPIError(404, "paymentmethod-not-found", "The LNURL Payment Method isn't activated");
}
return Ok(method);
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
public async Task<IActionResult> RemoveLNURLPayPaymentMethod(
string storeId,
string cryptoCode)
{
if (!GetNetwork(cryptoCode, out BTCPayNetwork _))
{
return NotFound();
}
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
var store = Store;
store.SetSupportedPaymentMethod(id, null);
await _storeRepository.UpdateStore(store);
return Ok();
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPut("~/api/v1/stores/{storeId}/payment-methods/LNURLPay/{cryptoCode}")]
public async Task<IActionResult> UpdateLNURLPayPaymentMethod(string storeId, string cryptoCode,
[FromBody] LNURLPayPaymentMethodData paymentMethodData)
{
var paymentMethodId = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
if (!GetNetwork(cryptoCode, out var network))
{
return NotFound();
}
var lnMethod = StoreLightningNetworkPaymentMethodsController.GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider,
cryptoCode, Store);
if ((lnMethod is null || lnMethod.Enabled is false) && paymentMethodData.Enabled)
{
ModelState.AddModelError(nameof(LNURLPayPaymentMethodData.Enabled),
"LNURL Pay cannot be enabled unless the lightning payment method is configured and enabled on this store");
}
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
LNURLPaySupportedPaymentMethod? paymentMethod = new LNURLPaySupportedPaymentMethod()
{
CryptoCode = cryptoCode,
UseBech32Scheme = paymentMethodData.UseBech32Scheme,
EnableForStandardInvoices = paymentMethodData.EnableForStandardInvoices
};
var store = Store;
var storeBlob = store.GetStoreBlob();
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
storeBlob.SetExcluded(paymentMethodId, !paymentMethodData.Enabled);
store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store);
return Ok(GetExistingLNURLPayPaymentMethod(cryptoCode, store));
}
private LNURLPayPaymentMethodData? GetExistingLNURLPayPaymentMethod(string cryptoCode,
StoreData? store = null)
{
store ??= Store;
var storeBlob = store.GetStoreBlob();
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
var paymentMethod = store
.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.OfType<LNURLPaySupportedPaymentMethod>()
.FirstOrDefault(method => method.PaymentId == id);
var excluded = storeBlob.IsExcluded(id);
return paymentMethod is null
? null
: new LNURLPayPaymentMethodData(
paymentMethod.PaymentId.CryptoCode,
!excluded,
paymentMethod.UseBech32Scheme, paymentMethod.EnableForStandardInvoices
);
}
private bool GetNetwork(string cryptoCode, [MaybeNullWhen(false)] out BTCPayNetwork network)
{
network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
network = network?.SupportLightning is true ? network : null;
return network != null;
}
}
}

View file

@ -84,7 +84,7 @@ namespace BTCPayServer.Controllers.GreenField
return NotFound(); return NotFound();
} }
var method = GetExistingLightningLikePaymentMethod(cryptoCode); var method = GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, Store);
if (method is null) if (method is null)
{ {
return NotFound(); return NotFound();
@ -97,8 +97,7 @@ namespace BTCPayServer.Controllers.GreenField
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")] [HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
public async Task<IActionResult> RemoveLightningNetworkPaymentMethod( public async Task<IActionResult> RemoveLightningNetworkPaymentMethod(
string storeId, string storeId,
string cryptoCode, string cryptoCode)
int offset = 0, int amount = 10)
{ {
if (!GetNetwork(cryptoCode, out BTCPayNetwork _)) if (!GetNetwork(cryptoCode, out BTCPayNetwork _))
{ {
@ -188,17 +187,17 @@ namespace BTCPayServer.Controllers.GreenField
storeBlob.SetExcluded(paymentMethodId, !request.Enabled); storeBlob.SetExcluded(paymentMethodId, !request.Enabled);
store.SetStoreBlob(storeBlob); store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store); await _storeRepository.UpdateStore(store);
return Ok(GetExistingLightningLikePaymentMethod(cryptoCode, store)); return Ok(GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, store));
} }
private LightningNetworkPaymentMethodData? GetExistingLightningLikePaymentMethod(string cryptoCode, public static LightningNetworkPaymentMethodData? GetExistingLightningLikePaymentMethod(BTCPayNetworkProvider btcPayNetworkProvider, string cryptoCode,
StoreData? store = null) StoreData store)
{ {
store ??= Store;
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var paymentMethod = store var paymentMethod = store
.GetSupportedPaymentMethods(_btcPayNetworkProvider) .GetSupportedPaymentMethods(btcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>() .OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(method => method.PaymentId == id); .FirstOrDefault(method => method.PaymentId == id);

View file

@ -374,7 +374,8 @@ namespace BTCPayServer.Controllers
Overpaid = _CurrencyNameTable.DisplayFormatCurrency( Overpaid = _CurrencyNameTable.DisplayFormatCurrency(
accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode), accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode),
Address = data.GetPaymentMethodDetails().GetPaymentDestination(), Address = data.GetPaymentMethodDetails().GetPaymentDestination(),
Rate = ExchangeRate(data) Rate = ExchangeRate(data),
PaymentMethodRaw = data
}; };
}).ToList() }).ToList()
}; };

View file

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Filters; using BTCPayServer.Filters;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
@ -41,7 +42,8 @@ namespace BTCPayServer.Controllers
{ {
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store); var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
var network = _BtcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode); var network = _BtcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var nodeInfo = await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails, network); var nodeInfo =
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails, network, new InvoiceLogs());
return View(new ShowLightningNodeInfoViewModel return View(new ShowLightningNodeInfoViewModel
{ {

View file

@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning; using BTCPayServer.Payments.Lightning;
@ -48,7 +49,6 @@ namespace BTCPayServer.Controllers
} }
var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike); var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store);
LightningSupportedPaymentMethod paymentMethod = null; LightningSupportedPaymentMethod paymentMethod = null;
if (vm.LightningNodeType == LightningNodeType.Internal) if (vm.LightningNodeType == LightningNodeType.Internal)
@ -92,6 +92,7 @@ namespace BTCPayServer.Controllers
CryptoCode = paymentMethodId.CryptoCode CryptoCode = paymentMethodId.CryptoCode
}; };
paymentMethod.SetLightningUrl(connectionString); paymentMethod.SetLightningUrl(connectionString);
} }
switch (command) switch (command)
@ -99,8 +100,22 @@ namespace BTCPayServer.Controllers
case "save": case "save":
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
storeBlob.Hints.Lightning = false; storeBlob.Hints.Lightning = false;
var lnurl = new PaymentMethodId(vm.CryptoCode, PaymentTypes.LNURLPay);
storeBlob.SetExcluded(lnurl, !vm.LNURLEnabled);
store.SetSupportedPaymentMethod(new LNURLPaySupportedPaymentMethod()
{
CryptoCode = vm.CryptoCode,
EnableForStandardInvoices = vm.LNURLStandardInvoiceEnabled,
UseBech32Scheme = vm.LNURLBech32Mode,
LUD12Enabled = vm.LUD12Enabled
});
store.SetStoreBlob(storeBlob); store.SetStoreBlob(storeBlob);
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod); store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
await _Repo.UpdateStore(store); await _Repo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node updated."; TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node updated.";
return RedirectToAction(nameof(UpdateStore), new { storeId }); return RedirectToAction(nameof(UpdateStore), new { storeId });
@ -109,7 +124,7 @@ namespace BTCPayServer.Controllers
var handler = _ServiceProvider.GetRequiredService<LightningLikePaymentHandler>(); var handler = _ServiceProvider.GetRequiredService<LightningLikePaymentHandler>();
try try
{ {
var info = await handler.GetNodeInfo(paymentMethod, network, Request.IsOnion()); var info = await handler.GetNodeInfo(paymentMethod, network, new InvoiceLogs(), Request.IsOnion());
if (!vm.SkipPortTest) if (!vm.SkipPortTest)
{ {
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
@ -163,7 +178,9 @@ namespace BTCPayServer.Controllers
{ {
vm.CanUseInternalNode = await CanUseInternalLightning(); vm.CanUseInternalNode = await CanUseInternalLightning();
var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store); var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store);
if (lightning != null)
var lnSet = lightning != null;
if (lnSet)
{ {
vm.LightningNodeType = lightning.IsInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom; vm.LightningNodeType = lightning.IsInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
vm.ConnectionString = lightning.GetDisplayableConnectionString(); vm.ConnectionString = lightning.GetDisplayableConnectionString();
@ -172,6 +189,20 @@ namespace BTCPayServer.Controllers
{ {
vm.LightningNodeType = vm.CanUseInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom; vm.LightningNodeType = vm.CanUseInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
} }
var lnurl = GetExistingLNURLSupportedPaymentMethod(vm.CryptoCode, store);
if (lnurl != null)
{
vm.LNURLEnabled = !store.GetStoreBlob().GetExcludedPaymentMethods().Match(lnurl.PaymentId);
vm.LNURLBech32Mode = lnurl.UseBech32Scheme;
vm.LNURLStandardInvoiceEnabled = lnurl.EnableForStandardInvoices;
vm.LUD12Enabled = lnurl.LUD12Enabled;
}
else
{
vm.LNURLEnabled = !lnSet;
vm.DisableBolt11PaymentMethod = false;
}
} }
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store) private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
@ -182,5 +213,13 @@ namespace BTCPayServer.Controllers
.FirstOrDefault(d => d.PaymentId == id); .FirstOrDefault(d => d.PaymentId == id);
return existing; return existing;
} }
private LNURLPaySupportedPaymentMethod GetExistingLNURLSupportedPaymentMethod(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LNURLPaySupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
} }
} }

View file

@ -372,7 +372,11 @@ namespace BTCPayServer.Controllers
var storeBlob = CurrentStore.GetStoreBlob(); var storeBlob = CurrentStore.GetStoreBlob();
var vm = new CheckoutExperienceViewModel(); var vm = new CheckoutExperienceViewModel();
SetCryptoCurrencies(vm, CurrentStore); SetCryptoCurrencies(vm, CurrentStore);
vm.PaymentMethodCriteria = CurrentStore.GetSupportedPaymentMethods(_NetworkProvider).Select(method => vm.PaymentMethodCriteria = CurrentStore.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !storeBlob.GetExcludedPaymentMethods().Match(s.PaymentId))
.Where(s => _NetworkProvider.GetNetwork(s.PaymentId.CryptoCode) != null)
.Where(s => s.PaymentId.PaymentType != PaymentTypes.LNURLPay)
.Select(method =>
{ {
var existing = var existing =
storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria => storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria =>
@ -461,13 +465,36 @@ namespace BTCPayServer.Controllers
return View(model); return View(model);
} }
blob.PaymentMethodCriteria = model.PaymentMethodCriteria // Payment criteria for Off-Chain should also affect LNUrl
.Where(viewModel => !string.IsNullOrEmpty(viewModel.Value)).Select(viewModel => foreach (var newCriteria in model.PaymentMethodCriteria.ToList())
{
var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod);
if (paymentMethodId.PaymentType == PaymentTypes.LightningLike)
model.PaymentMethodCriteria.Add(new PaymentMethodCriteriaViewModel()
{
PaymentMethod = new PaymentMethodId(paymentMethodId.CryptoCode, PaymentTypes.LNURLPay).ToString(),
Type = newCriteria.Type,
Value = newCriteria.Value
});
// Should not be able to set LNUrlPay criteria directly in UI
if (paymentMethodId.PaymentType == PaymentTypes.LNURLPay)
model.PaymentMethodCriteria.Remove(newCriteria);
}
blob.PaymentMethodCriteria ??= new List<PaymentMethodCriteria>();
foreach (var newCriteria in model.PaymentMethodCriteria)
{
var paymentMethodId = PaymentMethodId.Parse(newCriteria.PaymentMethod);
var existingCriteria = blob.PaymentMethodCriteria.FirstOrDefault(c => c.PaymentMethod == paymentMethodId);
if (existingCriteria != null)
blob.PaymentMethodCriteria.Remove(existingCriteria);
CurrencyValue.TryParse(newCriteria.Value, out var cv);
blob.PaymentMethodCriteria.Add(new PaymentMethodCriteria()
{ {
CurrencyValue.TryParse(viewModel.Value, out var cv); Above = newCriteria.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan,
return new PaymentMethodCriteria() { Above = viewModel.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan, Value = cv, PaymentMethod = PaymentMethodId.Parse(viewModel.PaymentMethod) }; Value = cv,
}).ToList(); PaymentMethod = paymentMethodId
});
}
blob.RequiresRefundEmail = model.RequiresRefundEmail; blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LazyPaymentMethods = model.LazyPaymentMethods; blob.LazyPaymentMethods = model.LazyPaymentMethods;
blob.RedirectAutomatically = model.RedirectAutomatically; blob.RedirectAutomatically = model.RedirectAutomatically;
@ -493,8 +520,8 @@ namespace BTCPayServer.Controllers
}); });
} }
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob,
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, StoreViewModel vm) out List<StoreDerivationScheme> derivationSchemes, out List<StoreLightningNode> lightningNodes)
{ {
var excludeFilters = storeBlob.GetExcludedPaymentMethods(); var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var derivationByCryptoCode = var derivationByCryptoCode =
@ -506,8 +533,12 @@ namespace BTCPayServer.Controllers
var lightningByCryptoCode = store var lightningByCryptoCode = store
.GetSupportedPaymentMethods(_NetworkProvider) .GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LightningSupportedPaymentMethod>() .OfType<LightningSupportedPaymentMethod>()
.Where(method => method.PaymentId.PaymentType == LightningPaymentType.Instance)
.ToDictionary(c => c.CryptoCode.ToUpperInvariant()); .ToDictionary(c => c.CryptoCode.ToUpperInvariant());
derivationSchemes = new List<StoreDerivationScheme>();
lightningNodes = new List<StoreLightningNode>();
foreach (var paymentMethodId in _paymentMethodHandlerDictionary.Distinct().SelectMany(handler => handler.GetSupportedPaymentMethods())) foreach (var paymentMethodId in _paymentMethodHandlerDictionary.Distinct().SelectMany(handler => handler.GetSupportedPaymentMethods()))
{ {
switch (paymentMethodId.PaymentType) switch (paymentMethodId.PaymentType)
@ -517,7 +548,7 @@ namespace BTCPayServer.Controllers
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode); var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
var value = strategy?.ToPrettyString() ?? string.Empty; var value = strategy?.ToPrettyString() ?? string.Empty;
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme() derivationSchemes.Add(new StoreDerivationScheme
{ {
Crypto = paymentMethodId.CryptoCode, Crypto = paymentMethodId.CryptoCode,
WalletSupported = network.WalletSupported, WalletSupported = network.WalletSupported,
@ -529,10 +560,14 @@ namespace BTCPayServer.Controllers
#endif #endif
}); });
break; break;
case LNURLPayPaymentType lnurlPayPaymentType:
break;
case LightningPaymentType _: case LightningPaymentType _:
var lightning = lightningByCryptoCode.TryGet(paymentMethodId.CryptoCode); var lightning = lightningByCryptoCode.TryGet(paymentMethodId.CryptoCode);
var isEnabled = !excludeFilters.Match(paymentMethodId) && lightning != null; var isEnabled = !excludeFilters.Match(paymentMethodId) && lightning != null;
vm.LightningNodes.Add(new StoreViewModel.LightningNode lightningNodes.Add(new StoreLightningNode
{ {
CryptoCode = paymentMethodId.CryptoCode, CryptoCode = paymentMethodId.CryptoCode,
Address = lightning?.GetDisplayableConnectionString(), Address = lightning?.GetDisplayableConnectionString(),
@ -544,30 +579,92 @@ namespace BTCPayServer.Controllers
} }
[HttpGet("{storeId}")] [HttpGet("{storeId}")]
public async Task<IActionResult> UpdateStore() public IActionResult UpdateStore()
{ {
var store = HttpContext.GetStoreData(); var store = HttpContext.GetStoreData();
if (store == null) if (store == null)
return NotFound(); return NotFound();
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
var vm = new StoreViewModel(); var vm = new StoreViewModel
vm.Id = store.Id; {
vm.StoreName = store.StoreName; Id = store.Id,
vm.StoreWebsite = store.StoreWebsite; CanDelete = _Repo.CanDeleteStores(),
vm.DefaultCurrency = storeBlob.DefaultCurrency; StoreName = store.StoreName,
vm.NetworkFeeMode = storeBlob.NetworkFeeMode; StoreWebsite = store.StoreWebsite,
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice; HintWallet = storeBlob.Hints.Wallet,
vm.SpeedPolicy = store.SpeedPolicy; HintLightning = storeBlob.Hints.Lightning
vm.CanDelete = _Repo.CanDeleteStores(); };
AddPaymentMethods(store, storeBlob, vm);
AddPaymentMethods(store, storeBlob,
out var derivationSchemes, out var lightningNodes);
vm.DerivationSchemes = derivationSchemes;
vm.LightningNodes = lightningNodes;
return View(vm);
}
[HttpPost("{storeId}")]
public async Task<IActionResult> UpdateStore(StoreViewModel model, string command = null)
{
bool needUpdate = false;
if (CurrentStore.StoreName != model.StoreName)
{
needUpdate = true;
CurrentStore.StoreName = model.StoreName;
}
if (CurrentStore.StoreWebsite != model.StoreWebsite)
{
needUpdate = true;
CurrentStore.StoreWebsite = model.StoreWebsite;
}
var blob = CurrentStore.GetStoreBlob();
if (CurrentStore.SetStoreBlob(blob))
{
needUpdate = true;
}
if (needUpdate)
{
await _Repo.UpdateStore(CurrentStore);
TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated";
}
return RedirectToAction(nameof(UpdateStore), new
{
storeId = CurrentStore.Id
});
}
[HttpGet("{storeId}/payment")]
public async Task<IActionResult> Payment()
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob();
var vm = new PaymentViewModel
{
NetworkFeeMode = storeBlob.NetworkFeeMode,
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
SpeedPolicy = store.SpeedPolicy,
PaymentTolerance = storeBlob.PaymentTolerance,
DefaultCurrency = storeBlob.DefaultCurrency
};
AddPaymentMethods(store, storeBlob,
out var derivationSchemes, out var lightningNodes);
vm.DerivationSchemes = derivationSchemes;
vm.LightningNodes = lightningNodes;
vm.MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes; vm.MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes;
vm.InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes; vm.InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate; vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
vm.PaymentTolerance = storeBlob.PaymentTolerance;
vm.PayJoinEnabled = storeBlob.PayJoinEnabled; vm.PayJoinEnabled = storeBlob.PayJoinEnabled;
vm.HintWallet = storeBlob.Hints.Wallet;
vm.HintLightning = storeBlob.Hints.Lightning;
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi; vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints; vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints;
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback; vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
@ -581,12 +678,12 @@ namespace BTCPayServer.Controllers
.GetSupportedPaymentMethods(_NetworkProvider) .GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>() .OfType<DerivationSchemeSettings>()
.Any(settings => settings.Network.SupportPayJoin && settings.IsHotWallet); .Any(settings => settings.Network.SupportPayJoin && settings.IsHotWallet);
return View(vm); return View(vm);
} }
[HttpPost("{storeId}")] [HttpPost("{storeId}/payment")]
public async Task<IActionResult> UpdateStore(StoreViewModel model, string command = null) public async Task<IActionResult> Payment(PaymentViewModel model, string command = null)
{ {
bool needUpdate = false; bool needUpdate = false;
if (CurrentStore.SpeedPolicy != model.SpeedPolicy) if (CurrentStore.SpeedPolicy != model.SpeedPolicy)
@ -594,16 +691,6 @@ namespace BTCPayServer.Controllers
needUpdate = true; needUpdate = true;
CurrentStore.SpeedPolicy = model.SpeedPolicy; CurrentStore.SpeedPolicy = model.SpeedPolicy;
} }
if (CurrentStore.StoreName != model.StoreName)
{
needUpdate = true;
CurrentStore.StoreName = model.StoreName;
}
if (CurrentStore.StoreWebsite != model.StoreWebsite)
{
needUpdate = true;
CurrentStore.StoreWebsite = model.StoreWebsite;
}
var blob = CurrentStore.GetStoreBlob(); var blob = CurrentStore.GetStoreBlob();
blob.DefaultCurrency = model.DefaultCurrency; blob.DefaultCurrency = model.DefaultCurrency;
@ -630,7 +717,7 @@ namespace BTCPayServer.Controllers
{ {
await _Repo.UpdateStore(CurrentStore); await _Repo.UpdateStore(CurrentStore);
TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated"; TempData[WellKnownTempData.SuccessMessage] = "Payment settings successfully updated";
if (payjoinChanged && blob.PayJoinEnabled) if (payjoinChanged && blob.PayJoinEnabled)
{ {
@ -646,13 +733,13 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel() TempData.SetStatusMessageModel(new StatusMessageModel()
{ {
Severity = StatusMessageModel.StatusSeverity.Warning, Severity = StatusMessageModel.StatusSeverity.Warning,
Html = $"The store was updated successfully. However, payjoin will not work for {string.Join(", ", problematicPayjoinEnabledMethods)} until you configure them to be a <a href='https://docs.btcpayserver.org/HotWallet/' class='alert-link' target='_blank'>hot wallet</a>." Html = $"The payment settings were updated successfully. However, payjoin will not work for {string.Join(", ", problematicPayjoinEnabledMethods)} until you configure them to be a <a href='https://docs.btcpayserver.org/HotWallet/' class='alert-link' target='_blank'>hot wallet</a>."
}); });
} }
} }
} }
return RedirectToAction(nameof(UpdateStore), new return RedirectToAction(nameof(Payment), new
{ {
storeId = CurrentStore.Id storeId = CurrentStore.Id
}); });

View file

@ -35,6 +35,7 @@ using NBitpayClient;
using NBXplorer.DerivationStrategy; using NBXplorer.DerivationStrategy;
using NBXplorer.Models; using NBXplorer.Models;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo;
namespace BTCPayServer namespace BTCPayServer
{ {

View file

@ -5,6 +5,7 @@ using System.Threading;
using BTCPayServer.Abstractions.Contracts; using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using BTCPayServer.Common; using BTCPayServer.Common;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Configuration; using BTCPayServer.Configuration;
@ -329,6 +330,8 @@ namespace BTCPayServer.Hosting
services.AddSingleton<LightningLikePaymentHandler>(); services.AddSingleton<LightningLikePaymentHandler>();
services.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<LightningLikePaymentHandler>()); services.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<LightningLikePaymentHandler>());
services.AddSingleton<LNURLPayPaymentHandler>();
services.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<LNURLPayPaymentHandler>());
services.AddSingleton<IHostedService, LightningListener>(); services.AddSingleton<IHostedService, LightningListener>();
services.AddSingleton<PaymentMethodHandlerDictionary>(); services.AddSingleton<PaymentMethodHandlerDictionary>();

View file

@ -0,0 +1,203 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Lightning;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using LNURL;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.Crypto;
using Newtonsoft.Json;
namespace BTCPayServer
{
[Route("~/{cryptoCode}/[controller]/")]
public class LNURLController : Controller
{
private readonly InvoiceRepository _invoiceRepository;
private readonly EventAggregator _eventAggregator;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
private readonly LightningLikePaymentHandler _lightningLikePaymentHandler;
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
private readonly InvoiceController _invoiceController;
public LNURLController(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
BTCPayNetworkProvider btcPayNetworkProvider,
LightningLikePaymentHandler lightningLikePaymentHandler,
StoreRepository storeRepository,
AppService appService,
InvoiceController invoiceController)
{
_invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
_btcPayNetworkProvider = btcPayNetworkProvider;
_lightningLikePaymentHandler = lightningLikePaymentHandler;
_storeRepository = storeRepository;
_appService = appService;
_invoiceController = invoiceController;
}
[HttpGet("pay/i/{invoiceId}")]
public async Task<IActionResult> GetLNURLForInvoice(string invoiceId, string cryptoCode,
[FromQuery] long? amount = null, string comment = null)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network is null || !network.SupportLightning)
{
return NotFound();
}
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
var i = await _invoiceRepository.GetInvoice(invoiceId, true);
if (i.Status == InvoiceStatusLegacy.New)
{
var isTopup = i.IsUnsetTopUp();
var lnurlSupportedPaymentMethod =
i.GetSupportedPaymentMethod<LNURLPaySupportedPaymentMethod>(pmi).FirstOrDefault();
if (lnurlSupportedPaymentMethod is null ||
(!isTopup && !lnurlSupportedPaymentMethod.EnableForStandardInvoices))
{
return NotFound();
}
var lightningPaymentMethod = i.GetPaymentMethod(pmi);
var accounting = lightningPaymentMethod.Calculate();
var paymentMethodDetails =
lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
if (paymentMethodDetails.LightningSupportedPaymentMethod is null)
{
return NotFound();
}
var min = new LightMoney(isTopup ? 1m : accounting.Due.ToUnit(MoneyUnit.Satoshi),
LightMoneyUnit.Satoshi);
var max = isTopup ? LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC) : min;
List<string[]> lnurlMetadata = new List<string[]>();
lnurlMetadata.Add(new[] { "text/plain", i.Id });
var metadata = JsonConvert.SerializeObject(lnurlMetadata);
if (amount.HasValue && (amount < min || amount > max))
{
return BadRequest(new LNUrlStatusResponse
{
Status = "ERROR", Reason = "Amount is out of bounds."
});
}
if (amount.HasValue && string.IsNullOrEmpty(paymentMethodDetails.BOLT11) ||
paymentMethodDetails.GeneratedBoltAmount != amount)
{
var client =
_lightningLikePaymentHandler.CreateLightningClient(
paymentMethodDetails.LightningSupportedPaymentMethod, network);
if (!string.IsNullOrEmpty(paymentMethodDetails.BOLT11))
{
try
{
await client.CancelInvoice(paymentMethodDetails.InvoiceId);
}
catch (Exception)
{
//not a fully supported option
}
}
var descriptionHash = new uint256(Hashes.SHA256(Encoding.UTF8.GetBytes(metadata)));
LightningInvoice invoice;
try
{
invoice = await client.CreateInvoice(new CreateInvoiceParams(amount.Value,
descriptionHash,
i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow));
if (!BOLT11PaymentRequest.Parse(invoice.BOLT11, network.NBitcoinNetwork)
.VerifyDescriptionHash(metadata))
{
return BadRequest(new LNUrlStatusResponse
{
Status = "ERROR",
Reason = "Lightning node could not generate invoice with a VALID description hash"
});
}
}
catch (Exception)
{
return BadRequest(new LNUrlStatusResponse
{
Status = "ERROR",
Reason = "Lightning node could not generate invoice with description hash"
});
}
paymentMethodDetails.BOLT11 = invoice.BOLT11;
paymentMethodDetails.InvoiceId = invoice.Id;
paymentMethodDetails.GeneratedBoltAmount = new LightMoney(amount.Value);
if (lnurlSupportedPaymentMethod.LUD12Enabled)
{
paymentMethodDetails.ProvidedComment = comment;
}
lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod);
_eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(invoiceId,
paymentMethodDetails, pmi));
return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse
{
Disposable = true, Routes = Array.Empty<string>(), Pr = paymentMethodDetails.BOLT11
});
}
if (amount.HasValue && paymentMethodDetails.GeneratedBoltAmount == amount)
{
if (lnurlSupportedPaymentMethod.LUD12Enabled && paymentMethodDetails.ProvidedComment != comment)
{
paymentMethodDetails.ProvidedComment = comment;
lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod);
}
return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse
{
Disposable = true, Routes = Array.Empty<string>(), Pr = paymentMethodDetails.BOLT11
});
}
if (amount is null)
{
return Ok(new LNURLPayRequest
{
Tag = "payRequest",
MinSendable = min,
MaxSendable = max,
CommentAllowed = lnurlSupportedPaymentMethod.LUD12Enabled ? 2000 : 0,
Metadata = metadata,
Callback = new Uri(Request.GetCurrentUrl())
});
}
}
return BadRequest(new LNUrlStatusResponse
{
Status = "ERROR", Reason = "Invoice not in a valid payable state"
});
}
}
}

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using BTCPayServer.Services.Invoices;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -79,7 +80,7 @@ namespace BTCPayServer.Models
} }
[JsonProperty("cryptoInfo")] [JsonProperty("cryptoInfo")]
public List<NBitpayClient.InvoiceCryptoInfo> CryptoInfo { get; set; } public List<InvoiceCryptoInfo> CryptoInfo { get; set; }
//"price":5 //"price":5
[JsonProperty("price")] [JsonProperty("price")]
@ -262,7 +263,7 @@ namespace BTCPayServer.Models
[JsonProperty("addresses")] [JsonProperty("addresses")]
public Dictionary<string, string> Addresses { get; set; } public Dictionary<string, string> Addresses { get; set; }
[JsonProperty("paymentCodes")] [JsonProperty("paymentCodes")]
public Dictionary<string, NBitpayClient.InvoicePaymentUrls> PaymentCodes { get; set; } public Dictionary<string, InvoiceCryptoInfo.InvoicePaymentUrls> PaymentCodes { get; set; }
[JsonProperty("buyer")] [JsonProperty("buyer")]
public JObject Buyer { get; set; } public JObject Buyer { get; set; }
} }

View file

@ -30,6 +30,7 @@ namespace BTCPayServer.Models.InvoicingModels
{ {
public string Crypto { get; set; } public string Crypto { get; set; }
public string BOLT11 { get; set; } public string BOLT11 { get; set; }
public PaymentType Type { get; set; }
} }
public class InvoiceDetailsModel public class InvoiceDetailsModel
@ -45,6 +46,8 @@ namespace BTCPayServer.Models.InvoicingModels
public string Overpaid { get; set; } public string Overpaid { get; set; }
[JsonIgnore] [JsonIgnore]
public PaymentMethodId PaymentMethodId { get; set; } public PaymentMethodId PaymentMethodId { get; set; }
public PaymentMethod PaymentMethodRaw { get; set; }
} }
public class AddressModel public class AddressModel
{ {

View file

@ -10,7 +10,22 @@ namespace BTCPayServer.Models.StoreViewModels
public class LightningNodeViewModel public class LightningNodeViewModel
{ {
[Display(Name = "Enable LNURL")]
public bool LNURLEnabled { get; set; }
[Display(Name = "LNURL Classic Mode")]
public bool LNURLBech32Mode { get; set; } = true;
[Display(Name = "LNURL enabled for standard invoices")]
public bool LNURLStandardInvoiceEnabled { get; set; }
[Display(Name = "Allow payee to pass a comment")]
public bool LUD12Enabled { get; set; }
[Display(Name = "Do not offer BOLT11 for standard invoices")]
public bool DisableBolt11PaymentMethod { get; set; }
public LightningNodeType LightningNodeType { get; set; } public LightningNodeType LightningNodeType { get; set; }
[Display(Name = "Connection string")] [Display(Name = "Connection string")]
public string ConnectionString { get; set; } public string ConnectionString { get; set; }
public string CryptoCode { get; set; } public string CryptoCode { get; set; }

View file

@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Client.Models;
using BTCPayServer.Validation;
using static BTCPayServer.Data.StoreBlob;
namespace BTCPayServer.Models.StoreViewModels
{
public class PaymentViewModel
{
public List<StoreDerivationScheme> DerivationSchemes { get; set; }
public List<StoreLightningNode> LightningNodes { get; set; }
public bool IsOnchainSetup { get; set; }
public bool IsLightningSetup { get; set; }
public bool CanUsePayJoin { get; set; }
[Display(Name = "Allow anyone to create invoice")]
public bool AnyoneCanCreateInvoice { get; set; }
[Display(Name = "Invoice expires if the full amount has not been paid after …")]
[Range(1, 60 * 24 * 24)]
public int InvoiceExpiration { get; set; }
[Display(Name = "Payment invalid if transactions fails to confirm … after invoice expiration")]
[Range(10, 60 * 24 * 24)]
public int MonitoringExpiration { get; set; }
[Display(Name = "Consider the invoice confirmed when the payment transaction …")]
public SpeedPolicy SpeedPolicy { get; set; }
[Display(Name = "Add additional fee (network fee) to invoice …")]
public NetworkFeeMode NetworkFeeMode { get; set; }
[Display(Name = "Description template of the lightning invoice")]
public string LightningDescriptionTemplate { get; set; }
[Display(Name = "Enable Payjoin/P2EP")]
public bool PayJoinEnabled { get; set; }
[Display(Name = "Show recommended fee")]
public bool ShowRecommendedFee { get; set; }
[Display(Name = "Recommended fee confirmation target blocks")]
[Range(1, double.PositiveInfinity)]
public int RecommendedFeeBlockTarget { get; set; }
[Display(Name = "Display Lightning payment amounts in Satoshis")]
public bool LightningAmountInSatoshi { get; set; }
[Display(Name = "Add hop hints for private channels to the Lightning invoice")]
public bool LightningPrivateRouteHints { get; set; }
[Display(Name = "Include Lightning invoice fallback to on-chain BIP21 payment URL")]
public bool OnChainWithLnInvoiceFallback { get; set; }
[Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")]
[Range(0, 100)]
public double PaymentTolerance { get; set; }
[Display(Name = "Default currency")]
[MaxLength(10)]
public string DefaultCurrency { get; set; }
}
}

View file

@ -0,0 +1,12 @@
namespace BTCPayServer.Models.StoreViewModels
{
public class StoreDerivationScheme
{
public string Crypto { get; set; }
public string Value { get; set; }
public WalletId WalletId { get; set; }
public bool WalletSupported { get; set; }
public bool Enabled { get; set; }
public bool Collapsed { get; set; }
}
}

View file

@ -0,0 +1,9 @@
namespace BTCPayServer.Models.StoreViewModels
{
public class StoreLightningNode
{
public string CryptoCode { get; set; }
public string Address { get; set; }
public bool Enabled { get; set; }
}
}

View file

@ -8,130 +8,24 @@ namespace BTCPayServer.Models.StoreViewModels
{ {
public class StoreViewModel public class StoreViewModel
{ {
public class DerivationScheme public List<StoreDerivationScheme> DerivationSchemes { get; set; }
{ public List<StoreLightningNode> LightningNodes { get; set; }
public string Crypto { get; set; } public bool HintWallet { get; set; }
public string Value { get; set; } public bool HintLightning { get; set; }
public WalletId WalletId { get; set; }
public bool WalletSupported { get; set; }
public bool Enabled { get; set; }
public bool Collapsed { get; set; }
}
public class AdditionalPaymentMethod
{
public string Provider { get; set; }
public bool Enabled { get; set; }
public string Action { get; set; }
}
public StoreViewModel()
{
}
public bool CanDelete { get; set; } public bool CanDelete { get; set; }
[Display(Name = "Store ID")] [Display(Name = "Store ID")]
public string Id { get; set; } public string Id { get; set; }
[Display(Name = "Store Name")] [Display(Name = "Store Name")]
[Required] [Required]
[MaxLength(50)] [MaxLength(50)]
[MinLength(1)] [MinLength(1)]
public string StoreName public string StoreName { get; set; }
{
get; set;
}
[Uri] [Uri]
[Display(Name = "Store Website")] [Display(Name = "Store Website")]
[MaxLength(500)] [MaxLength(500)]
public string StoreWebsite public string StoreWebsite { get; set; }
{
get;
set;
}
[Display(Name = "Default currency")]
[MaxLength(10)]
public string DefaultCurrency { get; set; }
[Display(Name = "Allow anyone to create invoice")]
public bool AnyoneCanCreateInvoice { get; set; }
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
[Display(Name = "Invoice expires if the full amount has not been paid after …")]
[Range(1, 60 * 24 * 24)]
public int InvoiceExpiration
{
get;
set;
}
[Display(Name = "Payment invalid if transactions fails to confirm … after invoice expiration")]
[Range(10, 60 * 24 * 24)]
public int MonitoringExpiration
{
get;
set;
}
[Display(Name = "Consider the invoice confirmed when the payment transaction …")]
public SpeedPolicy SpeedPolicy
{
get; set;
}
[Display(Name = "Add additional fee (network fee) to invoice …")]
public NetworkFeeMode NetworkFeeMode
{
get; set;
}
[Display(Name = "Description template of the lightning invoice")]
public string LightningDescriptionTemplate { get; set; }
[Display(Name = "Enable Payjoin/P2EP")]
public bool PayJoinEnabled { get; set; }
public bool CanUsePayJoin { get; set; }
public bool IsOnchainSetup { get; set; }
public bool IsLightningSetup { get; set; }
public bool HintWallet { get; set; }
public bool HintLightning { get; set; }
[Display(Name = "Show recommended fee")]
public bool ShowRecommendedFee { get; set; }
[Display(Name = "Recommended fee confirmation target blocks")]
[Range(1, double.PositiveInfinity)]
public int RecommendedFeeBlockTarget { get; set; }
[Display(Name = "Display Lightning payment amounts in Satoshis")]
public bool LightningAmountInSatoshi { get; set; }
[Display(Name = "Add hop hints for private channels to the Lightning invoice")]
public bool LightningPrivateRouteHints { get; set; }
[Display(Name = "Include Lightning invoice fallback to on-chain BIP21 payment URL")]
public bool OnChainWithLnInvoiceFallback { get; set; }
public class LightningNode
{
public string CryptoCode { get; set; }
public string Address { get; set; }
public bool Enabled { get; set; }
}
public List<LightningNode> LightningNodes
{
get; set;
} = new List<LightningNode>();
[Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")]
[Range(0, 100)]
public double PaymentTolerance
{
get;
set;
}
} }
} }

View file

@ -18,5 +18,6 @@ namespace BTCPayServer.Payments
decimal GetNextNetworkFee(); decimal GetNextNetworkFee();
bool Activated {get;set;} bool Activated {get;set;}
virtual string GetAdditionalDataPartialName() => null;
} }
} }

View file

@ -0,0 +1,170 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Client.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Options;
using NBitcoin;
namespace BTCPayServer.Payments.Lightning
{
public class LNURLPayPaymentHandler : PaymentMethodHandlerBase<LNURLPaySupportedPaymentMethod, BTCPayNetwork>
{
private readonly BTCPayNetworkProvider _networkProvider;
private readonly CurrencyNameTable _currencyNameTable;
private readonly LightningLikePaymentHandler _lightningLikePaymentHandler;
public LNURLPayPaymentHandler(
BTCPayNetworkProvider networkProvider,
CurrencyNameTable currencyNameTable,
IOptions<LightningNetworkOptions> options,
LightningLikePaymentHandler lightningLikePaymentHandler)
{
_networkProvider = networkProvider;
_currencyNameTable = currencyNameTable;
_lightningLikePaymentHandler = lightningLikePaymentHandler;
Options = options;
}
public override PaymentType PaymentType => PaymentTypes.LightningLike;
public IOptions<LightningNetworkOptions> Options { get; }
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(
InvoiceLogs logs,
LNURLPaySupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, Data.StoreData store,
BTCPayNetwork network, object preparePaymentObject)
{
if (!supportedPaymentMethod.EnableForStandardInvoices &&
paymentMethod.ParentEntity.Type == InvoiceType.Standard)
{
throw new PaymentMethodUnavailableException("LNURL is not enabled for standard invoices");
}
if (string.IsNullOrEmpty(paymentMethod.ParentEntity.Id))
{
var lnSupported = store.GetSupportedPaymentMethods(_networkProvider)
.OfType<LightningSupportedPaymentMethod>().SingleOrDefault(method =>
method.PaymentId.CryptoCode == supportedPaymentMethod.CryptoCode &&
method.PaymentId.PaymentType == LightningPaymentType.Instance);
if (lnSupported is null)
{
throw new PaymentMethodUnavailableException("LNURL requires a lightning node to be configured for the store.");
}
return new LNURLPayPaymentMethodDetails()
{
Activated = false, LightningSupportedPaymentMethod = lnSupported
};
}
var lnLightningSupportedPaymentMethod =
((LNURLPayPaymentMethodDetails)paymentMethod.GetPaymentMethodDetails()).LightningSupportedPaymentMethod;
NodeInfo? nodeInfo = null;
if (lnLightningSupportedPaymentMethod != null)
{
nodeInfo = (await _lightningLikePaymentHandler.GetNodeInfo(lnLightningSupportedPaymentMethod, _networkProvider.GetNetwork<BTCPayNetwork>(supportedPaymentMethod.CryptoCode), logs, paymentMethod.PreferOnion)).FirstOrDefault();
}
return new LNURLPayPaymentMethodDetails
{
Activated = true,
LightningSupportedPaymentMethod = lnLightningSupportedPaymentMethod,
BTCPayInvoiceId = paymentMethod.ParentEntity.Id,
Bech32Mode = supportedPaymentMethod.UseBech32Scheme,
NodeInfo = nodeInfo?.ToString()
};
}
public override IEnumerable<PaymentMethodId> GetSupportedPaymentMethods()
{
return _networkProvider
.GetAll()
.OfType<BTCPayNetwork>()
.Where(network => network.NBitcoinNetwork.Consensus.SupportSegwit && network.SupportLightning)
.Select(network => new PaymentMethodId(network.CryptoCode, PaymentTypes.LNURLPay));
}
public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse,
StoreBlob storeBlob, IPaymentMethod paymentMethod)
{
var paymentMethodId = paymentMethod.GetId();
var cryptoInfo = invoiceResponse.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
var network = _networkProvider.GetNetwork<BTCPayNetwork>(model.CryptoCode);
model.PaymentMethodName = GetPaymentMethodName(network);
model.InvoiceBitcoinUrl = cryptoInfo.PaymentUrls?.AdditionalData["LNURLP"].ToObject<string>();
model.InvoiceBitcoinUrlQR = model.InvoiceBitcoinUrl;
model.BtcAddress = model.InvoiceBitcoinUrl;
model.PeerInfo = ((LNURLPayPaymentMethodDetails) paymentMethod.GetPaymentMethodDetails()).NodeInfo;
if ( storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC")
{
var satoshiCulture = new CultureInfo(CultureInfo.InvariantCulture.Name);
satoshiCulture.NumberFormat.NumberGroupSeparator = " ";
model.CryptoCode = "Sats";
model.BtcDue = Money.Parse(model.BtcDue).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi)
.ToString("N0", satoshiCulture);
model.NetworkFee = new Money(model.NetworkFee, MoneyUnit.BTC).ToUnit(MoneyUnit.Satoshi);
model.Rate =
_currencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, model.InvoiceCurrency);
}
}
public override string GetCryptoImage(PaymentMethodId paymentMethodId)
{
var network = _networkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
return GetCryptoImage(network);
}
private string GetCryptoImage(BTCPayNetworkBase network)
{
return ((BTCPayNetwork)network).LightningImagePath;
}
public override string GetPaymentMethodName(PaymentMethodId paymentMethodId)
{
var network = _networkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
return GetPaymentMethodName(network);
}
public override CheckoutUIPaymentMethodSettings GetCheckoutUISettings()
{
return new CheckoutUIPaymentMethodSettings()
{
ExtensionPartial = "Lightning/LightningLikeMethodCheckout",
CheckoutBodyVueComponentName = "LightningLikeMethodCheckout",
CheckoutHeaderVueComponentName = "LightningLikeMethodCheckoutHeader",
NoScriptPartialName = "Lightning/LightningLikeMethodCheckoutNoScript"
};
}
private string GetPaymentMethodName(BTCPayNetworkBase network)
{
return $"{network.DisplayName} (Lightning LNURL)";
}
public override object PreparePayment(LNURLPaySupportedPaymentMethod supportedPaymentMethod,
Data.StoreData store,
BTCPayNetworkBase network)
{
// pass a non null obj, so that if lazy payment feature is used, it has a marker to trigger activation
return new { };
}
}
}

View file

@ -0,0 +1,34 @@
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.Lightning;
using BTCPayServer.Payments.Lightning;
using Newtonsoft.Json;
namespace BTCPayServer.Payments
{
public class LNURLPayPaymentMethodDetails : LightningLikePaymentMethodDetails
{
public LightningSupportedPaymentMethod LightningSupportedPaymentMethod { get; set; }
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney GeneratedBoltAmount { get; set; }
public string BTCPayInvoiceId { get; set; }
public bool Bech32Mode { get; set; }
public string ProvidedComment { get; set; }
public override PaymentType GetPaymentType()
{
return LNURLPayPaymentType.Instance;
}
public override string GetAdditionalDataPartialName()
{
if (string.IsNullOrEmpty(ProvidedComment))
{
return null;
}
return "LNURL/AdditionalPaymentMethodDetails";
}
}
}

View file

@ -0,0 +1,22 @@
#nullable enable
using System;
using BTCPayServer.Lightning;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning
{
public class LNURLPaySupportedPaymentMethod : ISupportedPaymentMethod
{
public string CryptoCode { get; set; } = string.Empty;
[JsonIgnore]
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LNURLPay);
public bool UseBech32Scheme { get; set; }
public bool EnableForStandardInvoices { get; set; } = false;
public bool LUD12Enabled { get; set; } = true;
}
}

View file

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers.GreenField;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Payments
{
public class LNURLPayPaymentType : LightningPaymentType
{
public new static LNURLPayPaymentType Instance { get; } = new LNURLPayPaymentType();
public override string ToPrettyString() => "LNURL-Pay";
public override string GetId() => "LNURLPAY";
public override string ToStringNormalized() => "LNURLPAY";
public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str)
{
return JsonConvert.DeserializeObject<LNURLPayPaymentMethodDetails>(str);
}
public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network,
JToken value)
{
return JsonConvert.DeserializeObject<LNURLPaySupportedPaymentMethod>(value.ToString());
}
public override string GetPaymentLink(BTCPayNetworkBase network, IPaymentMethodDetails paymentMethodDetails,
Money cryptoInfoDue, string serverUri)
{
if (!paymentMethodDetails.Activated)
{
return null;
}
var lnurlPaymentMethodDetails = (LNURLPayPaymentMethodDetails)paymentMethodDetails;
var uri = new Uri(
$"{serverUri.WithTrailingSlash()}{network.CryptoCode}/lnurl/pay/i/{lnurlPaymentMethodDetails.BTCPayInvoiceId}");
return LNURL.LNURL.EncodeUri(uri, "payRequest", lnurlPaymentMethodDetails.Bech32Mode).ToString();
}
public override string InvoiceViewPaymentPartialName { get; } = "Lightning/ViewLightningLikePaymentData";
public override object GetGreenfieldData(ISupportedPaymentMethod supportedPaymentMethod, bool canModifyStore)
{
if (supportedPaymentMethod is LNURLPaySupportedPaymentMethod lightningSupportedPaymentMethod)
return new LNURLPayPaymentMethodBaseData()
{
UseBech32Scheme = lightningSupportedPaymentMethod.UseBech32Scheme,
EnableForStandardInvoices = lightningSupportedPaymentMethod.EnableForStandardInvoices,
LUD12Enabled = lightningSupportedPaymentMethod.LUD12Enabled
};
return null;
}
public override bool IsPaymentType(string paymentType)
{
return IsPaymentTypeBase(paymentType);
}
public override void PopulateCryptoInfo(PaymentMethod details, InvoiceCryptoInfo invoiceCryptoInfo, string serverUrl)
{
invoiceCryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls()
{
AdditionalData = new Dictionary<string, JToken>()
{
{"LNURLP", JToken.FromObject(GetPaymentLink(details.Network, details.GetPaymentMethodDetails(), invoiceCryptoInfo.Due,
serverUrl))}
}
};
}
}
}

View file

@ -16,6 +16,7 @@ namespace BTCPayServer.Payments.Lightning
public string BOLT11 { get; set; } public string BOLT11 { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))] [JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
public uint256 PaymentHash { get; set; } public uint256 PaymentHash { get; set; }
public string PaymentType { get; set; }
public string GetDestination() public string GetDestination()
{ {
@ -33,7 +34,7 @@ namespace BTCPayServer.Payments.Lightning
public PaymentType GetPaymentType() public PaymentType GetPaymentType()
{ {
return PaymentTypes.LightningLike; return string.IsNullOrEmpty(PaymentType) ? PaymentTypes.LightningLike : PaymentTypes.Parse(PaymentType);
} }
public string[] GetSearchTerms() public string[] GetSearchTerms()

View file

@ -68,7 +68,7 @@ namespace BTCPayServer.Payments.Lightning
} }
//direct casting to (BTCPayNetwork) is fixed in other pull requests with better generic interfacing for handlers //direct casting to (BTCPayNetwork) is fixed in other pull requests with better generic interfacing for handlers
var storeBlob = store.GetStoreBlob(); var storeBlob = store.GetStoreBlob();
var test = GetNodeInfo(supportedPaymentMethod, network, paymentMethod.PreferOnion); var nodeInfo = GetNodeInfo(supportedPaymentMethod, network, logs, paymentMethod.PreferOnion);
var invoice = paymentMethod.ParentEntity; var invoice = paymentMethod.ParentEntity;
decimal due = Extensions.RoundUp(invoice.Price / paymentMethod.Rate, network.Divisibility); decimal due = Extensions.RoundUp(invoice.Price / paymentMethod.Rate, network.Divisibility);
@ -109,56 +109,80 @@ namespace BTCPayServer.Payments.Lightning
} }
} }
var nodeInfo = await test;
return new LightningLikePaymentMethodDetails return new LightningLikePaymentMethodDetails
{ {
Activated = true, Activated = true,
BOLT11 = lightningInvoice.BOLT11, BOLT11 = lightningInvoice.BOLT11,
PaymentHash = BOLT11PaymentRequest.Parse(lightningInvoice.BOLT11, network.NBitcoinNetwork).PaymentHash, PaymentHash = BOLT11PaymentRequest.Parse(lightningInvoice.BOLT11, network.NBitcoinNetwork).PaymentHash,
InvoiceId = lightningInvoice.Id, InvoiceId = lightningInvoice.Id,
NodeInfo = nodeInfo.First().ToString() NodeInfo = (await nodeInfo).FirstOrDefault()?.ToString()
}; };
} }
public async Task<NodeInfo[]> GetNodeInfo(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, bool? preferOnion = null) public async Task<NodeInfo[]> GetNodeInfo(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceLogs invoiceLogs, bool? preferOnion = null)
{ {
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary)) if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new PaymentMethodUnavailableException("Full node not available"); throw new PaymentMethodUnavailableException("Full node not available");
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT)) try
{ {
var client = supportedPaymentMethod.CreateLightningClient(network, Options.Value, _lightningClientFactory); using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
LightningNodeInformation info;
try
{ {
info = await client.GetInfo(cts.Token); var client = CreateLightningClient(supportedPaymentMethod, network);
} LightningNodeInformation info;
catch (OperationCanceledException) when (cts.IsCancellationRequested) try
{ {
throw new PaymentMethodUnavailableException("The lightning node did not reply in a timely manner"); info = await client.GetInfo(cts.Token);
} }
catch (Exception ex) catch (OperationCanceledException) when (cts.IsCancellationRequested)
{ {
throw new PaymentMethodUnavailableException($"Error while connecting to the API: {ex.Message}" + throw new PaymentMethodUnavailableException("The lightning node did not reply in a timely manner");
(!string.IsNullOrEmpty(ex.InnerException?.Message) ? $" ({ex.InnerException.Message})" : "")); }
} catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Error while connecting to the API: {ex.Message}" +
(!string.IsNullOrEmpty(ex.InnerException?.Message) ? $" ({ex.InnerException.Message})" : ""));
}
var nodeInfo = preferOnion != null && info.NodeInfoList.Any(i => i.IsTor == preferOnion) var nodeInfo = preferOnion != null && info.NodeInfoList.Any(i => i.IsTor == preferOnion)
? info.NodeInfoList.Where(i => i.IsTor == preferOnion.Value).ToArray() ? info.NodeInfoList.Where(i => i.IsTor == preferOnion.Value).ToArray()
: info.NodeInfoList.Select(i => i).ToArray(); : info.NodeInfoList.Select(i => i).ToArray();
if (!nodeInfo.Any()) // Maybe the user does not have an easily accessible ln node. Node info should be optional. The UI also supports this.
{ // if (!nodeInfo.Any())
throw new PaymentMethodUnavailableException("No lightning node public address has been configured"); // {
} // throw new PaymentMethodUnavailableException("No lightning node public address has been configured");
// }
var blocksGap = summary.Status.ChainHeight - info.BlockHeight; var blocksGap = summary.Status.ChainHeight - info.BlockHeight;
if (blocksGap > 10) if (blocksGap > 10)
{ {
throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)"); throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)");
} }
return nodeInfo; return nodeInfo;
}
}
catch(Exception e)
{
invoiceLogs.Write($"NodeInfo failed to be fetched: {e.Message}", InvoiceEventData.EventSeverity.Error);
}
return Array.Empty<NodeInfo>();
}
public ILightningClient CreateLightningClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
var external = supportedPaymentMethod.GetExternalLightningUrl();
if (external != null)
{
return _lightningClientFactory.Create(external, network);
}
else
{
if (!Options.Value.InternalLightningByCryptoCode.TryGetValue(network.CryptoCode, out var connectionString))
throw new PaymentMethodUnavailableException("No internal node configured");
return _lightningClientFactory.Create(connectionString, network);
} }
} }

View file

@ -10,7 +10,7 @@ namespace BTCPayServer.Payments.Lightning
public string InvoiceId { get; set; } public string InvoiceId { get; set; }
public string NodeInfo { get; set; } public string NodeInfo { get; set; }
public string GetPaymentDestination() public virtual string GetPaymentDestination()
{ {
return BOLT11; return BOLT11;
} }
@ -20,7 +20,7 @@ namespace BTCPayServer.Payments.Lightning
return PaymentHash ?? BOLT11PaymentRequest.Parse(BOLT11, network).PaymentHash; return PaymentHash ?? BOLT11PaymentRequest.Parse(BOLT11, network).PaymentHash;
} }
public PaymentType GetPaymentType() public virtual PaymentType GetPaymentType()
{ {
return PaymentTypes.LightningLike; return PaymentTypes.LightningLike;
} }
@ -35,5 +35,10 @@ namespace BTCPayServer.Payments.Lightning
return 0.0m; return 0.0m;
} }
public bool Activated { get; set; } public bool Activated { get; set; }
public virtual string GetAdditionalDataPartialName()
{
return null;
}
} }
} }

View file

@ -104,21 +104,42 @@ namespace BTCPayServer.Payments.Lightning
} }
} }
} }
private string GetCacheKey(string invoiceId)
{
return $"{nameof(GetListenedInvoices)}-{invoiceId}";
}
private Task<List<ListenedInvoice>> GetListenedInvoices(string invoiceId) private Task<List<ListenedInvoice>> GetListenedInvoices(string invoiceId)
{ {
return _memoryCache.GetOrCreateAsync($"{nameof(GetListenedInvoices)}-{invoiceId}", async (cacheEntry) => return _memoryCache.GetOrCreateAsync( GetCacheKey(invoiceId), async (cacheEntry) =>
{ {
var listenedInvoices = new List<ListenedInvoice>(); var listenedInvoices = new List<ListenedInvoice>();
var invoice = await _InvoiceRepository.GetInvoice(invoiceId); var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
foreach (var paymentMethod in invoice.GetPaymentMethods() foreach (var paymentMethod in invoice.GetPaymentMethods()
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike)) .Where(c => new []{PaymentTypes.LightningLike, LNURLPayPaymentType.Instance }.Contains(c.GetId().PaymentType)))
{ {
var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails; LightningLikePaymentMethodDetails lightningMethod;
if (lightningMethod == null || !lightningMethod.Activated) LightningSupportedPaymentMethod lightningSupportedMethod;
continue; switch (paymentMethod.GetPaymentMethodDetails())
var lightningSupportedMethod = invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>() {
.FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode); case LNURLPayPaymentMethodDetails lnurlPayPaymentMethodDetails:
if (lightningSupportedMethod == null)
lightningMethod = lnurlPayPaymentMethodDetails;
lightningSupportedMethod = lnurlPayPaymentMethodDetails.LightningSupportedPaymentMethod;
break;
case LightningLikePaymentMethodDetails { Activated: true } lightningLikePaymentMethodDetails:
lightningMethod = lightningLikePaymentMethodDetails;
lightningSupportedMethod = invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>()
.FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode);
break;
default:
continue;
}
if (lightningSupportedMethod == null || string.IsNullOrEmpty(lightningMethod.InvoiceId))
continue; continue;
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethod.GetId().CryptoCode); var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethod.GetId().CryptoCode);
@ -164,7 +185,6 @@ namespace BTCPayServer.Payments.Lightning
if (inv.State.Status == InvoiceStatusLegacy.New && if (inv.State.Status == InvoiceStatusLegacy.New &&
inv.State.ExceptionStatus == InvoiceExceptionStatus.PaidPartial) inv.State.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
{ {
var invoice = await _InvoiceRepository.GetInvoice(inv.InvoiceId); var invoice = await _InvoiceRepository.GetInvoice(inv.InvoiceId);
await CreateNewLNInvoiceForBTCPayInvoice(invoice); await CreateNewLNInvoiceForBTCPayInvoice(invoice);
} }
@ -174,6 +194,15 @@ namespace BTCPayServer.Payments.Lightning
{ {
if (inv.PaymentMethodId.PaymentType == LightningPaymentType.Instance) if (inv.PaymentMethodId.PaymentType == LightningPaymentType.Instance)
{ {
_memoryCache.Remove(GetCacheKey(inv.InvoiceId));
_CheckInvoices.Writer.TryWrite(inv.InvoiceId);
}
}));
leases.Add(_Aggregator.Subscribe<Events.InvoiceNewPaymentDetailsEvent>(inv =>
{
if (inv.PaymentMethodId.PaymentType == LNURLPayPaymentType.Instance)
{
_memoryCache.Remove(GetCacheKey(inv.InvoiceId));
_CheckInvoices.Writer.TryWrite(inv.InvoiceId); _CheckInvoices.Writer.TryWrite(inv.InvoiceId);
} }
})); }));
@ -196,7 +225,7 @@ namespace BTCPayServer.Payments.Lightning
private async Task CreateNewLNInvoiceForBTCPayInvoice(InvoiceEntity invoice) private async Task CreateNewLNInvoiceForBTCPayInvoice(InvoiceEntity invoice)
{ {
var paymentMethods = invoice.GetPaymentMethods() var paymentMethods = invoice.GetPaymentMethods()
.Where(method => method.GetId().PaymentType == PaymentTypes.LightningLike) .Where(method => new []{PaymentTypes.LightningLike, LNURLPayPaymentType.Instance}.Contains(method.GetId().PaymentType))
.ToArray(); .ToArray();
var store = await _storeRepository.FindStore(invoice.StoreId); var store = await _storeRepository.FindStore(invoice.StoreId);
if (paymentMethods.Any()) if (paymentMethods.Any())
@ -209,8 +238,60 @@ namespace BTCPayServer.Payments.Lightning
{ {
try try
{ {
var supportedMethod = invoice var oldDetails = (LightningLikePaymentMethodDetails) paymentMethod.GetPaymentMethodDetails();
if (!oldDetails.Activated)
{
continue;
}
if (oldDetails is LNURLPayPaymentMethodDetails lnurlPayPaymentMethodDetails && !string.IsNullOrEmpty(lnurlPayPaymentMethodDetails.BOLT11))
{
try
{
var client = _lightningLikePaymentHandler.CreateLightningClient(lnurlPayPaymentMethodDetails.LightningSupportedPaymentMethod,
(BTCPayNetwork)paymentMethod.Network);
await client.CancelInvoice(oldDetails.InvoiceId);
}
catch
{
//not a fully supported option
}
lnurlPayPaymentMethodDetails = new LNURLPayPaymentMethodDetails()
{
Activated = lnurlPayPaymentMethodDetails.Activated,
Bech32Mode = lnurlPayPaymentMethodDetails.Bech32Mode,
InvoiceId = null,
NodeInfo = lnurlPayPaymentMethodDetails.NodeInfo,
GeneratedBoltAmount = null,
BOLT11 = null,
LightningSupportedPaymentMethod = lnurlPayPaymentMethodDetails.LightningSupportedPaymentMethod,
BTCPayInvoiceId = lnurlPayPaymentMethodDetails.BTCPayInvoiceId
};
await _InvoiceRepository.NewPaymentDetails(invoice.Id, lnurlPayPaymentMethodDetails,
paymentMethod.Network);
_Aggregator.Publish(new Events.InvoiceNewPaymentDetailsEvent(invoice.Id,
lnurlPayPaymentMethodDetails, paymentMethod.GetId()));
continue;
}
LightningSupportedPaymentMethod supportedMethod = invoice
.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>(paymentMethod.GetId()).First(); .GetSupportedPaymentMethod<LightningSupportedPaymentMethod>(paymentMethod.GetId()).First();
try
{
var client = _lightningLikePaymentHandler.CreateLightningClient(supportedMethod,
(BTCPayNetwork)paymentMethod.Network);
await client.CancelInvoice(oldDetails.InvoiceId);
}
catch
{
//not a fully supported option
}
var prepObj = var prepObj =
_lightningLikePaymentHandler.PreparePayment(supportedMethod, store, paymentMethod.Network); _lightningLikePaymentHandler.PreparePayment(supportedMethod, store, paymentMethod.Network);
var newPaymentMethodDetails = var newPaymentMethodDetails =
@ -346,7 +427,7 @@ namespace BTCPayServer.Payments.Lightning
var client = _lightningClientFactory.Create(ConnectionString, _network); var client = _lightningClientFactory.Create(ConnectionString, _network);
LightningInvoice lightningInvoice = await client.GetInvoice(listenedInvoice.PaymentMethodDetails.InvoiceId, cancellation); LightningInvoice lightningInvoice = await client.GetInvoice(listenedInvoice.PaymentMethodDetails.InvoiceId, cancellation);
if (lightningInvoice?.Status is LightningInvoiceStatus.Paid && if (lightningInvoice?.Status is LightningInvoiceStatus.Paid &&
await AddPayment(lightningInvoice, listenedInvoice.InvoiceId)) await AddPayment(lightningInvoice, listenedInvoice.InvoiceId,listenedInvoice.PaymentMethod.GetId().PaymentType))
{ {
Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Payment detected via polling on {listenedInvoice.InvoiceId}"); Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Payment detected via polling on {listenedInvoice.InvoiceId}");
} }
@ -392,7 +473,7 @@ namespace BTCPayServer.Payments.Lightning
if (notification.Status == LightningInvoiceStatus.Paid && if (notification.Status == LightningInvoiceStatus.Paid &&
notification.PaidAt.HasValue && notification.Amount != null) notification.PaidAt.HasValue && notification.Amount != null)
{ {
if (await AddPayment(notification, listenedInvoice.InvoiceId)) if (await AddPayment(notification, listenedInvoice.InvoiceId, listenedInvoice.PaymentMethod.GetId().PaymentType))
{ {
Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Payment detected via notification ({listenedInvoice.InvoiceId})"); Logs.PayServer.LogInformation($"{_network.CryptoCode} (Lightning): Payment detected via notification ({listenedInvoice.InvoiceId})");
} }
@ -439,13 +520,14 @@ namespace BTCPayServer.Payments.Lightning
bool _ErrorAlreadyLogged = false; bool _ErrorAlreadyLogged = false;
readonly ConcurrentDictionary<string, ListenedInvoice> _ListenedInvoices = new ConcurrentDictionary<string, ListenedInvoice>(); readonly ConcurrentDictionary<string, ListenedInvoice> _ListenedInvoices = new ConcurrentDictionary<string, ListenedInvoice>();
public async Task<bool> AddPayment(LightningInvoice notification, string invoiceId) 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, BOLT11 = notification.BOLT11,
PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, _network.NBitcoinNetwork).PaymentHash, PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, _network.NBitcoinNetwork).PaymentHash,
Amount = notification.AmountReceived ?? notification.Amount, // if running old version amount received might be unavailable Amount = notification.AmountReceived ?? notification.Amount, // if running old version amount received might be unavailable,
PaymentType = paymentType.ToString()
}, _network, accounted: true); }, _network, accounted: true);
if (payment != null) if (payment != null)
{ {

View file

@ -2,12 +2,13 @@ using System;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using NBitcoin; using NBitcoin;
using BTCPayServer.BIP78.Sender; using BTCPayServer.BIP78.Sender;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using NBitpayClient;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo;
namespace BTCPayServer.Payments namespace BTCPayServer.Payments
{ {
@ -101,5 +102,14 @@ namespace BTCPayServer.Payments
{ {
return string.IsNullOrEmpty(paymentType) || base.IsPaymentType(paymentType); return string.IsNullOrEmpty(paymentType) || base.IsPaymentType(paymentType);
} }
public override void PopulateCryptoInfo(PaymentMethod details, InvoiceCryptoInfo cryptoInfo,
string serverUrl)
{
cryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls()
{
BIP21 = GetPaymentLink(details.Network, details.GetPaymentMethodDetails(), cryptoInfo.Due, serverUrl),
};
}
} }
} }

View file

@ -13,7 +13,7 @@ namespace BTCPayServer.Payments
{ {
public static LightningPaymentType Instance { get; } = new LightningPaymentType(); public static LightningPaymentType Instance { get; } = new LightningPaymentType();
private LightningPaymentType() { } private protected LightningPaymentType() { }
public override string ToPrettyString() => "Off-Chain"; public override string ToPrettyString() => "Off-Chain";
public override string GetId() => "LightningLike"; public override string GetId() => "LightningLike";
@ -87,5 +87,14 @@ namespace BTCPayServer.Payments
{ {
return paymentType?.Equals("offchain", StringComparison.InvariantCultureIgnoreCase) is true || base.IsPaymentType(paymentType); return paymentType?.Equals("offchain", StringComparison.InvariantCultureIgnoreCase) is true || base.IsPaymentType(paymentType);
} }
public override void PopulateCryptoInfo(PaymentMethod details, InvoiceCryptoInfo invoiceCryptoInfo, string serverUrl)
{
invoiceCryptoInfo.PaymentUrls = new InvoiceCryptoInfo.InvoicePaymentUrls()
{
BOLT11 = GetPaymentLink(details.Network, details.GetPaymentMethodDetails(), invoiceCryptoInfo.Due,
serverUrl)
};
}
} }
} }

View file

@ -17,7 +17,7 @@ namespace BTCPayServer.Payments
{ {
private static PaymentType[] _paymentTypes = private static PaymentType[] _paymentTypes =
{ {
BTCLike, LightningLike, BTCLike, LightningLike, LNURLPay,
#if ALTCOINS #if ALTCOINS
MoneroLike, MoneroLike,
EthereumPaymentType.Instance EthereumPaymentType.Instance
@ -31,6 +31,10 @@ namespace BTCPayServer.Payments
/// Lightning payment /// Lightning payment
/// </summary> /// </summary>
public static LightningPaymentType LightningLike => LightningPaymentType.Instance; public static LightningPaymentType LightningLike => LightningPaymentType.Instance;
/// <summary>
/// Lightning payment
/// </summary>
public static LNURLPayPaymentType LNURLPay => LNURLPayPaymentType.Instance;
#if ALTCOINS #if ALTCOINS
/// <summary> /// <summary>
@ -84,6 +88,11 @@ namespace BTCPayServer.Payments
public abstract object GetGreenfieldData(ISupportedPaymentMethod supportedPaymentMethod, bool canModifyStore); public abstract object GetGreenfieldData(ISupportedPaymentMethod supportedPaymentMethod, bool canModifyStore);
public virtual bool IsPaymentType(string paymentType) public virtual bool IsPaymentType(string paymentType)
{
return IsPaymentTypeBase(paymentType);
}
protected bool IsPaymentTypeBase(string paymentType)
{ {
paymentType = paymentType?.ToLowerInvariant(); paymentType = paymentType?.ToLowerInvariant();
return new[] return new[]
@ -94,5 +103,8 @@ namespace BTCPayServer.Payments
paymentType, paymentType,
StringComparer.InvariantCultureIgnoreCase); StringComparer.InvariantCultureIgnoreCase);
} }
public abstract void PopulateCryptoInfo(PaymentMethod details, Services.Invoices.InvoiceCryptoInfo invoiceCryptoInfo,
string serverUrl);
} }
} }

View file

@ -66,6 +66,10 @@ namespace BTCPayServer.Services.Altcoins.Ethereum.Payments
return null; return null;
} }
public override void PopulateCryptoInfo(PaymentMethod details, InvoiceCryptoInfo invoiceCryptoInfo, string serverUrl)
{
}
} }
} }
#endif #endif

View file

@ -69,6 +69,11 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
return null; return null;
} }
public override void PopulateCryptoInfo(PaymentMethod details, InvoiceCryptoInfo invoiceCryptoInfo, string serverUrl)
{
}
} }
} }
#endif #endif

View file

@ -19,7 +19,15 @@ using Newtonsoft.Json.Serialization;
namespace BTCPayServer.Services.Invoices namespace BTCPayServer.Services.Invoices
{ {
public class InvoiceCryptoInfo : NBitpayClient.InvoiceCryptoInfo
{
[JsonProperty("paymentUrls")]
public new InvoicePaymentUrls PaymentUrls { get; set; }
public class InvoicePaymentUrls : NBitpayClient.InvoicePaymentUrls
{
[JsonExtensionData] public Dictionary<string, JToken> AdditionalData { get; set; }
}
}
public class InvoiceMetadata public class InvoiceMetadata
{ {
public static readonly JsonSerializer MetadataSerializer; public static readonly JsonSerializer MetadataSerializer;
@ -443,19 +451,19 @@ namespace BTCPayServer.Services.Invoices
Flags = new Flags() { Refundable = Refundable }, Flags = new Flags() { Refundable = Refundable },
PaymentSubtotals = new Dictionary<string, decimal>(), PaymentSubtotals = new Dictionary<string, decimal>(),
PaymentTotals = new Dictionary<string, decimal>(), PaymentTotals = new Dictionary<string, decimal>(),
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>(), SupportedTransactionCurrencies = new Dictionary<string, NBitpayClient.InvoiceSupportedTransactionCurrency>(),
Addresses = new Dictionary<string, string>(), Addresses = new Dictionary<string, string>(),
PaymentCodes = new Dictionary<string, InvoicePaymentUrls>(), PaymentCodes = new Dictionary<string, InvoiceCryptoInfo.InvoicePaymentUrls>(),
ExchangeRates = new Dictionary<string, Dictionary<string, decimal>>() ExchangeRates = new Dictionary<string, Dictionary<string, decimal>>()
}; };
dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id; dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id;
dto.CryptoInfo = new List<NBitpayClient.InvoiceCryptoInfo>(); dto.CryptoInfo = new List<InvoiceCryptoInfo>();
dto.MinerFees = new Dictionary<string, MinerFeeInfo>(); dto.MinerFees = new Dictionary<string, MinerFeeInfo>();
foreach (var info in this.GetPaymentMethods()) foreach (var info in this.GetPaymentMethods())
{ {
var accounting = info.Calculate(); var accounting = info.Calculate();
var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo(); var cryptoInfo = new InvoiceCryptoInfo();
var subtotalPrice = accounting.TotalDue - accounting.NetworkFee; var subtotalPrice = accounting.TotalDue - accounting.NetworkFee;
var cryptoCode = info.GetId().CryptoCode; var cryptoCode = info.GetId().CryptoCode;
var details = info.GetPaymentMethodDetails(); var details = info.GetPaymentMethodDetails();
@ -500,39 +508,31 @@ namespace BTCPayServer.Services.Invoices
}).ToList(); }).ToList();
if (details?.Activated is true && paymentId.PaymentType == PaymentTypes.LightningLike) if (details?.Activated is true)
{ {
cryptoInfo.PaymentUrls = new InvoicePaymentUrls()
paymentId.PaymentType.PopulateCryptoInfo(info, cryptoInfo, ServerUrl);
if (paymentId.PaymentType == PaymentTypes.BTCLike)
{ {
BOLT11 = paymentId.PaymentType.GetPaymentLink(info.Network, details, cryptoInfo.Due, var minerInfo = new MinerFeeInfo();
ServerUrl) minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
}; minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate
} .GetFee(1).Satoshi;
else if (details?.Activated is true && paymentId.PaymentType == PaymentTypes.BTCLike) dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo);
{
var minerInfo = new MinerFeeInfo();
minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate
.GetFee(1).Satoshi;
dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo);
cryptoInfo.PaymentUrls = new InvoicePaymentUrls()
{
BIP21 = paymentId.PaymentType.GetPaymentLink(info.Network, details, cryptoInfo.Due,
ServerUrl)
};
#pragma warning disable 618 #pragma warning disable 618
if (info.CryptoCode == "BTC") if (info.CryptoCode == "BTC")
{ {
dto.BTCPrice = cryptoInfo.Price; dto.BTCPrice = cryptoInfo.Price;
dto.Rate = cryptoInfo.Rate; dto.Rate = cryptoInfo.Rate;
dto.ExRates = cryptoInfo.ExRates; dto.ExRates = cryptoInfo.ExRates;
dto.BitcoinAddress = cryptoInfo.Address; dto.BitcoinAddress = cryptoInfo.Address;
dto.BTCPaid = cryptoInfo.Paid; dto.BTCPaid = cryptoInfo.Paid;
dto.BTCDue = cryptoInfo.Due; dto.BTCDue = cryptoInfo.Due;
dto.PaymentUrls = cryptoInfo.PaymentUrls; dto.PaymentUrls = cryptoInfo.PaymentUrls;
} }
#pragma warning restore 618 #pragma warning restore 618
}
} }
dto.CryptoInfo.Add(cryptoInfo); dto.CryptoInfo.Add(cryptoInfo);

View file

@ -30,9 +30,9 @@
<td>@payment.PaymentMethod</td> <td>@payment.PaymentMethod</td>
@if (Model.ShowAddress) @if (Model.ShowAddress)
{ {
<td title="@payment.Address"> <td title="@payment.Address">
<span class="text-truncate d-block" style="max-width: 400px">@payment.Address</span> <span class="text-truncate d-block" style="max-width: 400px">@payment.Address</span>
</td> </td>
} }
<td class="text-end">@payment.Rate</td> <td class="text-end">@payment.Rate</td>
<td class="text-end">@payment.Paid</td> <td class="text-end">@payment.Paid</td>
@ -42,6 +42,12 @@
<td class="text-end">@payment.Overpaid</td> <td class="text-end">@payment.Overpaid</td>
} }
</tr> </tr>
var details = payment.PaymentMethodRaw.GetPaymentMethodDetails();
var name = details.GetAdditionalDataPartialName();
if (!string.IsNullOrEmpty(name))
{
<partial name="@name" model="@details" />
}
} }
</tbody> </tbody>
</table> </table>

View file

@ -0,0 +1,12 @@
@model BTCPayServer.Payments.LNURLPayPaymentMethodDetails
@if (!string.IsNullOrEmpty(Model.ProvidedComment))
{
<tr>
<td colspan="100% bg-tile">
LNURL Comment: @Model.ProvidedComment
</td>
</tr>
}

View file

@ -5,7 +5,8 @@
<div> <div>
<div class="bp-view payment scan" id="scan" v-bind:class="{ 'active': currentTab == 'scan'}"> <div class="bp-view payment scan" id="scan" v-bind:class="{ 'active': currentTab == 'scan'}">
<div class="wrapBtnGroup" v-bind:class="{ invisible: !scanDisplayQr }"> <div class="wrapBtnGroup" v-bind:class="{ invisible: !scanDisplayQr }">
<div class="btnGroupLnd"> <div class="btnGroupLnd"
v-if="srvModel.peerInfo" >
<button <button
v-on:click="toggleLightningData('bolt11')" v-on:click="toggleLightningData('bolt11')"
v-bind:class="{ active: currentLightningDisplay === 'bolt11' }" v-bind:class="{ active: currentLightningDisplay === 'bolt11' }"
@ -48,8 +49,8 @@
<img v-bind:src="srvModel.cryptoImage"/> <img v-bind:src="srvModel.cryptoImage"/>
</div> </div>
</div> </div>
<div class="separatorGem"></div> <div class="separatorGem" v-if="srvModel.peerInfo" ></div>
<div class="copySectionBox"> <div class="copySectionBox" v-if="srvModel.peerInfo">
<label>{{$t("Node Info")}}</label> <label>{{$t("Node Info")}}</label>
<div class="inputWithIcon _copyInput"> <div class="inputWithIcon _copyInput">
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.peerInfo" readonly="readonly"/> <input type="text" class="checkoutTextbox" v-bind:value="srvModel.peerInfo" readonly="readonly"/>

View file

@ -5,5 +5,8 @@
<p> <p>
<a href="@Model.InvoiceBitcoinUrl" style="word-break: break-word;" rel="noreferrer noopener">@Model.InvoiceBitcoinUrl</a> <a href="@Model.InvoiceBitcoinUrl" style="word-break: break-word;" rel="noreferrer noopener">@Model.InvoiceBitcoinUrl</a>
</p> </p>
<p>Peer Info: <b>@Model.PeerInfo</b></p> @if (!string.IsNullOrEmpty(Model.PeerInfo))
{
<p>Peer Info: <b>@Model.PeerInfo</b></p>
}
</div> </div>

View file

@ -3,7 +3,7 @@
@model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity> @model IEnumerable<BTCPayServer.Services.Invoices.PaymentEntity>
@{ @{
var offchainPayments = Model.Where(entity => entity.GetPaymentMethodId()?.PaymentType == LightningPaymentType.Instance).Select(payment => var offchainPayments = Model.Where(entity => entity.GetPaymentMethodId()?.PaymentType == LightningPaymentType.Instance || entity.GetPaymentMethodId()?.PaymentType == LNURLPayPaymentType.Instance).Select(payment =>
{ {
var offChainPaymentData = payment.GetCryptoPaymentData() as LightningLikePaymentData; var offChainPaymentData = payment.GetCryptoPaymentData() as LightningLikePaymentData;
if (offChainPaymentData is null) if (offChainPaymentData is null)
@ -13,7 +13,8 @@
return new OffChainPaymentViewModel() return new OffChainPaymentViewModel()
{ {
Crypto = payment.Network.CryptoCode, Crypto = payment.Network.CryptoCode,
BOLT11 = offChainPaymentData.BOLT11 BOLT11 = offChainPaymentData.BOLT11,
Type = payment.GetCryptoPaymentData().GetPaymentType()
}; };
}).Where(model => model != null); }).Where(model => model != null);
} }
@ -28,6 +29,7 @@
<thead class="thead-inverse"> <thead class="thead-inverse">
<tr> <tr>
<th class="w-150px">Crypto</th> <th class="w-150px">Crypto</th>
<th class="w-150px">Type</th>
<th>BOLT11</th> <th>BOLT11</th>
</tr> </tr>
</thead> </thead>
@ -36,6 +38,7 @@
{ {
<tr> <tr>
<td>@payment.Crypto</td> <td>@payment.Crypto</td>
<td>@payment.Type.ToPrettyString()</td>
<td><div class="wraptextAuto">@payment.BOLT11</div></td> <td><div class="wraptextAuto">@payment.BOLT11</div></td>
</tr> </tr>
} }

View file

@ -12,7 +12,7 @@
{ {
<div asp-validation-summary="All" class="text-danger"></div> <div asp-validation-summary="All" class="text-danger"></div>
} }
<h4 class="mb-3">Payment</h4> <h4 class="mb-3">Invoice Settings</h4>
@if (Model.PaymentMethods.Any()) @if (Model.PaymentMethods.Any())
{ {
<div class="form-group mb-4"> <div class="form-group mb-4">

View file

@ -0,0 +1,161 @@
@model PaymentViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.Payment, "Payment", Context.GetStoreData().StoreName);
}
<div class="row">
<div class="col-lg-10 col-xl-9">
<h4 class="mb-3">Payment</h4>
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
}
@if (Model.IsOnchainSetup || Model.IsLightningSetup)
{
<form method="post">
<div class="form-group">
<label asp-for="DefaultCurrency" class="form-label"></label>
<input asp-for="DefaultCurrency" class="form-control" />
<span asp-validation-for="DefaultCurrency" class="text-danger"></span>
</div>
<div class="form-group d-flex align-items-center">
<input asp-for="AnyoneCanCreateInvoice" type="checkbox" class="btcpay-toggle me-2"/>
<label asp-for="AnyoneCanCreateInvoice" class="form-label mb-0 me-1"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#allow-anyone-to-create-invoice" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</div>
<div class="form-group mt-4">
<label asp-for="NetworkFeeMode" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#add-network-fee-to-invoice-vary-with-mining-fees" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<select asp-for="NetworkFeeMode" class="form-select">
<option value="MultiplePaymentsOnly">... only if the customer makes more than one payment for the invoice</option>
<option value="Always">... on every payment</option>
<option value="Never">Never add network fee</option>
</select>
</div>
<div class="form-group">
<label asp-for="InvoiceExpiration" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#invoice-expires-if-the-full-amount-has-not-been-paid-after-minutes" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<div class="input-group">
<input asp-for="InvoiceExpiration" class="form-control" style="max-width:10ch;"/>
<span class="input-group-text">minutes</span>
</div>
<span asp-validation-for="InvoiceExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="PaymentTolerance" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#consider-the-invoice-paid-even-if-the-paid-amount-is-less-than-expected" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<div class="input-group">
<input asp-for="PaymentTolerance" class="form-control" style="max-width:10ch;"/>
<span class="input-group-text">percent</span>
</div>
<span asp-validation-for="PaymentTolerance" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DefaultCurrency" class="form-label"></label>
<input asp-for="DefaultCurrency" class="form-control" />
<span asp-validation-for="DefaultCurrency" class="text-danger"></span>
</div>
@if (Model.IsOnchainSetup)
{
<h5 class="mt-5 mb-3">On-Chain</h5>
@if (Model.CanUsePayJoin)
{
<div class="form-group">
<div class="d-flex align-items-center">
<input asp-for="PayJoinEnabled" type="checkbox" class="btcpay-toggle me-2"/>
<label asp-for="PayJoinEnabled" class="form-label mb-0 me-1"></label>
<a href="https://docs.btcpayserver.org/Payjoin/" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</div>
<span asp-validation-for="PayJoinEnabled" class="text-danger"></span>
</div>
}
<div class="form-group">
<label asp-for="MonitoringExpiration" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#payment-invalid-if-transactions-fails-to-confirm-minutes-after-invoice-expiration" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<div class="input-group">
<input asp-for="MonitoringExpiration" class="form-control" style="max-width:10ch;"/>
<span class="input-group-text">minutes</span>
</div>
<span asp-validation-for="MonitoringExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SpeedPolicy" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#consider-the-invoice-confirmed-when-the-payment-transaction" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<select asp-for="SpeedPolicy" class="form-select w-auto" onchange="document.getElementById('unconfirmed-warning').hidden = this.value !== '0';">
<option value="0">Is unconfirmed</option>
<option value="1">Has at least 1 confirmation</option>
<option value="3">Has at least 2 confirmations</option>
<option value="2">Has at least 6 confirmations</option>
</select>
<div class="alert alert-warning my-2" hidden="@(Model.SpeedPolicy != 0)" id="unconfirmed-warning" role="alert">
Choosing to accept an unconfirmed invoice can lead to double-spending and is strongly discouraged.
</div>
<span asp-validation-for="SpeedPolicy" class="text-danger"></span>
</div>
<div class="form-check my-1">
<input asp-for="ShowRecommendedFee" type="checkbox" class="form-check-input"/>
<label asp-for="ShowRecommendedFee" class="form-check-label"></label>
<p class="form-text text-muted mb-0">Fee will be shown for BTC and LTC onchain payments only.</p>
</div>
<div class="form-group mt-2 mb-4">
<label asp-for="RecommendedFeeBlockTarget" class="form-label"></label>
<input asp-for="RecommendedFeeBlockTarget" class="form-control" style="width:8ch" min="1" />
<span asp-validation-for="RecommendedFeeBlockTarget" class="text-danger"></span>
</div>
}
@if (Model.IsLightningSetup)
{
<h5 class="mt-5 mb-3">Lightning</h5>
<div class="form-check my-1">
<input asp-for="LightningAmountInSatoshi" type="checkbox" class="form-check-input"/>
<label asp-for="LightningAmountInSatoshi" class="form-check-label"></label>
</div>
<div class="form-check my-1">
<input asp-for="LightningPrivateRouteHints" type="checkbox" class="form-check-input"/>
<label asp-for="LightningPrivateRouteHints" class="form-check-label"></label>
</div>
<div class="form-check my-1">
<input asp-for="OnChainWithLnInvoiceFallback" type="checkbox" class="form-check-input"/>
<label asp-for="OnChainWithLnInvoiceFallback" class="form-check-label"></label>
</div>
<div class="form-group mt-3">
<label asp-for="LightningDescriptionTemplate" class="form-label"></label>
<input asp-for="LightningDescriptionTemplate" class="form-control"/>
<span asp-validation-for="LightningDescriptionTemplate" class="text-danger"></span>
<p class="form-text text-muted">
Available placeholders:
<code>{StoreName} {ItemDescription} {OrderId}</code>
</p>
</div>
}
<button name="command" type="submit" class="btn btn-primary" value="Save" id="Save">Save Payment Settings</button>
</form>
}
else
{
<p class="text-secondary mt-3">
Please configure either an on-chain wallet or Lightning node first.
</p>
}
</div>
</div>
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />
}

View file

@ -7,7 +7,7 @@
<header class="text-center"> <header class="text-center">
<h1>@ViewData["Title"]</h1> <h1>@ViewData["Title"]</h1>
<div class="d-flex mt-4 mb-5"> <div class="d-flex mt-4 mb-5">
<vc:icon symbol="warning" /> <vc:icon symbol="warning"/>
<p class="text-secondary text-start mb-0"> <p class="text-secondary text-start mb-0">
Please understand that the Lightning Network is still under active development and considered experimental. Please understand that the Lightning Network is still under active development and considered experimental.
Before you proceed, take time to familiarize yourself with the risks. Before you proceed, take time to familiarize yourself with the risks.
@ -178,11 +178,50 @@
</div> </div>
</div> </div>
</div> </div>
<div class="text-start">
<div class="d-flex align-items-center">
<input asp-for="LNURLEnabled" type="checkbox" class="btcpay-toggle me-2" data-bs-toggle="collapse" data-bs-target="#LNURLSettings" aria-expanded="@Model.LNURLEnabled" aria-controls="LNURLSettings"/>
<label asp-for="LNURLEnabled" class="form-label mb-0 me-1"></label>
</div>
<div class="collapse @(Model.LNURLEnabled ? "show" : "")" id="LNURLSettings">
<h5 class="mb-1" style="padding-top:var(--btcpay-space-l)">LNURL settings</h5>
<div class="form-group">
<div class="d-flex align-items-center pt-3">
<input type="checkbox" asp-for="LNURLBech32Mode" class="btcpay-toggle me-2"/>
<label asp-for="LNURLBech32Mode" class="form-label mb-0 me-1"></label>
<span asp-validation-for="LNURLBech32Mode" class="text-danger"></span>
</div>
<p class="form-text text-muted mb-0 ms-5">For wallet compatibility: Bech32 encoded (classic) vs. cleartext URL (upcoming)</p>
</div>
<div class="form-group">
<div class="d-flex align-items-center">
<input type="checkbox" asp-for="LNURLStandardInvoiceEnabled" class="btcpay-toggle me-2"/>
<label asp-for="LNURLStandardInvoiceEnabled" class="form-label mb-0 me-1"></label>
</div>
<p class="form-text text-muted mb-0 ms-5">Required for Lightning Address, the pay button and apps.</p>
</div>
<div class="form-group">
<div class="d-flex align-items-center">
<input type="checkbox" asp-for="DisableBolt11PaymentMethod" class="btcpay-toggle me-2"/>
<label asp-for="DisableBolt11PaymentMethod" class="form-label mb-0 me-1"></label>
</div>
<p class="form-text text-muted mb-0 ms-5">Performance: Turn it off if users should pay only via LNURL.</p>
</div>
<div class="form-group mb-0 pb-2">
<div class="d-flex align-items-center">
<input type="checkbox" asp-for="LUD12Enabled" class="btcpay-toggle me-2"/>
<label asp-for="LUD12Enabled" class="form-label mb-0 me-1"></label>
</div>
</div>
</div>
</div>
<div class="text-start mt-4"> <div class="text-start mt-4">
<button id="save" name="command" type="submit" value="save" class="btn btn-primary me-2">Save</button> <button id="save" name="command" type="submit" value="save" class="btn btn-primary me-2">Save</button>
</div> </div>
</form> </form>
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial"/>
} }

View file

@ -2,8 +2,6 @@ namespace BTCPayServer.Views.Stores
{ {
public enum StoreNavPages public enum StoreNavPages
{ {
Index, Create, Rates, Checkout, Tokens, Users, PayButton, Integrations, Wallet, Webhooks, ActivePage, Index, Create, Rates, Payment, Checkout, Tokens, Users, PayButton, Integrations, Wallet, Webhooks, ActivePage, PullPayments, Payouts
PullPayments,
Payouts
} }
} }

View file

@ -182,136 +182,7 @@
<input asp-for="StoreWebsite" class="form-control" /> <input asp-for="StoreWebsite" class="form-control" />
<span asp-validation-for="StoreWebsite" class="text-danger"></span> <span asp-validation-for="StoreWebsite" class="text-danger"></span>
</div> </div>
<div class="form-group">
<label asp-for="DefaultCurrency" class="form-label"></label>
<input asp-for="DefaultCurrency" class="form-control" />
<span asp-validation-for="DefaultCurrency" class="text-danger"></span>
</div>
@if (Model.IsOnchainSetup || Model.IsLightningSetup)
{
<h4 class="mt-5 mb-3">Payment</h4>
<div class="form-group d-flex align-items-center">
<input asp-for="AnyoneCanCreateInvoice" type="checkbox" class="btcpay-toggle me-2" />
<label asp-for="AnyoneCanCreateInvoice" class="form-label mb-0 me-1"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#allow-anyone-to-create-invoice" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</div>
<div class="form-group mt-4">
<label asp-for="NetworkFeeMode" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#add-network-fee-to-invoice-vary-with-mining-fees" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<select asp-for="NetworkFeeMode" class="form-select">
<option value="MultiplePaymentsOnly">... only if the customer makes more than one payment for the invoice</option>
<option value="Always">... on every payment</option>
<option value="Never">Never add network fee</option>
</select>
</div>
<div class="form-group">
<label asp-for="InvoiceExpiration" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#invoice-expires-if-the-full-amount-has-not-been-paid-after-minutes" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<div class="input-group">
<input asp-for="InvoiceExpiration" class="form-control" style="max-width:10ch;" />
<span class="input-group-text">minutes</span>
</div>
<span asp-validation-for="InvoiceExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="PaymentTolerance" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#consider-the-invoice-paid-even-if-the-paid-amount-is-less-than-expected" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<div class="input-group">
<input asp-for="PaymentTolerance" class="form-control" style="max-width:10ch;" />
<span class="input-group-text">percent</span>
</div>
<span asp-validation-for="PaymentTolerance" class="text-danger"></span>
</div>
@if (Model.IsOnchainSetup)
{
<h5 class="mt-5 mb-3">On-Chain</h5>
@if (Model.CanUsePayJoin)
{
<div class="form-group">
<div class="d-flex align-items-center">
<input asp-for="PayJoinEnabled" type="checkbox" class="btcpay-toggle me-2" />
<label asp-for="PayJoinEnabled" class="form-label mb-0 me-1"></label>
<a href="https://docs.btcpayserver.org/Payjoin/" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</div>
<span asp-validation-for="PayJoinEnabled" class="text-danger"></span>
</div>
}
<div class="form-group">
<label asp-for="MonitoringExpiration" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#payment-invalid-if-transactions-fails-to-confirm-minutes-after-invoice-expiration" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<div class="input-group">
<input asp-for="MonitoringExpiration" class="form-control" style="max-width:10ch;" />
<span class="input-group-text">minutes</span>
</div>
<span asp-validation-for="MonitoringExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SpeedPolicy" class="form-label"></label>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#consider-the-invoice-confirmed-when-the-payment-transaction" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
<select asp-for="SpeedPolicy" class="form-select w-auto">
<option value="0">Is unconfirmed</option>
<option value="1">Has at least 1 confirmation</option>
<option value="3">Has at least 2 confirmations</option>
<option value="2">Has at least 6 confirmations</option>
</select>
<div class="alert alert-warning my-2" hidden="@(Model.SpeedPolicy != 0)" id="unconfirmed-warning" role="alert">
Choosing to accept an unconfirmed invoice can lead to double-spending and is strongly discouraged.
</div>
<span asp-validation-for="SpeedPolicy" class="text-danger"></span>
</div>
<div class="form-check my-1">
<input asp-for="ShowRecommendedFee" type="checkbox" class="form-check-input" />
<label asp-for="ShowRecommendedFee" class="form-check-label"></label>
<p class="form-text text-muted mb-0">Fee will be shown for BTC and LTC onchain payments only.</p>
</div>
<div class="form-group mt-2 mb-4">
<label asp-for="RecommendedFeeBlockTarget" class="form-label"></label>
<input asp-for="RecommendedFeeBlockTarget" class="form-control" style="width:8ch" min="1" />
<span asp-validation-for="RecommendedFeeBlockTarget" class="text-danger"></span>
</div>
}
@if (Model.IsLightningSetup)
{
<h5 class="mt-5 mb-3">Lightning</h5>
<div class="form-check my-1">
<input asp-for="LightningAmountInSatoshi" type="checkbox" class="form-check-input" />
<label asp-for="LightningAmountInSatoshi" class="form-check-label"></label>
</div>
<div class="form-check my-1">
<input asp-for="LightningPrivateRouteHints" type="checkbox" class="form-check-input" />
<label asp-for="LightningPrivateRouteHints" class="form-check-label"></label>
</div>
<div class="form-check my-1">
<input asp-for="OnChainWithLnInvoiceFallback" type="checkbox" class="form-check-input" />
<label asp-for="OnChainWithLnInvoiceFallback" class="form-check-label"></label>
</div>
<div class="form-group mt-3">
<label asp-for="LightningDescriptionTemplate" class="form-label"></label>
<input asp-for="LightningDescriptionTemplate" class="form-control" />
<span asp-validation-for="LightningDescriptionTemplate" class="text-danger"></span>
<p class="form-text text-muted">
Available placeholders:
<code>{StoreName} {ItemDescription} {OrderId}</code>
</p>
</div>
}
}
<button name="command" type="submit" class="btn btn-primary" value="Save" id="Save">Save Store Settings</button> <button name="command" type="submit" class="btn btn-primary" value="Save" id="Save">Save Store Settings</button>
</form> </form>
@ -356,9 +227,4 @@
@section PageFootContent { @section PageFootContent {
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial" />
<script>
delegate('change', '#SpeedPolicy', e => {
document.getElementById('unconfirmed-warning').hidden = e.target.value !== '0';
});
</script>
} }

View file

@ -1,13 +1,14 @@
<nav id="sideNav" class="nav flex-column mb-4"> <nav id="sideNav" class="nav flex-column mb-4">
<a id="@(nameof(StoreNavPages.Index))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Index)" asp-controller="Stores" asp-action="UpdateStore" asp-route-storeId="@this.Context.GetRouteValue("storeId")">General settings</a> <a id="@(nameof(StoreNavPages.Index))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Index)" asp-controller="Stores" asp-action="UpdateStore" asp-route-storeId="@Context.GetRouteValue("storeId")">General settings</a>
<a id="@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-controller="Stores" asp-action="Rates" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Rates</a> <a id="@(nameof(StoreNavPages.Rates))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-controller="Stores" asp-action="Rates" asp-route-storeId="@Context.GetRouteValue("storeId")">Rates</a>
<a id="@(nameof(StoreNavPages.Checkout))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Checkout)" asp-controller="Stores" asp-action="CheckoutExperience" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Checkout experience</a> <a id="@(nameof(StoreNavPages.Payment))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Payment)" asp-controller="Stores" asp-action="Payment" asp-route-storeId="@Context.GetRouteValue("storeId")">Payment</a>
<a id="@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-controller="Stores" asp-action="ListTokens" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Access Tokens</a> <a id="@(nameof(StoreNavPages.Checkout))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Checkout)" asp-controller="Stores" asp-action="CheckoutExperience" asp-route-storeId="@Context.GetRouteValue("storeId")">Checkout experience</a>
<a id="@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-controller="Stores" asp-action="StoreUsers" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Users</a> <a id="@(nameof(StoreNavPages.Tokens))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-controller="Stores" asp-action="ListTokens" asp-route-storeId="@Context.GetRouteValue("storeId")">Access Tokens</a>
<a id="@(nameof(StoreNavPages.PayButton))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)" asp-controller="Stores" asp-action="PayButton" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Pay Button</a> <a id="@(nameof(StoreNavPages.Users))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-controller="Stores" asp-action="StoreUsers" asp-route-storeId="@Context.GetRouteValue("storeId")">Users</a>
<a id="@(nameof(StoreNavPages.Integrations))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Integrations)" asp-controller="Stores" asp-action="Integrations" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Integrations</a> <a id="@(nameof(StoreNavPages.PayButton))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)" asp-controller="Stores" asp-action="PayButton" asp-route-storeId="@Context.GetRouteValue("storeId")">Pay Button</a>
<a id="@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="Stores" asp-action="Webhooks" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Webhooks</a> <a id="@(nameof(StoreNavPages.Integrations))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Integrations)" asp-controller="Stores" asp-action="Integrations" asp-route-storeId="@Context.GetRouteValue("storeId")">Integrations</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.PullPayments)" asp-action="PullPayments" asp-controller="StorePullPayments" asp-route-storeId="@this.Context.GetRouteValue("storeId")" id="PullPayments">Pull payments</a> <a id="@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="Stores" asp-action="Webhooks" asp-route-storeId="@Context.GetRouteValue("storeId")">Webhooks</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.Payouts)" asp-action="Payouts" asp-controller="StorePullPayments" asp-route-storeId="@this.Context.GetRouteValue("storeId")" id="Payouts">Payouts</a> <a id="@(nameof(StoreNavPages.PullPayments))" class="nav-link @ViewData.IsActivePage(StoreNavPages.PullPayments)" asp-action="PullPayments" asp-controller="StorePullPayments" asp-route-storeId="@Context.GetRouteValue("storeId")">Pull payments</a>
<a id="@(nameof(StoreNavPages.Payouts))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Payouts)" asp-action="Payouts" asp-controller="StorePullPayments" asp-route-storeId="@Context.GetRouteValue("storeId")">Payouts</a>
<vc:ui-extension-point location="store-nav" model="@Model" /> <vc:ui-extension-point location="store-nav" model="@Model" />
</nav> </nav>

View file

@ -0,0 +1,291 @@
{
"paths": {
"/api/v1/stores/{storeId}/payment-methods/LNURL": {
"get": {
"tags": [
"Store Payment Methods (LNURL)"
],
"summary": "Get store LNURL payment methods",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to fetch",
"schema": {
"type": "string"
}
},
{
"name": "enabled",
"in": "query",
"required": false,
"description": "Fetch payment methods that are enabled/disabled only",
"schema": {
"type": "boolean"
}
}
],
"description": "View information about the stores' configured LNURL payment methods",
"operationId": "StoreLNURLPayPaymentMethods_GetLNURLPayPaymentMethods",
"responses": {
"200": {
"description": "list of payment methods",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LNURLPayPaymentMethodDataList"
}
}
}
}
},
"security": [
{
"API Key": [
"btcpay.store.canmodifystoresettings"
],
"Basic": []
}
]
}
},
"/api/v1/stores/{storeId}/payment-methods/LNURL/{cryptoCode}": {
"get": {
"tags": [
"Store Payment Methods (LNURL Pay)"
],
"summary": "Get store LNURL Pay payment method",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to fetch",
"schema": {
"type": "string"
}
},
{
"name": "cryptoCode",
"in": "path",
"required": true,
"description": "The crypto code of the payment method to fetch",
"schema": {
"type": "string"
}
}
],
"description": "View information about the specified payment method",
"operationId": "StoreLNURLPayPaymentMethods_GetLNURLPayPaymentMethod",
"responses": {
"200": {
"description": "specified payment method",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LNURLPayPaymentMethodData"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden to view the specified store"
},
"404": {
"description": "The key is not found for this store/payment method"
}
},
"security": [
{
"API Key": [
"btcpay.store.canmodifystoresettings"
],
"Basic": []
}
]
},
"put": {
"tags": [
"Store Payment Methods (LNURL Pay)"
],
"summary": "Update store LNURL Pay payment method",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to fetch",
"schema": {
"type": "string"
}
},
{
"name": "cryptoCode",
"in": "path",
"required": true,
"description": "The crypto code of the payment method to update",
"schema": {
"type": "string"
}
}
],
"description": "Update the specified store's payment method",
"requestBody": {
"x-name": "request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LNURLPayPaymentMethodData"
}
}
},
"required": true,
"x-position": 1
},
"operationId": "StoreLNURLPayPaymentMethods_UpdateLNURLPayPaymentMethod",
"responses": {
"200": {
"description": "updated specified payment method",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LNURLPayPaymentMethodData"
}
}
}
},
"400": {
"description": "A list of errors that occurred when updating the store payment method",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationProblemDetails"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden to update the specified store"
},
"404": {
"description": "The key is not found for this store"
}
},
"security": [
{
"API Key": [
"btcpay.store.canmodifystoresettings"
],
"Basic": []
}
]
},
"delete": {
"tags": [
"Store Payment Methods (LNURL Pay)"
],
"summary": "Remove store LNURL Pay payment method",
"parameters": [
{
"name": "storeId",
"in": "path",
"required": true,
"description": "The store to fetch",
"schema": {
"type": "string"
}
},
{
"name": "cryptoCode",
"in": "path",
"required": true,
"description": "The crypto code of the payment method to update",
"schema": {
"type": "string"
}
}
],
"description": "Removes the specified store payment method.",
"responses": {
"200": {
"description": "The payment method has been removed"
},
"400": {
"description": "A list of errors that occurred when removing the payment method",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationProblemDetails"
}
}
}
},
"403": {
"description": "If you are authenticated but forbidden to remove the specified payment method"
},
"404": {
"description": "The key is not found for this store/payment-method"
}
},
"security": [
{
"API Key": [
"btcpay.store.canmodifystoresettings"
],
"Basic": []
}
]
}
}
},
"components": {
"schemas": {
"LNURLPayPaymentMethodDataList": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LNURLPayPaymentMethodData"
}
},
"LNURLPayPaymentMethodBaseData": {
"type": "object",
"additionalProperties": false,
"properties": {
"useBech32Scheme": {
"type": "boolean",
"description": "Whether to use [LUD-01](https://github.com/fiatjaf/lnurl-rfc/blob/luds/01.md)'s bech32 format or to use [LUD-17](https://github.com/fiatjaf/lnurl-rfc/blob/luds/17.md) url formatting. "
},
"enableForStandardInvoices": {
"type": "boolean",
"description": "Whether to allow this payment method to also be used for standard invoices and not just topup invoices."
},
"lud12Enabled": {
"type": "boolean",
"description": "Allow comments to be passed on via lnurl."
}
}
},
"LNURLPayPaymentMethodData": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/LNURLPayPaymentMethodBaseData"
},
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether the payment method is enabled. Note that this can only enabled when a Lightning Network payment method is available and enabled"
},
"cryptoCode": {
"type": "string",
"description": "Crypto code of the payment method"
}
}
}
}
},
"tags": [
{
"name": "Store Payment Methods (LNURL Pay)"
}
]
}