Compare commits

...

12 Commits

Author SHA1 Message Date
af44d6aeac Partially Revert "Adapt controller and partially fix tests"
This reverts commit d4828f8d0edb0878ed1cca5cbafc061b3a46e2f0.
2023-06-26 10:48:27 +02:00
d99bd28386 prevent explicit rate if modify invoice is not available 2023-06-26 10:40:20 +02:00
86a44b6f1e make sure i is not stale 2023-06-26 10:22:05 +02:00
f1e5f1b759 try fix 2023-06-26 10:22:05 +02:00
d4828f8d0e Adapt controller and partially fix tests 2023-06-23 17:53:01 +02:00
a512cb90d4 Fix test and build warnings 2023-06-23 15:53:52 +02:00
b9a96ebb63 Merge branch 'master' into better-lnurl 2023-06-23 12:18:36 +02:00
351702930c Merge branch 'master' into better-lnurl 2023-06-21 14:05:37 +02:00
7f26a97eab fixes 2023-06-21 10:17:46 +02:00
b021039d87 cleanup 2023-06-21 09:56:02 +02:00
623d7e3056 reduce code 2023-06-21 09:17:13 +02:00
33f1f956f7 Do not create an invoice on every lnurl query 2023-06-20 13:21:00 +02:00
6 changed files with 263 additions and 112 deletions

View File

@ -87,6 +87,7 @@ namespace BTCPayServer.Client.Models
public string DefaultLanguage { get; set; }
public CheckoutType? CheckoutType { get; set; }
public bool? LazyPaymentMethods { get; set; }
public string ExplicitRateScript { get; set; }
}
}
public class InvoiceData : InvoiceDataBase

View File

