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>
<PackageReference Include="NBitcoin" Version="6.0.15" />
<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>
<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; }
#nullable restore
internal static void OnModelCreating(ModelBuilder builder)
{
builder.Entity<PayoutData>()

View file

@ -7,7 +7,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.11.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<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" />
</ItemGroup>

View file

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

View file

@ -20,7 +20,7 @@
<ItemGroup>
<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.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="94.0.4606.6100" />

View file

@ -137,10 +137,10 @@ namespace BTCPayServer.Tests
s.RegisterNewUser(true);
var store = s.CreateNewStore();
s.AddLightningNode();
s.GoToStore(store.storeId);
s.GoToStore(store.storeId, StoreNavPages.Payment);
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
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");
s.GoToInvoiceCheckout(invoiceId);

View file

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

View file

@ -2042,7 +2042,7 @@ namespace BTCPayServer.Tests
void VerifyLightning(Dictionary<string, GenericPaymentMethodData> dictionary)
{
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);
}
@ -2057,7 +2057,7 @@ namespace BTCPayServer.Tests
void VerifyOnChain(Dictionary<string, GenericPaymentMethodData> dictionary)
{
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);
}
@ -2091,6 +2091,5 @@ namespace BTCPayServer.Tests
}
}
}

View file

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

View file

@ -109,7 +109,7 @@ namespace BTCPayServer.Tests
{
await RegisterAsync(isAdmin);
await CreateStoreAsync();
var store = this.GetController<StoresController>();
var store = GetController<StoresController>();
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId);
@ -127,19 +127,19 @@ namespace BTCPayServer.Tests
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 response = await storeController.UpdateStore();
StoreViewModel store = (StoreViewModel)((ViewResult)response).Model;
modify(store);
storeController.UpdateStore(store).GetAwaiter().GetResult();
var response = await storeController.Payment();
PaymentViewModel payment = (PaymentViewModel)((ViewResult)response).Model;
modify(payment);
await storeController.Payment(payment);
}
public T GetController<T>(bool setImplicitStore = true) where T : Controller
@ -190,7 +190,7 @@ namespace BTCPayServer.Tests
public Task EnablePayJoin()
{
return ModifyStore(s => s.PayJoinEnabled = true);
return ModifyPayment(p => p.PayJoinEnabled = true);
}
public GenerateWalletResponse GenerateWalletResponseV { get; set; }
@ -240,23 +240,26 @@ namespace BTCPayServer.Tests
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 connectionString = parent.GetLightningConnectionString(connectionType, isMerchant);
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,
new LightningNodeViewModel { ConnectionString = connectionString, LightningNodeType = nodeType, SkipPortTest = true }, "save", cryptoCode);
vm, "save", cryptoCode);
if (storeController.ModelState.ErrorCount != 0)
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.Hosting;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Models;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.AppViewModels;
@ -818,11 +819,11 @@ namespace BTCPayServer.Tests
// Set tolerance to 50%
var stores = user.GetController<StoresController>();
var response = await stores.UpdateStore();
var vm = Assert.IsType<StoreViewModel>(Assert.IsType<ViewResult>(response).Model);
var response = await stores.Payment();
var vm = Assert.IsType<PaymentViewModel>(Assert.IsType<ViewResult>(response).Model);
Assert.Equal(0.0, vm.PaymentTolerance);
vm.PaymentTolerance = 50.0;
Assert.IsType<RedirectToActionResult>(stores.UpdateStore(vm).Result);
Assert.IsType<RedirectToActionResult>(stores.Payment(vm).Result);
var invoice = user.BitPay.CreateInvoice(
new Invoice()
@ -996,8 +997,7 @@ namespace BTCPayServer.Tests
Assert.Equal(4, tor.Services.Length);
}
[Fact(Timeout = 60 * 2 * 1000)]
[Trait("Integration", "Integration")]
[Trait("Lightning", "Lightning")]
@ -1012,7 +1012,7 @@ namespace BTCPayServer.Tests
await user.RegisterDerivationSchemeAsync("BTC");
await user.RegisterLightningNodeAsync("BTC", LightningConnectionType.CLightning);
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"));
await tester.WaitForEvent<InvoiceNewPaymentDetailsEvent>(async () =>
{
@ -1042,14 +1042,22 @@ namespace BTCPayServer.Tests
Assert.Contains(fetchedInvoice.Status, new[] { InvoiceStatusLegacy.Complete, InvoiceStatusLegacy.Confirmed });
Assert.Equal(InvoiceExceptionStatus.None, fetchedInvoice.ExceptionStatus);
Logs.Tester.LogInformation($"Paying invoice {invoice.Id} original full amount bolt11 invoice ");
evt = await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
//BTCPay will attempt to cancel previous bolt11 invoices so that there are less weird edge case scenarios
Logs.Tester.LogInformation($"Attempting to pay invoice {invoice.Id} original full amount bolt11 invoice ");
await Assert.ThrowsAsync<LightningRPCException>(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);
});
//NOTE: Eclair does not support cancelling invoice so the below test case would make sense for it
// 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)]
@ -1065,7 +1073,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess(true);
var storeController = user.GetController<StoresController>();
var storeResponse = await storeController.UpdateStore();
var storeResponse = storeController.UpdateStore();
Assert.IsType<ViewResult>(storeResponse);
Assert.IsType<ViewResult>(await storeController.SetupLightningNode(user.StoreId, "BTC"));
@ -1089,7 +1097,7 @@ namespace BTCPayServer.Tests
new LightningNodeViewModel { ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri },
"save", "BTC").GetAwaiter().GetResult());
storeResponse = await storeController.UpdateStore();
storeResponse = storeController.UpdateStore();
var storeVm =
Assert.IsType<StoreViewModel>(Assert
.IsType<ViewResult>(storeResponse).Model);
@ -1205,7 +1213,7 @@ namespace BTCPayServer.Tests
var acc = tester.NewAccount();
acc.GrantAccess();
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
{
Price = 5.0m,
@ -2032,7 +2040,7 @@ namespace BTCPayServer.Tests
});
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");
response = await tester.PayTester.HttpClient.SendAsync(
@ -2306,7 +2314,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess(true);
user.RegisterDerivationScheme("BTC");
await user.ModifyStore(s =>
await user.ModifyPayment(s =>
{
Assert.Equal("USD", s.DefaultCurrency);
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
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
.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()));
Assert.Equal(new PaymentMethodId("BTC", BitcoinPaymentType.Instance).ToString(), criteria.PaymentMethod);
criteria.Value = "5 USD";
@ -2448,12 +2456,12 @@ namespace BTCPayServer.Tests
Assert.DoesNotContain("&lightning=", paymentMethodFirst.InvoiceBitcoinUrlQR);
// enable unified QR code in settings
var vm = Assert.IsType<StoreViewModel>(Assert
.IsType<ViewResult>(await user.GetController<StoresController>().UpdateStore()).Model
var vm = Assert.IsType<PaymentViewModel>(Assert
.IsType<ViewResult>(await user.GetController<StoresController>().Payment()).Model
);
vm.OnChainWithLnInvoiceFallback = true;
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
@ -2470,7 +2478,7 @@ namespace BTCPayServer.Tests
Assert.True($"bitcoin:{paymentMethodSecond.BtcAddress.ToUpperInvariant()}" == split);
// 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);
}
}
@ -2488,10 +2496,8 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess(true);
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
var vm = Assert.IsType<CheckoutExperienceViewModel>(Assert
.IsType<ViewResult>(user.GetController<StoresController>().CheckoutExperience()).Model);
Assert.Single(vm.PaymentMethodCriteria);
var criteria = vm.PaymentMethodCriteria.First();
var vm = user.GetController<StoresController>().CheckoutExperience().AssertViewModel<CheckoutExperienceViewModel>();
var criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(new PaymentMethodId("BTC", LightningPaymentType.Instance).ToString(), criteria.PaymentMethod);
criteria.Value = "2 USD";
criteria.Type = PaymentMethodCriteriaViewModel.CriteriaType.LessThan;
@ -2499,18 +2505,42 @@ namespace BTCPayServer.Tests
.Result);
var invoice = user.BitPay.CreateInvoice(
new Invoice()
new Invoice
{
Price = 1.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
Currency = "USD"
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
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 NBitcoin;
using NBXplorer.Models;
using YamlDotNet.Core.Tokens;
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
using Language = BTCPayServer.Client.Models.Language;
using NotificationData = BTCPayServer.Client.Models.NotificationData;
@ -37,6 +36,7 @@ namespace BTCPayServer.Controllers.GreenField
private readonly StoreOnChainPaymentMethodsController _chainPaymentMethodsController;
private readonly StoreOnChainWalletsController _storeOnChainWalletsController;
private readonly StoreLightningNetworkPaymentMethodsController _storeLightningNetworkPaymentMethodsController;
private readonly StoreLNURLPayPaymentMethodsController _storeLnurlPayPaymentMethodsController;
private readonly HealthController _healthController;
private readonly GreenFieldPaymentRequestsController _paymentRequestController;
private readonly ApiKeysController _apiKeysController;
@ -58,6 +58,7 @@ namespace BTCPayServer.Controllers.GreenField
StoreOnChainPaymentMethodsController chainPaymentMethodsController,
StoreOnChainWalletsController storeOnChainWalletsController,
StoreLightningNetworkPaymentMethodsController storeLightningNetworkPaymentMethodsController,
StoreLNURLPayPaymentMethodsController storeLnurlPayPaymentMethodsController,
HealthController healthController,
GreenFieldPaymentRequestsController paymentRequestController,
ApiKeysController apiKeysController,
@ -79,6 +80,7 @@ namespace BTCPayServer.Controllers.GreenField
_chainPaymentMethodsController = chainPaymentMethodsController;
_storeOnChainWalletsController = storeOnChainWalletsController;
_storeLightningNetworkPaymentMethodsController = storeLightningNetworkPaymentMethodsController;
_storeLnurlPayPaymentMethodsController = storeLnurlPayPaymentMethodsController;
_healthController = healthController;
_paymentRequestController = paymentRequestController;
_apiKeysController = apiKeysController;
@ -141,6 +143,7 @@ namespace BTCPayServer.Controllers.GreenField
_storeLightningNodeApiController,
_internalLightningNodeApiController,
_storeLightningNetworkPaymentMethodsController,
_storeLnurlPayPaymentMethodsController,
_greenFieldInvoiceController,
_greenFieldServerInfoController,
_storeWebhooksController,
@ -165,6 +168,7 @@ namespace BTCPayServer.Controllers.GreenField
private readonly StoreLightningNodeApiController _storeLightningNodeApiController;
private readonly InternalLightningNodeApiController _lightningNodeApiController;
private readonly StoreLightningNetworkPaymentMethodsController _storeLightningNetworkPaymentMethodsController;
private readonly StoreLNURLPayPaymentMethodsController _storeLnurlPayPaymentMethodsController;
private readonly GreenFieldInvoiceController _greenFieldInvoiceController;
private readonly GreenFieldServerInfoController _greenFieldServerInfoController;
private readonly StoreWebhooksController _storeWebhooksController;
@ -183,6 +187,7 @@ namespace BTCPayServer.Controllers.GreenField
StoreLightningNodeApiController storeLightningNodeApiController,
InternalLightningNodeApiController lightningNodeApiController,
StoreLightningNetworkPaymentMethodsController storeLightningNetworkPaymentMethodsController,
StoreLNURLPayPaymentMethodsController storeLnurlPayPaymentMethodsController,
GreenFieldInvoiceController greenFieldInvoiceController,
GreenFieldServerInfoController greenFieldServerInfoController,
StoreWebhooksController storeWebhooksController,
@ -202,6 +207,7 @@ namespace BTCPayServer.Controllers.GreenField
_storeLightningNodeApiController = storeLightningNodeApiController;
_lightningNodeApiController = lightningNodeApiController;
_storeLightningNetworkPaymentMethodsController = storeLightningNetworkPaymentMethodsController;
_storeLnurlPayPaymentMethodsController = storeLnurlPayPaymentMethodsController;
_greenFieldInvoiceController = greenFieldInvoiceController;
_greenFieldServerInfoController = greenFieldServerInfoController;
_storeWebhooksController = storeWebhooksController;
@ -746,7 +752,39 @@ namespace BTCPayServer.Controllers.GreenField
{
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>>
GetStoreLightningNetworkPaymentMethods(string storeId, bool? enabled,
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();
}
var method = GetExistingLightningLikePaymentMethod(cryptoCode);
var method = GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, Store);
if (method is null)
{
return NotFound();
@ -97,8 +97,7 @@ namespace BTCPayServer.Controllers.GreenField
[HttpDelete("~/api/v1/stores/{storeId}/payment-methods/LightningNetwork/{cryptoCode}")]
public async Task<IActionResult> RemoveLightningNetworkPaymentMethod(
string storeId,
string cryptoCode,
int offset = 0, int amount = 10)
string cryptoCode)
{
if (!GetNetwork(cryptoCode, out BTCPayNetwork _))
{
@ -188,17 +187,17 @@ namespace BTCPayServer.Controllers.GreenField
storeBlob.SetExcluded(paymentMethodId, !request.Enabled);
store.SetStoreBlob(storeBlob);
await _storeRepository.UpdateStore(store);
return Ok(GetExistingLightningLikePaymentMethod(cryptoCode, store));
return Ok(GetExistingLightningLikePaymentMethod(_btcPayNetworkProvider, cryptoCode, store));
}
private LightningNetworkPaymentMethodData? GetExistingLightningLikePaymentMethod(string cryptoCode,
StoreData? store = null)
public static LightningNetworkPaymentMethodData? GetExistingLightningLikePaymentMethod(BTCPayNetworkProvider btcPayNetworkProvider, string cryptoCode,
StoreData store)
{
store ??= Store;
var storeBlob = store.GetStoreBlob();
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var paymentMethod = store
.GetSupportedPaymentMethods(_btcPayNetworkProvider)
.GetSupportedPaymentMethods(btcPayNetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(method => method.PaymentId == id);

View file

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

View file

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Stores;
@ -41,7 +42,8 @@ namespace BTCPayServer.Controllers
{
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
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
{

View file

@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Lightning;
using BTCPayServer.Logging;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
@ -48,7 +49,6 @@ namespace BTCPayServer.Controllers
}
var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store);
LightningSupportedPaymentMethod paymentMethod = null;
if (vm.LightningNodeType == LightningNodeType.Internal)
@ -92,6 +92,7 @@ namespace BTCPayServer.Controllers
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetLightningUrl(connectionString);
}
switch (command)
@ -99,8 +100,22 @@ namespace BTCPayServer.Controllers
case "save":
var storeBlob = store.GetStoreBlob();
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.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
await _Repo.UpdateStore(store);
TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node updated.";
return RedirectToAction(nameof(UpdateStore), new { storeId });
@ -109,7 +124,7 @@ namespace BTCPayServer.Controllers
var handler = _ServiceProvider.GetRequiredService<LightningLikePaymentHandler>();
try
{
var info = await handler.GetNodeInfo(paymentMethod, network, Request.IsOnion());
var info = await handler.GetNodeInfo(paymentMethod, network, new InvoiceLogs(), Request.IsOnion());
if (!vm.SkipPortTest)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
@ -163,7 +178,9 @@ namespace BTCPayServer.Controllers
{
vm.CanUseInternalNode = await CanUseInternalLightning();
var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store);
if (lightning != null)
var lnSet = lightning != null;
if (lnSet)
{
vm.LightningNodeType = lightning.IsInternalNode ? LightningNodeType.Internal : LightningNodeType.Custom;
vm.ConnectionString = lightning.GetDisplayableConnectionString();
@ -172,6 +189,20 @@ namespace BTCPayServer.Controllers
{
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)
@ -182,5 +213,13 @@ namespace BTCPayServer.Controllers
.FirstOrDefault(d => d.PaymentId == id);
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 vm = new CheckoutExperienceViewModel();
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 =
storeBlob.PaymentMethodCriteria.SingleOrDefault(criteria =>
@ -461,13 +465,36 @@ namespace BTCPayServer.Controllers
return View(model);
}
blob.PaymentMethodCriteria = model.PaymentMethodCriteria
.Where(viewModel => !string.IsNullOrEmpty(viewModel.Value)).Select(viewModel =>
// Payment criteria for Off-Chain should also affect LNUrl
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);
return new PaymentMethodCriteria() { Above = viewModel.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan, Value = cv, PaymentMethod = PaymentMethodId.Parse(viewModel.PaymentMethod) };
}).ToList();
Above = newCriteria.Type == PaymentMethodCriteriaViewModel.CriteriaType.GreaterThan,
Value = cv,
PaymentMethod = paymentMethodId
});
}
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LazyPaymentMethods = model.LazyPaymentMethods;
blob.RedirectAutomatically = model.RedirectAutomatically;
@ -493,8 +520,8 @@ namespace BTCPayServer.Controllers
});
}
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, StoreViewModel vm)
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob,
out List<StoreDerivationScheme> derivationSchemes, out List<StoreLightningNode> lightningNodes)
{
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var derivationByCryptoCode =
@ -506,8 +533,12 @@ namespace BTCPayServer.Controllers
var lightningByCryptoCode = store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.Where(method => method.PaymentId.PaymentType == LightningPaymentType.Instance)
.ToDictionary(c => c.CryptoCode.ToUpperInvariant());
derivationSchemes = new List<StoreDerivationScheme>();
lightningNodes = new List<StoreLightningNode>();
foreach (var paymentMethodId in _paymentMethodHandlerDictionary.Distinct().SelectMany(handler => handler.GetSupportedPaymentMethods()))
{
switch (paymentMethodId.PaymentType)
@ -517,7 +548,7 @@ namespace BTCPayServer.Controllers
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
var value = strategy?.ToPrettyString() ?? string.Empty;
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
derivationSchemes.Add(new StoreDerivationScheme
{
Crypto = paymentMethodId.CryptoCode,
WalletSupported = network.WalletSupported,
@ -529,10 +560,14 @@ namespace BTCPayServer.Controllers
#endif
});
break;
case LNURLPayPaymentType lnurlPayPaymentType:
break;
case LightningPaymentType _:
var lightning = lightningByCryptoCode.TryGet(paymentMethodId.CryptoCode);
var isEnabled = !excludeFilters.Match(paymentMethodId) && lightning != null;
vm.LightningNodes.Add(new StoreViewModel.LightningNode
lightningNodes.Add(new StoreLightningNode
{
CryptoCode = paymentMethodId.CryptoCode,
Address = lightning?.GetDisplayableConnectionString(),
@ -544,30 +579,92 @@ namespace BTCPayServer.Controllers
}
[HttpGet("{storeId}")]
public async Task<IActionResult> UpdateStore()
public IActionResult UpdateStore()
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob();
var vm = new StoreViewModel();
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.DefaultCurrency = storeBlob.DefaultCurrency;
vm.NetworkFeeMode = storeBlob.NetworkFeeMode;
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
vm.SpeedPolicy = store.SpeedPolicy;
vm.CanDelete = _Repo.CanDeleteStores();
AddPaymentMethods(store, storeBlob, vm);
var vm = new StoreViewModel
{
Id = store.Id,
CanDelete = _Repo.CanDeleteStores(),
StoreName = store.StoreName,
StoreWebsite = store.StoreWebsite,
HintWallet = storeBlob.Hints.Wallet,
HintLightning = storeBlob.Hints.Lightning
};
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.InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
vm.PaymentTolerance = storeBlob.PaymentTolerance;
vm.PayJoinEnabled = storeBlob.PayJoinEnabled;
vm.HintWallet = storeBlob.Hints.Wallet;
vm.HintLightning = storeBlob.Hints.Lightning;
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints;
vm.OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback;
@ -581,12 +678,12 @@ namespace BTCPayServer.Controllers
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationSchemeSettings>()
.Any(settings => settings.Network.SupportPayJoin && settings.IsHotWallet);
return View(vm);
}
[HttpPost("{storeId}")]
public async Task<IActionResult> UpdateStore(StoreViewModel model, string command = null)
[HttpPost("{storeId}/payment")]
public async Task<IActionResult> Payment(PaymentViewModel model, string command = null)
{
bool needUpdate = false;
if (CurrentStore.SpeedPolicy != model.SpeedPolicy)
@ -594,16 +691,6 @@ namespace BTCPayServer.Controllers
needUpdate = true;
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();
blob.DefaultCurrency = model.DefaultCurrency;
@ -630,7 +717,7 @@ namespace BTCPayServer.Controllers
{
await _Repo.UpdateStore(CurrentStore);
TempData[WellKnownTempData.SuccessMessage] = "Store successfully updated";
TempData[WellKnownTempData.SuccessMessage] = "Payment settings successfully updated";
if (payjoinChanged && blob.PayJoinEnabled)
{
@ -646,13 +733,13 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel()
{
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
});

View file

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

View file

@ -5,6 +5,7 @@ using System.Threading;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Abstractions.Services;
using BTCPayServer.Common;
using BTCPayServer.Client;
using BTCPayServer.Configuration;
@ -329,6 +330,8 @@ namespace BTCPayServer.Hosting
services.AddSingleton<LightningLikePaymentHandler>();
services.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<LightningLikePaymentHandler>());
services.AddSingleton<LNURLPayPaymentHandler>();
services.AddSingleton<IPaymentMethodHandler>(provider => provider.GetService<LNURLPayPaymentHandler>());
services.AddSingleton<IHostedService, LightningListener>();
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.Collections.Generic;
using BTCPayServer.Services.Invoices;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -79,7 +80,7 @@ namespace BTCPayServer.Models
}
[JsonProperty("cryptoInfo")]
public List<NBitpayClient.InvoiceCryptoInfo> CryptoInfo { get; set; }
public List<InvoiceCryptoInfo> CryptoInfo { get; set; }
//"price":5
[JsonProperty("price")]
@ -262,7 +263,7 @@ namespace BTCPayServer.Models
[JsonProperty("addresses")]
public Dictionary<string, string> Addresses { get; set; }
[JsonProperty("paymentCodes")]
public Dictionary<string, NBitpayClient.InvoicePaymentUrls> PaymentCodes { get; set; }
public Dictionary<string, InvoiceCryptoInfo.InvoicePaymentUrls> PaymentCodes { get; set; }
[JsonProperty("buyer")]
public JObject Buyer { get; set; }
}

View file

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

View file

@ -10,7 +10,22 @@ namespace BTCPayServer.Models.StoreViewModels
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; }
[Display(Name = "Connection string")]
public string ConnectionString { 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 DerivationScheme
{
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; }
}
public class AdditionalPaymentMethod
{
public string Provider { get; set; }
public bool Enabled { get; set; }
public string Action { get; set; }
}
public StoreViewModel()
{
}
public List<StoreDerivationScheme> DerivationSchemes { get; set; }
public List<StoreLightningNode> LightningNodes { get; set; }
public bool HintWallet { get; set; }
public bool HintLightning { get; set; }
public bool CanDelete { get; set; }
[Display(Name = "Store ID")]
public string Id { get; set; }
[Display(Name = "Store Name")]
[Required]
[MaxLength(50)]
[MinLength(1)]
public string StoreName
{
get; set;
}
public string StoreName { get; set; }
[Uri]
[Display(Name = "Store Website")]
[MaxLength(500)]
public string StoreWebsite
{
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;
}
public string StoreWebsite { get; set; }
}
}

