Compare commits
12 Commits
v2.0.1-roc
...
better-lnu
Author | SHA1 | Date | |
---|---|---|---|
af44d6aeac | |||
d99bd28386 | |||
86a44b6f1e | |||
f1e5f1b759 | |||
d4828f8d0e | |||
a512cb90d4 | |||
b9a96ebb63 | |||
351702930c | |||
7f26a97eab | |||
b021039d87 | |||
623d7e3056 | |||
33f1f956f7 |
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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")]
|
||||
|
@ -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())
|
||||
|
Reference in New Issue
Block a user