@ -2361,11 +2361,11 @@ namespace BTCPayServer.Tests
var lnaddress1 = Guid.NewGuid().ToString();
s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress1);
s.Driver.FindElement(By.CssSelector("button[value='add']")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.FindAlertMessage();
s.Driver.ToggleCollapse("AddAddress");
var lnaddress2 = "EUR" + Guid.NewGuid().ToString();
var lnaddress2 = "EUR" + Guid.NewGuid();
s.Driver.FindElement(By.Id("Add_Username")).SendKeys(lnaddress2);
lnaddress2 = lnaddress2.ToLowerInvariant();
@ -2375,20 +2375,22 @@ namespace BTCPayServer.Tests
s.Driver.FindElement(By.Id("Add_Max")).SendKeys("10");
s.Driver.FindElement(By.Id("Add_InvoiceMetadata")).SendKeys("{\"test\":\"lol\"}");
s.Driver.FindElement(By.CssSelector("button[value='add']")).Click();
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
s.FindAlertMessage();
var addresses = s.Driver.FindElements(By.ClassName("lightning-address-value"));
var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}";
Assert.Equal(2, addresses.Count);
LNURLPayRequest.LNURLPayRequestCallbackResponse lnAddressOneResponse = null;
LNURLPayRequest.LNURLPayRequestCallbackResponse lnAddressTwoResponse = null;
foreach (IWebElement webElement in addresses)
{
var value = webElement.GetAttribute("value");
//cannot test this directly as https is not supported on our e2e tests
// var request = await LNURL.LNURL.FetchPayRequestViaInternetIdentifier(value, new HttpClient());
var lnurl = new Uri(LNURL.LNURL.ExtractUriFromInternetIdentifier(value).ToString()
.Replace("https", "http"));
var request = (LNURL.LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurl, new HttpClient());
var lnurl = new Uri(LNURL.LNURL.ExtractUriFromInternetIdentifier(value).ToString().Replace("https", "http"));
var request = (LNURLPayRequest)await LNURL.LNURL.FetchInformation(lnurl, new HttpClient());
var m = request.ParsedMetadata.ToDictionary(o => o.Key, o => o.Value);
switch (value)
{
@ -2397,6 +2399,8 @@ namespace BTCPayServer.Tests
lnaddress2 = m["text/identifier"];
Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi));
lnAddressTwoResponse = await request.SendRequest(request.MinSendable, ((BTCPayNetwork)s.Server.DefaultNetwork).NBitcoinNetwork,
new HttpClient());
break;
case { } v when v.StartsWith(lnaddress1):
@ -2404,34 +2408,29 @@ namespace BTCPayServer.Tests
lnaddress1 = m["text/identifier"];
Assert.Equal(1, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(6.12m, request.MaxSendable.ToDecimal(LightMoneyUnit.BTC));
lnAddressOneResponse = await request.SendRequest(request.MinSendable, ((BTCPayNetwork)s.Server.DefaultNetwork).NBitcoinNetwork,
new HttpClient());
break;
case not null when value.Equals($"{lnaddress2}{emailSuffix}"):
lnaddress2 = m["text/identifier"];
Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi));
break;
default:
Assert.False(true, "Should have matched");
Assert.False(true, "Should have matched one of the Lightning addresses");
break;
}
}
// Check that no BTCPay invoice got generated on initial LNURL request
var repo = s.Server.PayTester.GetService<InvoiceRepository>();
var invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
Assert.Equal(2, invoices.Length);
var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}";
foreach (var i in invoices)
{
var lightningPaymentMethod = i.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay));
var paymentMethodDetails =
lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
Assert.Contains(
paymentMethodDetails.ConsumedLightningAddress,
new[] { lnaddress1, lnaddress2 });
if (paymentMethodDetails.ConsumedLightningAddress == lnaddress2)
{
Assert.Equal("lol", i.Metadata.AdditionalData["test"].Value<string>());
}
}
var invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } });
Assert.Empty(invoices);
var lnUsername = lnaddress1.Split('@')[0];
LNURLPayRequest req;
using (var resp = await s.Server.PayTester.HttpClient.GetAsync($"/.well-known/lnurlp/{lnUsername}"))
{
@ -2484,6 +2483,25 @@ namespace BTCPayServer.Tests
Assert.NotNull(succ.Pr);
Assert.Equal(new LightMoney(2001), BOLT11PaymentRequest.Parse(succ.Pr, Network.RegTest).MinimumAmount);
}
// Again, check the invoices
invoices = await repo.GetInvoices(new InvoiceQuery { StoreId = new[] { s.StoreId } });
Assert.Equal(2, invoices.Length);
foreach (var i in invoices)
{
var lightningPaymentMethod = i.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.LNURLPay));
var paymentMethodDetails =
lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
Assert.Contains(
paymentMethodDetails.ConsumedLightningAddress,
new[] { lnaddress1, lnaddress2 });
if (paymentMethodDetails.ConsumedLightningAddress == lnaddress2)
{
Assert.Equal("lol", i.Metadata.AdditionalData["test"].Value<string>());
}
}
}
[Fact]

View File