View file

@ -18,5 +18,6 @@ namespace BTCPayServer.Payments
decimal GetNextNetworkFee();
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; }
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
public uint256 PaymentHash { get; set; }
public string PaymentType { get; set; }
public string GetDestination()
{
@ -33,7 +34,7 @@ namespace BTCPayServer.Payments.Lightning
public PaymentType GetPaymentType()
{
return PaymentTypes.LightningLike;
return string.IsNullOrEmpty(PaymentType) ? PaymentTypes.LightningLike : PaymentTypes.Parse(PaymentType);
}
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
var storeBlob = store.GetStoreBlob();
var test = GetNodeInfo(supportedPaymentMethod, network, paymentMethod.PreferOnion);
var nodeInfo = GetNodeInfo(supportedPaymentMethod, network, logs, paymentMethod.PreferOnion);
var invoice = paymentMethod.ParentEntity;
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
{
Activated = true,
BOLT11 = lightningInvoice.BOLT11,
PaymentHash = BOLT11PaymentRequest.Parse(lightningInvoice.BOLT11, network.NBitcoinNetwork).PaymentHash,
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))
throw new PaymentMethodUnavailableException("Full node not available");
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
try
{
var client = supportedPaymentMethod.CreateLightningClient(network, Options.Value, _lightningClientFactory);
LightningNodeInformation info;
try
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
{
info = await client.GetInfo(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException("The lightning node did not reply in a timely manner");
}
catch (Exception ex)
{
throw new PaymentMethodUnavailableException($"Error while connecting to the API: {ex.Message}" +
(!string.IsNullOrEmpty(ex.InnerException?.Message) ? $" ({ex.InnerException.Message})" : ""));
}
var client = CreateLightningClient(supportedPaymentMethod, network);
LightningNodeInformation info;
try
{
info = await client.GetInfo(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException("The lightning node did not reply in a timely manner");
}
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)
? info.NodeInfoList.Where(i => i.IsTor == preferOnion.Value).ToArray()
: info.NodeInfoList.Select(i => i).ToArray();
var nodeInfo = preferOnion != null && info.NodeInfoList.Any(i => i.IsTor == preferOnion)
? info.NodeInfoList.Where(i => i.IsTor == preferOnion.Value).ToArray()
: info.NodeInfoList.Select(i => i).ToArray();
if (!nodeInfo.Any())
{
throw new PaymentMethodUnavailableException("No lightning node public address has been configured");
}
// 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");
// }
var blocksGap = summary.Status.ChainHeight - info.BlockHeight;
if (blocksGap > 10)
{
throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)");
}
var blocksGap = summary.Status.ChainHeight - info.BlockHeight;
if (blocksGap > 10)
{
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 NodeInfo { get; set; }
public string GetPaymentDestination()
public virtual string GetPaymentDestination()
{
return BOLT11;
}
@ -20,7 +20,7 @@ namespace BTCPayServer.Payments.Lightning
return PaymentHash ?? BOLT11PaymentRequest.Parse(BOLT11, network).PaymentHash;
}
public PaymentType GetPaymentType()
public virtual PaymentType GetPaymentType()
{
return PaymentTypes.LightningLike;
}
@ -35,5 +35,10 @@ namespace BTCPayServer.Payments.Lightning
return 0.0m;
}
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)
{
return _memoryCache.GetOrCreateAsync($"{nameof(GetListenedInvoices)}-{invoiceId}", async (cacheEntry) =>
return _memoryCache.GetOrCreateAsync( GetCacheKey(invoiceId), async (cacheEntry) =>
{
var listenedInvoices = new List<ListenedInvoice>();
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
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;
if (lightningMethod == null || !lightningMethod.Activated)
continue;
var lightningSupportedMethod = invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>()
.FirstOrDefault(c => c.CryptoCode == paymentMethod.GetId().CryptoCode);
if (lightningSupportedMethod == null)
LightningLikePaymentMethodDetails lightningMethod;
LightningSupportedPaymentMethod lightningSupportedMethod;
switch (paymentMethod.GetPaymentMethodDetails())
{
case LNURLPayPaymentMethodDetails lnurlPayPaymentMethodDetails:
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;
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethod.GetId().CryptoCode);
@ -164,7 +185,6 @@ namespace BTCPayServer.Payments.Lightning
if (inv.State.Status == InvoiceStatusLegacy.New &&
inv.State.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
{
var invoice = await _InvoiceRepository.GetInvoice(inv.InvoiceId);
await CreateNewLNInvoiceForBTCPayInvoice(invoice);
}
@ -174,6 +194,15 @@ namespace BTCPayServer.Payments.Lightning
{
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);
}
}));
@ -196,7 +225,7 @@ namespace BTCPayServer.Payments.Lightning
private async Task CreateNewLNInvoiceForBTCPayInvoice(InvoiceEntity invoice)
{
var paymentMethods = invoice.GetPaymentMethods()
.Where(method => method.GetId().PaymentType == PaymentTypes.LightningLike)
.Where(method => new []{PaymentTypes.LightningLike, LNURLPayPaymentType.Instance}.Contains(method.GetId().PaymentType))
.ToArray();
var store = await _storeRepository.FindStore(invoice.StoreId);
if (paymentMethods.Any())
@ -209,8 +238,60 @@ namespace BTCPayServer.Payments.Lightning
{
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();
try
{
var client = _lightningLikePaymentHandler.CreateLightningClient(supportedMethod,
(BTCPayNetwork)paymentMethod.Network);
await client.CancelInvoice(oldDetails.InvoiceId);
}
catch
{
//not a fully supported option
}
var prepObj =
_lightningLikePaymentHandler.PreparePayment(supportedMethod, store, paymentMethod.Network);
var newPaymentMethodDetails =
@ -346,7 +427,7 @@ namespace BTCPayServer.Payments.Lightning
var client = _lightningClientFactory.Create(ConnectionString, _network);
LightningInvoice lightningInvoice = await client.GetInvoice(listenedInvoice.PaymentMethodDetails.InvoiceId, cancellation);
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}");
}
@ -392,7 +473,7 @@ namespace BTCPayServer.Payments.Lightning
if (notification.Status == LightningInvoiceStatus.Paid &&
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})");
}
@ -439,13 +520,14 @@ namespace BTCPayServer.Payments.Lightning
bool _ErrorAlreadyLogged = false;
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()
{
BOLT11 = notification.BOLT11,
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);
if (payment != null)
{

View file

@ -2,12 +2,13 @@ using System;
using System.Globalization;
using System.Linq;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using BTCPayServer.BIP78.Sender;
using BTCPayServer.Client.Models;
using NBitpayClient;
using Newtonsoft.Json.Linq;
using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo;
namespace BTCPayServer.Payments
{
@ -101,5 +102,14 @@ namespace BTCPayServer.Payments
{
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();
private LightningPaymentType() { }
private protected LightningPaymentType() { }
public override string ToPrettyString() => "Off-Chain";
public override string GetId() => "LightningLike";
@ -87,5 +87,14 @@ namespace BTCPayServer.Payments
{
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 =
{
BTCLike, LightningLike,
BTCLike, LightningLike, LNURLPay,
#if ALTCOINS
MoneroLike,
EthereumPaymentType.Instance
@ -31,6 +31,10 @@ namespace BTCPayServer.Payments
/// Lightning payment
/// </summary>
public static LightningPaymentType LightningLike => LightningPaymentType.Instance;
/// <summary>
/// Lightning payment
/// </summary>
public static LNURLPayPaymentType LNURLPay => LNURLPayPaymentType.Instance;
#if ALTCOINS
/// <summary>
@ -84,6 +88,11 @@ namespace BTCPayServer.Payments
public abstract object GetGreenfieldData(ISupportedPaymentMethod supportedPaymentMethod, bool canModifyStore);
public virtual bool IsPaymentType(string paymentType)
{
return IsPaymentTypeBase(paymentType);
}
protected bool IsPaymentTypeBase(string paymentType)
{
paymentType = paymentType?.ToLowerInvariant();
return new[]
@ -94,5 +103,8 @@ namespace BTCPayServer.Payments
paymentType,
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;
}
public override void PopulateCryptoInfo(PaymentMethod details, InvoiceCryptoInfo invoiceCryptoInfo, string serverUrl)
{
}
}
}
#endif

View file

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

View file

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

View file

@ -30,9 +30,9 @@
<td>@payment.PaymentMethod</td>
@if (Model.ShowAddress)
{
<td title="@payment.Address">
<span class="text-truncate d-block" style="max-width: 400px">@payment.Address</span>
</td>
<td title="@payment.Address">
<span class="text-truncate d-block" style="max-width: 400px">@payment.Address</span>
</td>
}
<td class="text-end">@payment.Rate</td>
<td class="text-end">@payment.Paid</td>
@ -42,6 +42,12 @@
<td class="text-end">@payment.Overpaid</td>
}
</tr>
var details = payment.PaymentMethodRaw.GetPaymentMethodDetails();
var name = details.GetAdditionalDataPartialName();
if (!string.IsNullOrEmpty(name))
{
<partial name="@name" model="@details" />
}
}
</tbody>
</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 class="bp-view payment scan" id="scan" v-bind:class="{ 'active': currentTab == 'scan'}">
<div class="wrapBtnGroup" v-bind:class="{ invisible: !scanDisplayQr }">
<div class="btnGroupLnd">
<div class="btnGroupLnd"
v-if="srvModel.peerInfo" >
<button
v-on:click="toggleLightningData('bolt11')"
v-bind:class="{ active: currentLightningDisplay === 'bolt11' }"
@ -48,8 +49,8 @@
<img v-bind:src="srvModel.cryptoImage"/>
</div>
</div>
<div class="separatorGem"></div>
<div class="copySectionBox">
<div class="separatorGem" v-if="srvModel.peerInfo" ></div>
<div class="copySectionBox" v-if="srvModel.peerInfo">
<label>{{$t("Node Info")}}</label>
<div class="inputWithIcon _copyInput">
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.peerInfo" readonly="readonly"/>

View file

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

View file

@ -3,7 +3,7 @@
@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;
if (offChainPaymentData is null)
@ -13,7 +13,8 @@
return new OffChainPaymentViewModel()
{
Crypto = payment.Network.CryptoCode,
BOLT11 = offChainPaymentData.BOLT11
BOLT11 = offChainPaymentData.BOLT11,
Type = payment.GetCryptoPaymentData().GetPaymentType()
};
}).Where(model => model != null);
}
@ -28,6 +29,7 @@
<thead class="thead-inverse">
<tr>
<th class="w-150px">Crypto</th>
<th class="w-150px">Type</th>
<th>BOLT11</th>
</tr>
</thead>
@ -36,6 +38,7 @@
{
<tr>
<td>@payment.Crypto</td>
<td>@payment.Type.ToPrettyString()</td>
<td><div class="wraptextAuto">@payment.BOLT11</div></td>
</tr>
}

View file

@ -12,7 +12,7 @@
{
<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())
{
<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">
<h1>@ViewData["Title"]</h1>
<div class="d-flex mt-4 mb-5">
<vc:icon symbol="warning" />
<vc:icon symbol="warning"/>
<p class="text-secondary text-start mb-0">
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.
@ -178,11 +178,50 @@
</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">
<button id="save" name="command" type="submit" value="save" class="btn btn-primary me-2">Save</button>
</div>
</form>
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />
<partial name="_ValidationScriptsPartial"/>
}

View file

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

View file

@ -182,136 +182,7 @@
<input asp-for="StoreWebsite" class="form-control" />
<span asp-validation-for="StoreWebsite" 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 || 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>
</form>
@ -356,9 +227,4 @@
@section PageFootContent {
<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">
<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.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.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.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.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.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.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.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="Stores" asp-action="Webhooks" asp-route-storeId="@this.Context.GetRouteValue("storeId")">Webhooks</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 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.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="@Context.GetRouteValue("storeId")">Rates</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.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.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.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.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.Integrations))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Integrations)" asp-controller="Stores" asp-action="Integrations" asp-route-storeId="@Context.GetRouteValue("storeId")">Integrations</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 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" />
</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)"
}
]
}