@ -41,6 +41,7 @@ namespace BTCPayServer.Controllers.Greenfield
private readonly RateFetcher _rateProvider;
private readonly InvoiceActivator _invoiceActivator;
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly IAuthorizationService _authorizationService;
public LanguageService LanguageService { get; }
@ -48,7 +49,7 @@ namespace BTCPayServer.Controllers.Greenfield
LinkGenerator linkGenerator, LanguageService languageService, BTCPayNetworkProvider btcPayNetworkProvider,
CurrencyNameTable currencyNameTable, RateFetcher rateProvider,
InvoiceActivator invoiceActivator,
PullPaymentHostedService pullPaymentService, ApplicationDbContextFactory dbContextFactory)
PullPaymentHostedService pullPaymentService, ApplicationDbContextFactory dbContextFactory, IAuthorizationService authorizationService)
{
_invoiceController = invoiceController;
_invoiceRepository = invoiceRepository;
@ -59,6 +60,7 @@ namespace BTCPayServer.Controllers.Greenfield
_invoiceActivator = invoiceActivator;
_pullPaymentService = pullPaymentService;
_dbContextFactory = dbContextFactory;
_authorizationService = authorizationService;
LanguageService = languageService;
}
@ -188,6 +190,12 @@ namespace BTCPayServer.Controllers.Greenfield
{
ModelState.AddModelError(nameof(request.Amount), $"The amount should less than {GreenfieldConstants.MaxAmount}.");
}
if (!string.IsNullOrEmpty(request.Checkout.ExplicitRateScript) &&
!(await _authorizationService.AuthorizeAsync(User, Policies.CanModifyInvoices)).Succeeded)
{
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.ExplicitRateScript,
$"You are not authorized to use explicit rate script (missing {Policies.CanModifyInvoices} permission)", this);
}
request.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();
if (request.Checkout.PaymentMethods?.Any() is true)
{

View File

@ -238,6 +238,10 @@ namespace BTCPayServer.Controllers
{
var storeBlob = store.GetStoreBlob();
var entity = _InvoiceRepository.CreateNewInvoice();
if (!string.IsNullOrEmpty(invoice.Checkout.ExplicitRateScript) && RateRules.TryParse(invoice.Checkout.ExplicitRateScript, out var explicitRateRule) && explicitRateRule is not null)
{
entity.ExplicitRateRules = explicitRateRule;
}
entity.ServerUrl = serverUrl;
entity.ExpirationTime = entity.InvoiceTime + (invoice.Checkout.Expiration ?? storeBlob.InvoiceExpiration);
entity.MonitoringExpiration = entity.ExpirationTime + (invoice.Checkout.Monitoring ?? storeBlob.MonitoringExpiration);
@ -315,7 +319,6 @@ namespace BTCPayServer.Controllers
}
entity.Status = InvoiceStatusLegacy.New;
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider);
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
if (invoicePaymentMethodFilter != null)
{
@ -337,7 +340,7 @@ namespace BTCPayServer.Controllers
}
}
var rateRules = storeBlob.GetRateRules(_NetworkProvider);
var rateRules = entity.ExplicitRateRules?? storeBlob.GetRateRules(_NetworkProvider);
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken);
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);

View File

@ -23,6 +23,7 @@ using BTCPayServer.Payments.Lightning;
using BTCPayServer.Plugins.Crowdfund;
using BTCPayServer.Plugins.PointOfSale;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Rating;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
@ -34,11 +35,13 @@ using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Caching.Memory;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer
{
@ -60,6 +63,8 @@ namespace BTCPayServer
private readonly BTCPayNetworkJsonSerializerSettings _btcPayNetworkJsonSerializerSettings;
private readonly IPluginHookService _pluginHookService;
private readonly InvoiceActivator _invoiceActivator;
private readonly IMemoryCache _memoryCache;
private readonly RateFetcher _rateFetcher;
public UILNURLController(InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
@ -74,7 +79,9 @@ namespace BTCPayServer
PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
IPluginHookService pluginHookService,
InvoiceActivator invoiceActivator)
InvoiceActivator invoiceActivator,
IMemoryCache memoryCache,
RateFetcher rateFetcher)
{
_invoiceRepository = invoiceRepository;
_eventAggregator = eventAggregator;
@ -90,6 +97,8 @@ namespace BTCPayServer
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
_pluginHookService = pluginHookService;
_invoiceActivator = invoiceActivator;
_memoryCache = memoryCache;
_rateFetcher = rateFetcher;
}
[HttpGet("withdraw/pp/{pullPaymentId}")]
@ -226,12 +235,8 @@ namespace BTCPayServer
[HttpGet("pay/app/{appId}/{itemCode}")]
public async Task<IActionResult> GetLNURLForApp(string cryptoCode, string appId, string itemCode = null)
{
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network is null || !network.SupportLightning)
{
return NotFound();
}
if (!NetworkSupportsLightning(cryptoCode, out _))
return null;
var app = await _appService.GetApp(appId, null, true);
if (app is null)
{
@ -248,6 +253,10 @@ namespace BTCPayServer
{
return NotFound();
}
var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out var lnurlPM);
if (pmi is null)
return NotFound("LNUrl or LN is disabled");
ViewPointOfSaleViewModel.Item[] items;
string currencyCode;
@ -272,9 +281,6 @@ namespace BTCPayServer
ViewPointOfSaleViewModel.Item item = null;
if (!string.IsNullOrEmpty(itemCode))
{
var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out _);
if (pmi is null)
return NotFound("LNUrl or LN is disabled");
var escapedItemId = Extensions.UnescapeBackSlashUriString(itemCode);
item = items.FirstOrDefault(item1 =>
item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase) ||
@ -318,13 +324,12 @@ namespace BTCPayServer
createInvoice.Metadata = invoiceMetadata.ToJObject();
return await GetLNURLRequest(
cryptoCode,
store,
store.GetStoreBlob(),
return await CreateLNURLRequestWithoutInvoice( new LNURLRequestParams(
store.Id,
pmi,
createInvoice,
additionalTags: new List<string> { AppService.GetAppInternalTag(appId) },
allowOverpay: false);
allowOverpay: false), store, store.GetStoreBlob(), lnurlPM);
}
public class EditLightningAddressVM
@ -376,29 +381,23 @@ namespace BTCPayServer
if (store is null)
return NotFound("Unknown username");
var pmi = GetLNUrlPaymentMethodId("BTC", store, out var lnurlPaymentMethod);
if (pmi is null)
return NotFound("LNUrl or LN is disabled");
var blob = lightningAddressSettings.GetBlob();
return await GetLNURLRequest(
"BTC",
store,
store.GetStoreBlob(),
new CreateInvoiceRequest()
{
Currency = blob?.CurrencyCode,
Metadata = blob?.InvoiceMetadata
},
new LNURLPayRequest()
{
MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null,
MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null,
},
new Dictionary<string, string>()
{
{ "text/identifier", $"{username}@{Request.Host}" }
});
return await CreateLNURLRequestWithoutInvoice(new LNURLRequestParams(
store.Id,
pmi,
new CreateInvoiceRequest {Currency = blob?.CurrencyCode, Metadata = blob?.InvoiceMetadata},
new LNURLPayRequest
{
MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null,
MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null,
},
new Dictionary<string, string> {{"text/identifier", $"{username}@{Request.Host}"}}), store,store.GetStoreBlob(), lnurlPaymentMethod);
}
[HttpGet("pay")]
[EnableCors(CorsPolicies.All)]
[IgnoreAntiforgeryToken]
@ -411,49 +410,124 @@ namespace BTCPayServer
if (store is null)
return NotFound();
var pmi = GetLNUrlPaymentMethodId("BTC", store, out var lnurlPaymentMethod);
if (pmi is null)
return NotFound("LNUrl or LN is disabled");
var blob = store.GetStoreBlob();
if (!blob.AnyoneCanInvoice)
return NotFound("'Anyone can invoice' is turned off");
return await GetLNURLRequest(
cryptoCode,
store,
blob,
return await CreateLNURLRequestWithoutInvoice(new LNURLRequestParams(
storeId,
pmi,
new CreateInvoiceRequest
{
Currency = currencyCode
});
}), store, blob, lnurlPaymentMethod);
}
private async Task<IActionResult> GetLNURLRequest(
string cryptoCode,
Data.StoreData store,
Data.StoreBlob blob,
CreateInvoiceRequest createInvoice,
LNURLPayRequest lnurlRequest = null,
Dictionary<string, string> lnUrlMetadata = null,
List<string> additionalTags = null,
bool allowOverpay = true)
class LNURLRequestParams
{
var pmi = GetLNUrlPaymentMethodId(cryptoCode, store, out _);
if (pmi is null)
return NotFound("LNUrl or LN is disabled");
public LNURLRequestParams(
string storeId,
PaymentMethodId paymentMethodId,
CreateInvoiceRequest createInvoice,
LNURLPayRequest lnurlRequest = null,
Dictionary<string, string> lnUrlMetadata = null,
List<string> additionalTags = null,
bool allowOverpay = true)
{
StoreId = storeId;
PaymentMethodId = paymentMethodId;
CreateInvoice = createInvoice;
LNURLRequest = lnurlRequest;
LNURLMetadata = lnUrlMetadata;
AdditionalTags = additionalTags;
AllowOverpay = allowOverpay;
}
InvoiceEntity i;
try
{
createInvoice.Checkout ??= new InvoiceDataBase.CheckoutOptions();
createInvoice.Checkout.LazyPaymentMethods = false;
createInvoice.Checkout.PaymentMethods = new[] { pmi.ToStringNormalized() };
i = await _invoiceController.CreateInvoiceCoreRaw(createInvoice, store, Request.GetAbsoluteRoot(), additionalTags);
}
catch (Exception e)
{
return this.CreateAPIError(null, e.Message);
}
lnurlRequest = await CreateLNUrlRequestFromInvoice(cryptoCode, i, store, blob, lnurlRequest, lnUrlMetadata, allowOverpay);
return lnurlRequest is null ? NotFound() : Ok(lnurlRequest);
public string StoreId { get; }
public PaymentMethodId PaymentMethodId { get; set; }
public CreateInvoiceRequest CreateInvoice { get; set; }
public LNURLPayRequest LNURLRequest { get; set; }
public Dictionary<string, string> LNURLMetadata { get; set; }
public List<string> AdditionalTags { get; set; }
public bool AllowOverpay { get; set; }
}
private async Task<IActionResult> CreateLNURLRequestWithoutInvoice(LNURLRequestParams requestParams,
StoreData store, StoreBlob storeBlob, LNURLPaySupportedPaymentMethod lnurlPaySupportedPaymentMethod)
{
var k = Guid.NewGuid();
requestParams.LNURLRequest ??= new LNURLPayRequest();
requestParams.LNURLMetadata ??= new Dictionary<string, string>();
requestParams.CreateInvoice.Currency ??= storeBlob.DefaultCurrency;
// Set the callback endpoint to trigger invoice generation
requestParams.LNURLRequest.Tag = "payRequest";
requestParams.LNURLRequest.Callback = new Uri(_linkGenerator.GetUriByAction(
action: nameof(LNURLCallback),
controller: "UILNURL",
values: new {requestParams.PaymentMethodId.CryptoCode, k },
Request.Scheme, Request.Host, Request.PathBase));
if (!requestParams.LNURLMetadata.ContainsKey("text/plain"))
{
var invMetadata = InvoiceMetadata.FromJObject(requestParams.CreateInvoice.Metadata?? new JObject());
var invoiceDescription = storeBlob.LightningDescriptionTemplate
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{ItemDescription}", invMetadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
.Replace("{OrderId}", invMetadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
requestParams.LNURLMetadata.Add("text/plain", invoiceDescription);
}
requestParams.LNURLRequest.CommentAllowed = lnurlPaySupportedPaymentMethod.LUD12Enabled ? 2000 : 0;
requestParams.LNURLRequest.Metadata = JsonConvert.SerializeObject(requestParams.LNURLMetadata.Select(kv => new[] { kv.Key, kv.Value }));
// We don't think BTCPay handle well 0 sats payments, just in case make it minimum one sat.
if (requestParams.LNURLRequest.MinSendable is null || requestParams.LNURLRequest.MinSendable < LightMoney.Satoshis(1.0m))
requestParams.LNURLRequest.MinSendable = LightMoney.Satoshis(1.0m);
if (requestParams.LNURLRequest.MaxSendable is null)
requestParams.LNURLRequest.MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC);
if (requestParams.CreateInvoice.Type != InvoiceType.TopUp && requestParams.CreateInvoice.Amount is not null)
{
LightMoney cAmount;
if (requestParams.CreateInvoice.Currency != requestParams.PaymentMethodId.CryptoCode)
{
var rr = storeBlob.GetRateRules(_btcPayNetworkProvider);
var rate = _rateFetcher.FetchRates(
new HashSet<CurrencyPair>()
{
new CurrencyPair( requestParams.PaymentMethodId.CryptoCode, requestParams.CreateInvoice.Currency)
}, rr, CancellationToken.None).First();
var rateResult = await rate.Value;
cAmount = LightMoney.FromUnit(rateResult.BidAsk.Bid, LightMoneyUnit.BTC);
requestParams.CreateInvoice.Checkout.ExplicitRateScript =
$"{ requestParams.PaymentMethodId.CryptoCode}_{requestParams.CreateInvoice.Currency}={rateResult.BidAsk.Bid}";
}
else
{
cAmount = LightMoney.FromUnit(requestParams.CreateInvoice.Amount.Value!, LightMoneyUnit.BTC);
}
requestParams.LNURLRequest.MinSendable = cAmount;
if (!requestParams.AllowOverpay)
requestParams.LNURLRequest.MaxSendable = requestParams.LNURLRequest.MinSendable;
}
requestParams.LNURLRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", requestParams.LNURLRequest) as LNURLPayRequest;
var invoiceParamsCacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(5)); // Set an appropriate expiration time
// Store the invoice parameters in the cache
_memoryCache.Set($"{nameof(UILNURLController)}:{k}", requestParams, invoiceParamsCacheEntryOptions);
return Ok(requestParams.LNURLRequest);
}
private async Task<LNURLPayRequest> CreateLNUrlRequestFromInvoice(
string cryptoCode,
InvoiceEntity i,
@ -474,7 +548,7 @@ namespace BTCPayServer
return null;
var paymentMethodDetails = (LNURLPayPaymentMethodDetails)pm.GetPaymentMethodDetails();
bool updatePaymentMethodDetails = false;
if (lnUrlMetadata?.TryGetValue("text/identifier", out var lnAddress) is true && lnAddress is not null)
if (lnUrlMetadata.TryGetValue("text/identifier", out var lnAddress) && lnAddress is not null)
{
paymentMethodDetails.ConsumedLightningAddress = lnAddress;
updatePaymentMethodDetails = true;
@ -520,6 +594,7 @@ namespace BTCPayServer
{
pm.SetPaymentMethodDetails(paymentMethodDetails);
await _invoiceRepository.UpdateInvoicePaymentMethod(i.Id, pm);
i.SetPaymentMethod(pm);
}
return lnurlRequest;
}
@ -527,8 +602,7 @@ namespace BTCPayServer
PaymentMethodId GetLNUrlPaymentMethodId(string cryptoCode, Data.StoreData store, out LNURLPaySupportedPaymentMethod lnUrlSettings)
{
lnUrlSettings = null;
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
if (network is null || !network.SupportLightning)
if (!NetworkSupportsLightning(cryptoCode, out _))
return null;
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
@ -545,19 +619,58 @@ namespace BTCPayServer
return pmi;
}
[HttpGet("pay/{k}")]
public async Task<IActionResult> LNURLCallback(string k, long? amount = null, string comment = null)
{
if (!_memoryCache.TryGetValue<LNURLRequestParams>($"{nameof(UILNURLController)}:{k}", out var lnurlReq) || lnurlReq is null)
return NotFound();
if (amount is null)
{
return Ok(lnurlReq.LNURLRequest);
}
var lnurlPayRequest = lnurlReq.LNURLRequest;
var amt = new LightMoney(amount.Value);
if (amt < lnurlPayRequest.MinSendable || amt > lnurlPayRequest.MaxSendable)
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Amount is out of bounds." });
InvoiceEntity i;
try
{
var store = await _storeRepository.FindStore(lnurlReq.StoreId);
lnurlReq.CreateInvoice.Checkout ??= new InvoiceDataBase.CheckoutOptions();
lnurlReq.CreateInvoice.Checkout.LazyPaymentMethods = false;
lnurlReq.CreateInvoice.Checkout.PaymentMethods = new[] { lnurlReq.PaymentMethodId.ToStringNormalized() };
i = await _invoiceController.CreateInvoiceCoreRaw(lnurlReq.CreateInvoice, store, Request.GetAbsoluteRoot(), lnurlReq.AdditionalTags);
await CreateLNUrlRequestFromInvoice(lnurlReq.PaymentMethodId.CryptoCode,i, store,store.GetStoreBlob(),lnurlReq.LNURLRequest, lnurlReq.LNURLMetadata, lnurlReq.AllowOverpay);
return await GetLNURLForInvoice(i, lnurlReq.PaymentMethodId.CryptoCode, amount, comment);
}
catch (Exception e)
{
return this.CreateAPIError(null, e.Message);
}
}
[HttpGet("pay/i/{invoiceId}")]
[EnableCors(CorsPolicies.All)]
[IgnoreAntiforgeryToken]
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 i = await _invoiceRepository.GetInvoice(invoiceId, true);
return await GetLNURLForInvoice(i, cryptoCode, amount, comment);
}
[NonAction]
private async Task<IActionResult> GetLNURLForInvoice(InvoiceEntity i, string cryptoCode,
[FromQuery] long? amount = null, string comment = null)
{
if (!NetworkSupportsLightning(cryptoCode, out var network))
return null;
if (i is null)
return NotFound();
@ -578,7 +691,7 @@ namespace BTCPayServer
{
if (!await _invoiceActivator.ActivateInvoicePaymentMethod(pmi, i, store))
return NotFound();
i = await _invoiceRepository.GetInvoice(invoiceId, true);
i = await _invoiceRepository.GetInvoice(i.Id, true);
lightningPaymentMethod = i.GetPaymentMethod(pmi);
paymentMethodDetails = lightningPaymentMethod.GetPaymentMethodDetails() as LNURLPayPaymentMethodDetails;
}
@ -599,7 +712,7 @@ namespace BTCPayServer
return Ok(lnurlPayRequest);
var amt = new LightMoney(amount.Value);
if (amt < lnurlPayRequest.MinSendable || amount > lnurlPayRequest.MaxSendable)
if (amt < lnurlPayRequest.MinSendable || amt > lnurlPayRequest.MaxSendable)
return BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Amount is out of bounds." });
LNURLPayRequest.LNURLPayRequestCallbackResponse.ILNURLPayRequestSuccessAction successAction = null;
@ -613,7 +726,7 @@ namespace BTCPayServer
Url = _linkGenerator.GetUriByAction(
nameof(UIInvoiceController.InvoiceReceipt),
"UIInvoice",
new { invoiceId },
new { i.Id },
Request.Scheme,
Request.Host,
Request.PathBase)
@ -681,7 +794,7 @@ namespace BTCPayServer
string.IsNullOrEmpty(ex.Message) ? "" : $": {ex.Message}")
});
}
paymentMethodDetails.BOLT11 = invoice.BOLT11;
paymentMethodDetails.PaymentHash = string.IsNullOrEmpty(invoice.PaymentHash) ? null : uint256.Parse(invoice.PaymentHash);
paymentMethodDetails.Preimage = string.IsNullOrEmpty(invoice.Preimage) ? null : uint256.Parse(invoice.Preimage);
@ -693,8 +806,8 @@ namespace BTCPayServer
if (updatePaymentMethod)
{
lightningPaymentMethod.SetPaymentMethodDetails(paymentMethodDetails);
await _invoiceRepository.UpdateInvoicePaymentMethod(invoiceId, lightningPaymentMethod);
_eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(invoiceId, paymentMethodDetails, pmi));
await _invoiceRepository.UpdateInvoicePaymentMethod(i.Id, lightningPaymentMethod);
_eventAggregator.Publish(new InvoiceNewPaymentDetailsEvent(i.Id, paymentMethodDetails, pmi));
}
return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse
@ -713,6 +826,12 @@ namespace BTCPayServer
});
}
private bool NetworkSupportsLightning(string cryptoCode, out BTCPayNetwork network)
{
network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
return !(network is null || !network.SupportLightning);
}
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("~/stores/{storeId}/plugins/lightning-address")]

View File

@ -10,6 +10,7 @@ using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
@ -465,6 +466,7 @@ namespace BTCPayServer.Services.Invoices
[JsonConverter(typeof(StringEnumConverter))]
public CheckoutType? CheckoutType { get; set; }
public bool LazyPaymentMethods { get; set; }
public RateRules ExplicitRateRules { get; set; }
public bool IsExpired()
{
@ -490,13 +492,13 @@ namespace BTCPayServer.Services.Invoices
Currency = Currency,
PaymentSubtotals = new Dictionary<string, decimal>(),
PaymentTotals = new Dictionary<string, decimal>(),
SupportedTransactionCurrencies = new Dictionary<string, NBitpayClient.InvoiceSupportedTransactionCurrency>(),
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>(),
Addresses = new Dictionary<string, string>(),
PaymentCodes = new Dictionary<string, InvoiceCryptoInfo.InvoicePaymentUrls>(),
ExchangeRates = new Dictionary<string, Dictionary<string, decimal>>()
};
dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id;
dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id={Id}";
dto.CryptoInfo = new List<InvoiceCryptoInfo>();
dto.MinerFees = new Dictionary<string, MinerFeeInfo>();
foreach (var info in this.GetPaymentMethods())